Gerar arte ASCII

14

Dada uma imagem em preto e branco em qualquer formato razoável sem perdas como entrada, produza arte ASCII o mais próximo possível da imagem de entrada.

Regras

  • Somente alimentações de linha e bytes ASCII 32-127 podem ser usados.
  • A imagem de entrada será cortada para que não haja espaços em branco estranhos ao redor da imagem.
  • As submissões devem poder concluir o corpus de pontuação inteiro em menos de 5 minutos.
  • Somente texto bruto é aceitável; sem formatos de texto rico.
  • A fonte usada na pontuação é o Linux Libertine de 20 pontos .
  • O arquivo de texto de saída, quando convertido em uma imagem como descrito abaixo, deve ter as mesmas dimensões que a imagem de entrada, dentro de 30 pixels em qualquer dimensão.

Pontuação

Essas imagens serão usadas para pontuação:

Você pode baixar um arquivo zip das imagens aqui .

As submissões não devem ser otimizadas para este corpus; em vez disso, eles devem funcionar para quaisquer 8 imagens em preto e branco de dimensões semelhantes. Reservo-me o direito de alterar as imagens no corpus se suspeitar que os envios estão sendo otimizados para essas imagens específicas.

A pontuação será realizada através deste 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)))

O processo de pontuação:

  1. Execute o envio para cada imagem de corpus, produzindo os resultados em .txtarquivos com a mesma raiz que o arquivo de corpus (feito manualmente).
  2. Converta cada arquivo de texto em uma imagem PNG, usando uma fonte de 20 pontos, cortando o espaço em branco.
  3. Redimensione a imagem resultante para as dimensões da imagem original usando a reamostragem de Lanczos.
  4. Compare cada imagem de texto com a imagem original usando dssim.
  5. Saída da pontuação dssim para cada arquivo de texto.
  6. Saída a pontuação média.

A similaridade estrutural (a métrica pela qual dssimcalcula as pontuações) é uma métrica baseada na visão humana e na identificação de objetos nas imagens. Em outras palavras: se duas imagens se parecem com seres humanos, elas provavelmente terão uma pontuação baixa dssim.

O envio vencedor será o envio com a menor pontuação média.

relacionado

Mego
fonte
6
"Preto e branco" como em "zero / um" ou quantos níveis de cinza?
Luis Mendo
2
@DonMuesli 0 e 1.
Mego
Você poderia esclarecer o que quer dizer com "Enviando os resultados para os .txtarquivos"? O programa deve produzir texto que será canalizado para um arquivo ou devemos produzir um arquivo diretamente?
precisa
@DanTheMan Qualquer um é aceitável. Se você enviar para STDOUT, a saída precisará ser redirecionada para um arquivo para fins de pontuação.
Mego
Você não deve especificar restrições de resolução? Caso contrário, poderíamos produzir, digamos, uma imagem de 10000 por 10000 caracteres que, quando reduzida, corresponderia muito bem às imagens originais e os caracteres individuais seriam pontos ilegíveis. O tamanho da fonte não importa se a imagem de saída é enorme.
21416

Respostas:

6

Java, pontuação 0.57058675

Esta é realmente a minha primeira vez fazendo manipulação de imagens, por isso é meio estranho, mas acho que ficou bom.

Não consegui que o dssim funcionasse na minha máquina, mas consegui fazer imagens usando o PIL.

Curiosamente, a fonte me diz em Java que cada um dos caracteres que estou usando tem largura 6. Você pode ver que no meu programa FontMetrics::charWidthé 6para todos os caracteres que eu usei. O {}logotipo parece bastante decente em uma fonte monoespaçada. Mas, por algum motivo, as linhas não se alinham no arquivo de texto completo. Eu culpo as ligaduras. (E sim, eu deveria estar usando a fonte correta.)

Em fonte monoespaçada:

                                                                                      .
                         .,: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

Depois de executá-lo através da ferramenta de imagem:

{} logo

Enfim, aqui está o 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:

  • Verifique se você possui o JDK instalado
  • Certifique-se de que a bandeja JDK esteja no seu PATH (para mim é C:\Program Files\Java\jdk1.8.0_91\bin)
  • Salve o arquivo como AsciiArt.java
  • javac AsciiArt.java
  • jar cvfe WhateverNameYouWant.jar AsciiArt AsciiArt.class

Uso java -jar WhateverNameYouWant.jar C:\full\file\path.png:, imprime em STDOUT

SOLICITA que o arquivo de origem seja salvo com profundidade de 1 bit e a amostra de um pixel branco 1.

Pontuação de saída:

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
fonte
1
Execute com -eapara ativar asserções. Isso não mudará o comportamento (exceto, talvez, desacelerar uma pequena quantia) porque as asserções funcionam com falha no programa quando avaliam falsee todas essas asserções passam.
CAD97
Ahh, eu perdi que você removeu a declaração do pacote. Agora está funcionando. Vou pontuar quando chegar alguns minutos hoje.
Mego 23/06
A saída para board.png tem apenas 4 linhas, por algum motivo: gist.github.com/Mego/75eccefe555a81bde6022d7eade1424f . De fato, toda a saída parece estar prematuramente truncada quando a executo, com exceção do logotipo do PPCG.
Mego 23/06
@Mego Eu acho que tem a ver com a altura da fonte (24 px pelo relatório FontMetrics). Alterei o loop da linha para que ele erre ao lado de muitas linhas, em vez de poucas, e deve funcionar agora. (placa é de 5 linhas)
CAD97
Como regra geral, esse algoritmo luta contra as imagens menores, já que (pensa) todos os caracteres têm 6px de largura e 24px de altura, e tudo o que ele vê é quantos pixels estão ativados nesse super-pixel.
CAD97