hacks/painter/painter.py

211 lines
5.7 KiB
Python

"""Converts a bitmap image into a minimal number of covering
rectangles.
Mainly plays with different methods.
"""
# python painter.py images/*.png
from __future__ import print_function
import sys
import collections
import time
import numpy
import scipy.misc
# A rectangle.
Rect = collections.namedtuple('Rect', 'x y w h')
# Chracters to use when printing the image to the screen.
EXT = range(ord('1'), ord('9') + 1) \
+ range(ord('a'), ord('z') + 1) \
+ range(ord('A'), ord('Z') + 1)
CHARS = [ord('.'), ord('#')] + EXT*20
def parse(filename):
"""Parse a image into a (0, 1) numpy array."""
img = scipy.misc.imread(filename)
img = numpy.clip(img, 0, 1)
return img ^ 1
def render(shape, rects, with_ids=False):
"""Render rectangles into a new array of the given shape."""
img = numpy.zeros(shape, numpy.uint16)
for i, rect in enumerate(rects):
for y in range(rect.y, rect.y + rect.h):
for x in range(rect.x, rect.x + rect.w):
if with_ids:
img[y][x] = i + 2
else:
img[y][x] = 1
return img
def show(img):
"""Print an image to the console."""
for row in img:
print(''.join(chr(CHARS[x]) for x in row))
def pointwise(img):
"""Method where each point is a rectangle."""
for y, row in enumerate(img):
for x, pix in enumerate(row):
if pix:
yield Rect(x, y, 1, 1)
def rowwise(img):
"""Method where rectangles are one row high."""
for y, row in enumerate(img):
start = None
for x, pix in enumerate(row):
if pix:
if start is None:
start = x
else:
if start is not None:
yield Rect(start, y, x - start, 1)
start = None
if start is not None:
yield Rect(start, y, x - start + 1, 1)
def rotate(img, fun):
"""Rotate an image and rotate the final rectangles."""
h, w = img.shape
for rect in fun(numpy.rot90(img)):
yield Rect(w - rect.y - 1, rect.x, rect.h, rect.w)
def colwise(img):
"""Method where rectangles are one column wide."""
return list(rotate(img, rowwise))
def mergeup(rects):
"""Merge a rectangle into the identical width one below it."""
# TODO(michaelh): ugly.
while True:
out = []
merged = set()
rects.sort(key=lambda x: x.y)
for rect in rects:
if rect in merged:
continue
top = rect.y + rect.h
below = [x for x in rects if x.y == top]
matches = [x for x in below if x.x == rect.x and x.w == rect.w]
if matches:
match = matches[0]
merged.add(match)
out.append(Rect(rect.x, rect.y, rect.w, rect.h + match.h))
else:
out.append(rect)
if not merged:
return out
rects = out
def mergeleft(rects):
"""Merge a rectangle into the identical height one beside it."""
# TODO(michaelh): merge with mergeup().
while True:
out = []
merged = set()
rects.sort(key=lambda x: x.x)
for rect in rects:
if rect in merged:
continue
edge = rect.x + rect.w
adjacent = [x for x in rects if x.x == edge]
matches = [x for x in adjacent if x.y == rect.y and x.h == rect.h]
if matches:
match = matches[0]
merged.add(match)
out.append(Rect(rect.x, rect.y, rect.w + match.w, rect.h))
else:
out.append(rect)
if not merged:
return out
rects = out
def rowcolwise(img):
"""Method that picks the best of rowwise and colwise."""
rows = list(rowwise(img))
cols = list(colwise(img))
return rows if len(rows) < len(cols) else cols
def mergerowcolwise(img):
"""Method that picks the best of the merged rowwise and colwise."""
rows = mergeup(list(rowwise(img)))
cols = mergeleft(list(colwise(img)))
return rows if len(rows) < len(cols) else cols
def bothwise(img):
"""Method that combines row and col wise, then drops rectangles
that are completly covered by larger rectangles.
"""
rows = list(rowwise(img))
cols = list(colwise(img))
rects = rows + cols
# Sort by area. Smaller are less likely to contribute.
rects.sort(key=lambda x: x.w*x.h)
drop = set()
for rect in rects:
others = [x for x in rects if x != rect and x not in drop]
got = render(img.shape, others)
if not (got ^ img).any():
# Dropping this rect didn't change the image.
drop.add(rect)
rects = [x for x in rects if x not in drop]
return mergeleft(mergeup(rects))
def main():
# All methods to try.
methods = [
pointwise,
rowwise,
colwise,
rowcolwise,
mergerowcolwise,
bothwise,
]
for filename in sys.argv[1:]:
img = parse(filename)
best = None
print('{}:'.format(filename))
for method in methods:
start = time.clock()
scan = list(method(img))
elapsed = time.clock() - start
name = method.__name__
print(' {} -> {} rects in {:.3f} s'.format(name, len(scan), elapsed))
rendered = render(img.shape, scan)
diff = img ^ rendered
if diff.any():
print('Render error!')
else:
if best is None or len(scan) <= len(best):
best = scan
# Show the best.
show(render(img.shape, best, True))
if __name__ == '__main__':
main()