Un générateur de multi-drawstat à partir d'une image. L'un des plus optimisés à ce jour.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

sprite-optimizer.py 6.8KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240
  1. #!/usr/bin/env python3
  2. ### Readme ###################################################################
  3. # name: sprite-optimizer
  4. # version: 1.1
  5. # author: Dark-Storm
  6. # license: CeCILL v2.1
  7. #
  8. # comments:
  9. # lines are stored as arrays of pixels
  10. ##############################################################################
  11. from argparse import ArgumentParser
  12. from bresenham import bresenham
  13. from PIL import Image, ImageDraw
  14. from random import randint
  15. import sys
  16. from time import time
  17. rand = lambda: randint(0,200)
  18. progress = False
  19. def print_stats(img):
  20. """Print number and percentage of black pixels"""
  21. pixels = img.getdata()
  22. count = sum([1 for i in pixels if i == 0])
  23. if progress:
  24. print("{} black pixels over {} ({:.1%})".format(count, len(pixels), count/len(pixels)))
  25. def get_lines(img):
  26. """Generate all potential lines the image contains"""
  27. lines = set()
  28. lines_uniq = set()
  29. pixels = {(x, y) for x in range(img.width) for y in range(img.height) if img.getpixel((x, y)) == 0}
  30. i, n = 0, len(pixels) * len(pixels)
  31. if progress:
  32. print("Get lines:", end = "")
  33. # for each pair of pixels, get the line
  34. for a in pixels:
  35. for b in pixels:
  36. line = tuple(bresenham(*a, *b))
  37. line_uniq = tuple(sorted(line))
  38. # if image contains the line, put it on the array
  39. if set(line).issubset(pixels) and line_uniq not in lines_uniq:
  40. lines.add(line)
  41. lines_uniq.add(line_uniq)
  42. i += 1
  43. if progress:
  44. print("\rGet lines: {:.1%}".format(i / n), end = "")
  45. if progress:
  46. print("\rGet lines: complete")
  47. print("{} lines found".format(len(lines)))
  48. return list(lines)
  49. def removing_useless(lines):
  50. """Remove lines that are sub-lines of other ones"""
  51. results = []
  52. n = len(lines)
  53. if progress:
  54. print("Remove useless lines:", end = "")
  55. # for each line, see if there is a line that contains every pixel of it
  56. lines_set = [ set(l) for l in lines ]
  57. for i, l in enumerate(lines_set):
  58. inclusions = 0
  59. # others are all lines that are not l
  60. for j, k in enumerate(lines_set):
  61. if i == j: continue
  62. if l.issubset(k):
  63. inclusions += 1
  64. break
  65. # or len(l) == 1 : we keep single pixels to complete the image if necessary
  66. # TODO: do some tests to see if it's worth or not
  67. if inclusions == 0 or len(l) == 1:
  68. results.append((len(l), lines[i], l))
  69. if progress:
  70. print("\rRemove useless lines: {:.1%}".format(i / n), end = "")
  71. if progress:
  72. print("\rRemove useless lines: complete")
  73. if progress:
  74. print("{} lines kept".format(len(results)))
  75. return results
  76. def get_best_solution(img, lines):
  77. """Compute an optimized solution. The magic part of the algorithm"""
  78. px_left = {(x, y) for x in range(img.width) for y in range(img.height)
  79. if img.getpixel((x, y)) == 0}
  80. results = []
  81. n = len(px_left)
  82. if progress:
  83. print("Draw:", end = "")
  84. # while the entier image has not been drown
  85. while px_left:
  86. # define the length of lines
  87. lines = [(len(l_set.intersection(px_left)) - len(l)/(2*max(img.size)),
  88. l, l_set) for n, l, l_set in lines]
  89. # sort them by length
  90. lines = sorted(lines)
  91. # pop the longest
  92. (p, line, line_set) = lines.pop()
  93. # define the pixels that are not covered by any lines
  94. px_left = px_left.difference(line_set)
  95. results.append((line[0], line[-1]))
  96. if progress:
  97. print("\rDraw: {:.0%}".format(1 - len(px_left)/n), end="")
  98. if progress:
  99. print("\rDraw: complete")
  100. if progress:
  101. print("Solution found in {} lines".format(len(results)))
  102. return results
  103. def generate_code(lines, args):
  104. """Generate Basic Casio code"""
  105. str_x, str_y = "{", "{"
  106. # Casio's bresenham is reversed compared to classic ones
  107. # so we need to reverse the ends of the lines
  108. for (x_end, y_end), (x_start, y_start) in lines:
  109. x_start, y_start = x_start + int(args.offset[0]), y_start + int(args.offset[1])
  110. x_end, y_end = x_end + int(args.offset[0]), y_end + int(args.offset[1])
  111. str_x += "{}, ".format(get_coord(x_start, x_end))
  112. str_y += "{}, ".format(get_coord(y_start, y_end))
  113. str_x = str_x[:-2] + "}"
  114. str_y = str_y[:-2] + "}"
  115. code = "Graph(X,Y)=({}, {})".format(str_x, str_y)
  116. return code
  117. def generate_raw(lines, args):
  118. """Generate raw line coordinates"""
  119. ox, oy = int(args.offset[0]), int(args.offset[1])
  120. code = ""
  121. for (x_end, y_end), (x_start, y_start) in lines:
  122. code += f"{x_start} {y_start} {x_end} {y_end}\n"
  123. return code
  124. # From Zezeombye's BIDE
  125. def get_coord(start, end):
  126. """Convert a pair of coordonates to the appropriate x+yT Multi DrawStat output"""
  127. result = "";
  128. if start != 0:
  129. result += str(start)
  130. delta = end - start
  131. if delta != 0:
  132. if delta > 0 and start != 0:
  133. result += "+";
  134. if delta < 0:
  135. result += "-";
  136. if delta != 1 and delta != -1:
  137. result += str(abs(delta))
  138. result += "T";
  139. if len(result) == 0:
  140. result = "0"
  141. return result;
  142. if __name__ == "__main__":
  143. start = time()
  144. parser = ArgumentParser(description='Generate the Multi DrawStat code for an image.')
  145. parser.add_argument('path', type=str, help='path of the image to process')
  146. parser.add_argument('-e', '--export', nargs=1, help='export the code into <EXPORT>')
  147. parser.add_argument('-o', '--offset', nargs=2, default=(0, 0), help='offset for viewwindow. Default: (0, 0)')
  148. parser.add_argument('-f', '--flip', action='store_true', help='flip image vertically (for inverted ViewWindow)')
  149. parser.add_argument('-i', '--info', action='store_true', help='print informative stats')
  150. parser.add_argument('-p', '--progress', action='store_true', help='print progress info')
  151. parser.add_argument('-s', '--show', action='store_true', help='show the result')
  152. parser.add_argument('-d', '--draw', action='store_true', help='draw the result into a new file')
  153. parser.add_argument('-r', '--raw', action='store_true', help='print raw line coordinates')
  154. args = parser.parse_args()
  155. progress = args.progress
  156. try:
  157. image = Image.open(args.path)
  158. except:
  159. sys.exit("Error! Unable to open file.")
  160. try:
  161. image = image.convert('1')
  162. except:
  163. sys.exit("Error! Unable to convert to 1bit")
  164. if args.flip:
  165. image = image.transpose(Image.FLIP_TOP_BOTTOM)
  166. if args.info:
  167. print_stats(image)
  168. lines = get_lines(image)
  169. lines = removing_useless(lines)
  170. lines = get_best_solution(image, lines)
  171. if args.raw:
  172. code = generate_raw(lines, args)
  173. else:
  174. code = generate_code(lines, args)
  175. if args.draw or args.show:
  176. export = Image.new('RGB', image.size, 'white')
  177. drawer = ImageDraw.Draw(export)
  178. for ((a, b), (c, d)) in reversed(lines):
  179. drawer.line((a, b, c, d), fill=(rand(), rand(), rand()))
  180. export = export.resize((export.width * 8, export.height * 8))
  181. if args.draw:
  182. export.save(args.path[:-4] + "_gen.png")
  183. if args.show:
  184. export.show()
  185. if args.info:
  186. print("{} processed in {} lines ({:.3}s)".format(args.path, len(lines), time() - start))
  187. if args.export:
  188. try:
  189. f = open(args.export[0], 'w')
  190. f.write(code)
  191. f.close()
  192. print("Code saved into", args.export[0])
  193. except Exception as e:
  194. sys.exit("Error: " + str(e))
  195. print(code)