PaletteOptimiser

Change of optimiserlib.py

83:3f96e51bfacb
optimiserlib.py simpleimage-shedskin
     1.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.2 +++ b/optimiserlib.py	Sat Oct 10 15:36:48 2015 +0200
     1.3 @@ -0,0 +1,186 @@
     1.4 +#!/usr/bin/env python
     1.5 +
     1.6 +"""
     1.7 +Convert and optimise images for display in an Acorn Electron MODE 1 variant
     1.8 +with four colours per line but eight colours available for selection on each
     1.9 +line.
    1.10 +
    1.11 +Copyright (C) 2015 Paul Boddie <paul@boddie.org.uk>
    1.12 +
    1.13 +This program is free software; you can redistribute it and/or modify it under
    1.14 +the terms of the GNU General Public License as published by the Free Software
    1.15 +Foundation; either version 3 of the License, or (at your option) any later
    1.16 +version.
    1.17 +
    1.18 +This program is distributed in the hope that it will be useful, but WITHOUT ANY
    1.19 +WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
    1.20 +PARTICULAR PURPOSE.  See the GNU General Public License for more details.
    1.21 +
    1.22 +You should have received a copy of the GNU General Public License along
    1.23 +with this program.  If not, see <http://www.gnu.org/licenses/>.
    1.24 +"""
    1.25 +
    1.26 +from random import random, randrange
    1.27 +
    1.28 +corners = [
    1.29 +    (0, 0, 0), (255, 0, 0), (0, 255, 0), (255, 255, 0),
    1.30 +    (0, 0, 255), (255, 0, 255), (0, 255, 255), (255, 255, 255)
    1.31 +    ]
    1.32 +
    1.33 +# Basic colour operations.
    1.34 +
    1.35 +def within(v, lower, upper):
    1.36 +    return min(max(v, lower), upper)
    1.37 +
    1.38 +def clip(v):
    1.39 +    return int(within(v, 0, 255))
    1.40 +
    1.41 +def restore(srgb):
    1.42 +    r, g, b = srgb
    1.43 +    return int(r * 255.0), int(g * 255.0), int(b * 255.0)
    1.44 +
    1.45 +def scale(rgb):
    1.46 +    r, g, b = rgb
    1.47 +    return r / 255.0, g / 255.0, b / 255.0
    1.48 +
    1.49 +def invert(srgb):
    1.50 +    r, g, b = srgb
    1.51 +    return 1.0 - r, 1.0 - g, 1.0 - b
    1.52 +
    1.53 +scaled_corners = map(scale, corners)
    1.54 +zipped_corners = zip(corners, scaled_corners)
    1.55 +
    1.56 +# Colour distribution functions.
    1.57 +
    1.58 +def combination(rgb):
    1.59 +
    1.60 +    "Return the colour distribution for 'rgb'."
    1.61 +
    1.62 +    # Get the colour with components scaled from 0 to 1, plus the inverted
    1.63 +    # component values.
    1.64 +
    1.65 +    srgb = scale(rgb)
    1.66 +    rgbi = invert(srgb)
    1.67 +    pairs = zip(rgbi, srgb)
    1.68 +
    1.69 +    # For each corner of the colour cube (primary and secondary colours plus
    1.70 +    # black and white), calculate the corner value's contribution to the
    1.71 +    # input colour.
    1.72 +
    1.73 +    d = []
    1.74 +    for corner, scaled in zipped_corners:
    1.75 +        rs, gs, bs = scaled
    1.76 +
    1.77 +        # Obtain inverted channel values where corner channels are low;
    1.78 +        # obtain original channel values where corner channels are high.
    1.79 +
    1.80 +        d.append((pairs[0][int(rs)] * pairs[1][int(gs)] * pairs[2][int(bs)], corner))
    1.81 +
    1.82 +    # Balance the corner contributions.
    1.83 +
    1.84 +    return balance(d)
    1.85 +
    1.86 +def complements(rgb):
    1.87 +
    1.88 +    "Return 'rgb' and its complement."
    1.89 +
    1.90 +    r, g, b = rgb
    1.91 +    return rgb, restore(invert(scale(rgb)))
    1.92 +
    1.93 +bases = [(0, 0, 0), (255, 0, 0), (0, 255, 0), (0, 0, 255)]
    1.94 +base_complements = map(complements, bases)
    1.95 +
    1.96 +def balance(d):
    1.97 +
    1.98 +    """
    1.99 +    Balance distribution 'd', cancelling opposing values and their complements
   1.100 +    and replacing their common contributions with black and white contributions.
   1.101 +    """
   1.102 +
   1.103 +    dd = dict([(value, f) for f, value in d])
   1.104 +    for primary, secondary in base_complements:
   1.105 +        common = min(dd[primary], dd[secondary])
   1.106 +        dd[primary] -= common
   1.107 +        dd[secondary] -= common
   1.108 +    return [(f, value) for value, f in dd.items()]
   1.109 +
   1.110 +def combine(d):
   1.111 +
   1.112 +    "Combine distribution 'd' to get a colour value."
   1.113 +
   1.114 +    out = [0, 0, 0]
   1.115 +    for v, rgb in d:
   1.116 +        out[0] += v * rgb[0]
   1.117 +        out[1] += v * rgb[1]
   1.118 +        out[2] += v * rgb[2]
   1.119 +    return tuple(map(int, out))
   1.120 +
   1.121 +def pattern(rgb, chosen=None):
   1.122 +
   1.123 +    """
   1.124 +    Obtain a sorted colour distribution for 'rgb', optionally limited to any
   1.125 +    specified 'chosen' colours.
   1.126 +    """
   1.127 +
   1.128 +    l = [(f, value) for f, value in combination(rgb) if not chosen or value in chosen]
   1.129 +    l.sort(reverse=True)
   1.130 +    return l
   1.131 +
   1.132 +def get_value(rgb, chosen=None, fail=False):
   1.133 +
   1.134 +    """
   1.135 +    Get an output colour for 'rgb', optionally limited to any specified 'chosen'
   1.136 +    colours. If 'fail' is set to a true value, return None if the colour cannot
   1.137 +    be expressed using any of the chosen colours.
   1.138 +    """
   1.139 +
   1.140 +    l = pattern(rgb, chosen)
   1.141 +    limit = sum([f for f, c in l])
   1.142 +    if not limit:
   1.143 +        if fail:
   1.144 +            return None
   1.145 +        else:
   1.146 +            return l[randrange(0, len(l))][1]
   1.147 +
   1.148 +    choose = random() * limit
   1.149 +    threshold = 0
   1.150 +    for f, c in l:
   1.151 +        threshold += f
   1.152 +        if choose < threshold:
   1.153 +            return c
   1.154 +    return c
   1.155 +
   1.156 +# Colour processing operations.
   1.157 +
   1.158 +def sign(x):
   1.159 +    if x >= 0:
   1.160 +        return 1
   1.161 +    else:
   1.162 +        return -1
   1.163 +
   1.164 +def saturate_rgb(rgb, exp):
   1.165 +    r, g, b = rgb
   1.166 +    return saturate_value(r, exp), saturate_value(g, exp), saturate_value(b, exp)
   1.167 +
   1.168 +def saturate_value(x, exp):
   1.169 +    return int(127.5 + sign(x - 127.5) * 127.5 * pow(abs(x - 127.5) / 127.5, exp))
   1.170 +
   1.171 +def amplify_rgb(rgb, exp):
   1.172 +    r, g, b = rgb
   1.173 +    return amplify_value(r, exp), amplify_value(g, exp), amplify_value(b, exp)
   1.174 +
   1.175 +def amplify_value(x, exp):
   1.176 +    return int(pow(x / 255.0, exp) * 255.0)
   1.177 +
   1.178 +# Exercise functions for Shedskin.
   1.179 +
   1.180 +if __name__ == "__main__":
   1.181 +    rgb = (200, 100, 50)
   1.182 +    saturate_rgb(rgb, 1.0)
   1.183 +    amplify_rgb(rgb, 1.0)
   1.184 +    get_value(rgb)
   1.185 +    get_value(rgb, [(255, 255, 255), (255, 0, 0), (255, 255, 0), (0, 0, 0)])
   1.186 +    combine([(1.0, (255, 0, 0)), (0.0, (0, 0, 0))])
   1.187 +    clip(200.0)
   1.188 +
   1.189 +# vim: tabstop=4 expandtab shiftwidth=4