Generar arte ASCII

14

Dada una imagen en blanco y negro en cualquier formato razonable sin pérdidas como entrada, envíe el arte ASCII que esté lo más cerca posible de la imagen de entrada.

Reglas

  • Solo se pueden utilizar avances de línea y bytes ASCII 32-127.
  • La imagen de entrada se recortará para que no haya espacios en blanco extraños que rodeen la imagen.
  • Las presentaciones deben poder completar todo el corpus de puntuación en menos de 5 minutos.
  • Solo se acepta texto sin formato; No hay formatos de texto enriquecido.
  • La fuente utilizada en la puntuación es Linux Libertine de 20 puntos .
  • El archivo de texto de salida, cuando se convierte en una imagen como se describe a continuación, debe tener las mismas dimensiones que la imagen de entrada, dentro de 30 píxeles en cualquier dimensión.

Puntuación

Estas imágenes se utilizarán para puntuar:

Puede descargar un archivo zip de las imágenes aquí .

Las presentaciones no deben optimizarse para este corpus; más bien, deberían funcionar para 8 imágenes en blanco y negro de dimensiones similares. Me reservo el derecho de cambiar las imágenes en el corpus si sospecho que las presentaciones se están optimizando para estas imágenes específicas.

La puntuación se realizará a través de este script:

#!/usr/bin/env python
from __future__ import print_function
from __future__ import division
# modified from http://stackoverflow.com/a/29775654/2508324
# requires Linux Libertine fonts - get them at https://sourceforge.net/projects/linuxlibertine/files/linuxlibertine/5.3.0/
# requires dssim - get it at https://github.com/pornel/dssim
import PIL
import PIL.Image
import PIL.ImageFont
import PIL.ImageOps
import PIL.ImageDraw
import pathlib
import os
import subprocess
import sys

PIXEL_ON = 0  # PIL color to use for "on"
PIXEL_OFF = 255  # PIL color to use for "off"

def dssim_score(src_path, image_path):
    out = subprocess.check_output(['dssim', src_path, image_path])
    return float(out.split()[0])

def text_image(text_path):
    """Convert text file to a grayscale image with black characters on a white background.

    arguments:
    text_path - the content of this file will be converted to an image
    """
    grayscale = 'L'
    # parse the file into lines
    with open(str(text_path)) as text_file:  # can throw FileNotFoundError
        lines = tuple(l.rstrip() for l in text_file.readlines())

    # choose a font (you can see more detail in my library on github)
    large_font = 20  # get better resolution with larger size
    if os.name == 'posix':
        font_path = '/usr/share/fonts/linux-libertine/LinLibertineO.otf'
    else:
        font_path = 'LinLibertine_DRah.ttf'
    try:
        font = PIL.ImageFont.truetype(font_path, size=large_font)
    except IOError:
        print('Could not use Libertine font, exiting...')
        exit()

    # make the background image based on the combination of font and lines
    pt2px = lambda pt: int(round(pt * 96.0 / 72))  # convert points to pixels
    max_width_line = max(lines, key=lambda s: font.getsize(s)[0])
    # max height is adjusted down because it's too large visually for spacing
    test_string = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
    max_height = pt2px(font.getsize(test_string)[1])
    max_width = pt2px(font.getsize(max_width_line)[0])
    height = max_height * len(lines)  # perfect or a little oversized
    width = int(round(max_width + 40))  # a little oversized
    image = PIL.Image.new(grayscale, (width, height), color=PIXEL_OFF)
    draw = PIL.ImageDraw.Draw(image)

    # draw each line of text
    vertical_position = 5
    horizontal_position = 5
    line_spacing = int(round(max_height * 0.8))  # reduced spacing seems better
    for line in lines:
        draw.text((horizontal_position, vertical_position),
                  line, fill=PIXEL_ON, font=font)
        vertical_position += line_spacing
    # crop the text
    c_box = PIL.ImageOps.invert(image).getbbox()
    image = image.crop(c_box)
    return image

if __name__ == '__main__':
    compare_dir = pathlib.PurePath(sys.argv[1])
    corpus_dir = pathlib.PurePath(sys.argv[2])
    images = []
    scores = []
    for txtfile in os.listdir(str(compare_dir)):
        fname = pathlib.PurePath(sys.argv[1]).joinpath(txtfile)
        if fname.suffix != '.txt':
            continue
        imgpath = fname.with_suffix('.png')
        corpname = corpus_dir.joinpath(imgpath.name)
        img = text_image(str(fname))
        corpimg = PIL.Image.open(str(corpname))
        img = img.resize(corpimg.size, PIL.Image.LANCZOS)
        corpimg.close()
        img.save(str(imgpath), 'png')
        img.close()
        images.append(str(imgpath))
        score = dssim_score(str(corpname), str(imgpath))
        print('{}: {}'.format(corpname, score))
        scores.append(score)
    print('Score: {}'.format(sum(scores)/len(scores)))

El proceso de puntuación:

  1. Ejecute el envío para cada imagen de corpus, enviando los resultados a .txtarchivos con la misma raíz que el archivo de corpus (hecho manualmente).
  2. Convierta cada archivo de texto en una imagen PNG, utilizando una fuente de 20 puntos, recortando espacios en blanco.
  3. Cambie el tamaño de la imagen resultante a las dimensiones de la imagen original utilizando el remuestreo de Lanczos.
  4. Compare cada imagen de texto con la imagen original usando dssim.
  5. Salida de la puntuación dssim para cada archivo de texto.
  6. Salida del puntaje promedio.

La similitud estructural (la métrica por la cual se dssimcalculan los puntajes) es una métrica basada en la visión humana y la identificación de objetos en imágenes. Para decirlo claramente: si dos imágenes se parecen a las de los humanos, probablemente tendrán una puntuación baja dssim.

La presentación ganadora será la presentación con el puntaje promedio más bajo.

relacionado

Mego
fuente
66
"Blanco y negro" como en "cero / uno" o ¿cuántos niveles de gris?
Luis Mendo
2
@DonMuesli 0 y 1.
Mego
¿Podría aclarar lo que quiere decir con "Enviar los resultados a los .txtarchivos"? ¿Debería el programa generar texto que se canalizará a un archivo o deberíamos generar un archivo directamente?
DanTheMan
@DanTheMan Cualquiera de los dos es aceptable. Sin embargo, si realiza la salida a STDOUT, la salida deberá redirigirse a un archivo para fines de puntuación.
Mego
¿No deberías especificar restricciones de resolución? De lo contrario, podríamos producir, por ejemplo, una imagen de 10000 por 10000 caracteres que, cuando se reduzca, coincidiría bastante con las imágenes originales, y los caracteres individuales serían puntos ilegibles. El tamaño de fuente no importa si la imagen de salida es enorme.
DavidC

Respuestas:

6

Java, puntaje 0.57058675

Esta es la primera vez que hago manipulación de imágenes, así que es un poco incómodo, pero creo que resultó bien.

No pude hacer que dssim funcionara en mi máquina, pero pude hacer imágenes usando PIL.

Curiosamente, la fuente me dice en Java que cada uno de los caracteres que estoy usando son de ancho 6. Puedes ver que en mi programa FontMetrics::charWidthes 6para todos los personajes que he usado. El {}logotipo se ve bastante decente en una fuente monoespacial. Pero por alguna razón, las líneas no se alinean en el archivo de texto completo. Yo culpo a las ligaduras. (Y sí, debería estar usando la fuente correcta).

En fuente monoespaciada:

                                                                                      .
                         .,:ff:,                                                   ,:fff::,.
                ,ff .fIIIIIf,                                                         .:fIIIIIf.:f:.
            .,:III: ,ff::                       ..,,            ,,..                      ,:fff, IIII.,
          :IIf,f:,:fff:,                  .:fIIIIIII.          .IIIIIIIf:.                 .,:fff:,ff IIf,
       ,.fIIIf,:ffff,                   ,IIIIIII:,,.            .,,:IIIIIII.                  .:ffff:,IIII,:.
     ,III.::.,,,,,.                     IIIIII:                      ,IIIIII                     ,,,,,.,:,:IIf
     IIIII :ffIIf,                      IIIIII,                      .IIIIII                      :IIIf:,.IIIIf.
  ,II,fIf.:::,..                        IIIIII,                      .IIIIII                       ..,:::,,If::II
  IIIIf.  ,:fII:                       .IIIIII,                      .IIIIII.                       IIff:.  :IIII:
 ::IIIIf:IIIf: .                  ,::fIIIIIII,                        ,fIIIIIIf::,                   ,ffIII,IIIIf,,
:IIf:::    .,fI:                  IIIIIIIII:                            :IIIIIIIIf                  If:,    .::fIIf
 IIIIII, :IIIIf                     .,:IIIIIIf                        fIIIIII:,.                    ,IIIII. fIIIII:
 ,:IIIII ff:,   f,                      IIIIII,                      .IIIIII                      f.  .::f::IIIIf,.
 fIf::,,     ,fIII                      IIIIII,                      .IIIIII                     :III:      ,,:fII.
  fIIIIIIf, :IIIIf   ,                  IIIIII,                      .IIIIII                 .,  ,IIIII. :fIIIIII,
   .:IIIIIII,ff,    :II:                IIIIIIf                      fIIIIII               .fII.   .:ff:IIIIIIf,
     :fffff:,      IIIIIf   ,            :IIIIIIIfff            fffIIIIIII:           ..   IIIII:      ::fffff,
      .fIIIIIIIf:, fIIII,   ,IIf,           ,:ffIIII.          .IIIIff:,          .:fII    fIIII,.:ffIIIIIII:
         ,fIIIIIIIIIf:,     ,IIIII:  .,::,                               .,::,  .IIIIII      ::fIIIIIIIIf:.
             :fffffff,      .fIIIII,   .IIIIIf:                     ,:fIIII:    IIIIII:       :fffffff,
              .:fIIIIIIIIIIIIffffI:      IIIIIIII.                :IIIIIII:     .fIffffIIIIIIIIIIII:,
                   ,:fIIIIIIIIIIIf,       .:fIIIII               ,IIIIIf,        :IIIIIIIIIIIff,.
                         .:ffffffffIIIIIIIIIIIfff:.              ,ffffIIIIIIIIIIIfffffff:,
                             .,:ffIIIIIIIIIIIIIIIIf,   .,,,,.  .:fIIIIIIIIIIIIIIIIff:,.
                                       ....... .,,:fffff:.,:fffff:,.  .......
                                    ..,,:fffIIIIf:,.            .,:fIIIIff::,,..
                                   .IIIIIf:,.                          .,:fIIIII
                                     f,                                      ,f

Después de ejecutarlo a través de la herramienta de imagen:

{} logo

De todos modos, aquí está el código real.

//package cad97;

import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Rectangle;
import java.awt.Toolkit;
import java.awt.image.BufferedImage;
import java.awt.image.Raster;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Map;
import javax.imageio.ImageIO;

public final class AsciiArt {

    private static final Font LINUX_LIBERTINE = new Font("LinLibertine_DRah", Font.PLAIN, 20);
    private static final FontMetrics LL_METRICS = Toolkit.getDefaultToolkit().getFontMetrics(LINUX_LIBERTINE);
    // Toolkit::getFontMetrics is deprecated, but that's the only way to get FontMetrics without an explicit Graphics environment.
    // If there's a better way to get the widths of characters, please tell me.

    public static void main(String[] args) throws IOException {
        File jar = new java.io.File(AsciiArt.class.getProtectionDomain().getCodeSource().getLocation().getPath());
        if (args.length != 1) {
            String jarName = jar.getName();
            System.out.println("Usage: java -jar " + jarName + " file");
        } else {
            File image = new File(args[0]);
            try (InputStream input = new FileInputStream(image)) {
                String art = createAsciiArt(ImageIO.read(input), LINUX_LIBERTINE, LL_METRICS);
                System.out.print(art); // If you want to save as a file, change this.
            } catch (FileNotFoundException fnfe) {
                System.out.println("Unable to find file " + image + ".");
                System.out.println("Please note that you need to pass the full file path.");
            }
        }
    }

    private static String createAsciiArt(BufferedImage image, Font font, FontMetrics metrics) {
        final int height = metrics.getHeight();
        final Map<Character,Integer> width = new HashMap<>();
        for (char c=32; c<127; c++) { width.put(c, metrics.charWidth(c)); }

        StringBuilder art = new StringBuilder();

        for (int i=0; i<=image.getHeight(); i+=height) {
            final int tempHeight = Math.min(height, image.getHeight()-i);
            art.append(createAsciiLine(image.getSubimage(0, i, image.getWidth(), tempHeight), width));
        }

        return art.toString();
    }

    private static String createAsciiLine(BufferedImage image, Map<Character,Integer> charWidth) {
        if (image.getWidth()<6) return "\n";
        /*
        I'm passing in the charWidth Map because I could use it, and probably a later revision if I
        come back to this will actually use non-6-pixel-wide characters. As is, I'm only using the
        6-pixel-wide characters for simplicity. They are those in this set: { !,./:;I[\]ft|}
        */
        assert charWidth.get(' ') == 6; assert charWidth.get('!') == 6;
        assert charWidth.get(',') == 6; assert charWidth.get('.') == 6;
        assert charWidth.get('/') == 6; assert charWidth.get(':') == 6;
        assert charWidth.get(';') == 6; assert charWidth.get('I') == 6;
        assert charWidth.get('[') == 6; assert charWidth.get('\\') == 6;
        assert charWidth.get(']') == 6; assert charWidth.get('f') == 6;
        assert charWidth.get('t') == 6; assert charWidth.get('|') == 6;

        // Measure whiteness of 6-pixel-wide sample
        Raster sample = image.getData(new Rectangle(6, image.getHeight()));
        int whiteCount = 0;
        for (int x=sample.getMinX(); x<sample.getMinX()+sample.getWidth(); x++) {
            for (int y=sample.getMinY(); y<sample.getMinY()+sample.getHeight(); y++) {
                int pixel = sample.getPixel(x, y, new int[1])[0];
                whiteCount += pixel==1?0:1;
            }
        }

        char next;

        int area = sample.getWidth()*sample.getHeight();

        if (whiteCount > area*0.9) {
            next = ' ';
        } else if (whiteCount > area*0.8) {
            next = '.';
        } else if (whiteCount > area*0.65) {
            next = ',';
        } else if (whiteCount > area*0.5) {
            next = ':';
        } else if (whiteCount > area*0.3) {
            next = 'f';
        } else {
            next = 'I';
        }

        return next + createAsciiLine(image.getSubimage(charWidth.get(','), 0, image.getWidth()-sample.getWidth(), image.getHeight()), charWidth);
    }

}

Compilar:

  • Asegúrese de tener instalado el JDK
  • Asegúrese de que el contenedor JDK esté en su RUTA (para mí es C:\Program Files\Java\jdk1.8.0_91\bin)
  • Guarde el archivo como AsciiArt.java
  • javac AsciiArt.java
  • jar cvfe WhateverNameYouWant.jar AsciiArt AsciiArt.class

Uso:, se java -jar WhateverNameYouWant.jar C:\full\file\path.pngimprime en STDOUT

REQUIERE que el archivo fuente se guarde con una profundidad de 1 bit y la muestra para un píxel blanco 1.

Resultado de puntuación:

corp/board.png: 0.6384
corp/Doppelspalt.png: 0.605746
corp/down.png: 1.012326
corp/img2.png: 0.528794
corp/pcgm.png: 0.243618
corp/peng.png: 0.440982
corp/phi.png: 0.929552
corp/text2image.png: 0.165276
Score: 0.57058675
CAD97
fuente
1
Ejecutar con -eapara habilitar aserciones. No cambiará el comportamiento (excepto que tal vez lo ralentice un poco) porque las aserciones funcionan al fallar el programa cuando se evalúan falsey todas estas aserciones pasan.
CAD97
Ahh, extrañé que eliminaras la declaración del paquete. Ahora funciona. Lo marcaré cuando tenga unos minutos hoy.
Mego
La salida para board.png tiene solo 4 líneas de largo por alguna razón: gist.github.com/Mego/75eccefe555a81bde6022d7eade1424f . De hecho, toda la salida parece estar truncada prematuramente cuando la ejecuto, con la excepción del logotipo PPCG.
Mego
@Mego Creo que tiene que ver con la altura de la fuente (24 px según el informe FontMetrics). Cambié el bucle de línea para que erre en el lado de una línea demasiadas en lugar de una muy pocas, y debería funcionar ahora. (el tablero es de 5 líneas)
CAD97
Como regla general, este algoritmo lucha con las imágenes más pequeñas, ya que (piensa) que todos los caracteres tienen 6px de ancho y 24px de alto, y todo lo que se ve es cuántos píxeles están activados en ese superpíxel.
CAD97