1 | /* |
---|
2 | * img2oric Convert an image to Oric Atmos colours |
---|
3 | * Copyright (c) 2008 Sam Hocevar <sam@zoy.org> |
---|
4 | * All Rights Reserved |
---|
5 | * |
---|
6 | * $Id$ |
---|
7 | * |
---|
8 | * Changes: |
---|
9 | * Jan 18, 2008: initial release |
---|
10 | * Jan 23, 2008: add support for inverse video on attribute change |
---|
11 | * improve Floyd-Steinberg coefficient values |
---|
12 | * |
---|
13 | * This program is free software. It comes without any warranty, to |
---|
14 | * the extent permitted by applicable law. You can redistribute it |
---|
15 | * and/or modify it under the terms of the Do What The Fuck You Want |
---|
16 | * To Public License, Version 2, as published by Sam Hocevar. See |
---|
17 | * http://sam.zoy.org/wtfpl/COPYING for more details. |
---|
18 | * |
---|
19 | * To build this program on Linux: |
---|
20 | * cc -O3 -funroll-loops -W -Wall img2oric.c -o img2oric \ |
---|
21 | * $(pkg-config --cflags --libs sdl) -lSDL_image -lm |
---|
22 | */ |
---|
23 | |
---|
24 | #include <stdio.h> |
---|
25 | #include <stdlib.h> |
---|
26 | #include <string.h> |
---|
27 | #include <stdlib.h> |
---|
28 | |
---|
29 | #include <math.h> |
---|
30 | |
---|
31 | #include <SDL_image.h> |
---|
32 | |
---|
33 | /* |
---|
34 | * Image dimensions and recursion depth. DEPTH = 2 is a reasonable value, |
---|
35 | * DEPTH = 3 gives good quality, and higher values may improve the results |
---|
36 | * even more but at the cost of significantly longer computation times. |
---|
37 | */ |
---|
38 | #define WIDTH 240 |
---|
39 | #define HEIGHT 200 |
---|
40 | #define DEPTH 3 |
---|
41 | |
---|
42 | /* |
---|
43 | * Error diffusion table, similar to Floyd-Steinberg. I choose not to |
---|
44 | * propagate 100% of the error, because doing so creates awful artifacts |
---|
45 | * (full lines of the same colour, massive colour bleeding) for unclear |
---|
46 | * reasons. Atkinson dithering propagates 3/4 of the error, which is even |
---|
47 | * less than our 31/32. I also choose to propagate slightly more in the |
---|
48 | * X direction to avoid banding effects due to rounding errors. |
---|
49 | * It would be interesting, for future versions of this software, to |
---|
50 | * propagate the error to the second line, too. But right now I find it far |
---|
51 | * too complex to do. |
---|
52 | * |
---|
53 | * +-------+-------+ |
---|
54 | * | error |FS0/FSX| |
---|
55 | * +-------+-------+-------+ |
---|
56 | * |FS1/FSX|FS2/FSX|FS3/FSX| |
---|
57 | * +-------+-------+-------+ |
---|
58 | */ |
---|
59 | #define FS0 15 |
---|
60 | #define FS1 6 |
---|
61 | #define FS2 9 |
---|
62 | #define FS3 1 |
---|
63 | #define FSX 32 |
---|
64 | |
---|
65 | /* |
---|
66 | * The simple Oric RGB palette, made of the 8 Neugebauer primary colours. Each |
---|
67 | * colour is repeated 6 times so that we can point to the palette to paste |
---|
68 | * whole blocks of 6 pixels. It’s also organised so that palette[7-x] is the |
---|
69 | * RGB negative of palette[x]. Finally, it’s roughly ordered by brightness to |
---|
70 | * allow for other tricks later. |
---|
71 | */ |
---|
72 | #define O 0x0000 |
---|
73 | #define I 0xffff |
---|
74 | static const int palette[8][6 * 3] = |
---|
75 | { |
---|
76 | { O, O, O, O, O, O, O, O, O, O, O, O, O, O, O, O, O, O }, |
---|
77 | { O, O, I, O, O, I, O, O, I, O, O, I, O, O, I, O, O, I }, |
---|
78 | { I, O, O, I, O, O, I, O, O, I, O, O, I, O, O, I, O, O }, |
---|
79 | { I, O, I, I, O, I, I, O, I, I, O, I, I, O, I, I, O, I }, |
---|
80 | { O, I, O, O, I, O, O, I, O, O, I, O, O, I, O, O, I, O }, |
---|
81 | { O, I, I, O, I, I, O, I, I, O, I, I, O, I, I, O, I, I }, |
---|
82 | { I, I, O, I, I, O, I, I, O, I, I, O, I, I, O, I, I, O }, |
---|
83 | { I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I }, |
---|
84 | }; |
---|
85 | |
---|
86 | /* |
---|
87 | * Gamma correction tables. itoc_table and ctoi_table accept overflow and |
---|
88 | * underflow values to a reasonable extent, so that we don’t have to check |
---|
89 | * for these cases later in the code. Tests kill performance. |
---|
90 | */ |
---|
91 | #define PAD 2048 |
---|
92 | static int itoc_table_clip[PAD + 256 + PAD], ctoi_table_clip[PAD + 256 + PAD]; |
---|
93 | static int *itoc_table = itoc_table_clip + PAD; |
---|
94 | static int *ctoi_table = ctoi_table_clip + PAD; |
---|
95 | |
---|
96 | static void init_tables(void) |
---|
97 | { |
---|
98 | int i; |
---|
99 | |
---|
100 | for(i = 0; i < PAD + 256 + PAD; i++) |
---|
101 | { |
---|
102 | double f = 1.0 * (i - PAD) / 255.999; |
---|
103 | if(f >= 0.) |
---|
104 | { |
---|
105 | itoc_table_clip[i] = (int)(65535.999 * pow(f, 1./2.2)); |
---|
106 | ctoi_table_clip[i] = (int)(65535.999 * pow(f, 2.2)); |
---|
107 | } |
---|
108 | else |
---|
109 | { |
---|
110 | itoc_table_clip[i] = - (int)(65535.999 * pow(-f, 1./2.2)); |
---|
111 | ctoi_table_clip[i] = - (int)(65535.999 * pow(-f, 2.2)); |
---|
112 | } |
---|
113 | } |
---|
114 | } |
---|
115 | |
---|
116 | static inline int itoc(int p) { return itoc_table[p / 0x100]; } |
---|
117 | static inline int ctoi(int p) { return ctoi_table[p / 0x100]; } |
---|
118 | |
---|
119 | static inline void domove(uint8_t move, uint8_t *bg, uint8_t *fg) |
---|
120 | { |
---|
121 | if(move < 16) |
---|
122 | *fg = move & 0x7; |
---|
123 | else if(move < 32) |
---|
124 | *bg = move & 0x7; |
---|
125 | } |
---|
126 | |
---|
127 | /* |
---|
128 | * Clamp pixel value to avoid colour bleeding. Deactivated because it |
---|
129 | * does not give satisfactory results. |
---|
130 | */ |
---|
131 | #define CLAMP 0x1000 |
---|
132 | static inline int clamp(int p) |
---|
133 | { |
---|
134 | #if 0 |
---|
135 | /* FIXME: doesn’t give terribly good results on eg. eatme.png */ |
---|
136 | if(p < - CLAMP) return - CLAMP; |
---|
137 | if(p > 0xffff + CLAMP) return 0xffff + CLAMP; |
---|
138 | #endif |
---|
139 | return p; |
---|
140 | } |
---|
141 | |
---|
142 | /* |
---|
143 | * Compute the perceptual error caused by replacing the input pixels "in" |
---|
144 | * with the output pixels "out". "inerr" is the diffused error that should |
---|
145 | * be applied to "in"’s first pixel. "outerr" will hold the diffused error |
---|
146 | * to apply after "in"’s last pixel upon next call. The return value does |
---|
147 | * not mean much physically; it is one part of the algorithm where you need |
---|
148 | * to play a bit in order to get appealing results. That’s how image |
---|
149 | * processing works, dude. |
---|
150 | */ |
---|
151 | static inline int geterror(int const *in, int const *inerr, |
---|
152 | int const *out, int *outerr) |
---|
153 | { |
---|
154 | int tmperr[9 * 3]; |
---|
155 | int i, c, ret = 0; |
---|
156 | |
---|
157 | /* 9 cells: 1 for the end of line, 8 for the errors below */ |
---|
158 | memcpy(tmperr, inerr, 3 * sizeof(int)); |
---|
159 | memset(tmperr + 3, 0, 8 * 3 * sizeof(int)); |
---|
160 | |
---|
161 | for(i = 0; i < 6; i++) |
---|
162 | { |
---|
163 | for(c = 0; c < 3; c++) |
---|
164 | { |
---|
165 | /* Experiment shows that this is important at small depths */ |
---|
166 | int a = clamp(in[i * 3 + c] + tmperr[c]); |
---|
167 | int b = out[i * 3 + c]; |
---|
168 | tmperr[c] = (a - b) * FS0 / FSX; |
---|
169 | tmperr[c + (i * 3 + 3)] += (a - b) * FS1 / FSX; |
---|
170 | tmperr[c + (i * 3 + 6)] += (a - b) * FS2 / FSX; |
---|
171 | tmperr[c + (i * 3 + 9)] += (a - b) * FS3 / FSX; |
---|
172 | ret += (a - b) / 256 * (a - b) / 256; |
---|
173 | } |
---|
174 | } |
---|
175 | |
---|
176 | for(i = 0; i < 4; i++) |
---|
177 | { |
---|
178 | for(c = 0; c < 3; c++) |
---|
179 | { |
---|
180 | /* Experiment shows that this is important at large depths */ |
---|
181 | int a = itoc((in[i * 3 + c] + in[i * 3 + 3 + c] |
---|
182 | + in[i * 3 + 6 + c]) / 3); |
---|
183 | int b = itoc((out[i * 3 + c] + out[i * 3 + 3 + c] |
---|
184 | + out[i * 3 + 6 + c]) / 3); |
---|
185 | ret += (a - b) / 256 * (a - b) / 256; |
---|
186 | } |
---|
187 | } |
---|
188 | |
---|
189 | /* Using the diffused error as a perceptual error component is stupid, |
---|
190 | * because that’s not what it is at all, but I found that it helped a |
---|
191 | * bit in some cases. */ |
---|
192 | for(i = 0; i < 3; i++) |
---|
193 | ret += tmperr[i] / 256 * tmperr[i] / 256; |
---|
194 | |
---|
195 | memcpy(outerr, tmperr, 3 * sizeof(int)); |
---|
196 | |
---|
197 | return ret; |
---|
198 | } |
---|
199 | |
---|
200 | static uint8_t bestmove(int const *in, uint8_t bg, uint8_t fg, |
---|
201 | int const *errvec, int depth, int maxerror, |
---|
202 | int *error, int *out) |
---|
203 | { |
---|
204 | int voidvec[3], nvoidvec[3], bestrgb[6 * 3], tmprgb[6 * 3], tmpvec[3]; |
---|
205 | int const *voidrgb, *nvoidrgb, *vec, *rgb; |
---|
206 | int besterror, curerror, suberror, statice, voide, nvoide; |
---|
207 | int i, c; |
---|
208 | uint8_t command, bestcommand; |
---|
209 | |
---|
210 | /* Precompute error for the case where we change the foreground colour |
---|
211 | * and hence only print the background colour or its negative */ |
---|
212 | voidrgb = palette[bg]; |
---|
213 | voide = geterror(in, errvec, voidrgb, voidvec); |
---|
214 | nvoidrgb = palette[7 - bg]; |
---|
215 | nvoide = geterror(in, errvec, nvoidrgb, nvoidvec); |
---|
216 | |
---|
217 | /* Precompute sub-error for the case where we print pixels (and hence |
---|
218 | * don’t change the palette). It’s not the exact error because we should |
---|
219 | * be propagating the error to the first pixel here. */ |
---|
220 | if(depth > 0) |
---|
221 | { |
---|
222 | int tmp[3] = { 0, 0, 0 }; |
---|
223 | bestmove(in + 6 * 3, bg, fg, tmp, depth - 1, maxerror, &statice, NULL); |
---|
224 | } |
---|
225 | |
---|
226 | /* Check every likely command: |
---|
227 | * 0-7: change foreground to 0-7 |
---|
228 | * 8-15: change foreground to 0-7, print negative background |
---|
229 | * 16-23: change background to 0-7 |
---|
230 | * 24-31: change background to 0-7, print negative background |
---|
231 | * 32: normal stuff |
---|
232 | * 33: inverse video stuff */ |
---|
233 | besterror = 0x7ffffff; |
---|
234 | bestcommand = 16; |
---|
235 | memcpy(bestrgb, voidrgb, 6 * 3 * sizeof(int)); |
---|
236 | for(command = 0; command < 34; command++) |
---|
237 | { |
---|
238 | uint8_t newbg = bg, newfg = fg; |
---|
239 | domove(command, &newbg, &newfg); |
---|
240 | |
---|
241 | /* Keeping bg and fg is useless, because we could use commands |
---|
242 | * 32 and 33 instead */ |
---|
243 | if(command < 32 && newbg == bg && newfg == fg) |
---|
244 | continue; |
---|
245 | |
---|
246 | /* I *think* having newfg == newbg is useless, too, but I don’t |
---|
247 | * want to miss some corner case where swapping bg and fg may be |
---|
248 | * interesting, so we continue anyway. */ |
---|
249 | |
---|
250 | if(command < 8) |
---|
251 | { |
---|
252 | curerror = voide; |
---|
253 | rgb = voidrgb; |
---|
254 | vec = voidvec; |
---|
255 | } |
---|
256 | else if(command < 16) |
---|
257 | { |
---|
258 | curerror = nvoide; |
---|
259 | rgb = nvoidrgb; |
---|
260 | vec = nvoidvec; |
---|
261 | } |
---|
262 | else if(command < 24) |
---|
263 | { |
---|
264 | rgb = palette[newbg]; |
---|
265 | curerror = geterror(in, errvec, rgb, tmpvec); |
---|
266 | vec = tmpvec; |
---|
267 | } |
---|
268 | else if(command < 32) |
---|
269 | { |
---|
270 | rgb = palette[7 - newbg]; |
---|
271 | curerror = geterror(in, errvec, rgb, tmpvec); |
---|
272 | vec = tmpvec; |
---|
273 | } |
---|
274 | else |
---|
275 | { |
---|
276 | int const *bgcolor, *fgcolor; |
---|
277 | |
---|
278 | if(command == 32) |
---|
279 | { |
---|
280 | bgcolor = palette[bg]; fgcolor = palette[fg]; |
---|
281 | } |
---|
282 | else |
---|
283 | { |
---|
284 | bgcolor = palette[7 - bg]; fgcolor = palette[7 - fg]; |
---|
285 | } |
---|
286 | |
---|
287 | memcpy(tmpvec, errvec, 3 * sizeof(int)); |
---|
288 | curerror = 0; |
---|
289 | |
---|
290 | for(i = 0; i < 6; i++) |
---|
291 | { |
---|
292 | int vec1[3], vec2[3]; |
---|
293 | int smalle1 = 0, smalle2 = 0; |
---|
294 | |
---|
295 | memcpy(vec1, tmpvec, 3 * sizeof(int)); |
---|
296 | memcpy(vec2, tmpvec, 3 * sizeof(int)); |
---|
297 | for(c = 0; c < 3; c++) |
---|
298 | { |
---|
299 | int delta1, delta2; |
---|
300 | delta1 = clamp(in[i * 3 + c] + tmpvec[c]) - bgcolor[c]; |
---|
301 | vec1[c] = delta1 * FS0 / FSX; |
---|
302 | smalle1 += delta1 / 256 * delta1; |
---|
303 | delta2 = clamp(in[i * 3 + c] + tmpvec[c]) - fgcolor[c]; |
---|
304 | vec2[c] = delta2 * FS0 / FSX; |
---|
305 | smalle2 += delta2 / 256 * delta2; |
---|
306 | } |
---|
307 | |
---|
308 | if(smalle1 < smalle2) |
---|
309 | { |
---|
310 | memcpy(tmpvec, vec1, 3 * sizeof(int)); |
---|
311 | memcpy(tmprgb + i * 3, bgcolor, 3 * sizeof(int)); |
---|
312 | } |
---|
313 | else |
---|
314 | { |
---|
315 | memcpy(tmpvec, vec2, 3 * sizeof(int)); |
---|
316 | memcpy(tmprgb + i * 3, fgcolor, 3 * sizeof(int)); |
---|
317 | } |
---|
318 | } |
---|
319 | |
---|
320 | /* Recompute full error */ |
---|
321 | curerror += geterror(in, errvec, tmprgb, tmpvec); |
---|
322 | |
---|
323 | rgb = tmprgb; |
---|
324 | vec = tmpvec; |
---|
325 | } |
---|
326 | |
---|
327 | if(curerror > besterror) |
---|
328 | continue; |
---|
329 | |
---|
330 | /* Try to avoid bad decisions now that will have a high cost |
---|
331 | * later in the line by making the next error more important than |
---|
332 | * the current error. */ |
---|
333 | curerror = curerror * 3 / 4; |
---|
334 | |
---|
335 | if(depth == 0) |
---|
336 | suberror = 0; /* It’s the end of the tree */ |
---|
337 | else if(command < 32) |
---|
338 | { |
---|
339 | bestmove(in + 6 * 3, newbg, newfg, vec, depth - 1, |
---|
340 | besterror - curerror, &suberror, NULL); |
---|
341 | |
---|
342 | #if 0 |
---|
343 | /* Penalty for background changes; they're hard to revert. The |
---|
344 | * value of 2 was determined empirically. 1.5 is not enough and |
---|
345 | * 3 is too much. */ |
---|
346 | if(newbg != bg) |
---|
347 | suberror *= 2; |
---|
348 | #endif |
---|
349 | } |
---|
350 | else |
---|
351 | suberror = statice; |
---|
352 | |
---|
353 | if(curerror + suberror < besterror) |
---|
354 | { |
---|
355 | besterror = curerror + suberror; |
---|
356 | bestcommand = command; |
---|
357 | memcpy(bestrgb, rgb, 6 * 3 * sizeof(int)); |
---|
358 | } |
---|
359 | } |
---|
360 | |
---|
361 | *error = besterror; |
---|
362 | if(out) |
---|
363 | memcpy(out, bestrgb, 6 * 3 * sizeof(int)); |
---|
364 | |
---|
365 | return bestcommand; |
---|
366 | } |
---|
367 | |
---|
368 | int main(int argc, char *argv[]) |
---|
369 | { |
---|
370 | SDL_Surface *tmp, *surface; |
---|
371 | int *src, *dst, *srcl, *dstl; |
---|
372 | uint8_t *pixels; |
---|
373 | int stride, x, y, depth, c; |
---|
374 | |
---|
375 | if(argc < 2) |
---|
376 | return 1; |
---|
377 | |
---|
378 | tmp = IMG_Load(argv[1]); |
---|
379 | if(!tmp) |
---|
380 | return 2; |
---|
381 | |
---|
382 | init_tables(); |
---|
383 | |
---|
384 | /* Load the image into a friendly array of fast integers. We create it |
---|
385 | * slightly bigger than the image so that we don’t have to care about |
---|
386 | * borders when propagating the error later */ |
---|
387 | src = calloc((WIDTH + 1) * (HEIGHT + 1) * 3, sizeof(int)); |
---|
388 | dst = calloc((WIDTH + 1) * (HEIGHT + 1) * 3, sizeof(int)); |
---|
389 | stride = (WIDTH + 1) * 3; |
---|
390 | |
---|
391 | /* FIXME: endianness */ |
---|
392 | surface = SDL_CreateRGBSurface(SDL_SWSURFACE, WIDTH, HEIGHT, 32, |
---|
393 | 0xff0000, 0xff00, 0xff, 0x0); |
---|
394 | SDL_BlitSurface(tmp, NULL, surface, NULL); |
---|
395 | pixels = (uint8_t *)surface->pixels; |
---|
396 | for(y = 0; y < HEIGHT; y++) |
---|
397 | for(x = 0; x < WIDTH; x++) |
---|
398 | for(c = 0; c < 3; c++) |
---|
399 | { |
---|
400 | src[y * stride + x * 3 + c] |
---|
401 | = ctoi(pixels[y * surface->pitch + x * 4 + 2 - c] * 0x101); |
---|
402 | dst[y * stride + x * 3 + c] = 0; |
---|
403 | } |
---|
404 | |
---|
405 | /* Let the fun begin */ |
---|
406 | for(y = 0; y < HEIGHT; y++) |
---|
407 | { |
---|
408 | uint8_t bg = 0, fg = 7; |
---|
409 | |
---|
410 | fprintf(stderr, "\rProcessing... %i%%", (y + 1) / 2); |
---|
411 | |
---|
412 | for(x = 0; x < WIDTH; x += 6) |
---|
413 | { |
---|
414 | int errvec[3] = { 0, 0, 0 }; |
---|
415 | int dummy, i; |
---|
416 | uint8_t move; |
---|
417 | |
---|
418 | depth = (x + DEPTH < WIDTH) ? DEPTH : (WIDTH - x) / 6 - 1; |
---|
419 | srcl = src + y * stride + x * 3; |
---|
420 | dstl = dst + y * stride + x * 3; |
---|
421 | |
---|
422 | /* Recursively compute and apply best move */ |
---|
423 | move = bestmove(srcl, bg, fg, errvec, depth, 0x7fffff, |
---|
424 | &dummy, dstl); |
---|
425 | /* Propagate error */ |
---|
426 | for(c = 0; c < 3; c++) |
---|
427 | { |
---|
428 | for(i = 0; i < 6; i++) |
---|
429 | { |
---|
430 | int error = srcl[i * 3 + c] - dstl[i * 3 + c]; |
---|
431 | srcl[i * 3 + c + 3] = |
---|
432 | clamp(srcl[i * 3 + c + 3] + error * FS0 / FSX); |
---|
433 | srcl[i * 3 + c + stride - 3] += error * FS1 / FSX; |
---|
434 | srcl[i * 3 + c + stride] += error * FS2 / FSX; |
---|
435 | srcl[i * 3 + c + stride + 3] += error * FS3 / FSX; |
---|
436 | } |
---|
437 | |
---|
438 | for(i = -1; i < 7; i++) |
---|
439 | srcl[i * 3 + c + stride] = clamp(srcl[i * 3 + c + stride]); |
---|
440 | } |
---|
441 | /* Iterate */ |
---|
442 | domove(move, &bg, &fg); |
---|
443 | } |
---|
444 | } |
---|
445 | |
---|
446 | fprintf(stderr, " done.\n"); |
---|
447 | |
---|
448 | /* Save everything */ |
---|
449 | for(y = 0; y < HEIGHT; y++) |
---|
450 | for(x = 0; x < WIDTH; x++) |
---|
451 | for(c = 0; c < 3; c++) |
---|
452 | pixels[y * surface->pitch + x * 4 + 2 - c] |
---|
453 | = itoc(dst[y * stride + x * 3 + c]) / 0x100; |
---|
454 | SDL_SaveBMP(surface, "output.bmp"); |
---|
455 | |
---|
456 | return 0; |
---|
457 | } |
---|
458 | |
---|