Генерация ASCII Art


14

Учитывая в качестве входного сигнала черно-белое изображение в любом приемлемом формате без потерь , выведите изображение ASCII, максимально приближенное к входному изображению.

правила

  • Можно использовать только перевод строки и байты ASCII 32-127.
  • Исходное изображение будет обрезано, чтобы вокруг изображения не было посторонних пробелов.
  • Материалы должны быть в состоянии завершить весь сборник менее чем за 5 минут.
  • Только сырой текст приемлем; без форматированных текстовых форматов.
  • Шрифт, используемый при подсчете очков, - 20-пт Linux Libertine .
  • Выходной текстовый файл при преобразовании в изображение, как описано ниже, должен иметь те же размеры, что и входное изображение, в пределах 30 пикселей в любом измерении.

счет

Эти изображения будут использованы для оценки:

Вы можете скачать zip-файл с изображениями здесь .

Материалы не должны быть оптимизированы для этого корпуса; скорее, они должны работать для любых 8 черно-белых изображений аналогичных размеров. Я оставляю за собой право изменять изображения в корпусе, если я подозреваю, что представления оптимизируются для этих конкретных изображений.

Оценка будет выполняться с помощью этого сценария:

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

Процесс подсчета очков:

  1. Запустите отправку для каждого изображения корпуса, выводя результаты в .txtфайлы с тем же основанием, что и файл корпуса (выполняется вручную).
  2. Конвертируйте каждый текстовый файл в изображение PNG, используя 20-точечный шрифт, обрезая пробелы.
  3. Измените размер полученного изображения до размеров исходного изображения, используя повторную выборку Ланцоша.
  4. Сравните каждое текстовое изображение с исходным изображением, используя dssim.
  5. Выведите оценку dssim для каждого текстового файла.
  6. Выведите средний балл.

Структурное сходство (показатель, по которому dssimвычисляются баллы) - это показатель, основанный на человеческом зрении и идентификации объекта на изображениях. Проще говоря: если два изображения похожи на людей, у них (вероятно) будет низкий балл dssim.

Победившим представлением будет представление с самым низким средним баллом.

Связанный


6
«Черное и белое» как в «ноль / один» или сколько уровней серого?
Луис Мендо

2
@DonMuesli 0 и 1.
Мего

Не могли бы вы уточнить, что вы подразумеваете под «Вывод результатов в .txtфайлы»? Должна ли программа выводить текст, который будет передан в файл, или мы должны вывести файл напрямую?
DanTheMan

@DanTheMan Либо приемлемо. Если вы выводите в STDOUT, то для оценки нужно будет перенаправить вывод в файл.
Мего

Разве вы не должны указывать ограничения разрешения? В противном случае мы могли бы создать, скажем, изображение размером 10000 на 10000 символов, которое при уменьшении масштабирования будет очень близко соответствовать исходным изображениям, а отдельные символы будут неразборчивыми точками. Размер шрифта не имеет значения, если изображение будет огромным.
DavidC

Ответы:


6

Java, оценка 0,57058675

На самом деле я впервые делаю манипуляции с изображениями, так что это немного неловко, но я думаю, что все в порядке.

Я не мог заставить dssim работать на моей машине, но я мог делать изображения, используя PIL.

Интересно, что шрифт говорит мне в Java, что каждый из символов, которые я использую, имеет ширину 6. Вы можете видеть , что в моей программе FontMetrics::charWidthесть 6для всех символов , которые я использовал. {}Логотип выглядит довольно прилично в моноширинный шрифт. Но по какой-то причине строки на самом деле не выстраиваются в полный текстовый файл. Я обвиняю лигатуры. (И да, я должен использовать правильный шрифт.)

Моноширинным шрифтом:

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

После запуска через инструмент изображения:

{} логотип

Во всяком случае, вот фактический код.

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

}

Обобщение:

  • Убедитесь , что у вас есть JDK установлен
  • Убедитесь, что корзина JDK находится в вашем PATH (для меня это C:\Program Files\Java\jdk1.8.0_91\bin)
  • Сохраните файл как AsciiArt.java
  • javac AsciiArt.java
  • jar cvfe WhateverNameYouWant.jar AsciiArt AsciiArt.class

Использование: java -jar WhateverNameYouWant.jar C:\full\file\path.png печать на STDOUT

ТРЕБУЕТ исходный файл для сохранения с глубиной 1 бит и образец для белого пикселя, который будет 1 .

Оценка результата:

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

1
Запустите с, -eaчтобы включить утверждения. Это не изменит поведение (за исключением, может быть, небольшого замедления), потому что утверждения работают из-за сбоя программы, когда они выполняют оценку falseи все эти утверждения проходят.
CAD97

Ааа, я пропустил, что вы удалили декларацию пакета. Это работает сейчас. Я забью это, когда у меня будет несколько минут сегодня.
Мего

По некоторым причинам выходная информация для board.png составляет всего 4 строки: gist.github.com/Mego/75eccefe555a81bde6022d7eade1424f . На самом деле, весь вывод кажется преждевременно урезанным, когда я запускаю его, за исключением логотипа PPCG.
Мего

@Mego Я думаю, что это связано с высотой шрифта (24 px по отчету FontMetrics). Я изменил цикл строки, чтобы он ошибался на стороне одной слишком многих строк, а не одной слишком маленькой, и теперь это должно работать. (доска 5 строк)
CAD97

Как правило, этот алгоритм борется с изображениями меньшего размера, поскольку (он думает), что все символы имеют ширину 6 пикселей и высоту 24 пикселя, и все, на что он смотрит, это то, сколько пикселей включено в этом суперпикселе.
CAD97
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.