PaletteOptimiser

optimiserlib.py

84:86d83f74ef9a
2015-10-10 Paul Boddie Moved the get_combinations function into the extension module. 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_combinations(c, n):   179    180     """   181     Get combinations of colours from 'c' of size 'n' in decreasing order of   182     probability.   183     """   184    185     all = []   186     for l in itertools.combinations(c, n):   187         total = 0   188         for f, value in l:   189             total += f   190         all.append((total, l))   191     all.sort(reverse=True)   192     return [l for total, l in all]   193    194 # Exercise functions for Shedskin.   195    196 if __name__ == "__main__":   197     rgb = (200, 100, 50)   198     saturate_rgb(rgb, 1.0)   199     amplify_rgb(rgb, 1.0)   200     get_value(rgb)   201     get_value(rgb, [(255, 255, 255), (255, 0, 0), (255, 255, 0), (0, 0, 0)])   202     combine([(1.0, (255, 0, 0)), (0.0, (0, 0, 0))])   203     clip(200.0)   204     get_combinations([(0.5, (255, 0, 0)), (0.25, (255, 255, 0)), (0.25, (0, 0, 0))], 2)   205    206 # vim: tabstop=4 expandtab shiftwidth=4