PaletteOptimiser

optimiser.py

19:783e5bbb92a5
2015-09-18 Paul Boddie Impose a maximum height on portrait images. Adjusted the colourmap to employ combinations of light/dark colours as approximations of the preferred colour in some cases.
     1 #!/usr/bin/env python     2      3 from array import array     4 from itertools import combinations     5 import EXIF     6 import PIL.Image     7 import sys     8      9 def scale(v):    10     return (v + 43) / 85    11     12 def point(rgb):    13     return tuple(map(scale, rgb))    14     15 def index(p):    16     return p[0] * 16 + p[1] * 4 + p[2]    17     18 def colour(i):    19     return (255 * (i % 2), 255 * ((i / 2) % 2), 255 * ((i / 4) % 2))    20     21 def add(d, v):    22     d[v] = (d.has_key(v) and d[v] or 0) + 1    23     24 def by_frequency(d):    25     l = [(f, t) for (t, f) in d.items()]    26     l.sort(reverse=True)    27     return [i[1] for i in l]    28     29 def match(b, bases):    30     return b in bases and b    31     32 def fallback(bases):    33     for b in by_frequency(bases):    34         if b not in ["_", "W"]:    35             return b    36     return by_frequency(bases)[0]    37     38 tones = [    39     "___", "_BB", "_BB", "BBB", # 00x    40     "_GG", "__C", "_BC", "BCC", # 01x    41     "_GG", "GGC", "BCW", "CCC", # 02x    42     "GGG", "GGC", "GCW", "CCW", # 03x    43     "_RR", "_MM", "BMM", "MBB", # 10x    44     "_YY", "_**", "**B", "BBC", # 11x    45     "_GY", "GGC", "*CC", "BCW", # 12x    46     "BGY", "GGC", "CCW", "CCW", # 13x    47     "_RR", "_RM", "*MM", "RMM", # 20x    48     "RYY", "*RY", "RMM", "MMM", # 21x    49     "YYY", "YYW", "*WW", "*WW", # 22x    50     "YYY", "GYY", "GWW", "CWW", # 23x    51     "RRR", "RRM", "BMR", "BMR", # 30x    52     "RRY", "RRY", "RMW", "RMW", # 31x    53     "YYY", "YYW", "YYW", "WWW", # 32x    54     "YYY", "YYW", "YYW", "WWW", # 33x    55     ]    56     57 colours = ["_", "R", "G", "Y", "B", "M", "C", "W"]    58     59 if __name__ == "__main__":    60     width = 320    61     height = 256    62     63     input_filename, output_filename = sys.argv[1:3]    64     rotate = "-r" in sys.argv[3:]    65     66     x = EXIF.process_file(open(input_filename))    67     im = PIL.Image.open(input_filename)    68     69     if rotate or x and x["Image Orientation"].values == [6L]:    70         im = im.rotate(270)    71     72     w, h = im.size    73     if w > h:    74         height = (width * h) / w    75     else:    76         width = (height * w) / h    77     78     im = im.resize((width, height))    79     80     usage = []    81     base_usage = []    82     toned = []    83     84     for row in range(0, height):    85         u = {}    86         usage.append(u)    87         bu = {}    88         base_usage.append(bu)    89         tr = []    90         toned.append(tr)    91         for column in range(0, width):    92             rgb = im.getpixel((column, row))    93             p = point(rgb)    94             i = index(p)    95             t = tones[i]    96             add(u, t)    97             if t[0] != "*":    98                 add(bu, t[0])    99             if t[1] != "*":   100                 add(bu, t[1])   101             if t[2] != "*":   102                 add(bu, t[2])   103             tr.append(t)   104    105     chosen = []   106    107     for row, (u, bu) in enumerate(zip(usage, base_usage)):   108         light = row % 2   109         best = 0   110         best_bases = None   111         best_missing = None   112         best_map = None   113    114         for bases in combinations(bu, min(len(bu), 4)):   115             bases = dict([(base, bu[base]) for base in bases])   116             count = 0   117             missing = []   118             tone_map = {}   119             for tone, freq in u.items():   120                 base = match(tone[1], bases)   121                 if base:   122                     tone_map[tone] = base   123                     count += freq   124                 else:   125                     base = light and match(tone[2], bases) or match(tone[0], bases) or match(tone[2], bases)   126                     if base:   127                         tone_map[tone] = base   128                         count += freq / 2   129                     else:   130                         missing.append(tone)   131             if count > best:   132                 best_bases = bases   133                 best_missing = missing   134                 best_map = tone_map   135                 best = count   136    137         chosen.append((best, best_bases or bases, best_map or tone_map, best_missing or missing))   138    139     output = []   140    141     for row, (tr, ch) in enumerate(zip(toned, chosen)):   142         o = []   143         for column, t in enumerate(tr):   144             best, bases, tone_map, missing = ch   145             base = tone_map.get(t) or fallback(bases)   146             o.append(base)   147             i = colours.index(base)   148             im.putpixel((column, row), colour(i))   149    150         output.append("".join(o))   151    152     im.save(output_filename)   153    154 # vim: tabstop=4 expandtab shiftwidth=4