Générer de l'art ASCII

14

Étant donné une image en noir et blanc dans n'importe quel format raisonnable sans perte en entrée, affichez une image ASCII aussi proche que possible de l'image en entrée.

Règles

  • Seuls les sauts de ligne et les octets ASCII 32-127 peuvent être utilisés.
  • L'image d'entrée sera rognée de sorte qu'il n'y ait pas d'espace blanc étranger entourant l'image.
  • Les soumissions doivent être en mesure de compléter l'ensemble du corpus de notation en moins de 5 minutes.
  • Seul le texte brut est acceptable; aucun format de texte riche.
  • La police utilisée dans la notation est Linux Libertine 20 points .
  • Le fichier texte de sortie, lorsqu'il est converti en une image comme décrit ci-dessous, doit avoir les mêmes dimensions que l'image d'entrée, dans les 30 pixels dans l'une ou l'autre dimension.

Notation

Ces images seront utilisées pour la notation:

Vous pouvez télécharger un fichier zip des images ici .

Les soumissions ne doivent pas être optimisées pour ce corpus; ils devraient plutôt fonctionner pour 8 images en noir et blanc de dimensions similaires. Je me réserve le droit de modifier les images du corpus si je soupçonne que les soumissions sont optimisées pour ces images spécifiques.

La notation sera effectuée via ce 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)))

Le processus de notation:

  1. Exécutez la soumission pour chaque image de corpus, en sortant les résultats dans des .txtfichiers avec la même racine que le fichier de corpus (fait manuellement).
  2. Convertissez chaque fichier texte en une image PNG, en utilisant une police de 20 points, en coupant les espaces.
  3. Redimensionnez l'image résultante aux dimensions de l'image d'origine à l'aide du rééchantillonnage Lanczos.
  4. Comparez chaque image de texte avec l'image d'origine à l'aide de dssim.
  5. Générez le score dssim pour chaque fichier texte.
  6. Afficher le score moyen.

La similitude structurelle (la métrique par laquelle dssimcalcule les scores) est une métrique basée sur la vision humaine et l'identification des objets dans les images. Pour le dire clairement: si deux images ressemblent à des humains, elles auront (probablement) un score faible de dssim.

La soumission gagnante sera la soumission avec le score moyen le plus bas.

en relation

Mego
la source
6
"Noir et blanc" comme dans "zéro / un" ou combien de niveaux de gris?
Luis Mendo
2
@DonMuesli 0 et 1.
Mego
Pourriez-vous clarifier ce que vous entendez par «exporter les résultats dans des .txtfichiers»? Le programme devrait-il afficher le texte qui sera acheminé vers un fichier ou devrions-nous produire un fichier directement?
DanTheMan
@DanTheMan Soit est acceptable. Si vous effectuez une sortie vers STDOUT, la sortie devra cependant être redirigée dans un fichier à des fins de notation.
Mego
Ne devez-vous pas spécifier des contraintes de résolution? Sinon, nous pourrions produire, disons, une image de 10000 par 10000 caractères qui, lorsque réduite, correspondrait assez étroitement aux images originales, et les caractères individuels seraient des points illisibles. La taille de la police n'a pas d'importance si l'image de sortie est énorme.
DavidC

Réponses:

6

Java, score 0,57058675

C'est en fait ma première fois que je fais de la manipulation d'images, donc c'est un peu gênant, mais je pense que ça s'est bien passé.

Je n'ai pas pu faire fonctionner dssim sur ma machine, mais j'ai pu créer des images à l'aide de PIL.

Fait intéressant, la police me dit en Java que chacun des caractères que j'utilise est de largeur 6 . Vous pouvez voir que dans mon programme FontMetrics::charWidthest 6pour tous les personnages que j'ai utilisés. Le {}logo semble assez décent dans une police monospace. Mais pour une raison quelconque, les lignes ne s'alignent pas réellement dans le fichier de texte intégral. Je blâme les ligatures. (Et oui, je devrais utiliser la bonne police.)

En police à espacement fixe:

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

Après l'avoir exécuté via l'outil d'image:

{} logo

Quoi qu'il en soit, voici le code réel.

//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);
    }

}

Compiler:

  • Assurez-vous d'avoir JDK est installé
  • Assurez-vous que le bac JDK est sur votre PATH (pour moi, c'est C:\Program Files\Java\jdk1.8.0_91\bin )
  • Enregistrez le fichier sous AsciiArt.java
  • javac AsciiArt.java
  • jar cvfe WhateverNameYouWant.jar AsciiArt AsciiArt.class

Usage: java -jar WhateverNameYouWant.jar C:\full\file\path.png :, imprime sur STDOUT

EXIGE le fichier source à enregistrer avec une profondeur de 1 bit et l'échantillon pour un pixel blanc à 1 .

Sortie de notation:

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
la source
1
Exécutez avec -eapour activer les assertions. Cela ne changera pas le comportement (sauf peut-être le ralentir un peu) parce que les assertions fonctionnent en échouant le programme lors de leur évaluation falseet toutes ces assertions passent.
CAD97
Ahh, j'ai raté que vous ayez supprimé la déclaration de package. Ça fonctionne maintenant. Je marquerai quand j'aurai quelques minutes aujourd'hui.
Mego
La sortie de board.png ne fait que 4 lignes pour une raison quelconque: gist.github.com/Mego/75eccefe555a81bde6022d7eade1424f . En fait, toute la sortie semble être tronquée prématurément lorsque je l'exécute, à l'exception du logo PPCG.
Mego
@Mego, je pense que cela a à voir avec la hauteur de la police (24 px par rapport FontMetrics). J'ai changé la boucle de ligne pour qu'elle s'égare du côté d'une trop de lignes plutôt que d'une trop petite, et cela devrait fonctionner maintenant. (le tableau est composé de 5 lignes)
CAD97
En règle générale, cet algorithme a du mal avec les images plus petites, car (il pense) que tous les personnages ont une largeur de 6 pixels et une hauteur de 24 pixels, et tout ce qu'il regarde est le nombre de pixels activés dans ce super-pixel.
CAD97