source: www/labs/img2oric.c @ 2212

Last change on this file since 2212 was 2212, checked in by Sam Hocevar, 15 years ago
  • Add support for inverse video on attribute change.
  • Improve Floyd-Steinberg coefficient values.
File size: 14.8 KB
Line 
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
74static 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
92static int itoc_table_clip[PAD + 256 + PAD], ctoi_table_clip[PAD + 256 + PAD];
93static int *itoc_table = itoc_table_clip + PAD;
94static int *ctoi_table = ctoi_table_clip + PAD;
95
96static 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
116static inline int itoc(int p) { return itoc_table[p / 0x100]; }
117static inline int ctoi(int p) { return ctoi_table[p / 0x100]; }
118
119static 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
132static 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 */
151static 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
200static 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
368int 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
Note: See TracBrowser for help on using the repository browser.