source: www/labs/img2oric.c @ 2207

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