sprite-optimizer/sprite-optimizer.py

224 lines
6.4 KiB
Python
Raw Normal View History

2018-08-18 19:34:41 +02:00
#!/usr/bin/env python3
2018-09-13 20:04:08 +02:00
### Readme ###################################################################
# name: sprite-optimizer
# version: 1.1
# author: Dark-Storm
# license: CeCILL v2.1
#
# comments:
# lines are stored as arrays of pixels
##############################################################################
2018-08-18 19:34:41 +02:00
from argparse import ArgumentParser
from bresenham import bresenham
from PIL import Image, ImageDraw
from random import randint
import sys
from time import time
2018-08-18 19:34:41 +02:00
rand = lambda: randint(0,200)
2018-08-19 12:42:45 +02:00
progress = False
2018-08-18 19:34:41 +02:00
def print_stats(img):
2018-09-13 20:04:08 +02:00
"""Print number and percentage of black pixels"""
2018-08-18 19:34:41 +02:00
pixels = img.getdata()
count = sum([1 for i in pixels if i == 0])
2018-08-19 12:42:45 +02:00
if progress:
print("{} black pixels over {} ({:.1%})".format(count, len(pixels), count/len(pixels)))
2018-08-18 19:34:41 +02:00
def get_lines(img):
2018-09-13 20:04:08 +02:00
"""Generate all potential lines the image contains"""
2019-06-05 00:08:22 +02:00
lines = set()
lines_uniq = set()
pixels = {(x, y) for x in range(img.width) for y in range(img.height) if img.getpixel((x, y)) == 0}
2018-08-18 19:34:41 +02:00
i, n = 0, len(pixels) * len(pixels)
2018-08-19 12:42:45 +02:00
if progress:
print("Get lines:", end = "")
2018-09-13 20:04:08 +02:00
# for each pair of pixels, get the line
2018-08-18 19:34:41 +02:00
for a in pixels:
for b in pixels:
2019-06-05 00:08:22 +02:00
line = tuple(bresenham(*a, *b))
line_uniq = tuple(sorted(line))
2018-09-13 20:04:08 +02:00
# if image contains the line, put it on the array
2019-06-05 00:08:22 +02:00
if set(line).issubset(pixels) and line_uniq not in lines_uniq:
lines.add(line)
lines_uniq.add(line_uniq)
2018-08-18 19:34:41 +02:00
i += 1
2018-08-19 12:42:45 +02:00
if progress:
print("\rGet lines: {:.1%}".format(i / n), end = "")
2018-08-18 19:34:41 +02:00
2018-08-19 12:42:45 +02:00
if progress:
2019-06-05 00:08:22 +02:00
print("\rGet lines: complete")
print("{} lines found".format(len(lines)))
2019-06-05 00:08:22 +02:00
return list(lines)
2018-08-18 19:34:41 +02:00
def removing_useless(lines):
2018-09-13 20:04:08 +02:00
"""Remove lines that are sub-lines of other ones"""
2018-08-18 19:34:41 +02:00
results = []
2018-09-04 21:10:30 +02:00
n = len(lines)
2018-08-18 19:34:41 +02:00
2018-08-19 12:42:45 +02:00
if progress:
print("Remove useless lines:", end = "")
2018-09-13 20:04:08 +02:00
# for each line, see if there is a line that contains every pixel of it
2019-06-05 00:08:22 +02:00
lines_set = [ set(l) for l in lines ]
for i, l in enumerate(lines_set):
2018-08-18 19:34:41 +02:00
inclusions = 0
2018-09-13 20:04:08 +02:00
# others are all lines that are not l
2019-06-05 00:08:22 +02:00
for j, k in enumerate(lines_set):
if i == j: continue
if l.issubset(k):
2018-08-18 19:34:41 +02:00
inclusions += 1
break
2018-09-13 20:04:08 +02:00
# or len(l) == 1 : we keep single pixels to complete the image if necessary
# TODO: do some tests to see if it's worth or not
2018-08-18 19:34:41 +02:00
if inclusions == 0 or len(l) == 1:
2019-06-05 00:08:22 +02:00
results.append((len(l), lines[i], l))
2018-08-19 12:42:45 +02:00
if progress:
2018-09-04 21:10:30 +02:00
print("\rRemove useless lines: {:.1%}".format(i / n), end = "")
2018-08-19 12:42:45 +02:00
if progress:
print("\rRemove useless lines: complete")
2018-08-18 19:34:41 +02:00
2018-08-19 12:42:45 +02:00
if progress:
print("{} lines kept".format(len(results)))
2018-08-18 19:34:41 +02:00
return results
def get_best_solution(img, lines):
2018-09-13 20:04:08 +02:00
"""Compute an optimized solution. The magic part of the algorithm"""
2019-06-05 00:08:22 +02:00
px_left = {(x, y) for x in range(img.width) for y in range(img.height)
if img.getpixel((x, y)) == 0}
2018-08-18 19:34:41 +02:00
results = []
n = len(px_left)
2018-08-19 12:42:45 +02:00
if progress:
print("Draw:", end = "")
2018-09-13 20:04:08 +02:00
# while the entier image has not been drown
2019-06-05 00:08:22 +02:00
while px_left:
2018-09-13 20:04:08 +02:00
# define the length of lines
2019-06-05 00:08:22 +02:00
lines = [(len(l_set.intersection(px_left)) - len(l)/(2*max(img.size)),
l, l_set) for n, l, l_set in lines]
2018-09-13 20:04:08 +02:00
# sort them by length
2018-08-18 19:34:41 +02:00
lines = sorted(lines)
2018-09-13 20:04:08 +02:00
# pop the longest
2019-06-05 00:08:22 +02:00
(p, line, line_set) = lines.pop()
2018-09-13 20:04:08 +02:00
# define the pixels that are not covered by any lines
2019-06-05 00:08:22 +02:00
px_left = px_left.difference(line_set)
2018-08-18 19:34:41 +02:00
results.append((line[0], line[-1]))
2018-08-19 12:42:45 +02:00
if progress:
print("\rDraw: {:.0%}".format(1 - len(px_left)/n), end="")
2018-08-19 12:42:45 +02:00
if progress:
print("\rDraw: complete")
2018-08-18 19:34:41 +02:00
2018-08-19 12:42:45 +02:00
if progress:
print("Solution found in {} lines".format(len(results)))
2018-08-18 19:34:41 +02:00
return results
def generate_code(lines, args):
2018-09-13 20:04:08 +02:00
"""Generate Basic Casio code"""
str_x, str_y = "{", "{"
2018-09-13 20:04:08 +02:00
# Casio's bresenham is reversed compared to classic ones
# so we need to reverse the ends of the lines
for (x_end, y_end), (x_start, y_start) in lines:
x_start, y_start = x_start + int(args.offset[0]), y_start + int(args.offset[1])
x_end, y_end = x_end + int(args.offset[0]), y_end + int(args.offset[1])
str_x += "{}, ".format(get_coord(x_start, x_end))
str_y += "{}, ".format(get_coord(y_start, y_end))
str_x = str_x[:-2] + "}"
str_y = str_y[:-2] + "}"
2018-08-19 13:22:01 +02:00
code = "Graph(X,Y)=({}, {})".format(str_x, str_y)
return code
2018-08-18 19:34:41 +02:00
# From Zezeombye's BIDE
def get_coord(start, end):
2018-09-13 20:04:08 +02:00
"""Convert a pair of coordonates to the appropriate x+yT Multi DrawStat output"""
result = "";
if start != 0:
result += str(start)
delta = end - start
if delta != 0:
if delta > 0 and start != 0:
result += "+";
if delta < 0:
result += "-";
if delta != 1 and delta != -1:
result += str(abs(delta))
result += "T";
if len(result) == 0:
result = "0"
return result;
2018-08-18 19:34:41 +02:00
if __name__ == "__main__":
start = time()
2018-08-18 19:34:41 +02:00
parser = ArgumentParser(description='Generate the Multi DrawStat code for an image.')
parser.add_argument('path', type=str, help='path of the image to process')
parser.add_argument('-d', '--draw', action='store_true', help='draw the result into a new file')
2018-08-29 23:11:55 +02:00
parser.add_argument('-e', '--export', nargs=1, help='export the code into <EXPORT>')
parser.add_argument('-f', '--flip', action='store_true', help='flip image vertically (for inverted ViewWindow)')
parser.add_argument('-i', '--info', action='store_true', help='print informative stats')
parser.add_argument('-o', '--offset', nargs=2, default=(0, 0), help='offset for viewwindow. Default: (0, 0)')
2018-08-19 12:42:45 +02:00
parser.add_argument('-p', '--progress', action='store_true', help='print progress info')
parser.add_argument('-s', '--show', action='store_true', help='show the result')
2018-08-18 19:34:41 +02:00
args = parser.parse_args()
2018-08-19 12:42:45 +02:00
progress = args.progress
2018-08-18 19:34:41 +02:00
try:
image = Image.open(args.path)
except:
sys.exit("Error! Unable to open file.")
try:
image = image.convert('1')
except:
sys.exit("Error! Unable to convert to 1bit")
if args.flip:
image = image.transpose(Image.FLIP_TOP_BOTTOM)
2018-08-19 12:42:45 +02:00
if args.info:
print_stats(image)
2018-08-18 19:34:41 +02:00
lines = get_lines(image)
lines = removing_useless(lines)
lines = get_best_solution(image, lines)
code = generate_code(lines, args)
2018-08-18 19:34:41 +02:00
if args.draw or args.show:
2018-08-18 19:34:41 +02:00
export = Image.new('RGB', image.size, 'white')
drawer = ImageDraw.Draw(export)
for ((a, b), (c, d)) in reversed(lines):
2018-08-18 19:34:41 +02:00
drawer.line((a, b, c, d), fill=(rand(), rand(), rand()))
export = export.resize((export.width * 8, export.height * 8))
if args.draw:
export.save(args.path[:-4] + "_gen.png")
if args.show:
export.show()
2018-08-18 19:34:41 +02:00
2018-08-19 12:42:45 +02:00
if args.info:
print("{} processed in {} lines ({:.3}s)".format(args.path, len(lines), time() - start))
2018-08-29 23:11:55 +02:00
if args.export:
try:
f = open(args.export[0], 'w')
f.write(code)
f.close()
print("Code saved into", args.export[0])
except Exception as e:
2018-08-29 23:14:54 +02:00
sys.exit("Error: " + str(e))
print(code)