Wygeneruj ASCII art

14

Biorąc pod uwagę czarno-biały obraz w dowolnym rozsądnym formacie bezstratnym jako dane wejściowe, należy wyprowadzić grafikę ASCII możliwie najbliższą obrazowi wejściowemu.

Zasady

  • Można stosować tylko sygnały liniowe i bajty ASCII 32-127.
  • Obraz wejściowy zostanie przycięty, aby wokół obrazu nie było żadnych białych znaków.
  • Zgłoszenia muszą być w stanie wypełnić cały korpus punktacji w czasie krótszym niż 5 minut.
  • Akceptowany jest tylko surowy tekst; brak formatów tekstu sformatowanego.
  • Czcionka użyta w punktacji to 20-pkt Linux Libertine .
  • Wyjściowy plik tekstowy po przekonwertowaniu na obraz, jak opisano poniżej, musi mieć takie same wymiary jak obraz wejściowy, w granicach 30 pikseli w każdym z wymiarów.

Punktacja

Te obrazy zostaną wykorzystane do oceny:

Możesz pobrać plik zip z obrazkami tutaj .

Zgłoszenia nie powinny być optymalizowane dla tego korpusu; powinny raczej działać na dowolne 8 czarno-białych obrazów o podobnych wymiarach. Zastrzegam sobie prawo do zmiany obrazów w korpusie, jeśli podejrzewam, że zgłoszenia są optymalizowane dla tych konkretnych obrazów.

Punktacja zostanie przeprowadzona za pomocą tego skryptu:

#!/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)))

Proces punktacji:

  1. Uruchom przesyłanie dla każdego obrazu korpusu, wysyłając wyniki do .txtplików o tym samym rdzeniu co plik korpusu (wykonywane ręcznie).
  2. Konwertuj każdy plik tekstowy na obraz PNG, używając 20-punktowej czcionki, usuwając spacje.
  3. Zmień rozmiar obrazu wynikowego do wymiarów oryginalnego obrazu za pomocą próbkowania Lanczosa.
  4. Porównaj każdy obraz tekstowy z oryginalnym obrazem, używając dssim.
  5. Wyjście wyniku dssim dla każdego pliku tekstowego.
  6. Podaj średni wynik.

Podobieństwo strukturalne (metryka, za pomocą której dssimobliczane są wyniki) to metryka oparta na wizji człowieka i identyfikacji obiektu na obrazach. Mówiąc wprost: jeśli dwa obrazy wyglądają podobnie do ludzi, (prawdopodobnie) będą miały niski wynik od dssim.

Zwycięskie zgłoszenie będzie zgłoszeniem o najniższym średnim wyniku.

związane z

Mego
źródło
6
„Czarno-biały” jak w „zero / jeden” lub ile poziomów szarości?
Luis Mendo,
2
@DonMuesli 0 i 1.
Mego
Czy możesz wyjaśnić, co masz na myśli przez „zapisywanie wyników w .txtplikach”? Czy program powinien wypisywać tekst, który zostanie przesłany potokiem do pliku, czy też powinniśmy wydrukować plik bezpośrednio?
DanTheMan
@DanTheMan Either jest do przyjęcia. Jeśli jednak wyprowadzasz dane do STDOUT, dane wyjściowe będą musiały zostać przekierowane do pliku w celu oceny.
Mego
Czy nie powinieneś określać ograniczeń rozdzielczości? W przeciwnym razie moglibyśmy stworzyć, powiedzmy, obraz o wielkości 10000 na 10000 znaków, który, po zmniejszeniu, dość ściśle pasowałby do oryginalnych obrazów, a poszczególne znaki byłyby nieczytelnymi kropkami. Rozmiar czcionki nie ma znaczenia, jeśli obraz wyjściowy jest ogromny.
DavidC

Odpowiedzi:

6

Java, wynik 0,57058675

To właściwie moja pierwsza operacja na obrazie, więc jest to trochę niezręczne, ale myślę, że wyszło.

Nie mogłem zmusić dssim do pracy na moim komputerze, ale mogłem tworzyć obrazy przy użyciu PIL.

Co ciekawe, czcionka mówi mi w Javie, że każdy ze znaków, których używam, ma szerokość 6. Widać, że w moim programie FontMetrics::charWidthjest 6dla wszystkich znaków, których użyłem. {}Logo wygląda całkiem przyzwoity w czcionce o stałej szerokości. Ale z jakiegoś powodu wiersze tak naprawdę nie pokrywają się w pełnym pliku tekstowym. Obwiniam ligatury. (I tak, powinienem używać poprawnej czcionki.)

W czcionce o stałej szerokości:

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

Po uruchomieniu go za pomocą narzędzia obrazu:

Logo {}

Tak czy inaczej, oto aktualny kod.

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

}

Skompilować:

  • Upewnij się, że masz zainstalowany JDK
  • Upewnij się, że bin JDK jest na twojej ŚCIEŻCE (dla mnie to C:\Program Files\Java\jdk1.8.0_91\bin)
  • Zapisz plik jako AsciiArt.java
  • javac AsciiArt.java
  • jar cvfe WhateverNameYouWant.jar AsciiArt AsciiArt.class

Użycie java -jar WhateverNameYouWant.jar C:\full\file\path.png:, drukuje do STDOUT

WYMAGA pliku źródłowego do zapisania z głębokością 1-bitową i próbki dla białego piksela 1.

Wynik punktacji:

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
źródło
1
Uruchom z, -eaaby włączyć asercje. Nie zmieni to zachowania (z wyjątkiem być może spowolnienia go w niewielkim stopniu), ponieważ asercje działają przez to, że zawiodły program, gdy oceniają falsei wszystkie te asercje miną.
97 CAD
Ahh, tęskniłem za usunięciem deklaracji paczki. Teraz działa. Zdobędę go, gdy zdobędę dziś kilka minut.
Mego
Dane wyjściowe dla board.png mają z jakiegoś powodu tylko 4 linie: gist.github.com/Mego/75eccefe555a81bde6022d7eade1424f . W rzeczywistości wszystkie wyniki wydają się przedwcześnie obcięte, gdy je uruchamiam, z wyjątkiem logo PPCG.
Mego
@Mego Myślę, że ma to związek z wysokością czcionki (24 px według raportu FontMetrics). Zmieniłem pętlę linii, więc popsuła się ona o jedną za dużo linii zamiast o jedną za mało i powinna działać teraz. (tablica ma 5 linii)
97
Algorytm zmaga się z mniejszymi obrazami, ponieważ (jak się wydaje) wszystkie postacie mają szerokość 6 pikseli i wysokość 24 pikseli, a wszystko, na co patrzy, to ile pikseli jest włączonych w tym superpikselu.
97 CAD