source: www/study/study.py @ 2190

Revision 2190, 86.4 KB checked in by sam, 5 years ago (diff)
  • Direct binary search.
  • Property svn:executable set to *
Line 
1#!/usr/bin/env python
2
3import math, gd, random, sys, os
4
5# Select which chapters to run
6def chapter(n):
7    if len(sys.argv) == 1:
8        return True
9    return str(n) in sys.argv
10
11##############################################################################
12
13# Tiny image class to make examples short and readable
14class Image(gd.image):
15    gd.gdMaxColors = 256 * 256 * 256
16    def __init__(self, *args):
17        if args[0].__class__ == str:
18            print "[LOAD] %s" % (args[0],)
19        gd.image.__init__(self, *args)
20    def save(self, name):
21        print "[PNG] %s" % (name,)
22        self.writePng(name)
23    def getGray(self, x, y):
24        p = self.getPixel((x, y))
25        c = self.colorComponents(p)[0] / 255.0
26        return c
27    def getRgb(self, x, y):
28        p = self.getPixel((x, y))
29        rgb = self.colorComponents(p)
30        return [rgb[0] / 255.0, rgb[1] / 255.0, rgb[2] / 255.0]
31    def setGray(self, x, y, t):
32        p = (int)(t * 255.999)
33        c = self.colorResolve((p, p, p))
34        self.setPixel((x, y), c)
35    def setRgb(self, x, y, r, g, b):
36        r = (int)(r * 255.999)
37        g = (int)(g * 255.999)
38        b = (int)(b * 255.999)
39        c = self.colorResolve((r, g, b))
40        self.setPixel((x, y), c)
41    def getRegion(self, x, y, w, h):
42        dest = Image((w, h), True)
43        self.copyTo(dest, (-x, -y))
44        return dest
45    def getZoom(self, z):
46        (w, h) = self.size()
47        dest = Image((w * z, h * z), True)
48        for y in range(h):
49            for x in range(w):
50                rgb = self.getRgb(x, y)
51                for j in range(z):
52                    for i in range(z):
53                        dest.setRgb(x * z + i, y * z + j, *rgb)
54        return dest
55
56# Manipulate gamma values
57class Gamma:
58    def CtoI(x):
59        if x < 0:
60            return - math.pow(-x, 2.2)
61        return math.pow(x, 2.2)
62    def ItoC(x):
63        if x < 0:
64            return - math.pow(-x, 1 / 2.2)
65        return math.pow(x, 1 / 2.2)
66    CtoI = staticmethod(CtoI)
67    ItoC = staticmethod(ItoC)
68    def Cto2(x):
69        if x < Gamma.CtoI(0.50):
70            return 0.
71        return 1.
72    def Cto3(x):
73        if x < Gamma.CtoI(0.25):
74            return 0.
75        elif x < Gamma.CtoI(0.75):
76            return Gamma.CtoI(0.5)
77        return 1.
78    def Cto4(x):
79        if x < Gamma.CtoI(0.17):
80            return 0.
81        elif x < Gamma.CtoI(0.50):
82            return Gamma.CtoI(0.3333)
83        elif x < Gamma.CtoI(0.83):
84            return Gamma.CtoI(0.6666)
85        return 1.
86    Cto2 = staticmethod(Cto2)
87    Cto3 = staticmethod(Cto3)
88    Cto4 = staticmethod(Cto4)
89
90# Create matrices
91def Matrix(w, h, val = 0):
92    return [[val] * w for n in range(h)]
93
94##############################################################################
95
96# Create SVG files from matrix data
97class Svg:
98    _data = ''
99    _w, _h = 0, 0
100    def _colorise(val):
101        return '#fff'
102    def _reduce(this, val):
103        if type(val) == float:
104            for x in range(1, 1000):
105                if abs(val * x - round(val * x)) < 0.001:
106                    return (int)(round(val * x)), x
107        return val, 1
108    def __init__(self, mat, colorise = _colorise):
109        # Check whether it is an error diffusion matrix
110        ed = False
111        for l in mat:
112            for x in l:
113                if type(x) == str or math.floor(x) != x:
114                    ed = True
115        # Generate SVG file
116        (w, h) = (len(mat[0]), len(mat))
117        s = \
118         '<?xml version="1.0" encoding="UTF-8" standalone="no"?>\n' \
119         '<svg\n' \
120         ' xmlns="http://www.w3.org/2000/svg"\n' \
121         ' xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"\n' \
122         ' xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"\n' \
123         ' width="%u"\n' \
124         ' height="%u">\n' \
125         '  <sodipodi:namedview inkscape:document-units="px" />\n' \
126         '  <g>\n' % (64 * w + 2, 64 * h + 2)
127        line = \
128         '  <path sodipodi:nodetypes="cc" d="M %u,%u L %u,%u" ' \
129               'style="stroke:#000;stroke-width:1" />\n'
130        box = \
131         '  <path sodipodi:nodetypes="cccc" ' \
132               'd="M %u,%u L %u,%u L %u,%u L %u,%u z" ' \
133               'style="fill:%s;stroke:#000;stroke-width:2" />\n'
134        text = \
135         '  <text style="font-size:%upx;text-align:center;text-anchor:middle;' \
136               'font-family:Bitstream Vera Serif" x="%u" y="%u"%s>%s</text>\n'
137        for y in range(h):
138            for x in range(w):
139                val = mat[y][x]
140                if not ed and val == -1:
141                    continue
142                if ed and val == 0:
143                    continue
144                # Put box
145                (ix, iy) = (64. * x + 1, 64. * y + 1)
146                if ed and val == -1:
147                    val = ''
148                    c = '#fbb'
149                else:
150                    c = colorise(val)
151                s += box % (ix, iy, ix + 64, iy, ix + 64, iy + 64, ix, \
152                            iy + 64, c)
153                # Put value
154                a, b = self._reduce(val)
155                extra = ''
156                if b == 1:
157                    (tx, ty) = (ix + 32, iy + 44)
158                    n = len(str(val))
159                    if n > 3 and type(val) == str:
160                        extra = ' transform="scale(0.7,0.7)"'
161                        tx = tx / 0.7
162                        ty = ty / 0.7
163                    elif n > 3:
164                        extra = ' transform="scale(0.7,1.)"'
165                        tx = tx / 0.7
166                    elif n > 2:
167                        extra = ' transform="scale(0.8,1.)"'
168                        tx = tx / 0.8
169                    s += text % (32, tx, ty, extra, val)
170                else:
171                    s += line % (ix + 8, iy + 32, ix + 56, iy + 32)
172                    (tx, ty) = (ix + 32, iy + 26)
173                    s += text % (24, tx, ty, extra, a)
174                    (tx, ty) = (ix + 32, iy + 56)
175                    s += text % (24, tx, ty, extra, b)
176        s += \
177         '  </g>\n' \
178         '</svg>\n'
179        self._w = 64 * w + 2
180        self._h = 64 * h + 2
181        self._data = s
182    def save(self, name, size):
183        svgname = name + ".tmp.svg"
184        f = open(svgname, 'w')
185        f.write(self._data)
186        f.close()
187        f = os.popen("inkscape %s -a %u:%u:%u:%u -w%u -h%u -e %s >/dev/null 2>&1" % (svgname, 0, 0, self._w, self._h, size * self._w / 64., size * self._h / 64., name))
188        print "[SVG] %s" % (name,)
189        f.close()
190        os.unlink(svgname)
191
192##############################################################################
193print "Initialisation"
194
195# Load the original Lenna image
196lenna512 = Image("lenna512.png")
197(w, h) = lenna512.size()
198
199# Image 1: greyscale conversion
200# Read the compression FAQ [55] for the rationale behind using the green
201# channel (http://www.faqs.org/faqs/compression-faq/part1/section-30.html)
202if chapter(0):
203    (w, h) = lenna512.size()
204    lenna512bw = Image((w, h))
205    for y in range(h):
206        for x in range(w):
207            rgb = lenna512.getRgb(x, y)
208            c = rgb[1]
209            lenna512bw.setGray(x, y, c)
210    lenna512bw.save("lenna512bw.png")
211else:
212    lenna512bw = Image("lenna512bw.png")
213   
214# Image 2: 50% greyscale
215# Image 3: 50% scaling
216if chapter(0):
217    (w, h) = lenna512.size()
218    lenna256bw = Image((w / 2, h / 2))
219    lenna256 = Image((w / 2, h / 2), True)
220    for y in range(h / 2):
221        for x in range(w / 2):
222            r = g = b = 0.
223            for j in range(2):
224                for i in range(2):
225                    rgb = lenna512.getRgb(x * 2 + i, y * 2 + j)
226                    r += Gamma.CtoI(rgb[0])
227                    g += Gamma.CtoI(rgb[1])
228                    b += Gamma.CtoI(rgb[2])
229            r = Gamma.ItoC(r / 4)
230            g = Gamma.ItoC(g / 4)
231            b = Gamma.ItoC(b / 4)
232            lenna256bw.setGray(x, y, g)
233            lenna256.setRgb(x, y, r, g, b)
234    lenna256bw.save("lenna256bw.png")
235    lenna256.save("lenna256.png")
236else:
237    lenna256bw = Image("lenna256bw.png")
238    lenna256 = Image("lenna256.png")
239
240# Create a 32x256 greyscale gradient
241if chapter(0):
242    grad256bw = Image((32, 256))
243    for y in range(256):
244        for x in range(32):
245            grad256bw.setGray(x, 255 - y, y / 255.)
246    grad256bw.save("gradient256bw.png")
247else:
248    grad256bw = Image("gradient256bw.png")
249
250# Create a 64x256 colour gradient
251if chapter(0):
252    grad256 = Image((64, 256), True)
253    for y in range(255, -1, -1):
254        for x in range(64):
255            grad256.setRgb(x, y, x / 63., (255. - y) / 255, x / 63.)
256    grad256.save("gradient256.png")
257else:
258    grad256 = Image("gradient256.png")
259
260##############################################################################
261if chapter(1):
262    print "Chapter 1. Colour quantisation"
263
264# Output 1.1.1: 50% threshold
265# Output 1.1.2: 40% threshold
266# Output 1.1.3: 60% threshold
267def test11x(src, threshold):
268    (w, h) = src.size()
269    dest = Image((w, h))
270    for y in range(h):
271        for x in range(w):
272            c = src.getGray(x, y) > threshold
273            dest.setGray(x, y, c)
274    return dest
275
276if chapter(1):
277    test11x(grad256bw, 0.5).save("grad1-1-1.png")
278    test11x(lenna256bw, 0.5).save("out1-1-1.png")
279    test11x(grad256bw, 0.4).save("grad1-1-2.png")
280    test11x(lenna256bw, 0.4).save("out1-1-2.png")
281    test11x(grad256bw, 0.6).save("grad1-1-3.png")
282    test11x(lenna256bw, 0.6).save("out1-1-3.png")
283
284# Output 1.2.1: 3-colour threshold
285# Output 1.2.2: 5-colour threshold
286def test12x(src, colors):
287    (w, h) = src.size()
288    dest = Image((w, h))
289    q = colors - 1
290    p = -.00001 + colors
291    for y in range(h):
292        for x in range(w):
293            c = src.getGray(x, y)
294            c = math.floor(c * p) / q
295            dest.setGray(x, y, c)
296    return dest
297
298if chapter(1):
299    test12x(grad256bw, 3).save("grad1-2-1.png")
300    test12x(lenna256bw, 3).save("out1-2-1.png")
301    test12x(grad256bw, 5).save("grad1-2-2.png")
302    test12x(lenna256bw, 5).save("out1-2-2.png")
303
304# Output 1.2.3: 3-colour threshold, minimal error
305# Output 1.2.4: 5-colour threshold, minimal error
306def test12y(src, colors):
307    (w, h) = src.size()
308    dest = Image((w, h))
309    q = colors - 1
310    p = -.00001 + colors - 1
311    for y in range(h):
312        for x in range(w):
313            c = src.getGray(x, y)
314            c = math.floor((c + 0.5 / p) * p) / q
315            dest.setGray(x, y, c)
316    return dest
317
318if chapter(1):
319    test12y(grad256bw, 3).save("grad1-2-3.png")
320    test12y(lenna256bw, 3).save("out1-2-3.png")
321    test12y(grad256bw, 5).save("grad1-2-4.png")
322    test12y(lenna256bw, 5).save("out1-2-4.png")
323
324# Output 1.3.1: 2-colour threshold, dynamic thresholding
325# Output 1.3.2: 5-colour threshold, dynamic thresholding
326def test13x(src, n, fuzzy = None):
327    random.seed(0)
328    (w, h) = src.size()
329    dest = Image((w, h))
330    # Compute histogram
331    histo = [0] * 256
332    for y in range(h):
333        for x in range(w):
334            histo[(int)(src.getGray(x, y) * 255.9999)] += 1
335    thresholds = [1. * (1. + i) / n for i in range(n - 1)]
336    values = [i / (n - 1.) for i in range(n)]
337    # Parse histogram
338    total = 0
339    t = 0
340    for i in range(256):
341        total += histo[i]
342        if total > thresholds[t] * w * h:
343            thresholds[t] = i / 255.0
344            t += 1
345            if t + 1 > n - 1:
346                break
347    # Compute image
348    for y in range(h):
349        for x in range(w):
350            c = src.getGray(x, y)
351            for (i, t) in enumerate(thresholds):
352                if fuzzy:
353                    t += fuzzy()
354                if c < t:
355                    dest.setGray(x, y, values[i])
356                    break
357            else:
358                dest.setGray(x, y, values[n - 1])
359    return dest
360
361if chapter(1):
362    test13x(grad256bw, 2).save("grad1-3-1.png")
363    test13x(lenna256bw, 2).save("out1-3-1.png")
364    test13x(grad256bw, 5).save("grad1-3-2.png")
365    test13x(lenna256bw, 5).save("out1-3-2.png")
366
367# Output 1.4.1: uniform random dithering
368def test141(src):
369    random.seed(0)
370    (w, h) = src.size()
371    dest = Image((w, h))
372    for y in range(h):
373        for x in range(w):
374            c = src.getGray(x, y)
375            d = c > random.random()
376            dest.setGray(x, y, d)
377    return dest
378
379if chapter(1):
380    test141(grad256bw).save("grad1-4-1.png")
381    test141(lenna256bw).save("out1-4-1.png")
382
383# Output 1.4.2: gaussian random dithering
384def test142(src):
385    random.seed(0)
386    (w, h) = src.size()
387    dest = Image((w, h))
388    for y in range(h):
389        for x in range(w):
390            c = src.getGray(x, y)
391            d = c > random.gauss(0.5, 0.15)
392            dest.setGray(x, y, d)
393    return dest
394
395if chapter(1):
396    test142(grad256bw).save("grad1-4-2.png")
397    test142(lenna256bw).save("out1-4-2.png")
398
399# Output 1.4.3: 3-colour threshold, dynamic thresholding, gaussian perturbation
400if chapter(1):
401    fuzzy = lambda : random.gauss(0., 0.08)
402    test13x(grad256bw, 4, fuzzy).save("grad1-4-3.png")
403    test13x(lenna256bw, 4, fuzzy).save("out1-4-3.png")
404
405##############################################################################
406if chapter(2):
407    print "Chapter 2. Halftoning patterns"
408
409# Pattern 2.1.1: a 50% halftone pattern with various block sizes
410# Pattern 2.1.2: 25% and 75% halftone patterns with various block sizes
411if chapter(2):
412    dest = Image((320, 80))
413    for x in range(320):
414        d = 8 >> (x / 80)
415        for y in range(80):
416            c = (x / d + y / d) & 1
417            dest.setGray(x, y, c)
418    dest.save("pat2-1-1.png")
419
420    dest = Image((320, 80))
421    for x in range(320):
422        d = 8 >> (x / 80)
423        for y in range(40):
424            c = ((x / d + y / d) & 1) or (y / d & 1)
425            dest.setGray(x, y, c)
426        for y in range(40, 80):
427            c = ((x / d + y / d) & 1) and (y / d & 1)
428            dest.setGray(x, y, c)
429    dest.save("pat2-1-2.png")
430
431# Output 2.1.1: 20/40/60/80% threshold with 25/50/75% patterns inbetween:
432def test211(src):
433    (w, h) = src.size()
434    dest = Image((w, h))
435    for y in range(h):
436        for x in range(w):
437            c = src.getGray(x, y)
438            if c < 0.2:
439                c = 0.
440            elif c < 0.4:
441                c = ((x + y) & 1) and (y & 1)
442            elif c < 0.6:
443                c = (x + y) & 1
444            elif c < 0.8:
445                c = ((x + y) & 1) or (y & 1)
446            else:
447                c = 1.
448            dest.setGray(x, y, c)
449    return dest
450
451if chapter(2):
452    test211(grad256bw).save("grad2-1-1.png")
453    test211(lenna256bw).save("out2-1-1.png")
454
455# Pattern 2.2.1: vertical, mixed and horizontal black-white halftones
456# Pattern 2.2.2: two different 25% patterns
457if chapter(2):
458    dest = Image((240, 80))
459    for y in range(80):
460        for x in range(80):
461            c = x & 1
462            dest.setGray(x, y, c)
463        for x in range(80, 160):
464            c = (x / d + y / d) & 1
465            dest.setGray(x, y, c)
466        for x in range(160, 240):
467            c = y & 1
468            dest.setGray(x, y, c)
469    dest.save("pat2-2-1.png")
470
471    dest = Image((320, 80))
472    for y in range(80):
473        for x in range(80):
474            c = (x / 2 & 1) and (y / 2 & 1)
475            dest.setGray(x, y, c)
476        for x in range(80, 160):
477            c = (x & 1) and (y & 1)
478            dest.setGray(x, y, c)
479        for x in range(160, 240):
480            c = (x & 1) and ((y + x / 2) & 1)
481            dest.setGray(x, y, c)
482        for x in range(240, 320):
483            c = (x / 2 & 1) and ((y / 2 + x / 4) & 1)
484            dest.setGray(x, y, c)
485    dest.save("pat2-2-2.png")
486
487# Output 2.3.1: 4x4 Bayer dithering
488# Output 2.3.2: 4x4 cluster dot
489# Output 2.3.3: 5x3 line dithering
490def ordereddither(src, mat):
491    (w, h) = src.size()
492    dest = Image((w, h))
493    dx = len(mat[0])
494    dy = len(mat)
495    for y in range(h):
496        for x in range(w):
497            c = src.getGray(x, y)
498            threshold = (1. + mat[y % dy][x % dx]) / (dx * dy + 1)
499            c = c > threshold
500            dest.setGray(x, y, c)
501    return dest
502
503def makebayer(rank, mat = False):
504    if not mat:
505        mat = Matrix(1, 1)
506    if not rank:
507        return mat
508    n = len(mat)
509    newmat = Matrix(n * 2, n * 2)
510    for j in range(n):
511        for i in range(n):
512            x = mat[j][i]
513            newmat[j * 2][i * 2] = x
514            newmat[j * 2][i * 2 + 1] = x + n * n * 3
515            newmat[j * 2 + 1][i * 2] = x + n * n * 2
516            newmat[j * 2 + 1][i * 2 + 1] = x + n * n
517    return makebayer(rank - 1, newmat)
518
519DITHER_BAYER22 = makebayer(1)
520DITHER_BAYER44 = makebayer(2)
521DITHER_BAYER88 = makebayer(3)
522
523DITHER_CLUSTER44 = \
524    [[ 12,  5,  6, 13],
525     [  4,  0,  1,  7],
526     [ 11,  3,  2,  8],
527     [ 15, 10,  9, 14]]
528DITHER_CLUSTER88 = \
529    [[ 24, 10, 12, 26, 35, 47, 49, 37],
530     [  8,  0,  2, 14, 45, 59, 61, 51],
531     [ 22,  6,  4, 16, 43, 57, 63, 53],
532     [ 30, 20, 18, 28, 33, 41, 55, 39],
533     [ 34, 46, 48, 36, 25, 11, 13, 27],
534     [ 44, 58, 60, 50,  9,  1,  3, 15],
535     [ 42, 56, 62, 52, 23,  7,  5, 17],
536     [ 32, 40, 54, 38, 31, 21, 19, 29]]
537DITHER_LINE53 = \
538    [[  9,  3,  0,  6, 12],
539     [ 10,  4,  1,  7, 13],
540     [ 11,  5,  2,  8, 14]]
541
542def bayercol(val): return ['#fdf', '#dfd', '#ffd', '#dff'][val % 4]
543
544if chapter(2):
545    ordereddither(grad256bw, DITHER_BAYER22).save("grad2-3-0.png")
546    ordereddither(lenna256bw, DITHER_BAYER22).save("out2-3-0.png")
547
548    Svg(DITHER_BAYER44, bayercol).save("fig2-3-2.png", 40)
549    ordereddither(grad256bw, DITHER_BAYER44).save("grad2-3-1.png")
550    ordereddither(lenna256bw, DITHER_BAYER44).save("out2-3-1.png")
551
552    Svg(DITHER_BAYER88, bayercol).save("fig2-3-2b.png", 30)
553    ordereddither(grad256bw, DITHER_BAYER88).save("grad2-3-1b.png")
554    ordereddither(lenna256bw, DITHER_BAYER88).save("out2-3-1b.png")
555
556    Svg(DITHER_CLUSTER44).save("fig2-3-3.png", 40)
557    ordereddither(grad256bw, DITHER_CLUSTER44).save("grad2-3-2.png")
558    ordereddither(lenna256bw, DITHER_CLUSTER44).save("out2-3-2.png")
559
560    Svg(DITHER_CLUSTER88).save("fig2-3-3b.png", 30)
561    ordereddither(grad256bw, DITHER_CLUSTER88).save("grad2-3-2b.png")
562    ordereddither(lenna256bw, DITHER_CLUSTER88).save("out2-3-2b.png")
563
564    Svg(DITHER_LINE53).save("fig2-3-4.png", 40)
565    ordereddither(grad256bw, DITHER_LINE53).save("grad2-3-3.png")
566    ordereddither(lenna256bw, DITHER_LINE53).save("out2-3-3.png")
567
568# Output 2.4.1: 4x4 Bayer dithering with gaussian perturbation
569def test241(src, mat):
570    random.seed(0)
571    (w, h) = src.size()
572    dest = Image((w, h))
573    dx = len(mat[0])
574    dy = len(mat)
575    for y in range(h):
576        for x in range(w):
577            c = src.getGray(x, y)
578            threshold = (1. + mat[y % dy][x % dx]) / (dx * dy + 1)
579            threshold += random.gauss(0, 0.08)
580            c = c > threshold
581            dest.setGray(x, y, c)
582    return dest
583
584if chapter(2):
585    test241(grad256bw, DITHER_BAYER88).save("grad2-4-1.png")
586    test241(lenna256bw, DITHER_BAYER88).save("out2-4-1.png")
587
588# Output 2.4.2: random dither matrice selection
589def test242(src, mlist):
590    random.seed(0)
591    (w, h) = src.size()
592    dest = Image((w, h))
593    dx = len(mlist[0][0])
594    dy = len(mlist[0])
595    for y in range(h / dy):
596        for x in range(w / dx):
597            mat = random.choice(mlist)
598            for j in range(dy):
599                for i in range(dx):
600                    c = src.getGray(x * dx + i, y * dy + j)
601                    threshold = (1. + mat[j][i]) / (dx * dy + 1)
602                    d = c > threshold
603                    dest.setGray(x * dx + i, y * dy + j, d)
604    return dest
605
606if chapter(2):
607    m1 = [[1, 4, 7],
608          [6, 0, 2],
609          [3, 8, 5]]
610    m2 = [[4, 6, 3],
611          [8, 1, 5],
612          [0, 3, 7]]
613    m3 = [[5, 0, 3],
614          [2, 8, 6],
615          [7, 4, 1]]
616    m4 = [[8, 2, 5],
617          [6, 4, 0],
618          [1, 7, 3]]
619    m5 = [[2, 5, 8],
620          [0, 7, 3],
621          [4, 1, 6]]
622    m6 = [[7, 4, 1],
623          [3, 6, 8],
624          [2, 0, 5]]
625    mlist = [m1, m2, m3, m4, m5, m6]
626    test242(grad256bw, mlist).save("grad2-4-2.png")
627    test242(lenna256bw, mlist).save("out2-4-2.png")
628
629# Output 2.5.1: cross pattern
630# Output 2.5.2: hex pattern
631# Output 2.5.3: square pattern
632def test25x(src, mat, vec):
633    # 1. count positive cells
634    n = 0
635    for line in mat:
636        for x in line:
637            if x >= 0:
638                n += 1
639    # 2. create list of vectors
640    l = []
641    x = y = 0
642    while (x, y) not in l:
643        l.append((x, y))
644        (x, y) = ((x + vec[0][0]) % n, (y + vec[0][1]) % n)
645        if (x, y) in l:
646            (x, y) = ((x + vec[1][0]) % n, (y + vec[1][1]) % n)
647    # 3. create big matrix
648    m = Matrix(n, n)
649    for v in l:
650        for y in range(len(mat)):
651            for x in range(len(mat[0])):
652                if mat[y][x] < 0:
653                    continue
654                m[(v[1] + y + n) % n][(v[0] + x + n) % n] = mat[y][x]
655    # 4. dither image
656    (w, h) = src.size()
657    dest = Image((w, h))
658    for y in range(h):
659        for x in range(w):
660            c = src.getGray(x, y)
661            threshold = (1. + m[y % n][x % n]) / (1. + n)
662            d = c > threshold
663            dest.setGray(x, y, d)
664    return dest
665
666if chapter(2):
667    mat = [[-1,  4, -1],
668           [ 3,  0,  1],
669           [-1,  2, -1]]
670    vec = [(2, -1), (1, 2)]
671    test25x(grad256bw, mat, vec).save("grad2-5-1.png")
672    test25x(lenna256bw, mat, vec).save("out2-5-1.png")
673    mat = [[-1,  4,  2, -1],
674           [ 6,  0,  5,  3],
675           [-1,  7,  1, -1]]
676    vec = [(2, -2), (3, 1)]
677    test25x(grad256bw, mat, vec).save("grad2-5-2.png")
678    test25x(lenna256bw, mat, vec).save("out2-5-2.png")
679    mat = [[-1, -1, -1,  7, -1],
680           [-1,  2,  6,  9,  8],
681           [ 3,  0,  1,  5, -1],
682           [-1,  4, -1, -1, -1]]
683    vec = [(2, 4), (3, 1)]
684    test25x(grad256bw, mat, vec).save("grad2-5-3.png")
685    test25x(lenna256bw, mat, vec).save("out2-5-3.png")
686    mat = [[-1, -1,  2, -1],
687           [ 0,  1,  3,  8],
688           [ 5,  4,  7,  6],
689           [-1,  9, -1, -1]]
690    vec = [(2, 2), (0, 5)]
691    test25x(grad256bw, mat, vec).save("grad2-5-4.png")
692    test25x(lenna256bw, mat, vec).save("out2-5-4.png")
693
694# Output 2.6.1: 4-wise cross pattern
695# Output 2.6.2: 3-wise hex pattern
696# Output 2.6.3: 4-wise square pattern
697if chapter(2):
698    mat = [[-1, -1, -1, 18, -1, -1],
699           [-1, 16, 14,  2,  6, -1],
700           [12,  0,  4, 10, 17, -1],
701           [-1,  8, 19, 13,  1,  5],
702           [-1, 15,  3,  7,  9, -1],
703           [-1, -1, 11, -1, -1, -1]]
704    vec = [(4, -2), (2, 4)]
705    test25x(grad256bw, mat, vec).save("grad2-6-1.png")
706    test25x(lenna256bw, mat, vec).save("out2-6-1.png")
707
708    mat = [[-1, 12,  6, -1, -1, -1, -1],
709           [18,  0, 15, 9,  14,  8, -1],
710           [-1, 21,  3, 20,  2, 17, 11],
711           [-1, -1, 13,  7, 23,  5, -1],
712           [-1, 19,  1, 16, 10, -1, -1],
713           [-1, -1, 22,  4, -1, -1, -1]]
714    vec = [(5, -1), (-1, 5)]
715    test25x(grad256bw, mat, vec).save("grad2-6-2.png")
716    test25x(lenna256bw, mat, vec).save("out2-6-2.png")
717
718    mat = [[-1, -1, -1, -1, 30, -1, -1, -1, -1],
719           [-1, -1, 10, 26, 38, 34, -1, 29, -1],
720           [-1, 14,  2,  6, 22,  9, 25, 37, 33],
721           [-1, -1, 18, 28, 13,  1,  5, 21, -1],
722           [-1,  8, 24, 36, 32, 17, 31, -1, -1],
723           [12,  0,  4, 20, 11, 27, 39, 35, -1],
724           [-1, 16, -1, 15,  3,  7, 23, -1, -1],
725           [-1, -1, -1, -1, 19, -1, -1, -1, -1]]
726    vec = [(6, 2), (-2, 6)]
727    test25x(grad256bw, mat, vec).save("grad2-6-3.png")
728    test25x(lenna256bw, mat, vec).save("out2-6-3.png")
729
730    mat = [[-1, -1,  3, 35, -1, -1,  1, 33, -1, -1, -1, -1, -1],
731           [-1, -1, -1, 19, 51, -1, -1, 17, 49, -1, -1, -1, -1],
732           [ 7, 39, 11, 43,  5, 37,  9, 41, -1, -1, -1, -1, -1],
733           [-1, 23, 55, 27, 59, 21, 53, 25, 57, -1, -1, -1, -1],
734           [15, 47, -1, -1, 13, 45,  2, 34, -1, -1,  0, 32, -1],
735           [-1, 31, 63, -1, -1, 29, 61, 18, 50, -1, -1, 16, 48],
736           [-1, -1, -1, -1,  6, 38, 10, 42,  4, 36,  8, 40, -1],
737           [-1, -1, -1, -1, -1, 22, 54, 26, 58, 20, 52, 24, 56],
738           [-1, -1, -1, -1, 14, 46, -1, -1, 12, 44, -1, -1, -1],
739           [-1, -1, -1, -1, -1, 30, 62, -1, -1, 28, 60, -1, -1]]
740    vec = [(8, 0), (0, 8)]
741    def colorise(val):
742        if val == 0: return '#fff'
743        if (val % 64) == 32: return '#ddf'
744        if (val % 32) == 16: return '#fdd'
745        if (val % 16) == 8: return '#dff'
746        if (val % 8) == 4: return '#ffd'
747        if (val % 4) == 2: return '#fdf'
748        return '#dfd'
749    Svg(mat, colorise).save("fig2-6-4.png", 25)
750    test25x(grad256bw, mat, vec).save("grad2-6-4.png")
751    test25x(lenna256bw, mat, vec).save("out2-6-4.png")
752
753    mat = [[ -1, -1, -1, -1, -1, -1,  8, -1],
754           [ -1, -1,  6, -1,  2,  5, 11, 26],
755           [  0,  3,  9, 24, 17, 14, 23, 20],
756           [ 15, 12, 21, 18,  7, 29, -1, -1],
757           [ -1, 27,  1,  4, 10, 25, -1, -1],
758           [ -1, -1, 16, 13, 22, 19, -1, -1],
759           [ -1, -1, -1, 28, -1, -1, -1, -1]]
760    vec = [(6, 1), (0, 5)]
761    def colorise(val): return ['#dff', '#ffd', '#fdf'][val % 3]
762    #Svg(mat, colorise).save("fig2-6-5.png", 30)
763    test25x(grad256bw, mat, vec).save("grad2-6-5.png")
764    test25x(lenna256bw, mat, vec).save("out2-6-5.png")
765
766    mat = [[ -1, -1, -1, -1, -1, -1, 24, -1, -1, -1, -1, -1, -1, -1],
767           [ -1, -1, 18, -1,  6, 15, 33, 78, -1, -1, -1, -1, 26, -1],
768           [  0,  9, 27, 72, 51, 42, 69, 60, 20, -1,  8, 17, 35, 80],
769           [ 45, 36, 63, 54, 21, 87,  2, 11, 29, 74, 53, 44, 71, 62],
770           [ -1, 81,  3, 12, 30, 75, 47, 38, 65, 56, 23, 89, -1, -1],
771           [ -1, -1, 48, 39, 66, 57, 25, 83,  5, 14, 32, 77, -1, -1],
772           [ -1, -1, 19, 84,  7, 16, 34, 79, 50, 41, 68, 59, -1, -1],
773           [  1, 10, 28, 73, 52, 43, 70, 61, -1, 86, -1, -1, -1, -1],
774           [ 46, 37, 64, 55, 22, 88, -1, -1, -1, -1, -1, -1, -1, -1],
775           [ -1, 82,  4, 13, 31, 76, -1, -1, -1, -1, -1, -1, -1, -1],
776           [ -1, -1, 49, 40, 67, 58, -1, -1, -1, -1, -1, -1, -1, -1],
777           [ -1, -1, -1, 85, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1]]
778    test25x(grad256bw, mat, vec).save("grad2-6-6.png")
779    test25x(lenna256bw, mat, vec).save("out2-6-6.png")
780
781# Output 2.7.1: void and cluster matrix generation
782def makegauss(n):
783    c = (-1. + n) / 2
784    mat = Matrix(n, n)
785    for y in range(n):
786        for x in range(n):
787            mat[y][x] = math.exp(- ((c - x) * (c - x) + (c - y) * (c - y)) / (0.05 * n * n))
788    return mat
789
790def countones(mat):
791    total = 0
792    for l in mat:
793        for x in l:
794            if x:
795                total += 1
796    return total
797
798GAUSS77 = makegauss(7)
799GAUSS99 = makegauss(9)
800
801def getminmax(mat, c):
802    min = 9999.
803    max = 0.
804    h = len(mat)
805    w = len(mat[0])
806    for y in range(h):
807        for x in range(w):
808            if mat[y][x] != c:
809                continue
810            total = 0.
811            for j in range(7):
812                for i in range(7):
813                    total += mat[(y + j - 3 + h) % h][(x + i - 3 + w) % w] \
814                              * GAUSS77[j][i]
815            if total > max:
816                (max, max_x, max_y) = (total, x, y)
817            if total < min:
818                (min, min_x, min_y) = (total, x, y)
819    return (min_x, min_y, max_x, max_y)
820
821def makeuniform(n):
822    random.seed(0)
823    mat = Matrix(n, n)
824    for t in range(n * n / 10):
825        x = (int)(random.random() * n)
826        y = (int)(random.random() * n)
827        mat[y][x] = 1
828    while True:
829        (dummy0, dummy1, x, y) = getminmax(mat, 1.)
830        mat[y][x] = 0.
831        (x2, y2, dummy0, dummy1) = getminmax(mat, 0.)
832        mat[y2][x2] = 1.
833        if x2 == x and y2 == y:
834            break
835    return mat
836
837def makevoidandcluster(n):
838    vnc = Matrix(n, n)
839    # Step 1: step down to zero
840    mat = makeuniform(n)
841    rank = countones(mat)
842    while rank > 0:
843        rank -= 1
844        (dummy0, dummy1, x, y) = getminmax(mat, 1.)
845        mat[y][x] = 0.
846        vnc[y][x] = rank
847    # Step 2: step up to n * n
848    mat = makeuniform(n)
849    rank = countones(mat)
850    while rank < n * n:
851        (x, y, dummy0, dummy1) = getminmax(mat, 0.)
852        mat[y][x] = 1.
853        vnc[y][x] = rank
854        rank += 1
855    return vnc
856
857def vnccol(n):
858    return lambda val : ['#fff', '#ddd'][val < n * n / 10]
859
860if chapter(2):
861    tmp = makevoidandcluster(14)
862    Svg(tmp, vnccol(14)).save("fig2-7-1.png", 25)
863    ordereddither(grad256bw, tmp).save("grad2-7-1.png")
864    ordereddither(lenna256bw, tmp).save("out2-7-1.png")
865    tmp = makevoidandcluster(25)
866    Svg(tmp, vnccol(25)).save("fig2-7-2.png", 20)
867    ordereddither(grad256bw, tmp).save("grad2-7-2.png")
868    ordereddither(lenna256bw, tmp).save("out2-7-2.png")
869
870##############################################################################
871if chapter(3):
872    print "Chapter 3. Error diffusion"
873
874# Output 3.0.1: naive error diffusion
875# Output 3.1.1: standard Floyd-Steinberg
876# Output 3.1.2: serpentine Floyd-Steinberg
877# FIXME: serpentine only works if rows == offset * 2 + 1
878# Output 3.2.1: Fan (modified Floyd-Steinberg)
879# Output 3.2.1b: Shiau-Fan 1
880# Output 3.2.1c: Shiau-Fan 2
881# Output 3.2.2: Jarvis, Judice and Ninke
882# Output 3.2.3: Stucki
883# Output 3.2.4: Burkes
884# Output 3.2.5: Sierra
885# Output 3.2.6: Two-line Sierra
886# Output 3.2.7: Sierra's Filter Lite
887# Output 3.2.8: Atkinson
888def test3xx(src, mat, serpentine):
889    (w, h) = src.size()
890    dest = Image((w, h))
891    lines = len(mat)
892    rows = len(mat[0])
893    offset = mat[0].index(-1)
894    ey = Matrix(w + rows - 1, lines, 0.)
895    for y in range(h):
896        ex = [0.] * (rows - offset)
897        if serpentine and y & 1:
898            xrange = range(w - 1, -1, -1)
899        else:
900            xrange = range(w)
901        for x in xrange:
902            # Set pixel
903            c = src.getGray(x, y) + ex[0] + ey[0][x + offset]
904            d = c > 0.5
905            dest.setGray(x, y, d)
906            error = c - d
907            # Propagate first line of error
908            for dx in range(rows - offset - 2):
909                ex[dx] = ex[dx + 1] + error * mat[0][offset + 1 + dx]
910            ex[rows - offset - 2] = error * mat[0][rows - 1]
911            # Propagate next lines
912            if serpentine and y & 1:
913                for dy in range(1, lines):
914                    for dx in range(rows):
915                        ey[dy][x + dx] += error * mat[dy][rows - 1 - dx]
916            else:
917                for dy in range(1, lines):
918                    for dx in range(rows):
919                        ey[dy][x + dx] += error * mat[dy][dx]
920        for dy in range(lines - 1):
921            ey[dy] = ey[dy + 1]
922        ey[lines - 1] = [0.] * (w + rows - 1)
923    return dest
924
925ERROR_NAIVE = \
926    [[ -1, 1]]
927ERROR_FSTEIN = \
928    [[    0.,    -1, 7./16],
929     [ 3./16, 5./16, 1./16]]
930ERROR_FAN = \
931    [[    0.,    0.,    -1, 7./16],
932     [ 1./16, 3./16, 5./16,    0.]]
933ERROR_SHIAUFAN = \
934    [[    0.,    0.,    -1, 8./16],
935     [ 2./16, 2./16, 4./16,    0.]]
936ERROR_SHIAUFAN2 = \
937    [[    0.,    0.,    0.,    -1, 8./16],
938     [ 1./16, 1./16, 2./16, 4./16,    0.]]
939ERROR_JAJUNI = \
940    [[    0.,    0.,    -1, 7./48, 5./48],
941     [ 3./48, 5./48, 7./48, 5./48, 3./48],
942     [ 1./48, 3./48, 5./48, 3./48, 1./48]]
943ERROR_STUCKI = \
944    [[    0.,    0.,    -1, 8./42, 4./42],
945     [ 2./42, 4./42, 8./42, 4./42, 2./42],
946     [ 1./42, 2./42, 4./42, 2./42, 1./42]]
947ERROR_BURKES = \
948    [[    0.,    0.,    -1, 8./32, 4./32],
949     [ 2./32, 4./32, 8./32, 4./32, 2./32]]
950ERROR_SIERRA = \
951    [[    0.,    0.,    -1, 5./32, 3./32],
952     [ 2./32, 4./32, 5./32, 4./32, 2./32],
953     [    0., 2./32, 3./32, 2./32,    0.]]
954ERROR_SIERRA2 = \
955    [[    0.,    0.,    -1, 4./16, 3./16],
956     [ 1./16, 2./16, 3./16, 2./16, 1./16]]
957ERROR_FILTERLITE = \
958    [[   0.,   -1, 2./4],
959     [ 1./4, 1./4,   0.]]
960ERROR_ATKINSON = \
961    [[   0.,   -1, 1./8, 1./8],
962     [ 1./8, 1./8, 1./8,   0.],
963     [   0., 1./8,   0.,   0.]]
964## This is Stevenson-Arce in hex lattice
965#ERROR_STAR = \
966#    [[      0.,      0.,      0.,      -1,      0.,  32./200,      0.],
967#     [ 12./200,      0., 26./200,      0., 30./200,       0., 16./200],
968#     [      0., 12./200,      0., 26./200,      0.,  12./200,      0.],
969#     [  5./200,      0., 12./200,      0., 12./200,       0.,  5./200]]
970## This is an attempt at implementing Stevenson-Arce in square lattice
971#ERROR_STAR = \
972#    [[      0.,      0.,      -1, 32./200,      0.],
973#     [  6./200, 19./200, 28./200, 23./200,  8./200],
974#     [      0., 12./200, 26./200, 12./200,      0.],
975#     [  2./200,  9./200, 12./200,  9./200,  2./200]]
976
977if chapter(3):
978    test3xx(grad256bw, ERROR_NAIVE, False).save("grad3-0-1.png")
979    test3xx(lenna256bw, ERROR_NAIVE, False).save("out3-0-1.png")
980
981    Svg(ERROR_FSTEIN).save("fig3-1-1.png", 40)
982    test3xx(grad256bw, ERROR_FSTEIN, False).save("grad3-1-1.png")
983    test3xx(lenna256bw, ERROR_FSTEIN, False).save("out3-1-1.png")
984    test3xx(grad256bw, ERROR_FSTEIN, True).save("grad3-1-2.png")
985    test3xx(lenna256bw, ERROR_FSTEIN, True).save("out3-1-2.png")
986
987    Svg(ERROR_FAN).save("fig3-2-1.png", 40)
988    test3xx(grad256bw, ERROR_FAN, False).save("grad3-2-1.png")
989    test3xx(lenna256bw, ERROR_FAN, False).save("out3-2-1.png")
990    Svg(ERROR_SHIAUFAN).save("fig3-2-1b.png", 40)
991    test3xx(grad256bw, ERROR_SHIAUFAN, False).save("grad3-2-1b.png")
992    test3xx(lenna256bw, ERROR_SHIAUFAN, False).save("out3-2-1b.png")
993    Svg(ERROR_SHIAUFAN2).save("fig3-2-1c.png", 40)
994    test3xx(grad256bw, ERROR_SHIAUFAN2, False).save("grad3-2-1c.png")
995    test3xx(lenna256bw, ERROR_SHIAUFAN2, False).save("out3-2-1c.png")
996
997    Svg(ERROR_JAJUNI).save("fig3-2-2.png", 40)
998    test3xx(grad256bw, ERROR_JAJUNI, False).save("grad3-2-2.png")
999    test3xx(lenna256bw, ERROR_JAJUNI, False).save("out3-2-2.png")
1000
1001    Svg(ERROR_STUCKI).save("fig3-2-3.png", 40)
1002    test3xx(grad256bw, ERROR_STUCKI, False).save("grad3-2-3.png")
1003    test3xx(lenna256bw, ERROR_STUCKI, False).save("out3-2-3.png")
1004
1005    Svg(ERROR_BURKES).save("fig3-2-4.png", 40)
1006    test3xx(grad256bw, ERROR_BURKES, False).save("grad3-2-4.png")
1007    test3xx(lenna256bw, ERROR_BURKES, False).save("out3-2-4.png")
1008
1009    Svg(ERROR_SIERRA).save("fig3-2-5.png", 40)
1010    test3xx(grad256bw, ERROR_SIERRA, False).save("grad3-2-5.png")
1011    test3xx(lenna256bw, ERROR_SIERRA, False).save("out3-2-5.png")
1012
1013    Svg(ERROR_SIERRA2).save("fig3-2-6.png", 40)
1014    test3xx(grad256bw, ERROR_SIERRA2, False).save("grad3-2-6.png")
1015    test3xx(lenna256bw, ERROR_SIERRA2, False).save("out3-2-6.png")
1016
1017    Svg(ERROR_FILTERLITE).save("fig3-2-7.png", 40)
1018    test3xx(grad256bw, ERROR_FILTERLITE, False).save("grad3-2-7.png")
1019    test3xx(lenna256bw, ERROR_FILTERLITE, False).save("out3-2-7.png")
1020
1021    Svg(ERROR_ATKINSON).save("fig3-2-8.png", 40)
1022    test3xx(grad256bw, ERROR_ATKINSON, False).save("grad3-2-8.png")
1023    test3xx(lenna256bw, ERROR_ATKINSON, False).save("out3-2-8.png")
1024
1025    #test3xx(grad256bw, ERROR_STAR, False).save("grad3-2-9.png")
1026    #test3xx(lenna256bw, ERROR_STAR, False).save("out3-2-9.png")
1027
1028    #test3xx(grad256bw, ERROR_STAR, False).save("grad3-2-9.png")
1029    #test3xx(lenna256bw, ERROR_STAR, False).save("out3-2-9.png")
1030
1031# Output 3.3.1: Floyd-Steinberg on grey 90%
1032# Output 3.3.2: serpentine Floyd-Steinberg on grey 90%
1033if chapter(3):
1034    tmp = Image((128, 128))
1035    for y in range(128):
1036        for x in range(128):
1037            tmp.setGray(x, y, 0.90)
1038    test3xx(tmp, ERROR_FSTEIN, True).getZoom(2).save("out3-3-2.png")
1039    test3xx(tmp, ERROR_FSTEIN, False).getZoom(2).save("out3-3-1.png")
1040
1041# Output 3.3.3: Riemersma dither on a Hilbert curve
1042# Output 3.3.4: Riemersma dither on a Hilbert 2 curve
1043def hilbert(x, y, n):
1044    d1 = [0, 4, 3, 2, 1]
1045    d2 = [0, 3, 4, 1, 2]
1046    m = n/2
1047
1048    if x == n - 1 and y == 0: return 0
1049    if x == 0 and y == m - 1: return 1
1050    if x == m - 1 and y == m: return 4
1051    if x == n - 1 and y == m: return 2
1052
1053    if y >= m: return hilbert(x % m, y - m, m)
1054    if x < m: return d1[hilbert(y, x, m)]
1055    else: return d2[hilbert(m - 1 - y, n - 1 - x, m)]
1056
1057def hilbert2(x, y, n):
1058    d1 = [0, 2, 1, 3, 4]
1059    d2 = [0, 1, 2, 4, 3]
1060    d3 = [0, 2, 1, 4, 3]
1061    m = n/3
1062
1063    if x == n - 1 and y == n - 1: return 0
1064    if x == m - 1 and y == m - 1: return 4
1065    if x == 2 * m - 1 and y == 0: return 4
1066    if x == n - 1 and y == m - 1: return 1
1067    if x == 0 and y == 2 * m - 1: return 1
1068    if x == m and y == m: return 3
1069    if x == m * 2 and y == m * 2 - 1: return 3
1070    if x == m - 1 and y == n - 1: return 4
1071    if x == 2 * m - 1 and y == 2 * m: return 4
1072
1073    if (x < m or x >= m * 2) and (y < m or y >= m * 2):
1074        return hilbert2(x % m, y % m, m)
1075    if x >= m and x < m * 2 and y >= m and y < m * 2:
1076        return d3[hilbert2(2 * m - 1 - x, 2 * m - 1 - y, m)]
1077    if x >= m and x < m * 2:
1078        return d1[hilbert2(x - m, m - 1 - (y % m), m)]
1079    else: # if y >= m and y < m * 2
1080        return d2[hilbert2(m - 1 - (x % m), y - m, m)]
1081
1082def riemersma(src, iterator, order):
1083    (w, h) = src.size()
1084    dest = Image((w, h))
1085    q = 16
1086    r = 16
1087    size = 1
1088    while size < w or size < h: size *= order
1089    coord = [(0, 0), (0, 1), (0, -1), (-1, 0), (1, 0)]
1090    err = [0] * q
1091    list = [math.exp(math.log(r) * i / (q - 1)) / r for i in range(q)]
1092    (x, y) = (0, 0)
1093    out = False
1094    while True:
1095        if not out:
1096            a = c = src.getGray(x, y)
1097            for i in range(q):
1098                a += err[i] * list[i]
1099            d = a > 0.5
1100            dest.setGray(x, y, d)
1101            for i in range(q - 1):
1102                err[i] = err[i + 1]
1103            err[q - 1] = c - d
1104        t = iterator(x, y, size)
1105        if t == 0:
1106            break
1107        dx, dy = coord[t]
1108        x += dx
1109        y += dy
1110        if out:
1111            if x < w and y < h:
1112                err = [0] * q
1113                out = False
1114            continue
1115        # Did we fall off the screen?
1116        out = (x > w + q or y > h + q)
1117    return dest
1118
1119if chapter(3):
1120    riemersma(grad256bw, hilbert, 2).save("grad3-3-3.png")
1121    riemersma(lenna256bw, hilbert, 2).save("out3-3-3.png")
1122    riemersma(grad256bw, hilbert2, 3).save("grad3-3-4.png")
1123    riemersma(lenna256bw, hilbert2, 3).save("out3-3-4.png")
1124
1125# Output 3.3.5: spatial Hilbert dither on a Hilbert curve
1126# Output 3.3.6: spatial Hilbert dither on a Hilbert 2 curve
1127def spatialhilbert(src, iterator, order):
1128    (w, h) = src.size()
1129    dest = Image((w, h))
1130    q = 16
1131    size = 1
1132    while size < w or size < h: size *= order
1133    coord = [(0, 0), (0, 1), (0, -1), (-1, 0), (1, 0)]
1134    err = [0] * q
1135    dx = [0] * q
1136    dy = [0] * q
1137    dist = [0] * q
1138    (x, y) = (0, 0)
1139    out = False
1140    while True:
1141        if not out:
1142            c = src.getGray(x, y) + err[0]
1143            d = c > 0.5
1144            dest.setGray(x, y, d)
1145        t = iterator(x, y, size)
1146        if t == 0:
1147            break
1148        dx[0], dy[0] = coord[t]
1149        if not out:
1150            error = c - d
1151            errdiv = 0.
1152            for i in range(q - 1):
1153                t = coord[iterator(x + dx[i], y + dy[i], size)]
1154                dx[i + 1] = dx[i] + t[0]
1155                dy[i + 1] = dy[i] + t[1]
1156            for i in range(q):
1157                dist[i] = dx[i] * dx[i] + dy[i] * dy[i]
1158                errdiv += 1. / dist[i]
1159            error /= errdiv
1160            for i in range(q - 1):
1161                err[i] = err[i + 1] + error / dist[i]
1162            err[q - 1] = error / dist[q - 1]
1163        x += dx[0]
1164        y += dy[0]
1165        if out:
1166            if x < w and y < h:
1167                err = [0] * q
1168                out = False
1169            continue
1170        # Did we fall off the screen?
1171        out = (x > w + q or y > h + q)
1172    return dest
1173
1174if chapter(3):
1175    spatialhilbert(grad256bw, hilbert, 2).save("grad3-3-5.png")
1176    spatialhilbert(lenna256bw, hilbert, 2).save("out3-3-5.png")
1177    spatialhilbert(grad256bw, hilbert2, 3).save("grad3-3-6.png")
1178    spatialhilbert(lenna256bw, hilbert2, 3).save("out3-3-6.png")
1179
1180# Output 3.3.7: Knuth's dot diffusion
1181# Output 3.3.8: Knuth's dot diffusion, sharpen = 0.9
1182# Output 3.3.9: Knuth's dot diffusion, sharpen = 0.9, zeta = 0.2
1183# Output 3.3.10: serpentine Floyd-Steinberg, sharpen = 0.9
1184def sharpen(src, sharpening):
1185    (w, h) = src.size()
1186    dest = Image((w, h))
1187    for y in range(h):
1188        for x in range(w):
1189            c = src.getGray(x, y)
1190            total = 0.
1191            for j in range(-1, 2):
1192                for i in range(-1, 2):
1193                    total += src.getGray(x + i, y + j)
1194            total /= 9.
1195            d = (c - sharpening * total) / (1.0 - sharpening)
1196            if d < 0.:
1197                d = 0.
1198            elif d > 1.:
1199                d = 1.
1200            dest.setGray(x, y, d)
1201    return dest
1202
1203def test337(src, mat, propagate, zeta):
1204    (w, h) = src.size()
1205    dest = Image((w, h))
1206    dx = len(mat[0])
1207    dy = len(mat)
1208    # 0. analyse diffusion matrix to speed up things later
1209    diff = []
1210    cx, cy = -1, -1
1211    for y in range(len(propagate)):
1212        for x in range(len(propagate[0])):
1213            if propagate[y][x] == -1:
1214                cx, cy = x, y
1215    for y in range(len(propagate)):
1216        for x in range(len(propagate[0])):
1217            diff.append((x - cx, y - cy, propagate[y][x]))
1218    # 1. analyse order matrix to get equivalence classes
1219    nclasses = 0
1220    for l in mat:
1221        for v in l:
1222            if v + 1 > nclasses:
1223                nclasses = v + 1
1224    classes = [[] for n in range(nclasses)]
1225    for y in range(h):
1226        for x in range(w):
1227            classes[mat[y % dy][x % dx]].append((x, y))
1228    # 2. copy image
1229    img = [[src.getGray(x, y) for x in range(w)] for y in range(h)]
1230    aa = Matrix(w, h, 1.)
1231    # 3. parse all classes
1232    for l in classes:
1233        for x, y in l:
1234            c = img[y][x]
1235            if aa[y][x] == 1.:
1236                error = c + 4. * zeta
1237            else:
1238                error = c - zeta
1239                if x > 0 and aa[y][x-1] == 1.: error += zeta
1240                if y > 0 and aa[y-1][x] == 1.: error += zeta
1241                if x < w-1 and aa[y][x+1] == 1.: error += zeta
1242                if y < h-1 and aa[y+1][x] == 1.: error += zeta
1243            if c + error < 1.:
1244                aa[y][x] = 0.
1245                if x > 0 and aa[y][x-1] == 1.: aa[y][x-1] = .5
1246                if y > 0 and aa[y-1][x] == 1.: aa[y-1][x] = .5
1247                if x < w-1 and aa[y][x+1] == 1.: aa[y][x+1] = .5
1248                if y < h-1 and aa[y+1][x] == 1.: aa[y+1][x] = .5
1249            else:
1250                error = c - 1.
1251            # Propagate first line of error
1252            total = 0
1253            err = []
1254            for (i, j, e) in diff:
1255                if x + i < 0 or x + i >= w \
1256                    or y + j < 0 or y + j >= h:
1257                    continue
1258                n = mat[(y + j) % dy][(x + i) % dx] - mat[y % dy][x % dx]
1259                if n <= 0:
1260                    continue
1261                err.append((i, j, e))
1262                total += e
1263            for (i, j, e) in err:
1264                img[y + j][x + i] += error * e / total
1265    # 4. copy image, replacing grey with white
1266    for y in range(h):
1267        for x in range(w):
1268            dest.setGray(x, y, aa[y][x] > 0.)
1269    return dest
1270
1271ERROR_DOT = \
1272    [[1./12, 1./6, 1./12],
1273     [ 1./6,   -1,  1./6],
1274     [1./12, 1./6, 1./12]]
1275
1276if chapter(3):
1277    Svg(ERROR_DOT).save("fig3-3-7b.png", 40)
1278    test337(grad256bw, DITHER_CLUSTER88, ERROR_DOT, 0.).save("grad3-3-7.png")
1279    test337(lenna256bw, DITHER_CLUSTER88, ERROR_DOT, 0.).save("out3-3-7.png")
1280    tmp = sharpen(grad256bw, 0.9)
1281    test337(tmp, DITHER_CLUSTER88, ERROR_DOT, 0.).save("grad3-3-8.png")
1282    test337(tmp, DITHER_CLUSTER88, ERROR_DOT, 0.2).save("grad3-3-9.png")
1283    test3xx(tmp, ERROR_FSTEIN, True).save("grad3-3-10.png")
1284    tmp = sharpen(lenna256bw, 0.9)
1285    test337(tmp, DITHER_CLUSTER88, ERROR_DOT, 0.).save("out3-3-8.png")
1286    test337(tmp, DITHER_CLUSTER88, ERROR_DOT, 0.2).save("out3-3-9.png")
1287    test3xx(tmp, ERROR_FSTEIN, True).save("out3-3-10.png")
1288
1289# Output 3.3.11: omni-directional error diffusion
1290def test3311(src):
1291    w, h = src.size()
1292    g = { 0: 0, 1: 1, 2: 1 }
1293    g[-1] = g[2] - g[1]
1294    g[-2] = g[1] - g[0]
1295    n = 2
1296    while g[n] < w and g[n] < h:
1297        n += 1
1298        g[n] = g[n - 1] + g[n - 3]
1299        g[-n] = g[-n + 3] - g[-n + 2]
1300    u = g[n - 2]
1301    v = g[n - 1]
1302    a = [[(i * u + j * v) % g[n] for i in range(g[n])] for j in range(g[n])]
1303    return a
1304
1305ERROR_OMNI = \
1306    [[0.1, 0.2, 0.1],
1307     [0.1,  -1, 0.1],
1308     [0.1, 0.2, 0.1]]
1309
1310if chapter(3):
1311    Svg(ERROR_OMNI).save("fig3-3-11.png", 40)
1312
1313    mat = test3311(grad256bw)
1314    test337(grad256bw, mat, ERROR_OMNI, 0.).save("grad3-3-11.png")
1315
1316    mat = test3311(lenna256bw)
1317    tmp = [[str(mat[y][x]) for x in range(16)] for y in range(12)]
1318    for x in range(16): tmp[11][x] = '...'
1319    for y in range(12): tmp[y][15] = '...'
1320    Svg(tmp).save("fig3-3-11b.png", 20)
1321    test337(lenna256bw, mat, ERROR_OMNI, 0.).save("out3-3-11.png")
1322
1323# Output 3.4.1: Ostromoukhov's variable error diffusion
1324def test341(src, serpentine):
1325    m = [[13, 0, 5], [13, 0, 5], [21, 0, 10], [7, 0, 4],
1326         [8, 0, 5], [47, 3, 28], [23, 3, 13], [15, 3, 8],
1327         [22, 6, 11], [43, 15, 20], [7, 3, 3], [501, 224, 211],
1328         [249, 116, 103], [165, 80, 67], [123, 62, 49], [489, 256, 191],
1329         [81, 44, 31], [483, 272, 181], [60, 35, 22], [53, 32, 19],
1330         [237, 148, 83], [471, 304, 161], [3, 2, 1], [481, 314, 185],
1331         [354, 226, 155], [1389, 866, 685], [227, 138, 125], [267, 158, 163],
1332         [327, 188, 220], [61, 34, 45], [627, 338, 505], [1227, 638, 1075],
1333         [20, 10, 19], [1937, 1000, 1767], [977, 520, 855], [657, 360, 551],
1334         [71, 40, 57], [2005, 1160, 1539], [337, 200, 247], [2039, 1240, 1425],
1335         [257, 160, 171], [691, 440, 437], [1045, 680, 627], [301, 200, 171],
1336         [177, 120, 95], [2141, 1480, 1083], [1079, 760, 513], [725, 520, 323],
1337         [137, 100, 57], [2209, 1640, 855], [53, 40, 19], [2243, 1720, 741],
1338         [565, 440, 171], [759, 600, 209], [1147, 920, 285], [2311, 1880, 513],
1339         [97, 80, 19], [335, 280, 57], [1181, 1000, 171], [793, 680, 95],
1340         [599, 520, 57], [2413, 2120, 171], [405, 360, 19], [2447, 2200, 57],
1341         [11, 10, 0], [158, 151, 3], [178, 179, 7], [1030, 1091, 63],
1342         [248, 277, 21], [318, 375, 35], [458, 571, 63], [878, 1159, 147],
1343         [5, 7, 1], [172, 181, 37], [97, 76, 22], [72, 41, 17],
1344         [119, 47, 29], [4, 1, 1], [4, 1, 1], [4, 1, 1],
1345         [4, 1, 1], [4, 1, 1], [4, 1, 1], [4, 1, 1],
1346         [4, 1, 1], [4, 1, 1], [65, 18, 17], [95, 29, 26],
1347         [185, 62, 53], [30, 11, 9], [35, 14, 11], [85, 37, 28],
1348         [55, 26, 19], [80, 41, 29], [155, 86, 59], [5, 3, 2],
1349         [5, 3, 2], [5, 3, 2], [5, 3, 2], [5, 3, 2],
1350         [5, 3, 2], [5, 3, 2], [5, 3, 2], [5, 3, 2],
1351         [5, 3, 2], [5, 3, 2], [5, 3, 2], [5, 3, 2],
1352         [305, 176, 119], [155, 86, 59], [105, 56, 39], [80, 41, 29],
1353         [65, 32, 23], [55, 26, 19], [335, 152, 113], [85, 37, 28],
1354         [115, 48, 37], [35, 14, 11], [355, 136, 109], [30, 11, 9],
1355         [365, 128, 107], [185, 62, 53], [25, 8, 7], [95, 29, 26],
1356         [385, 112, 103], [65, 18, 17], [395, 104, 101], [4, 1, 1]]
1357    (w, h) = src.size()
1358    dest = Image((w, h))
1359    ey = [0.] * (w + 2)
1360    for y in range(h):
1361        ex = 0
1362        newey = [0.] * (w + 2)
1363        if serpentine and y & 1:
1364            xrange = range(w - 1, -1, -1)
1365        else:
1366            xrange = range(w)
1367        for x in xrange:
1368            # Set pixel
1369            c = src.getGray(x, y) + ex + ey[x + 1]
1370            d = c > 0.5
1371            dest.setGray(x, y, d)
1372            error = c - d
1373            i = (int)(c * 255.9999)
1374            if i > 127:
1375                i = 255 - i
1376            (d1, d2, d3) = m[i]
1377            t = d1 + d2 + d3
1378            # Propagate error
1379            ex = error * d1 / t
1380            if serpentine and y & 1:
1381                newey[x + 2] += error * d3 / t
1382                newey[x + 1] += error * d2 / t
1383            else:
1384                newey[x] += error * d2 / t
1385                newey[x + 1] += error * d3 / t
1386        ey = newey
1387    return dest
1388
1389if chapter(3):
1390    mat = [[0, -1,
1391            '<tspan style="font-style:italic">d1(i)</tspan>'],
1392           ['<tspan style="font-style:italic">d2(i)</tspan>',
1393            '<tspan style="font-style:italic">d3(i)</tspan>', 0]]
1394    Svg(mat).save("fig3-4-1.png", 40)
1395    test341(grad256bw, True).save("grad3-4-1.png")
1396    test341(lenna256bw, True).save("out3-4-1.png")
1397
1398def test351(src, mat, tiles, diff, serpentine, glob):
1399    random.seed(0)
1400    (w, h) = src.size()
1401    dest = Image((w, h))
1402    ntiles = len(tiles)
1403    ty = len(tiles[0])
1404    tx = len(tiles[0][0])
1405    cur = Matrix(tx, ty, 0.)
1406    w, h = w / tx, h / ty
1407    lines = len(mat)
1408    rows = len(mat[0])
1409    offset = mat[0].index(-1)
1410    ey = Matrix(w + rows - 1, lines, 0.)
1411    for y in range(h):
1412        ex = [0.] * (rows - offset)
1413        if serpentine and y & 1:
1414            xrange = range(w - 1, -1, -1)
1415        else:
1416            xrange = range(w)
1417        for x in xrange:
1418            # Get block value
1419            for j in range(ty):
1420                for i in range(tx):
1421                    e = ex[0] + ey[0][x + offset]
1422                    cur[j][i] = src.getGray(x * tx + i, y * ty + j) \
1423                                  + diff[j][i] * e
1424            # Compute closest block and associated error
1425            dist = tx * ty
1426            for n in range(ntiles):
1427                e = 0.
1428                d1 = 0.
1429                d2 = random.random() / 1000.
1430                for j in range(ty):
1431                    for i in range(tx):
1432                        e += cur[j][i] - tiles[n][j][i]
1433                        d1 += diff[j][i] * (cur[j][i] - tiles[n][j][i])
1434                        d2 += diff[j][i] * abs(cur[j][i] - tiles[n][j][i])
1435                if glob:
1436                    d = abs(d1) + d2 / 1000.
1437                else:
1438                    d = abs(d1) / (tx * ty) + d2
1439                if d < dist:
1440                    dist = d
1441                    error = e
1442                    best = n
1443            # Set pixel
1444            for j in range(ty):
1445                for i in range(tx):
1446                    dest.setGray(x * tx + i, y * ty + j, tiles[best][j][i])
1447            # Propagate first line of error
1448            for dx in range(rows - offset - 2):
1449                ex[dx] = ex[dx + 1] + error * mat[0][offset + 1 + dx]
1450            ex[rows - offset - 2] = error * mat[0][rows - 1]
1451            # Propagate next lines
1452            if serpentine and y & 1:
1453                for dy in range(1, lines):
1454                    for dx in range(rows):
1455                        ey[dy][x + dx] += error * mat[dy][rows - 1 - dx]
1456            else:
1457                for dy in range(1, lines):
1458                    for dx in range(rows):
1459                        ey[dy][x + dx] += error * mat[dy][dx]
1460        for dy in range(lines - 1):
1461            ey[dy] = ey[dy + 1]
1462        ey[lines - 1] = [0.] * (w + rows - 1)
1463    return dest
1464
1465LINES22 = \
1466    [[[0., 0.], [0., 0.]],
1467     [[0., 1.], [0., 1.]],
1468     [[1., 0.], [1., 0.]],
1469     [[1., 1.], [0., 0.]],
1470     [[0., 0.], [1., 1.]],
1471     [[1., 1.], [1., 1.]]]
1472
1473SQUARES33 = \
1474    [[[0., 0., 0.], [0., 0., 0.], [0., 0., 0.]],
1475     [[1., 0., 0.], [0., 0., 0.], [0., 0., 0.]],
1476     [[0., 1., 0.], [0., 0., 0.], [0., 0., 0.]],
1477     [[0., 0., 1.], [0., 0., 0.], [0., 0., 0.]],
1478     [[0., 0., 0.], [1., 0., 0.], [0., 0., 0.]],
1479     [[0., 0., 0.], [0., 1., 0.], [0., 0., 0.]],
1480     [[0., 0., 0.], [0., 0., 1.], [0., 0., 0.]],
1481     [[0., 0., 0.], [0., 0., 0.], [1., 0., 0.]],
1482     [[0., 0., 0.], [0., 0., 0.], [0., 1., 0.]],
1483     [[0., 0., 0.], [0., 0., 0.], [0., 0., 1.]],
1484     [[1., 1., 0.], [1., 1., 0.], [0., 0., 0.]],
1485     [[0., 1., 1.], [0., 1., 1.], [0., 0., 0.]],
1486     [[0., 0., 0.], [1., 1., 0.], [1., 1., 0.]],
1487     [[0., 0., 0.], [0., 1., 1.], [0., 1., 1.]],
1488     [[1., 1., 1.], [1., 1., 1.], [1., 1., 1.]]]
1489
1490TILES22 = []
1491for n in range(1 << 4):
1492    mat = Matrix(2, 2)
1493    for y in range(2):
1494        for x in range(2):
1495            if n & (1 << (y * 2 + x)):
1496                mat[y][x] = 1.
1497    TILES22.append(mat)
1498
1499DIFF_EVEN22 = \
1500    [[1./4, 1./4],
1501     [1./4, 1./4]]
1502
1503DIFF_EVEN33 = \
1504    [[1./9, 1./9, 1./9],
1505     [1./9, 1./9, 1./9],
1506     [1./9, 1./9, 1./9]]
1507
1508if chapter(3):
1509    def colorise(val):
1510        if type(val) == str:
1511            return '#fbb'
1512        return '#fff'
1513
1514    mat = [[.25, .25],
1515           [.25, .25]]
1516    Svg(mat).save("fig3-5-2b.png", 40)
1517    mat = [[0, 0, 'a', 'b', 7./64, 7./64],
1518           [0, 0, 'c', 'd', 7./64, 7./64],
1519           [3./64, 3./64, 5./64, 5./64, 1./64, 1./64],
1520           [3./64, 3./64, 5./64, 5./64, 1./64, 1./64]]
1521    Svg(mat, colorise).save("fig3-5-2.png", 40)
1522
1523    test351(grad256bw, [[-1, 0]],
1524            LINES22, DIFF_EVEN22, True, True).save("grad3-5-1.png")
1525    test351(lenna256bw, [[-1, 0]],
1526            LINES22, DIFF_EVEN22, True, True).save("out3-5-1.png")
1527    test351(grad256bw, ERROR_FSTEIN,
1528            LINES22, DIFF_EVEN22, True, True).save("grad3-5-2.png")
1529    test351(lenna256bw, ERROR_FSTEIN,
1530            LINES22, DIFF_EVEN22, True, True).save("out3-5-2.png")
1531
1532    test351(grad256bw, ERROR_FSTEIN,
1533            SQUARES33, DIFF_EVEN33, True, True).save("grad3-5-3.png")
1534    test351(lenna256bw, ERROR_FSTEIN,
1535            SQUARES33, DIFF_EVEN33, True, True).save("out3-5-3.png")
1536
1537    test351(grad256bw, ERROR_FSTEIN,
1538            TILES22, DIFF_EVEN22, True, True).save("grad3-5-4.png")
1539    test351(lenna256bw, ERROR_FSTEIN,
1540            TILES22, DIFF_EVEN22, True, True).save("out3-5-4.png")
1541
1542DIFF_WEIGHED22 = \
1543    [[51./128, 33./128],
1544     [25./128, 19./128]]
1545
1546if chapter(3):
1547    mat = [[6./16, 5./16],
1548           [3./16, 2./16]]
1549    Svg(mat).save("fig3-5-5b.png", 40)
1550    mat = [[0, 0, 'a', 'b', 6*7./256, 5*7./256],
1551           [0, 0, 'c', 'd', 3*7./256, 2*7./256],
1552           [6*3./256, 5*3./256, 6*5./256, 5*5./256, 6*1./256, 5*1./256],
1553           [3*3./256, 2*3./256, 3*5./256, 2*5./256, 3*1./256, 2*1./256]]
1554    Svg(mat, colorise).save("fig3-5-5.png", 40)
1555
1556    test351(grad256bw, ERROR_FSTEIN,
1557            TILES22, DIFF_WEIGHED22, True, False).save("grad3-5-5.png")
1558    test351(lenna256bw, ERROR_FSTEIN,
1559            TILES22, DIFF_WEIGHED22, True, False).save("out3-5-5.png")
1560
1561# Output 3.6.1: sub-block error diffusion
1562def test361(src, tiles, propagate, diff):
1563    (w, h) = src.size()
1564    # Propagating the error to a temporary buffer is becoming more and
1565    # more complicated. We decide to use an intermediate matrix instead.
1566    tmp = Matrix(w, h, 0.)
1567    for y in range(h):
1568        for x in range(w):
1569            tmp[y][x] = Gamma.CtoI(src.getGray(x, y))
1570    dest = Image((w, h))
1571    # Analyse tile list
1572    ntiles = len(tiles)
1573    ty = len(tiles[0])
1574    tx = len(tiles[0][0])
1575    cur = Matrix(tx, ty, 0.)
1576    w, h = w / tx, h / ty
1577    # Analyse error propagate list
1578    for y in range(h):
1579        for x in range(w):
1580            # Get block value
1581            for j in range(ty):
1582                for i in range(tx):
1583                    cur[j][i] = Gamma.ItoC(tmp[y * ty + j][x * tx + i])
1584            # Select closest block
1585            dist = tx * ty
1586            for n in range(ntiles):
1587                d = 0.
1588                e = 0.
1589                for j in range(ty):
1590                    for i in range(tx):
1591                        d += cur[j][i] - tiles[n][j][i]
1592                        e += diff[j][i] * abs(cur[j][i] - tiles[n][j][i])
1593                if abs(d) / (tx * ty) + e < dist:
1594                    dist = abs(d) / (tx * ty) + e
1595                    best = n
1596            # Set pixel
1597            for j in range(ty):
1598                for i in range(tx):
1599                    dest.setGray(x * tx + i, y * ty + j, tiles[best][j][i])
1600            # Propagate error
1601            for j in range(ty):
1602                for i in range(tx):
1603                    e = Gamma.CtoI(cur[j][i]) - Gamma.CtoI(tiles[best][j][i])
1604                    m = propagate[j][i]
1605                    for py in range(len(m)):
1606                        for px in range(len(m[0])):
1607                            if m[py][px] == 0:
1608                                continue
1609                            if m[py][px] == -1:
1610                                cx, cy = px, py
1611                                continue
1612                            tmpx = x * tx + i + px - cx
1613                            tmpy = y * ty + j + py - cy
1614                            if tmpx > w * tx - 1 or tmpy > h * ty - 1:
1615                                continue
1616                            tmp[tmpy][tmpx] += m[py][px] * e
1617    return dest
1618
1619ERROR_SUBFS22 = \
1620    [[[[0, -1, 0, 8./64],
1621       [0, 0, 0, 10./64],
1622       [7./64, 22./64, 15./64, 2./64]],
1623      [[0, 0, -1, 20./64],
1624       [0, 0, 0, 14./64],
1625       [2./64, 11./64, 15./64, 2./64]]],
1626     [[[0, 0, 0, 0./64],
1627       [0, -1, 0, 6./64],
1628       [12./64, 32./64, 13./64, 1./64]],
1629      [[0, 0, 0, 0./64],
1630       [0, 0, -1, 20./64],
1631       [0./64, 12./64, 28./64, 4./64]]]]
1632
1633if chapter(3):
1634    test361(grad256bw, TILES22,
1635            ERROR_SUBFS22, DIFF_WEIGHED22).save("grad3-6-1.png")
1636    test361(lenna256bw, TILES22,
1637            ERROR_SUBFS22, DIFF_WEIGHED22).save("out3-6-1.png")
1638
1639    test361(grad256bw, LINES22,
1640            ERROR_SUBFS22, DIFF_WEIGHED22).save("grad3-6-2.png")
1641    test361(lenna256bw, LINES22,
1642            ERROR_SUBFS22, DIFF_WEIGHED22).save("out3-6-2.png")
1643
1644    def colorise(val):
1645        if val == '':
1646            return '#ccc'
1647        if type(val) == str:
1648            return '#fbb'
1649        return '#fff'
1650
1651    # XXX: hack, we modify ERROR_SUBFS22 because it's so much more convenient
1652    for y in range(2):
1653        for x in range(2):
1654            for j in range(2):
1655                for i in range(1, 3):
1656                    ERROR_SUBFS22[y][x][j][i] = ''
1657            ERROR_SUBFS22[y][x][y][1 + x] = chr(ord('a') + 2 * y + x)
1658    Svg(ERROR_SUBFS22[0][0], colorise).save("fig3-6-1a.png", 40)
1659    Svg(ERROR_SUBFS22[0][1], colorise).save("fig3-6-1b.png", 40)
1660    Svg(ERROR_SUBFS22[1][0], colorise).save("fig3-6-1c.png", 40)
1661    Svg(ERROR_SUBFS22[1][1], colorise).save("fig3-6-1d.png", 40)
1662    for y in range(2):
1663        for x in range(2):
1664            for j in range(2):
1665                for i in range(1, 3):
1666                    ERROR_SUBFS22[y][x][j][i] = 0
1667            ERROR_SUBFS22[y][x][y][1 + x] = -1
1668
1669ERROR_SUBFS33 = \
1670    [[[[     0,     -1,      0,      0,  2./64],
1671       [     0,      0,      0,      0,  5./64],
1672       [     0,      0,      0,      0,  6./64],
1673       [ 5./64, 17./64, 17./64,  9./64,  1./64]],
1674      [[     0,      0,     -1,      0,  6./64],
1675       [     0,      0,      0,      0,  9./64],
1676       [     0,      0,      0,      0,  8./64],
1677       [ 2./64, 11./64, 16./64, 11./64,  1./64]],
1678      [[     0,      0,      0,     -1, 20./64],
1679       [     0,      0,      0,      0, 14./64],
1680       [     0,      0,      0,      0,  8./64],
1681       [     0,  3./64,  9./64,  9./64,  1./64]]],
1682     [[[     0,      0,      0,      0,      0],
1683       [     0,     -1,      0,      0,  2./64],
1684       [     0,      0,      0,      0,  5./64],
1685       [ 7./64, 23./64, 18./64,  8./64,  1./64]],
1686      [[     0,      0,      0,      0,      0],
1687       [     0,      0,     -1,      0,  6./64],
1688       [     0,      0,      0,      0,  9./64],
1689       [ 2./64, 12./64, 21./64, 13./64,  1./64]],
1690      [[     0,      0,      0,      0,      0],
1691       [     0,      0,      0,     -1, 20./64],
1692       [     0,      0,      0,      0, 14./64],
1693       [     0,  2./64, 11./64, 15./64,  2./64]]],
1694     [[[     0,      0,      0,      0,      0],
1695       [     0,      0,      0,      0,      0],
1696       [     0,     -1,      0,      0,  2./64],
1697       [12./64, 32./64, 14./64,  4./64,      0]],
1698      [[     0,      0,      0,      0,      0],
1699       [     0,      0,      0,      0,      0],
1700       [     0,      0,     -1,      0,  6./64],
1701       [     0, 12./64, 32./64, 13./64,  1./64]],
1702      [[     0,      0,      0,      0,      0],
1703       [     0,      0,      0,      0,      0],
1704       [     0,      0,      0,     -1, 20./64],
1705       [     0,      0, 12./64, 28./64,  4./64]]]]
1706
1707TILES33 = []
1708for n in range(1 << 9):
1709    mat = Matrix(3, 3)
1710    for y in range(3):
1711        for x in range(3):
1712            if n & (1 << (y * 3 + x)):
1713                mat[y][x] = 1.
1714    TILES33.append(mat)
1715
1716DIFF_WEIGHED33 = \
1717    [[15./64, 10./64,  6./64],
1718     [10./64,  6./64,  4./64],
1719     [ 6./64,  4./64,  3./64]]
1720
1721if chapter(3):
1722    test361(grad256bw, TILES33,
1723            ERROR_SUBFS33, DIFF_WEIGHED33).save("grad3-6-3.png")
1724    test361(lenna256bw, TILES33,
1725            ERROR_SUBFS33, DIFF_WEIGHED33).save("out3-6-3.png")
1726    test361(grad256bw, SQUARES33,
1727            ERROR_SUBFS33, DIFF_WEIGHED33).save("grad3-6-4.png")
1728    test361(lenna256bw, SQUARES33,
1729            ERROR_SUBFS33, DIFF_WEIGHED33).save("out3-6-4.png")
1730
1731    # XXX: hack, we modify ERROR_SUBFS33 because it's so much more convenient
1732    for y in range(3):
1733        for x in range(3):
1734            for j in range(3):
1735                for i in range(1, 4):
1736                    ERROR_SUBFS33[y][x][j][i] = ''
1737            ERROR_SUBFS33[y][x][y][1 + x] = chr(ord('a') + 3 * y + x)
1738    Svg(ERROR_SUBFS33[0][0], colorise).save("fig3-6-3a.png", 30)
1739    Svg(ERROR_SUBFS33[0][1], colorise).save("fig3-6-3b.png", 30)
1740    Svg(ERROR_SUBFS33[0][2], colorise).save("fig3-6-3c.png", 30)
1741    Svg(ERROR_SUBFS33[1][0], colorise).save("fig3-6-3d.png", 30)
1742    Svg(ERROR_SUBFS33[1][1], colorise).save("fig3-6-3e.png", 30)
1743    Svg(ERROR_SUBFS33[1][2], colorise).save("fig3-6-3f.png", 30)
1744    Svg(ERROR_SUBFS33[2][0], colorise).save("fig3-6-3g.png", 30)
1745    Svg(ERROR_SUBFS33[2][1], colorise).save("fig3-6-3h.png", 30)
1746    Svg(ERROR_SUBFS33[2][2], colorise).save("fig3-6-3i.png", 30)
1747    for y in range(3):
1748        for x in range(3):
1749            for j in range(3):
1750                for i in range(1, 4):
1751                    ERROR_SUBFS33[y][x][j][i] = 0
1752            ERROR_SUBFS33[y][x][y][1 + x] = -1
1753
1754# Output 3.7.1: direct binary search, iteration 0
1755# Output 3.7.2: direct binary search, iteration 1
1756# Output 3.7.3: direct binary search, iteration 2
1757# Output 3.7.4: direct binary search, iteration 5
1758def test37x(src):
1759    random.seed(0)
1760    (w, h) = src.size()
1761    dest = Image((w, h))
1762    for y in range(h):
1763        for x in range(w):
1764            c = src.getGray(x, y) + random.random() - 0.5
1765            d = c > 0.5
1766            dest.setGray(x, y, d)
1767    return dest
1768
1769def test37y(src, dest):
1770    threshold = 0.4
1771    kernel = Matrix(6, 6, 0.) # have a border of zeroes
1772    for j in range(4):
1773        for i in range(4):
1774             kernel[j][i] = math.pow(math.e, - math.sqrt(i * i + j * j))
1775    (w, h) = src.size()
1776    # Build fast pixel lookup tables
1777    srcmat = Matrix(w, h, 0.)
1778    destmat = Matrix(w, h, 0.)
1779    for y in range(h):
1780        for x in range(w):
1781            srcmat[y][x] = src.getGray(x, y)
1782            destmat[y][x] = dest.getGray(x, y)
1783    # Build human perception model for both source and destination
1784    srchvs = Matrix(w, h, 0.)
1785    desthvs = Matrix(w, h, 0.)
1786    for y in range(h):
1787        for x in range(w):
1788            srcp = destp = 0.
1789            for j in range(-3, 4):
1790                if y + j < 0 or y + j >= h:
1791                    continue
1792                for i in range(-3, 4):
1793                    if x + i < 0 or x + i >= w:
1794                        continue
1795                    m = kernel[abs(j)][abs(i)]
1796                    srcp += m * srcmat[y + j][x + i]
1797                    destp += m * destmat[y + j][x + i]
1798            srchvs[y][x] = srcp
1799            desthvs[y][x] = destp
1800    swaps = toggles = 0
1801    for y in range(h):
1802        for x in range(w):
1803            d = destmat[y][x]
1804            best = 0.
1805            # Compute the effect of a toggle
1806            e = 0.
1807            for j in range(-3, 4):
1808                if y + j < 0 or y + j >= h:
1809                    continue
1810                for i in range(-3, 4):
1811                    if x + i < 0 or x + i >= w:
1812                        continue
1813                    m = kernel[abs(j)][abs(i)]
1814                    p = srchvs[y + j][x + i]
1815                    q1 = desthvs[y + j][x + i]
1816                    q2 = q1 - m * d + m * (1. - d)
1817                    e += abs(q1 - p) - abs(q2 - p)
1818            if e > best:
1819                best = e
1820                op = False
1821            # Compute the effect of swaps
1822            for dx, dy in [(0, 1), (0, -1), (-1, 0), (1, 0)]:
1823                if y + dy < 0 or y + dy >= h or x + dx < 0 or x + dx >= w:
1824                    continue
1825                d2 = destmat[y + dy][x + dx]
1826                if d2 == d:
1827                    continue
1828                e = 0.
1829                for j in range(-4, 5):
1830                    for i in range(-4, 5):
1831                        if y + j < 0 or y + j >= h or x + i < 0 or x + i >= w:
1832                            continue
1833                        ma = kernel[abs(j)][abs(i)]
1834                        mb = kernel[abs(j - dy)][abs(i - dx)]
1835                        p = srchvs[y + j][x + i]
1836                        q1 = desthvs[y + j][x + i]
1837                        q2 = q1 - ma * d + ma * d2 - mb * d2 + mb * d
1838                        e += abs(q1 - p) - abs(q2 - p)
1839                if e > best:
1840                    best = e
1841                    op = (dx, dy)
1842            # Apply the change if interesting
1843            if best <= 0.:
1844                continue
1845            if op:
1846                dx, dy = op
1847                d2 = destmat[y + dy][x + dx]
1848                destmat[y + dy][x + dx] = d
1849            else:
1850                d2 = 1. - d
1851            destmat[y][x] = d2
1852            for j in range(-3, 4):
1853                for i in range(-3, 4):
1854                    m = kernel[abs(j)][abs(i)]
1855                    if y + j >= 0 and y + j < h and x + i >= 0 and x + i < w:
1856                        desthvs[y + j][x + i] -= m * d
1857                        desthvs[y + j][x + i] += m * d2
1858                    if op and y + dy + j >= 0 and y + dy + j < h \
1859                       and x + dx + i >= 0 and x + dx + i < w:
1860                        desthvs[y + dy + j][x + dx + i] -= m * d2
1861                        desthvs[y + dy + j][x + dx + i] += m * d
1862    for y in range(h):
1863        for x in range(w):
1864            dest.setGray(x, y, destmat[y][x])
1865    return dest
1866
1867if chapter(3):
1868    tmp = test37x(grad256bw)
1869    tmp.save("grad3-7-1.png")
1870    tmp = test37y(grad256bw, tmp)
1871    tmp.save("grad3-7-2.png")
1872    tmp = test37y(grad256bw, tmp)
1873    tmp.save("grad3-7-3.png")
1874    tmp = test37y(grad256bw, tmp)
1875    tmp = test37y(grad256bw, tmp)
1876    tmp = test37y(grad256bw, tmp)
1877    tmp.save("grad3-7-4.png")
1878
1879if chapter(3):
1880    tmp = test37x(lenna256bw)
1881    tmp.save("out3-7-1.png")
1882    tmp = test37y(lenna256bw, tmp)
1883    tmp.save("out3-7-2.png")
1884    tmp = test37y(lenna256bw, tmp)
1885    tmp.save("out3-7-3.png")
1886    tmp = test37y(lenna256bw, tmp)
1887    tmp = test37y(lenna256bw, tmp)
1888    tmp = test37y(lenna256bw, tmp)
1889    tmp.save("out3-7-4.png")
1890
1891##############################################################################
1892if chapter(4):
1893    print "Chapter 4. Greyscale dithering"
1894
1895# Output 4.0.1: 4x4 Bayer dithering, 3 colours
1896def test401(src, mat):
1897    (w, h) = src.size()
1898    dest = Image((w, h))
1899    dx = len(mat[0])
1900    dy = len(mat)
1901    for y in range(h):
1902        for x in range(w):
1903            c = src.getGray(x, y)
1904            threshold = (1. + mat[y % dy][x % dx]) / (dx * dy + 1)
1905            if c < 0.5:
1906                c = 0.5 * (c > threshold / 2)
1907            else:
1908                c = 0.5 + 0.5 * (c > 0.5 + threshold / 2)
1909            dest.setGray(x, y, c)
1910    return dest
1911
1912if chapter(4):
1913    test401(grad256bw, DITHER_BAYER88).save("grad4-0-1.png")
1914    test401(lenna256bw, DITHER_BAYER88).save("out4-0-1.png")
1915
1916# Output 4.0.2: standard Floyd-Steinberg, 3 colours
1917def test402(src, mat, serpentine):
1918    (w, h) = src.size()
1919    dest = Image((w, h))
1920    lines = len(mat)
1921    rows = len(mat[0])
1922    offset = mat[0].index(-1)
1923    ey = Matrix(w + rows - 1, lines, 0.)
1924    for y in range(h):
1925        ex = [0.] * (rows - offset)
1926        if serpentine and y & 1:
1927            xrange = range(w - 1, -1, -1)
1928        else:
1929            xrange = range(w)
1930        for x in xrange:
1931            # Set pixel
1932            c = src.getGray(x, y) + ex[0] + ey[0][x + offset]
1933            d = 0.5 * (c > 0.25) + 0.5 * (c > 0.75)
1934            dest.setGray(x, y, d)
1935            error = c - d
1936            # Propagate first line of error
1937            for dx in range(rows - offset - 2):
1938                ex[dx] = ex[dx + 1] + error * mat[0][offset + 1 + dx]
1939            ex[rows - offset - 2] = error * mat[0][rows - 1]
1940            # Propagate next lines
1941            if serpentine and y & 1:
1942                for dy in range(1, lines):
1943                    for dx in range(rows):
1944                        ey[dy][x + dx] += error * mat[dy][rows - 1 - dx]
1945            else:
1946                for dy in range(1, lines):
1947                    for dx in range(rows):
1948                        ey[dy][x + dx] += error * mat[dy][dx]
1949        for dy in range(lines - 1):
1950            ey[dy] = ey[dy + 1]
1951        ey[lines - 1] = [0.] * (w + rows - 1)
1952    return dest
1953
1954if chapter(4):
1955    test402(grad256bw, ERROR_FSTEIN, True).save("grad4-0-2.png")
1956    test402(lenna256bw, ERROR_FSTEIN, True).save("out4-0-2.png")
1957
1958# Pattern 4.1.1: gamma-corrected 50% grey, black-white halftone, 50% grey
1959if chapter(4):
1960    dest = Image((240, 80))
1961    for y in range(80):
1962        for x in range(80):
1963            dest.setGray(x, y, Gamma.ItoC(0.5))
1964        for x in range(80, 160):
1965            c = (x + y) & 1
1966            dest.setGray(x, y, c)
1967        for x in range(160, 240):
1968            dest.setGray(x, y, 0.5)
1969    dest.save("pat4-1-1.png")
1970
1971# Output 4.2.1: gamma-corrected 2-colour Floyd-Steinberg
1972# Output 4.2.2: gamma-corrected 3-colour Floyd-Steinberg
1973# Output 4.2.3: gamma-corrected 4-colour Floyd-Steinberg
1974def test42x(src, mat, serpentine, threshold):
1975    (w, h) = src.size()
1976    dest = Image((w, h))
1977    lines = len(mat)
1978    rows = len(mat[0])
1979    offset = mat[0].index(-1)
1980    ey = Matrix(w + rows - 1, lines, 0.)
1981    for y in range(h):
1982        ex = [0.] * (rows - offset)
1983        if serpentine and y & 1:
1984            xrange = range(w - 1, -1, -1)
1985        else:
1986            xrange = range(w)
1987        for x in xrange:
1988            # Set pixel
1989            c = Gamma.CtoI(src.getGray(x, y)) + ex[0] + ey[0][x + offset]
1990            d = threshold(c)
1991            dest.setGray(x, y, Gamma.ItoC(d))
1992            error = c - d
1993            # Propagate first line of error
1994            for dx in range(rows - offset - 2):
1995                ex[dx] = ex[dx + 1] + error * mat[0][offset + 1 + dx]
1996            ex[rows - offset - 2] = error * mat[0][rows - 1]
1997            # Propagate next lines
1998            if serpentine and y & 1:
1999                for dy in range(1, lines):
2000                    for dx in range(rows):
2001                        ey[dy][x + dx] += error * mat[dy][rows - 1 - dx]
2002            else:
2003                for dy in range(1, lines):
2004                    for dx in range(rows):
2005                        ey[dy][x + dx] += error * mat[dy][dx]
2006        for dy in range(lines - 1):
2007            ey[dy] = ey[dy + 1]
2008        ey[lines - 1] = [0.] * (w + rows - 1)
2009    return dest
2010
2011if chapter(4):
2012    test42x(grad256bw, ERROR_FSTEIN, True, Gamma.Cto2).save("grad4-2-1.png")
2013    test42x(lenna256bw, ERROR_FSTEIN, True, Gamma.Cto2).save("out4-2-1.png")
2014    test42x(grad256bw, ERROR_FSTEIN, True, Gamma.Cto3).save("grad4-2-2.png")
2015    test42x(lenna256bw, ERROR_FSTEIN, True, Gamma.Cto3).save("out4-2-2.png")
2016    test42x(grad256bw, ERROR_FSTEIN, True, Gamma.Cto4).save("grad4-2-3.png")
2017    test42x(lenna256bw, ERROR_FSTEIN, True, Gamma.Cto4).save("out4-2-3.png")
2018
2019# Output 4.3.1: full 4-colour block error diffusion
2020GREY22 = []
2021for n in range(4*4*4*4):
2022    vals = [0., 0.333, 0.666, 1.]
2023    a, b, c, d = n & 3, (n >> 2) & 3, (n >> 4) & 3, (n >> 6) & 3
2024    GREY22.append([[vals[a], vals[b]], [vals[c], vals[d]]])
2025
2026if chapter(4):
2027    test361(grad256bw, GREY22,
2028            ERROR_SUBFS22, DIFF_WEIGHED22).save("grad4-3-1.png")
2029    test361(lenna256bw, GREY22,
2030            ERROR_SUBFS22, DIFF_WEIGHED22).save("out4-3-1.png")
2031
2032# Output 4.3.2: 4-colour block error diffusion with only line tiles
2033GREYLINES22 = []
2034for n in range(4*4*4*4):
2035    vals = [0., 0.333, 0.666, 1.]
2036    a, b, c, d = n & 3, (n >> 2) & 3, (n >> 4) & 3, (n >> 6) & 3
2037    if (a != b or c != d) and (a != c or b != d):
2038        continue
2039    GREYLINES22.append([[vals[a], vals[b]], [vals[c], vals[d]]])
2040
2041if chapter(4):
2042    test361(grad256bw, GREYLINES22,
2043            ERROR_SUBFS22, DIFF_WEIGHED22).save("grad4-3-2.png")
2044    test361(lenna256bw, GREYLINES22,
2045            ERROR_SUBFS22, DIFF_WEIGHED22).save("out4-3-2.png")
2046
2047##############################################################################
2048if chapter(5):
2049    print "Chapter 5. Colour dithering"
2050
2051# Pattern 5.1.1: 8-colour palette
2052if chapter(5):
2053    dest = Image((512, 64))
2054    for x in range(512):
2055        d = x / 64
2056        r = (d & 2) >> 1
2057        g = (d & 4) >> 2
2058        b = d & 1
2059        for y in range(64):
2060            dest.setRgb(x, y, r, g, b)
2061    dest.save("pat5-1-1.png")
2062
2063# Output 5.1.1: 8-colour Floyd-Steinberg RGB dithering
2064# Output 5.1.2: 8-colour gamma-corrected Floyd-Steinberg RGB dithering
2065def test51x(src, mat, func):
2066    (w, h) = src.size()
2067    dest = Image((w, h))
2068    tmp = [Image((w, h)), Image((w, h)), Image((w, h))]
2069    for y in range(h):
2070        for x in range(w):
2071            rgb = src.getRgb(x, y)
2072            for i in range(3):
2073                tmp[i].setGray(x, y, rgb[i])
2074    for i in range(3):
2075        tmp[i] = func(tmp[i], mat, True, Gamma.Cto2)
2076    for y in range(h):
2077        for x in range(w):
2078            (r, g, b) = [tmp[i].getGray(x, y) for i in range(3)]
2079            dest.setRgb(x, y, r, g, b)
2080    return dest
2081
2082def test51y(src, mat, serpentine, threshold):
2083    return test3xx(src, mat, serpentine)
2084
2085if chapter(5):
2086    test51x(grad256, ERROR_FSTEIN, test51y).save("grad5-1-1.png")
2087    test51x(lenna256, ERROR_FSTEIN, test51y).save("out5-1-1.png")
2088    test51x(grad256, ERROR_FSTEIN, test42x).save("grad5-1-2.png")
2089    test51x(lenna256, ERROR_FSTEIN, test42x).save("out5-1-2.png")
2090
2091# Pattern 5.2.1: different colours give the same result
2092if chapter(5):
2093    dest = Image((320, 160))
2094    for x in range(80):
2095        for y in range(80):
2096            r = DITHER_BAYER44[(y / 8) % 4][(x / 8) % 4] > 7
2097            g = DITHER_BAYER44[(y / 8) % 4][(x / 8) % 4] > 13
2098            b = DITHER_BAYER44[(y / 8) % 4][(x / 8) % 4] > 13
2099            dest.setRgb(x, y, b, g, r)
2100        for y in range(80, 160):
2101            r = DITHER_BAYER44[y % 4][x % 4] > 7
2102            g = DITHER_BAYER44[y % 4][x % 4] > 13
2103            b = DITHER_BAYER44[y % 4][x % 4] > 13
2104            dest.setRgb(x, y, b, g, r)
2105    for x in range(80, 160):
2106        for y in range(80):
2107            r = DITHER_BAYER44[(y / 8) % 4][(x / 8 - 1) % 4] > 7
2108            g = DITHER_BAYER44[(y / 8) % 4][(x / 8) % 4] > 13
2109            b = DITHER_BAYER44[(y / 8) % 4][(x / 8) % 4] > 13
2110            dest.setRgb(x, y, b, g, r)
2111        for y in range(80, 160):
2112            r = DITHER_BAYER44[y % 4][(x - 1) % 4] > 7
2113            g = DITHER_BAYER44[y % 4][x % 4] > 13
2114            b = DITHER_BAYER44[y % 4][x % 4] > 13
2115            dest.setRgb(x, y, b, g, r)
2116    for x in range(160, 240):
2117        for y in range(80):
2118            r = DITHER_BAYER44[(y / 8 + 1) % 4][(x / 8 + 1) % 4] > 7
2119            g = DITHER_BAYER44[(y / 8) % 4][(x / 8) % 4] > 13
2120            b = DITHER_BAYER44[(y / 8 + 1) % 4][(x / 8) % 4] > 13
2121            dest.setRgb(x, y, b, g, r)
2122        for y in range(80, 160):
2123            r = DITHER_BAYER44[(y + 1) % 4][(x + 1) % 4] > 7
2124            g = DITHER_BAYER44[y % 4][x % 4] > 13
2125            b = DITHER_BAYER44[(y + 1) % 4][x % 4] > 13
2126            dest.setRgb(x, y, b, g, r)
2127    for x in range(240, 320):
2128        for y in range(80):
2129            r = DITHER_BAYER44[(y / 8 + 1) % 4][(x / 8) % 4] > 7
2130            g = DITHER_BAYER44[(y / 8) % 4][(x / 8) % 4] > 13
2131            b = DITHER_BAYER44[(y / 8) % 4][(x / 8 + 2) % 4] > 13
2132            dest.setRgb(x, y, b, g, r)
2133        for y in range(80, 160):
2134            r = DITHER_BAYER44[(y + 1) % 4][x % 4] > 7
2135            g = DITHER_BAYER44[y % 4][x % 4] > 13
2136            b = DITHER_BAYER44[y % 4][(x + 2) % 4] > 13
2137            dest.setRgb(x, y, b, g, r)
2138    dest.save("pat5-2-1.png")
2139
2140# Pattern 5.2.2: 16-colour palette
2141if chapter(5):
2142    dest = Image((512, 128))
2143    for x in range(64):
2144        for y in range(64):
2145            dest.setGray(x, y, 0)
2146            dest.setGray(448 + x, y, 0.7)
2147            dest.setGray(x, 64 + y, 0.3)
2148            dest.setGray(448 + x, 64 + y, 1.)
2149    for x in range(64, 448):
2150        d = x / 64
2151        r = (d & 2) >> 1
2152        g = (d & 4) >> 2
2153        b = d & 1
2154        for y in range(64, 128):
2155            dest.setRgb(x, y, r, g, b)
2156        r /= 2.
2157        g /= 2.
2158        b /= 2.
2159        for y in range(64):
2160            dest.setRgb(x, y, r, g, b)
2161    dest.save("pat5-2-2.png")
2162
2163# Output 5.2.1: gamma-corrected Floyd-Steinberg with ANSI palette (sigma-abs)
2164# Output 5.2.2: gamma-corrected Floyd-Steinberg with ANSI palette (euclidian)
2165def test52x(src, mat, cpal, distance, serpentine):
2166    (w, h) = src.size()
2167    ipal = [[Gamma.CtoI(c[i]) for i in range(3)] for c in cpal]
2168    dest = Image((w, h))
2169    lines = len(mat)
2170    rows = len(mat[0])
2171    offset = mat[0].index(-1)
2172    ey = [[[0.] * 3 for n in range(w + rows - 1)] for n in range(lines)]
2173    for y in range(h):
2174        ex = [[0.] * 3 for n in range(rows - offset)]
2175        if serpentine and y & 1:
2176            xrange = range(w - 1, -1, -1)
2177        else:
2178            xrange = range(w)
2179        for x in xrange:
2180            # Get pixel, add error, set pixel
2181            crgb = src.getRgb(x, y)
2182            irgb = [Gamma.CtoI(crgb[i]) + ex[0][i] + ey[0][x + offset][i] \
2183                        for i in range(3)]
2184            crgb = [Gamma.ItoC(irgb[i]) for i in range(3)]
2185            max = 999999.
2186            for n in range(len(cpal)):
2187                d = distance(crgb, cpal[n])
2188                if d < max:
2189                    max = d
2190                    cbest = cpal[n]
2191                    ibest = ipal[n]
2192            dest.setRgb(x, y, *cbest)
2193            # Compute error and propagate it
2194            for i in range(3):
2195                # Propagate first line of error
2196                error = irgb[i] - ibest[i]
2197                for dx in range(rows - offset - 2):
2198                    ex[dx][i] = ex[dx + 1][i] + error * mat[0][offset + 1 + dx]
2199                ex[rows - offset - 2][i] = error * mat[0][rows - 1]
2200                # Propagate next lines
2201                if serpentine and y & 1:
2202                    for dy in range(1, lines):
2203                        for dx in range(rows):
2204                            ey[dy][x + dx][i] += error * mat[dy][rows - 1 - dx]
2205                else:
2206                    for dy in range(1, lines):
2207                        for dx in range(rows):
2208                            ey[dy][x + dx][i] += error * mat[dy][dx]
2209        for dy in range(lines - 1):
2210            ey[dy] = ey[dy + 1]
2211        ey[lines - 1] = [[0.] * 3 for n in range(w + rows - 1)]
2212    return dest
2213
2214RGB_PALETTE = \
2215    [[0, 0, 0],
2216     [0, 0, 1],
2217     [0, 1, 0],
2218     [0, 1, 1],
2219     [1, 0, 0],
2220     [1, 0, 1],
2221     [1, 1, 0],
2222     [1, 1, 1]]
2223
2224ANSI_PALETTE = \
2225    [[0, 0, 0],
2226     [0, 0, 0.5],
2227     [0, 0.5, 0],
2228     [0, 0.5, 0.5],
2229     [0.5, 0, 0],
2230     [0.5, 0, 0.5],
2231     [0.5, 0.5, 0],
2232     [0.7, 0.7, 0.7],
2233     [0.3, 0.3, 0.3],
2234     [0, 0, 1],
2235     [0, 1, 0],
2236     [0, 1, 1],
2237     [1, 0, 0],
2238     [1, 0, 1],
2239     [1, 1, 0],
2240     [1, 1, 1]]
2241
2242def distmax(u, v):
2243    r, g, b = [abs(u[i] - v[i]) for i in range(3)]
2244    return r + g + b
2245
2246def disteuclidian(u, v):
2247    r, g, b = [u[i] - v[i] for i in range(3)]
2248    return r*r + g*g + b*b
2249
2250if chapter(5):
2251    test52x(grad256, ERROR_FSTEIN,
2252            ANSI_PALETTE, distmax, True).save("grad5-2-1.png")
2253    test52x(lenna256, ERROR_FSTEIN,
2254            ANSI_PALETTE, distmax, True).save("out5-2-1.png")
2255    test52x(grad256, ERROR_FSTEIN,
2256            ANSI_PALETTE, disteuclidian, True).save("grad5-2-2.png")
2257    test52x(lenna256, ERROR_FSTEIN,
2258            ANSI_PALETTE, disteuclidian, True).save("out5-2-2.png")
2259
2260def rgb2hsv(r, g, b):
2261    m = (float)(min(r, g, b))
2262    v = (float)(max(r, g, b))
2263    if v == m or v == 0:
2264        return 0., 0., v
2265    s = (v - m) / v
2266    if r == v:
2267        h = (g - b) / (v - m)
2268        if h < 0.:
2269            h += 6
2270    elif g == v:
2271        h = 2. + (b - r) / (v - m)
2272    elif b == v:
2273        h = 4. + (r - g) / (v - m)
2274    return math.pi * h / 3, s, v
2275
2276def disthsv(u, v):
2277    u = rgb2hsv(*u)
2278    v = rgb2hsv(*v)
2279    d1 = u[2] - v[2]
2280    d2 = u[2] * u[1] * math.cos(u[0]) - v[2] * v[1] * math.cos(v[0])
2281    d3 = u[2] * u[1] * math.sin(u[0]) - v[2] * v[1] * math.sin(v[0])
2282    return d1*d1 + d2*d2 + d3*d3
2283
2284def disthsv3(u, v):
2285    u = rgb2hsv(*u)
2286    v = rgb2hsv(*v)
2287    d1 = u[2] - v[2]
2288    d2 = u[2] * u[1] * math.cos(u[0]) - v[2] * v[1] * math.cos(v[0])
2289    d3 = u[2] * u[1] * math.sin(u[0]) - v[2] * v[1] * math.sin(v[0])
2290    return 9*d1*d1 + d2*d2 + d3*d3
2291
2292if chapter(5):
2293    test52x(grad256, ERROR_FSTEIN,
2294            ANSI_PALETTE, disthsv, True).save("grad5-2-3.png")
2295    test52x(lenna256, ERROR_FSTEIN,
2296            ANSI_PALETTE, disthsv, True).save("out5-2-3.png")
2297    test52x(grad256, ERROR_FSTEIN,
2298            ANSI_PALETTE, disthsv3, True).save("grad5-2-4.png")
2299    test52x(lenna256, ERROR_FSTEIN,
2300            ANSI_PALETTE, disthsv3, True).save("out5-2-4.png")
2301
2302##############################################################################
2303if chapter(6):
2304    print "Chapter 6. Photographic mosaics"
2305
2306# Output 6.0.1: create a mosaic from Lenna
2307def mosaic_split(src, tnw, tnh):
2308    random.seed(0)
2309    thumbs = []
2310    (w, h) = src.size()
2311    sw = w / tnw
2312    sh = h / tnh
2313    for y in range(sh):
2314        for x in range(sw):
2315            thumbs.append(src.getRegion(x * tnw, y * tnh, tnw, tnh))
2316    random.shuffle(thumbs)
2317    return thumbs
2318
2319def mosaic_analyse(tnlist, dx, dy):
2320    coeffs = []
2321    for (n, img) in enumerate(tnlist):
2322        tmp = [[[0] * 3 for x in range(dx)] for y in range(dy)]
2323        (w, h) = img.size()
2324        for y in range(h):
2325            my = y * dy / h
2326            for x in range(w):
2327                mx = x * dx / w
2328                (r, g, b) = img.getRgb(x, y)
2329                tmp[my][mx][0] += Gamma.CtoI(r) / (w / dx * h / dy)
2330                tmp[my][mx][1] += Gamma.CtoI(g) / (w / dx * h / dy)
2331                tmp[my][mx][2] += Gamma.CtoI(b) / (w / dx * h / dy)
2332        coeffs.append(tmp)
2333    return coeffs
2334
2335def test601(tnlist, cols):
2336    (tnw, tnh) = tnlist[0].size()
2337    dw = cols
2338    dh = (len(tnlist) + cols - 1) / cols
2339    dest = Image((dw * tnw + 8 * (dw + 1), dh * tnh + 8 * (dh + 1)), True)
2340    for (n, img) in enumerate(tnlist):
2341        di = 8 + (n % dw) * (tnw + 8)
2342        dj = 8 + (n / dw) * (tnh + 8)
2343        img.copyTo(dest, (di, dj))
2344    return dest
2345
2346if chapter(6):
2347    tnlist = mosaic_split(lenna256, 32, 32)
2348    test601(tnlist, 10).save("out6-0-1.png")
2349
2350# Output 6.1.1: extract 1 colour feature from mosaic tiles
2351# Output 6.1.2: crop Lenna
2352# Output 6.1.3: generate a mosaic from the 1-feature database
2353# Output 6.1.4: extract 4 colour features from mosaic tiles
2354# Output 6.1.5: generate a mosaic from the 4-feature database
2355def test61x(coeffs, cols, tnw, tnh):
2356    dx = len(coeffs[0][0])
2357    dy = len(coeffs[0])
2358    dw = cols
2359    dh = (len(coeffs) + cols - 1) / cols
2360    dest = Image((dw * tnw + 8 * (dw + 1), dh * tnh + 8 * (dh + 1)), True)
2361    for (n, tab) in enumerate(coeffs):
2362        di = 8 + (n % dw) * (tnw + 8)
2363        dj = 8 + (n / dw) * (tnh + 8)
2364        for y in range(tnh):
2365            for x in range(tnw):
2366                (r, g, b) = tab[y * dy / tnh][x * dx / tnw]
2367                dest.setRgb(di + x, dj + y, Gamma.ItoC(r), Gamma.ItoC(g), Gamma.ItoC(b))
2368    return dest
2369
2370def test61y(src, sqw, sqh, tnlist, coeffs):
2371    (w, h) = src.size()
2372    (tnw, tnh) = tnlist[0].size()
2373    dx = len(coeffs[0][0])
2374    dy = len(coeffs[0])
2375    nx = w / sqw
2376    ny = h / sqh
2377    dest = Image((nx * tnw, ny * tnh), True)
2378    for Y in range(ny):
2379        for X in range(nx):
2380            # 1. create statistics about the current square
2381            cur = [[[0] * 3 for x in range(dx)] for y in range(dy)]
2382            for y in range(sqh):
2383                my = y * dy / sqh
2384                for x in range(sqw):
2385                    mx = x * dx / sqw
2386                    (r, g, b) = src.getRgb(X * sqw + x, Y * sqh + y)
2387                    cur[my][mx][0] += Gamma.CtoI(r) / (sqw / dx * sqh / dy)
2388                    cur[my][mx][1] += Gamma.CtoI(g) / (sqw / dx * sqh / dy)
2389                    cur[my][mx][2] += Gamma.CtoI(b) / (sqw / dx * sqh / dy)
2390            # 2. find the best mosaic part
2391            best = -1
2392            dist = 5.
2393            for (n, tmp) in enumerate(coeffs):
2394                d = 0
2395                for j in range(dy):
2396                    for i in range(dx):
2397                        for c in range(3):
2398                            t = cur[j][i][c] - tmp[j][i][c]
2399                            d += t * t
2400                if d < dist:
2401                    dist = d
2402                    best = n
2403            # 3. blit mosaic chunk
2404            tnlist[best].copyTo(dest, (X * tnw, Y * tnh))
2405    return dest
2406
2407if chapter(6):
2408    coeffs1x1 = mosaic_analyse(tnlist, 1, 1)
2409    test61x(coeffs1x1, 10, 8, 8).save("out6-1-1.png")
2410    out612 = lenna256.getRegion(100, 90, 80, 80)
2411    out612.save("out6-1-2.png")
2412    test61y(out612, 6, 6, tnlist, coeffs1x1).save("out6-1-3.png")
2413
2414    coeffs2x2 = mosaic_analyse(tnlist, 2, 2)
2415    test61x(coeffs2x2, 10, 16, 16).save("out6-1-4.png")
2416    test61y(out612, 6, 6, tnlist, coeffs2x2).save("out6-1-5.png")
2417
2418##############################################################################
2419print "Finished"
2420
2421# Place temporary cruft below this
2422sys.exit(0)
2423
2424# XXX: test -- ranked dither -- it SUCKS
2425def test26x(src, mat):
2426    (w, h) = src.size()
2427    dest = Image((w, h))
2428    dx = len(mat[0])
2429    dy = len(mat)
2430    for y in range(h / dy):
2431        for x in range(w / dx):
2432            # Step 1: get the pixels and count groups
2433            groups = {}
2434            for j in range(dy):
2435                for i in range(dx):
2436                    p = src.getGray(x * dx + i, y * dy + j)
2437                    if groups.has_key(p):
2438                        groups[p].append((i, j))
2439                    else:
2440                        groups[p] = [(i, j)]
2441            # Step 2: create the ranked dither
2442            ranked = Matrix(dx, dy)
2443            for p, g in groups.items():
2444                n = (int)(round(p * len(g)))
2445                if not n:
2446                    continue
2447                v = [(mat[j][i], (i, j)) for (i, j) in g]
2448                v.sort()
2449                v = v[0 : n - 1]
2450                for (k, (i, j)) in v:
2451                    ranked[j][i] = 1
2452            # Step 3: blit the ranked dither
2453            for j in range(dy):
2454                for i in range(dx):
2455                    dest.setGray(x * dx + i, y * dy + j, ranked[j][i])
2456    return dest
2457
2458if chapter(2):
2459    #test26x(lenna256bw, DITHER_BAYER88).save("out2-6-1.png")
2460    #test26x(grad256bw, DITHER_BAYER88).save("grad2-6-1.png")
2461    test26x(lenna256bw, DITHER_CLUSTER88).save("out2-6-1.png")
2462    test26x(grad256bw, DITHER_CLUSTER88).save("grad2-6-1.png")
2463
2464#####################
2465#CIE-L*a*b* transformation -- euclidian distance doesn't seem to work great
2466def rgb2lab(r, g, b):
2467    Xw50 = 0.9642;
2468    Yw50 = 1.0000;
2469    Zw50 = 0.8249;
2470    # RGB to sRGB
2471    r = Gamma.CtoI(r)
2472    g = Gamma.CtoI(g)
2473    b = Gamma.CtoI(b)
2474    # sRGB to XYZ(D65)
2475    x65 =  0.4124*r + 0.3576*g + 0.1805*b
2476    y65 =  0.2126*r + 0.7152*g + 0.0722*b
2477    z65 =  0.0193*r + 0.1192*g + 0.9505*b
2478    # XYZ(D65) to XYZ(D50)
2479    x50 =  1.0282015*x65 + 0.0500707*y65 - 0.0579688*z65
2480    y50 =  0.0197032*x65 + 0.9871848*y65 - 0.0054285*z65
2481    z50 = -0.0002329*x65 + 0.0006862*y65 + 0.7573070*z65
2482    # XYZ(D50) to Lab(D50)
2483    if x50 / Xw50 > 0.008856:
2484        xx50 = math.pow(x50 / Xw50, 1. / 3.)
2485    else:
2486        xx50 = 7.78 * (x50 / Xw50) + 16. / 116.
2487    if y50 / Yw50 > 0.008856:
2488        yy50 = math.pow(y50 / Yw50, 1. / 3.)
2489    else:
2490        yy50 = 7.78 * (y50 / Yw50) + 16. / 116.
2491    if z50 / Zw50 > 0.008856:
2492        zz50 = math.pow(z50 / Zw50, 1. / 3.)
2493    else:
2494        zz50 = 7.78 * (z50 / Zw50) + 16. / 116.
2495    lo = 116. * yy50 - 16.
2496    ao = 500. * (xx50 - yy50)
2497    bo = 200. * (yy50 - zz50)
2498    return lo, ao, bo
2499
2500def distlab(u, v):
2501    u = rgb2lab(*u)
2502    v = rgb2lab(*v)
2503    l, a, b = u[0] - v[0], u[1] - v[1], u[2] - v[2]
2504    return l*l + a*a + b*b
2505
2506
Note: See TracBrowser for help on using the repository browser.