#!/usr/bin/env python3 ### Readme ################################################################### # name: sprite-optimizer # version: 1.1 # author: Dark-Storm # license: CeCILL v2.1 # # comments: # lines are stored as arrays of pixels ############################################################################## from argparse import ArgumentParser from bresenham import bresenham from PIL import Image, ImageDraw from random import randint import sys from time import time rand = lambda: randint(0,200) progress = False def print_stats(img): """Print number and percentage of black pixels""" pixels = img.getdata() count = sum([1 for i in pixels if i == 0]) if progress: print("{} black pixels over {} ({:.1%})".format(count, len(pixels), count/len(pixels))) def get_lines(img): """Generate all potential lines the image contains""" lines = [] pixels = [(x, y) for x in range(img.width) for y in range(img.height) if img.getpixel((x, y)) == 0] i, n = 0, len(pixels) * len(pixels) if progress: print("Get lines:", end = "") # for each pair of pixels, get the line for a in pixels: for b in pixels: line = list(bresenham(a[0], a[1], b[0], b[1])) is_in = True for p in line: if p not in pixels: is_in = False break # if image contains the line, put it on the array if is_in: lines.append(line) i += 1 if progress: print("\rGet lines: {:.1%}".format(i / n), end = "") if progress: print("\rGet lines: complete") if progress: print("{} lines found".format(len(lines))) return lines def removing_doubles(lines): """Remove lines that are symetric""" results = [] n = len(lines) if progress: print("Remove duplicated lines:", end = "") # for each line, see if it's already in the output array # beware not to change the orientation of the line (bresenham is not symetric) # TODO: optimize a bit this operation for i, l in enumerate(lines): s = sorted(l) same = False for o in results: if sorted(o) == s: same = True break if same == False: results.append(l) if progress: print("\rRemove double lines: {:.1%}".format(i / n), end = "") if progress: print("\rRemove double lines: complete") if progress: print("{} lines kept".format(len(results))) return results def removing_useless(lines): """Remove lines that are sub-lines of other ones""" results = [] n = len(lines) if progress: print("Remove useless lines:", end = "") # for each line, see if there is a line that contains every pixel of it for i, l in enumerate(lines): inclusions = 0 # others are all lines that are not l others = (x for x in lines if x != l) for k in others: if len(list(set(l).intersection(set(k)))) == len(l): inclusions += 1 break # 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 if inclusions == 0 or len(l) == 1: results.append((len(l), l)) if progress: print("\rRemove useless lines: {:.1%}".format(i / n), end = "") if progress: print("\rRemove useless lines: complete") if progress: print("{} lines kept".format(len(results))) return results def get_best_solution(img, lines): """Compute an optimized solution. The magic part of the algorithm""" px_left = [(x, y) for x in range(img.width) for y in range(img.height) if img.getpixel((x, y)) == 0] results = [] n = len(px_left) if progress: print("Draw:", end = "") # while the entier image has not been drown while len(px_left): # define the length of lines lines = [(sum([1 for p in l if p in px_left]) - len(l)/(2*max(img.size)), l) for n, l in lines] # sort them by length lines = sorted(lines) # pop the longest (p, line) = lines.pop() # define the pixels that are not covered by any lines px_left = [p for p in px_left if p not in line] results.append((line[0], line[-1])) if progress: print("\rDraw: {:.0%}".format(1 - len(px_left)/n), end="") if progress: print("\rDraw: complete") if progress: print("Solution found in {} lines".format(len(results))) return results def generate_code(lines, args): """Generate Basic Casio code""" str_x, str_y = "{", "{" # 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] + "}" code = "Graph(X,Y)=({}, {})".format(str_x, str_y) return code # From Zezeombye's BIDE def get_coord(start, end): """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; if __name__ == "__main__": start = time() 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') parser.add_argument('-e', '--export', nargs=1, help='export the code into ') 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)') parser.add_argument('-p', '--progress', action='store_true', help='print progress info') parser.add_argument('-s', '--show', action='store_true', help='show the result') args = parser.parse_args() progress = args.progress 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) if args.info: print_stats(image) lines = get_lines(image) lines = removing_doubles(lines) lines = removing_useless(lines) lines = get_best_solution(image, lines) code = generate_code(lines, args) if args.draw or args.show: export = Image.new('RGB', image.size, 'white') drawer = ImageDraw.Draw(export) for ((a, b), (c, d)) in reversed(lines): 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() if args.info: print("{} processed in {} lines ({:.3}s)".format(args.path, len(lines), time() - start)) 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: sys.exit("Error: " + str(e)) print(code)