Mercurial > repos > blastem
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) |