PaletteOptimiser

optimiser.py

71:ba632fd74cbb
2015-10-09 Paul Boddie Introduced a separate main program for potential Shedskin analysis. shedskin
     1 #!/usr/bin/env python     2      3 """     4 Convert and optimise images for display in an Acorn Electron MODE 1 variant     5 with four colours per line but eight colours available for selection on each     6 line.     7      8 Copyright (C) 2015 Paul Boddie <paul@boddie.org.uk>     9     10 This program is free software; you can redistribute it and/or modify it under    11 the terms of the GNU General Public License as published by the Free Software    12 Foundation; either version 3 of the License, or (at your option) any later    13 version.    14     15 This program is distributed in the hope that it will be useful, but WITHOUT ANY    16 WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A    17 PARTICULAR PURPOSE.  See the GNU General Public License for more details.    18     19 You should have received a copy of the GNU General Public License along    20 with this program.  If not, see <http://www.gnu.org/licenses/>.    21 """    22     23 from random import random, randrange    24 import itertools    25 import math    26 import sys    27     28 corners = [    29     (0, 0, 0), (255, 0, 0), (0, 255, 0), (255, 255, 0),    30     (0, 0, 255), (255, 0, 255), (0, 255, 255), (255, 255, 255)    31     ]    32     33 # Basic colour operations.    34     35 def within(v, lower, upper):    36     return min(max(v, lower), upper)    37     38 def clip(v):    39     return int(within(v, 0, 255))    40     41 def distance(rgb1, rgb2):    42     r1, g1, b1 = rgb1    43     r2, g2, b2 = rgb2    44     return math.sqrt(pow(r1 - r2, 2) + pow(g1 - g2, 2) + pow(b1 - b2, 2))    45     46 def restore(srgb):    47     return tuple(map(lambda x: int(x * 255.0), srgb))    48     49 def scale(rgb):    50     return tuple(map(lambda x: x / 255.0, rgb))    51     52 def invert(srgb):    53     return tuple(map(lambda x: 1.0 - x, srgb))    54     55 # Colour distribution functions.    56     57 def combination(rgb):    58     59     "Return the colour distribution for 'rgb'."    60     61     # Get the colour with components scaled from 0 to 1, plus the inverted    62     # component values.    63     64     rgb = scale(rgb)    65     rgbi = invert(rgb)    66     pairs = zip(rgbi, rgb)    67     68     # For each corner of the colour cube (primary and secondary colours plus    69     # black and white), calculate the corner value's contribution to the    70     # input colour.    71     72     d = []    73     for corner in corners:    74         rs, gs, bs = scale(corner)    75     76         # Obtain inverted channel values where corner channels are low;    77         # obtain original channel values where corner channels are high.    78     79         d.append((pairs[0][int(rs)] * pairs[1][int(gs)] * pairs[2][int(bs)], corner))    80     81     # Balance the corner contributions.    82     83     return balance(d)    84     85 def complements(rgb):    86     87     "Return 'rgb' and its complement."    88     89     r, g, b = rgb    90     return rgb, restore(invert(scale(rgb)))    91     92 def balance(d):    93     94     """    95     Balance distribution 'd', cancelling opposing values and their complements    96     and replacing their common contributions with black and white contributions.    97     """    98     99     d = dict([(value, f) for f, value in d])   100     for primary, secondary in map(complements, [(0, 0, 0), (255, 0, 0), (0, 255, 0), (0, 0, 255)]):   101         common = min(d[primary], d[secondary])   102         d[primary] -= common   103         d[secondary] -= common   104     return [(f, value) for value, f in d.items()]   105    106 def combine(d):   107    108     "Combine distribution 'd' to get a colour value."   109    110     out = [0, 0, 0]   111     for v, rgb in d:   112         out[0] += v * rgb[0]   113         out[1] += v * rgb[1]   114         out[2] += v * rgb[2]   115     return out   116    117 def pattern(rgb, chosen=None):   118    119     """   120     Obtain a sorted colour distribution for 'rgb', optionally limited to any   121     specified 'chosen' colours.   122     """   123    124     l = [(f, value) for f, value in combination(rgb) if not chosen or value in chosen]   125     l.sort(reverse=True)   126     return l   127    128 def get_value(rgb, chosen=None, fail=False):   129    130     """   131     Get an output colour for 'rgb', optionally limited to any specified 'chosen'   132     colours. If 'fail' is set to a true value, return None if the colour cannot   133     be expressed using any of the chosen colours.   134     """   135    136     l = pattern(rgb, chosen)   137     limit = sum([f for f, c in l])   138     if not limit:   139         if fail:   140             return None   141         else:   142             return l[randrange(0, len(l))][1]   143    144     choose = random() * limit   145     threshold = 0   146     for f, c in l:   147         threshold += f   148         if choose < threshold:   149             return c   150     return c   151    152 # Colour processing operations.   153    154 def sign(x):   155     return x >= 0 and 1 or -1   156    157 def saturate_rgb(rgb, exp):   158     return tuple([saturate_value(x, exp) for x in rgb])   159    160 def saturate_value(x, exp):   161     return int(127.5 + sign(x - 127.5) * 127.5 * pow(abs(x - 127.5) / 127.5, exp))   162    163 def amplify_rgb(rgb, exp):   164     return tuple([amplify_value(x, exp) for x in rgb])   165    166 def amplify_value(x, exp):   167     return int(pow(x / 255.0, exp) * 255.0)   168    169 # Image operations.   170    171 def get_colours(im, y):   172    173     "Get a colour distribution from image 'im' for the row 'y'."   174    175     width, height = im.size   176     c = {}   177     for x in range(0, width):   178         rgb = im.getpixel((x, y))   179    180         # Sum the colour probabilities.   181    182         for f, value in combination(rgb):   183             if not c.has_key(value):   184                 c[value] = f   185             else:   186                 c[value] += f   187    188     c = [(n/width, value) for value, n in c.items()]   189     c.sort(reverse=True)   190     return c   191    192 def get_combinations(c, n):   193    194     """   195     Get combinations of colours from 'c' of size 'n' in decreasing order of   196     probability.   197     """   198    199     all = []   200     for l in itertools.combinations(c, n):   201         total = 0   202         for f, value in l:   203             total += f   204         all.append((total, l))   205     all.sort(reverse=True)   206     return [l for total, l in all]   207    208 def test_slice(im, size, r):   209     for g in range(0, size):   210         for b in range(0, size):   211             value = get_value((r, (g * 256) / size, (b * 256 / size)))   212             im.putpixel((g, b), value)   213    214 def test_flat_slice(im, size, rgb):   215     for y in range(0, size):   216         for x in range(0, size):   217             im.putpixel((x, y), get_value(rgb))   218    219 def count_colours(im, colours):   220    221     """   222     Count colours on each row of image 'im', returning a tuple indicating the   223     first row with more than the given number of 'colours' together with the   224     found colours; otherwise returning None.   225     """   226    227     width, height = im.size   228    229     for y in range(0, height):   230         l = set(im.getdata()[y * width:(y+1) * width])   231         if len(l) > colours:   232             return (y, l)   233     return None   234    235 def process_image(im, saturate, desaturate, darken, brighten):   236    237     """   238     Process image 'im' using the given options: 'saturate', 'desaturate',   239     'darken', 'brighten'.   240     """   241    242     width, height = im.size   243    244     if saturate or desaturate or darken or brighten:   245         for y in range(0, height):   246             for x in range(0, width):   247                 rgb = im.getpixel((x, y))   248                 if saturate or desaturate:   249                     rgb = saturate_rgb(rgb, saturate and 0.5 / saturate or 2 * desaturate)   250                 if darken or brighten:   251                     rgb = amplify_rgb(rgb, brighten and 0.5 / brighten or 2 * darken)   252                 im.putpixel((x, y), rgb)   253    254 def preview_image(im, half_resolution_preview=False):   255    256     "Return a preview copy of image 'im'."   257    258     width, height = im.size   259    260     imp = im.copy()   261     step = half_resolution_preview and 2 or 1   262    263     for y in range(0, height):   264         for x in range(0, width, step):   265             rgb = imp.getpixel((x, y))   266             value = get_value(rgb)   267             imp.putpixel((x, y), value)   268             if half_resolution_preview:   269                 imp.putpixel((x+1, y), value)   270    271     return imp   272    273 def convert_image(im):   274    275     "Convert image 'im' to an appropriate output representation."   276    277     width, height = im.size   278    279     for y in range(0, height):   280         c = get_colours(im, y)   281    282         for l in get_combinations(c, 4):   283             most = [value for f, value in l]   284             for x in range(0, width):   285                 rgb = im.getpixel((x, y))   286                 value = get_value(rgb, most, True)   287                 if value is None:   288                     break # try next combination   289             else:   290                 break # use this combination   291         else:   292             most = [value for f, value in c[:4]] # use the first four   293    294         for x in range(0, width):   295             rgb = im.getpixel((x, y))   296             value = get_value(rgb, most)   297             im.putpixel((x, y), value)   298    299             if x < width - 1:   300                 rgbn = im.getpixel((x+1, y))   301                 rgbn = tuple(map(lambda i: clip(i[0] + (i[1] - i[2]) / 4.0), zip(rgbn, rgb, value)))   302                 im.putpixel((x+1, y), rgbn)   303    304             if y < height - 1:   305                 rgbn = im.getpixel((x, y+1))   306                 rgbn = tuple(map(lambda i: clip(i[0] + (i[1] - i[2]) / 2.0), zip(rgbn, rgb, value)))   307                 im.putpixel((x, y+1), rgbn)   308    309 class SimpleImage:   310    311     "An image behaving like PIL.Image."   312    313     def __init__(self, data, size):   314         self.data = data   315         self.width, self.height = self.size = size   316    317     def copy(self):   318         return SimpleImage(self.data[:], self.size)   319    320     def getpixel(self, xy):   321         x, y = xy   322         return self.data[y * self.width + x]   323    324     def putpixel(self, xy, value):   325         x, y = xy   326         self.data[y * self.width + x] = value   327    328     def getdata(self):   329         return self.data   330    331 # Test program.   332    333 if __name__ == "__main__":   334     data = [(0, 0, 0)] * 1024   335     size = (32, 32)   336    337     im = SimpleImage(data, size)   338    339     process_image(im, 1.0, 0.0, 1.0, 0.0)   340     imp = preview_image(im, False)   341     convert_image(im)   342    343     test_im = SimpleImage(data, size)   344     test_slice(test_im, 32, 0)   345    346     test_flat_im = SimpleImage(data, size)   347     test_flat_slice(test_flat_im, 32, (200, 100, 50))   348    349 # vim: tabstop=4 expandtab shiftwidth=4