PaletteOptimiser

optimiserlib.py

98:e4db053e9a3b
2015-10-10 Paul Boddie Added tag snapshot-20151010 for changeset cd6bc22cd40b simpleimage-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     26 corners = [    27     (0, 0, 0), (255, 0, 0), (0, 255, 0), (255, 255, 0),    28     (0, 0, 255), (255, 0, 255), (0, 255, 255), (255, 255, 255)    29     ]    30     31 # Basic colour operations.    32     33 def within(v, lower, upper):    34     return min(max(v, lower), upper)    35     36 def clip(v):    37     return int(within(v, 0, 255))    38     39 def restore(srgb):    40     r, g, b = srgb    41     return int(r * 255.0), int(g * 255.0), int(b * 255.0)    42     43 def scale(rgb):    44     r, g, b = rgb    45     return r / 255.0, g / 255.0, b / 255.0    46     47 def invert(srgb):    48     r, g, b = srgb    49     return 1.0 - r, 1.0 - g, 1.0 - b    50     51 scaled_corners = map(scale, corners)    52 zipped_corners = zip(corners, scaled_corners)    53     54 # Colour distribution functions.    55     56 def combination(rgb):    57     58     "Return the colour distribution for 'rgb'."    59     60     # Get the colour with components scaled from 0 to 1, plus the inverted    61     # component values.    62     63     srgb = scale(rgb)    64     rgbi = invert(srgb)    65     pairs = zip(rgbi, srgb)    66     67     # For each corner of the colour cube (primary and secondary colours plus    68     # black and white), calculate the corner value's contribution to the    69     # input colour.    70     71     d = []    72     for corner, scaled in zipped_corners:    73         rs, gs, bs = scaled    74     75         # Obtain inverted channel values where corner channels are low;    76         # obtain original channel values where corner channels are high.    77     78         d.append((pairs[0][int(rs)] * pairs[1][int(gs)] * pairs[2][int(bs)], corner))    79     80     # Balance the corner contributions.    81     82     return balance(d)    83     84 def complements(rgb):    85     86     "Return 'rgb' and its complement."    87     88     r, g, b = rgb    89     return rgb, restore(invert(scale(rgb)))    90     91 bases = [(0, 0, 0), (255, 0, 0), (0, 255, 0), (0, 0, 255)]    92 base_complements = map(complements, bases)    93     94 def balance(d):    95     96     """    97     Balance distribution 'd', cancelling opposing values and their complements    98     and replacing their common contributions with black and white contributions.    99     """   100    101     dd = dict([(value, f) for f, value in d])   102     for primary, secondary in base_complements:   103         common = min(dd[primary], dd[secondary])   104         dd[primary] -= common   105         dd[secondary] -= common   106     return [(f, value) for value, f in dd.items()]   107    108 def combine(d):   109    110     "Combine distribution 'd' to get a colour value."   111    112     out = [0, 0, 0]   113     for v, rgb in d:   114         out[0] += v * rgb[0]   115         out[1] += v * rgb[1]   116         out[2] += v * rgb[2]   117     return tuple(map(int, out))   118    119 def pattern(rgb, chosen=None):   120    121     """   122     Obtain a sorted colour distribution for 'rgb', optionally limited to any   123     specified 'chosen' colours.   124     """   125    126     l = [(f, value) for f, value in combination(rgb) if not chosen or value in chosen]   127     l.sort(reverse=True)   128     return l   129    130 def get_value(rgb, chosen=None, fail=False):   131    132     """   133     Get an output colour for 'rgb', optionally limited to any specified 'chosen'   134     colours. If 'fail' is set to a true value, return None if the colour cannot   135     be expressed using any of the chosen colours.   136     """   137    138     l = pattern(rgb, chosen)   139     limit = sum([f for f, c in l])   140     if not limit:   141         if fail:   142             return None   143         else:   144             return l[randrange(0, len(l))][1]   145    146     choose = random() * limit   147     threshold = 0   148     for f, c in l:   149         threshold += f   150         if choose < threshold:   151             return c   152     return c   153    154 # Colour processing operations.   155    156 def sign(x):   157     if x >= 0:   158         return 1   159     else:   160         return -1   161    162 def saturate_rgb(rgb, exp):   163     r, g, b = rgb   164     return saturate_value(r, exp), saturate_value(g, exp), saturate_value(b, exp)   165    166 def saturate_value(x, exp):   167     return int(127.5 + sign(x - 127.5) * 127.5 * pow(abs(x - 127.5) / 127.5, exp))   168    169 def amplify_rgb(rgb, exp):   170     r, g, b = rgb   171     return amplify_value(r, exp), amplify_value(g, exp), amplify_value(b, exp)   172    173 def amplify_value(x, exp):   174     return int(pow(x / 255.0, exp) * 255.0)   175    176 # Image operations.   177    178 def get_colours(im, y):   179    180     "Get a colour distribution from image 'im' for the row 'y'."   181    182     width, height = im.size   183     c = {}   184     for x in range(0, width):   185         rgb = im.getpixel((x, y))   186    187         # Sum the colour probabilities.   188    189         for f, value in combination(rgb):   190             if not c.has_key(value):   191                 c[value] = f   192             else:   193                 c[value] += f   194    195     d = [(n/width, value) for value, n in c.items()]   196     d.sort(reverse=True)   197     return d   198    199 def get_combinations(c, n):   200    201     """   202     Get combinations of colours from 'c' of size 'n' in decreasing order of   203     probability.   204     """   205    206     all = []   207     for l in itertools.combinations(c, n):   208         total = 0   209         for f, value in l:   210             total += f   211         all.append((total, l))   212     all.sort(reverse=True)   213     return [l for total, l in all]   214    215 def count_colours(im, colours):   216    217     """   218     Count colours on each row of image 'im', returning a tuple indicating the   219     first row with more than the given number of 'colours' together with the   220     found colours; otherwise returning None.   221     """   222    223     width, height = im.size   224    225     for y in range(0, height):   226         l = set()   227         for x in range(0, width):   228             l.add(im.getpixel((x, y)))   229         if len(l) > colours:   230             return (y, l)   231     return None   232    233 def process_image(im, saturate, desaturate, darken, brighten):   234    235     """   236     Process image 'im' using the given options: 'saturate', 'desaturate',   237     'darken', 'brighten'.   238     """   239    240     width, height = im.size   241    242     if saturate or desaturate or darken or brighten:   243         for y in range(0, height):   244             for x in range(0, width):   245                 rgb = im.getpixel((x, y))   246                 if saturate or desaturate:   247                     rgb = saturate_rgb(rgb, saturate and 0.5 / saturate or 2 * desaturate)   248                 if darken or brighten:   249                     rgb = amplify_rgb(rgb, brighten and 0.5 / brighten or 2 * darken)   250                 im.putpixel((x, y), rgb)   251    252 def preview_image(im, half_resolution_preview=False):   253    254     "Return a preview copy of image 'im'."   255    256     width, height = im.size   257     imp = im.copy()   258     if half_resolution_preview:   259         step = 2   260     else:   261         step = 1   262    263     for y in range(0, height):   264         for x in range(0, width, step):   265             rgb = im.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 = (   302                     clip(rgbn[0] + (rgb[0] - value[0]) / 4.0),   303                     clip(rgbn[1] + (rgb[1] - value[1]) / 4.0),   304                     clip(rgbn[2] + (rgb[2] - value[2]) / 4.0)   305                     )   306                 im.putpixel((x+1, y), rgbn)   307    308             if y < height - 1:   309                 rgbn = im.getpixel((x, y+1))   310                 rgbn = (   311                     clip(rgbn[0] + (rgb[0] - value[0]) / 2.0),   312                     clip(rgbn[1] + (rgb[1] - value[1]) / 2.0),   313                     clip(rgbn[2] + (rgb[2] - value[2]) / 2.0)   314                     )   315                 im.putpixel((x, y+1), rgbn)   316    317 class SimpleImage:   318    319     "An image behaving like PIL.Image."   320    321     def __init__(self, data, size):   322         self.data = data   323         self.width, self.height = self.size = size   324    325     def copy(self):   326         return SimpleImage(self.data[:], self.size)   327    328     def getpixel(self, xy):   329         x, y = xy   330         return self.data[y * self.width + x]   331    332     def putpixel(self, xy, value):   333         x, y = xy   334         self.data[y * self.width + x] = value   335    336     def getdata(self):   337         return self.data   338    339 # Exercise functions for Shedskin.   340    341 if __name__ == "__main__":   342     rgb = (200, 100, 50)   343     saturate_rgb(rgb, 1.0)   344     amplify_rgb(rgb, 1.0)   345     get_value(rgb)   346     get_value(rgb, [(255, 255, 255), (255, 0, 0), (255, 255, 0), (0, 0, 0)])   347     combine([(1.0, (255, 0, 0)), (0.0, (0, 0, 0))])   348     clip(200.0)   349     get_combinations([(0.5, (255, 0, 0)), (0.25, (255, 255, 0)), (0.25, (0, 0, 0))], 2)   350    351     data = [(0, 0, 0), (0, 0, 0)]   352     im = SimpleImage(data, (2, 1))   353     im2 = im.copy()   354     im2.getpixel((0, 0)) == (0, 0, 0)   355     im2.putpixel((0, 0), (255, 255, 255))   356     im2.getdata() == [(255, 255, 255), (0, 0, 0)]   357    358     get_colours(im, 0) == [(1.0, (0, 0, 0))]   359     count_colours(im, 4)   360    361     process_image(im, 1.0, 0.0, 1.0, 0.0)   362     preview_image(im, False)   363     convert_image(im)   364    365 # vim: tabstop=4 expandtab shiftwidth=4