comparison img2tiles.py @ 864:40703069bb68

Get everything needed to build menu.bin either committed or buildable via the Makefile
author Michael Pavone <pavone@retrodev.com>
date Thu, 05 Nov 2015 19:35:48 -0800
parents
children dbbf0100f249
comparison
equal deleted inserted replaced
863:b6f42044c8a7 864:40703069bb68
1 #!/usr/bin/env python
2 from PIL import Image
3
4 def gchannel(Val):
5 return (Val >> 4) & 0xE
6
7 threshold = 127
8
9 def get_rgba(im, pixels=None, idx=None, color=None):
10 A = 255
11 if color is None:
12 color = pixels[idx]
13 if type(color) == int:
14 R, G, B = im.getpalette()[color*3:color*3+3]
15 else:
16 if len(color) == 4:
17 R, G, B, A = color
18 elif len(color) == 3:
19 R, G, B = color
20 else:
21 L, A = color
22 R = G = B = L
23 return (R, G, B, A)
24
25 def get_gcolor(im, threshold, pixels=None, idx=None, color=None):
26 R,G,B,A = get_rgba(im, pixels, idx, color)
27 if A > threshold:
28 return (gchannel(R), gchannel(G), gchannel(B))
29 return 0
30
31 def get_color_info(im, pixels, rng, threshold, exclude={}):
32 gencolors = {}
33 A = 255
34 pal = None
35 transparent = False
36 for idx in rng:
37 gcolor = get_gcolor(im, threshold, pixels, idx)
38 if type(gcolor) == tuple:
39 if not gcolor in exclude:
40 if gcolor in gencolors:
41 gencolors[gcolor] += 1
42 else:
43 gencolors[gcolor] = 1
44 else:
45 transparent = True
46 glist = [(gencolors[color], color) for color in gencolors]
47 glist.sort()
48 glist.reverse()
49 return glist
50
51 def get_color_info_both(im, pixels, rng, threshold, exclude={}):
52 gencolors = {}
53 A = 255
54 pal = None
55 transparent = False
56 for idx in rng:
57 gcolor = get_gcolor(im, threshold, pixels, idx)
58 if type(gcolor) == tuple:
59 if not gcolor in exclude:
60 if not gcolor in gencolors:
61 _,best = best_match(gcolor, (exclude,))
62 gencolors[gcolor] = (color_dist(gcolor, best), 1)
63 else:
64 (dist, count) = gencolors[gcolor]
65 gencolors[gcolor] = (dist, count+1)
66 else:
67 transparent = True
68 glist = [(gencolors[color][0] * gencolors[color][1], color) for color in gencolors]
69 glist.sort()
70 glist.reverse()
71
72 return glist
73
74 def make_palette(im, trans_thresh, max_global, max_line):
75 pixels = im.getdata()
76 (width, height) = im.size
77 colors = get_color_info(im, pixels, xrange(0, height * width), trans_thresh)
78 print len(colors), 'distinct 9-bit colors in image'
79 glob_pal = {}
80 print 'Static Palette:'
81 while len(glob_pal) < max_global and len(colors):
82 idx = len(glob_pal)
83 (count, color) = colors[0]
84 print str(idx) + ':', color
85 glob_pal[color] = idx
86 colors = get_color_info_both(im, pixels, xrange(0, height * width), trans_thresh, glob_pal)
87 line_pals = []
88 if max_global < len(colors):
89 for line in xrange(0, height):
90 linestart = line * width
91 if len(glob_pal):
92 linecolors = get_color_info_both(im, pixels, xrange(linestart, linestart+width), trans_thresh, glob_pal)
93 else:
94 linecolors = get_color_info(im, pixels, xrange(linestart, linestart+width), trans_thresh)
95 line_pal = {}
96 while len(line_pal) < max_line and len(linecolors):
97 (score, color) = linecolors[0]
98 line_pal[color] = len(line_pal) + max_global
99 if len(line_pal) < max_line:
100 combo = dict(glob_pal)
101 for color in line_pal:
102 combo[color] = line_pal[color]
103 linecolors = get_color_info_both(im, pixels, xrange(linestart, linestart+width), trans_thresh, combo)
104 #for idx in xrange(0, min(max_line, len(linecolors))):
105 # (count, color) = linecolors[idx]
106 # line_pal[color] = idx + max_global
107 line_pals.append(line_pal)
108 return (glob_pal, line_pals, max_global, max_line)
109
110 def color_dist(a, b):
111 (ra, ga, ba) = a
112 (rb, gb, bb) = b
113 return (ra-rb)**2 + (ga-gb)**2 + (ba-bb)**2
114
115 def best_match(gpixel, pals):
116 bestdist = color_dist((0,0,0), (15, 15, 15))
117 bestpalidx = 0
118 bestcolor = (0,0,0)
119 for i in xrange(0, len(pals)):
120 pal = pals[i]
121 for cur in pal:
122 curdist = color_dist(gpixel, cur)
123 if curdist < bestdist:
124 bestdist = curdist
125 bestpalidx = pal[cur]
126 bestcolor = cur
127 return (bestpalidx, bestcolor)
128
129 def trans_image(im, trans_thresh, pal, chunky):
130 (global_pal, line_pals, _, _) = pal
131 pixels = im.getdata()
132 (width, height) = im.size
133 gpixels = []
134 A = 255
135 pal = None
136 x = 0
137 y = 0
138 numchannels = len(im.getbands())
139 offset = 1 if numchannels == 4 or numchannels == 2 else 0
140 for pixel in pixels:
141 if x == width:
142 x = 0
143 y += 1
144 if width % 8 and not chunky:
145 for i in xrange(0, 8-(width%8)):
146 gpixels.append(0)
147 gpixel = get_gcolor(im, trans_thresh, color=pixel)
148 if type(gpixel) == tuple:
149 if gpixel in global_pal:
150 val = global_pal[gpixel]
151 elif gpixel in line_pals[y]:
152 val = line_pals[y][gpixel]
153 else:
154 bestpal,color = best_match(gpixel, (global_pal, line_pals[y]))
155 val = bestpal
156 gpixels.append(offset+val)
157 else:
158 gpixels.append(gpixel)
159 x += 1
160 if width % 8 and not chunky:
161 for i in xrange(0, 8-(width%8)):
162 gpixels.append(0)
163 width += 8-(width%8)
164 if height % 8 and not chunky:
165 for y in xrange(0, 8-(height%8)):
166 for x in xrange(0, width):
167 gpixels.append(0)
168 height += 8-(height%8)
169
170 return (width, height, gpixels)
171
172 def appendword(b, word):
173 b.append(word >> 8)
174 b.append(word & 0xff)
175
176 def to_tiles(palpix, raw=False, chunky=False, tile_height=8, sprite_order = False):
177 (width, height, pixels) = palpix
178 b = bytearray()
179 if chunky:
180 if not raw:
181 appendword(b, width)
182 appendword(b, height)
183 for pixel in pixels:
184 b.append(pixel)
185 else:
186 cwidth = width/8
187 cheight = height/tile_height
188 words = len(pixels)/4
189 if not raw:
190 appendword(b, words)
191 appendword(b, cwidth)
192 appendword(b, cheight)
193
194 if sprite_order:
195 for cx in xrange(0, cwidth):
196 xstart = cx * 8
197 for cy in xrange(0, cheight):
198 startoff = cy*tile_height*width + xstart
199 for row in xrange(0, tile_height):
200 rowoff = startoff + row*width
201 for bytecol in xrange(0, 4):
202 boff = bytecol * 2 + rowoff
203 #print 'boff:', boff, 'len(pixels)', len(pixels), 'cx', cx, 'cy', cy, 'cwidth', cwidth, 'cheight', cheight
204 #print 'pixels[boff]:', pixels[boff]
205 b.append(pixels[boff] << 4 | pixels[boff+1])
206 else:
207 for cy in xrange(0, cheight):
208 ystart = cy*tile_height*width
209 for cx in xrange(0, cwidth):
210 startoff = (cx*8) + ystart
211 for row in xrange(0, tile_height):
212 rowoff = startoff + row*width
213 for bytecol in xrange(0, 4):
214 boff = bytecol * 2 + rowoff
215 #print 'boff:', boff, 'len(pixels)', len(pixels), 'cx', cx, 'cy', cy, 'cwidth', cwidth, 'cheight', cheight
216 #print 'pixels[boff]:', pixels[boff]
217 b.append(pixels[boff] << 4 | pixels[boff+1])
218 return b
219
220 def add_pal_entries(tiles, pal):
221 (global_pal, line_pals, max_global, max_line) = pal
222 tiles.append(max_global)
223 tiles.append(max_line)
224 pal_list = [(0, 0, 0)] * max_global
225 for entry in global_pal:
226 pal_list[global_pal[entry]] = entry
227 for entry in pal_list:
228 (R, G, B) = entry
229 tiles.append(B)
230 tiles.append(G << 4 | R)
231 for line in line_pals:
232 pal_list = [(0, 0, 0)] * max_line
233 for entry in line:
234 pal_list[line[entry]-max_global] = entry
235 for entry in pal_list:
236 (R, G, B) = entry
237 tiles.append(B)
238 tiles.append(G << 4 | R)
239
240
241
242 def main(argv):
243
244 posargs = []
245 omit_pal = raw = chunky = False
246 expect_option = None
247 options = {}
248 tile_height = 8
249 sprite_order = False
250 for i in xrange(1, len(argv)):
251 if argv[i].startswith('-'):
252 if argv[i] == '-r':
253 raw = True
254 elif argv[i] == '-p':
255 omit_pal = True
256 elif argv[i] == '-o':
257 sprite_order = True
258 elif argv[i] == '-c':
259 chunky = True
260 elif argv[i] == '-i':
261 tile_height = 16
262 elif argv[i] == '-s' or argv[i] == '--spec':
263 expect_option = 'specfile'
264 else:
265 print 'Unrecognized switch', argv[i]
266 return
267 elif not expect_option is None:
268 options[expect_option] = argv[i]
269 expect_option = None
270 else:
271 posargs.append(argv[i])
272 if len(posargs) < 2 and not ('specfile' in options and len(posargs) >= 1):
273 print "Usage: img2tiles.py [OPTIONS] infile outfile [STATIC_COLORS [DYNAMIC_COLORS]]"
274 return
275 if 'specfile' in options:
276 props = open(options['specfile']).read().strip().split(',')
277 fname,static_colors,dynamic_colors = props[0:3]
278 for prop in props[3:]:
279 if prop == 'chunky':
280 chunky = True
281 elif prop == 'raw':
282 raw = True
283 elif prop == 'nopal':
284 omit_pal = True
285 elif prop == 'interlace':
286 tile_height = 16
287 elif prop == 'sprite':
288 sprite_order = True
289 static_colors = int(static_colors)
290 dynamic_colors = int(dynamic_colors)
291 outfile = posargs[0]
292 else:
293 fname = posargs[0]
294 outfile = posargs[1]
295 static_colors = 8
296 dynamic_colors = 8
297 if len(posargs) > 2:
298 static_colors = int(posargs[2])
299 dynamic_colors = 16-static_colors
300 if len(posargs) > 3:
301 dynamic_colors = int(posargs[3])
302 if dynamic_colors + static_colors > 16:
303 print "No more than 16 combined dynamic and static colors are allowed"
304 return
305 im = Image.open(fname)
306 pal = make_palette(im, threshold, static_colors, dynamic_colors)
307 palpix = trans_image(im, threshold, pal, chunky)
308 tiles = to_tiles(palpix, raw, chunky, tile_height, sprite_order)
309 if not omit_pal:
310 bits = add_pal_entries(tiles, pal)
311 out = open(outfile, 'wb')
312 out.write(tiles)
313
314 if __name__ == '__main__':
315 import sys
316 main(sys.argv)