Given a black-and-white image in any reasonable lossless format as input, output ASCII art that is as close to the input image as possible.
Rules
- Only linefeeds and ASCII bytes 32-127 may be used.
- The input image will be cropped so that there is no extraneous whitespace surrounding the image.
- Submissions must be able to complete the entire scoring corpus in under 5 minutes.
- Only raw text is acceptable; no rich text formats.
- The font used in the scoring is 20-pt Linux Libertine.
- The output text file, when converted to an image as described below, must be the same dimensions as the input image, within 30 pixels in either dimension.
Scoring
These images will be used for scoring:
You can download a zipfile of the images here.
Submissions should not be optimized for this corpus; rather, they should work for any 8 black-and-white images of similar dimensions. I reserve the right to change the images in the corpus if I suspect submissions are being optimized for these specific images.
The scoring will be performed via this 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)))
The scoring process:
- Run the submission for each corpus image, outputting the results to
.txt
files with the same stem as the corpus file (done manually). - Convert each text file to a PNG image, using 20-point font, cropping out whitespace.
- Resize the result image to the dimensions of the original image using Lanczos resampling.
- Compare each text image with the original image using
dssim
. - Output the dssim score for each text file.
- Output the average score.
Structural Similarity (the metric by which dssim
calculates scores) is a metric based on human vision and object identification in images. To put it plainly: if two images look similar to humans, they will (probably) have a low score from dssim
.
The winning submission will be the submission with the lowest average score.
.txt
files"? Should the program output text that will get piped to a file or should we output a file directly? \$\endgroup\$