PaletteOptimiser

Changeset

83:3f96e51bfacb
2015-10-10 Paul Boddie raw files shortlog changelog graph Made a new module for Shedskin to compile as an extension module. simpleimage-shedskin
optimiser.py (file) optimiserlib.py (file)
     1.1 --- a/optimiser.py	Sat Oct 10 15:25:12 2015 +0200
     1.2 +++ b/optimiser.py	Sat Oct 10 15:36:48 2015 +0200
     1.3 @@ -20,160 +20,13 @@
     1.4  with this program.  If not, see <http://www.gnu.org/licenses/>.
     1.5  """
     1.6  
     1.7 -from random import random, randrange
     1.8 +from optimiserlib import *
     1.9  from os.path import split, splitext
    1.10  import EXIF
    1.11  import PIL.Image
    1.12  import itertools
    1.13  import sys
    1.14  
    1.15 -corners = [
    1.16 -    (0, 0, 0), (255, 0, 0), (0, 255, 0), (255, 255, 0),
    1.17 -    (0, 0, 255), (255, 0, 255), (0, 255, 255), (255, 255, 255)
    1.18 -    ]
    1.19 -
    1.20 -# Basic colour operations.
    1.21 -
    1.22 -def within(v, lower, upper):
    1.23 -    return min(max(v, lower), upper)
    1.24 -
    1.25 -def clip(v):
    1.26 -    return int(within(v, 0, 255))
    1.27 -
    1.28 -def restore(srgb):
    1.29 -    r, g, b = srgb
    1.30 -    return int(r * 255.0), int(g * 255.0), int(b * 255.0)
    1.31 -
    1.32 -def scale(rgb):
    1.33 -    r, g, b = rgb
    1.34 -    return r / 255.0, g / 255.0, b / 255.0
    1.35 -
    1.36 -def invert(srgb):
    1.37 -    r, g, b = srgb
    1.38 -    return 1.0 - r, 1.0 - g, 1.0 - b
    1.39 -
    1.40 -scaled_corners = map(scale, corners)
    1.41 -zipped_corners = zip(corners, scaled_corners)
    1.42 -
    1.43 -# Colour distribution functions.
    1.44 -
    1.45 -def combination(rgb):
    1.46 -
    1.47 -    "Return the colour distribution for 'rgb'."
    1.48 -
    1.49 -    # Get the colour with components scaled from 0 to 1, plus the inverted
    1.50 -    # component values.
    1.51 -
    1.52 -    srgb = scale(rgb)
    1.53 -    rgbi = invert(srgb)
    1.54 -    pairs = zip(rgbi, srgb)
    1.55 -
    1.56 -    # For each corner of the colour cube (primary and secondary colours plus
    1.57 -    # black and white), calculate the corner value's contribution to the
    1.58 -    # input colour.
    1.59 -
    1.60 -    d = []
    1.61 -    for corner, scaled in zipped_corners:
    1.62 -        rs, gs, bs = scaled
    1.63 -
    1.64 -        # Obtain inverted channel values where corner channels are low;
    1.65 -        # obtain original channel values where corner channels are high.
    1.66 -
    1.67 -        d.append((pairs[0][int(rs)] * pairs[1][int(gs)] * pairs[2][int(bs)], corner))
    1.68 -
    1.69 -    # Balance the corner contributions.
    1.70 -
    1.71 -    return balance(d)
    1.72 -
    1.73 -def complements(rgb):
    1.74 -
    1.75 -    "Return 'rgb' and its complement."
    1.76 -
    1.77 -    r, g, b = rgb
    1.78 -    return rgb, restore(invert(scale(rgb)))
    1.79 -
    1.80 -bases = [(0, 0, 0), (255, 0, 0), (0, 255, 0), (0, 0, 255)]
    1.81 -base_complements = map(complements, bases)
    1.82 -
    1.83 -def balance(d):
    1.84 -
    1.85 -    """
    1.86 -    Balance distribution 'd', cancelling opposing values and their complements
    1.87 -    and replacing their common contributions with black and white contributions.
    1.88 -    """
    1.89 -
    1.90 -    d = dict([(value, f) for f, value in d])
    1.91 -    for primary, secondary in base_complements:
    1.92 -        common = min(d[primary], d[secondary])
    1.93 -        d[primary] -= common
    1.94 -        d[secondary] -= common
    1.95 -    return [(f, value) for value, f in d.items()]
    1.96 -
    1.97 -def combine(d):
    1.98 -
    1.99 -    "Combine distribution 'd' to get a colour value."
   1.100 -
   1.101 -    out = [0, 0, 0]
   1.102 -    for v, rgb in d:
   1.103 -        out[0] += v * rgb[0]
   1.104 -        out[1] += v * rgb[1]
   1.105 -        out[2] += v * rgb[2]
   1.106 -    return tuple(map(int, out))
   1.107 -
   1.108 -def pattern(rgb, chosen=None):
   1.109 -
   1.110 -    """
   1.111 -    Obtain a sorted colour distribution for 'rgb', optionally limited to any
   1.112 -    specified 'chosen' colours.
   1.113 -    """
   1.114 -
   1.115 -    l = [(f, value) for f, value in combination(rgb) if not chosen or value in chosen]
   1.116 -    l.sort(reverse=True)
   1.117 -    return l
   1.118 -
   1.119 -def get_value(rgb, chosen=None, fail=False):
   1.120 -
   1.121 -    """
   1.122 -    Get an output colour for 'rgb', optionally limited to any specified 'chosen'
   1.123 -    colours. If 'fail' is set to a true value, return None if the colour cannot
   1.124 -    be expressed using any of the chosen colours.
   1.125 -    """
   1.126 -
   1.127 -    l = pattern(rgb, chosen)
   1.128 -    limit = sum([f for f, c in l])
   1.129 -    if not limit:
   1.130 -        if fail:
   1.131 -            return None
   1.132 -        else:
   1.133 -            return l[randrange(0, len(l))][1]
   1.134 -
   1.135 -    choose = random() * limit
   1.136 -    threshold = 0
   1.137 -    for f, c in l:
   1.138 -        threshold += f
   1.139 -        if choose < threshold:
   1.140 -            return c
   1.141 -    return c
   1.142 -
   1.143 -# Colour processing operations.
   1.144 -
   1.145 -def sign(x):
   1.146 -    return x >= 0 and 1 or -1
   1.147 -
   1.148 -def saturate_rgb(rgb, exp):
   1.149 -    r, g, b = rgb
   1.150 -    return saturate_value(r, exp), saturate_value(g, exp), saturate_value(b, exp)
   1.151 -
   1.152 -def saturate_value(x, exp):
   1.153 -    return int(127.5 + sign(x - 127.5) * 127.5 * pow(abs(x - 127.5) / 127.5, exp))
   1.154 -
   1.155 -def amplify_rgb(rgb, exp):
   1.156 -    r, g, b = rgb
   1.157 -    return amplify_value(r, exp), amplify_value(g, exp), amplify_value(b, exp)
   1.158 -
   1.159 -def amplify_value(x, exp):
   1.160 -    return int(pow(x / 255.0, exp) * 255.0)
   1.161 -
   1.162  # Image operations.
   1.163  
   1.164  def get_colours(im, y):
     2.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     2.2 +++ b/optimiserlib.py	Sat Oct 10 15:36:48 2015 +0200
     2.3 @@ -0,0 +1,186 @@
     2.4 +#!/usr/bin/env python
     2.5 +
     2.6 +"""
     2.7 +Convert and optimise images for display in an Acorn Electron MODE 1 variant
     2.8 +with four colours per line but eight colours available for selection on each
     2.9 +line.
    2.10 +
    2.11 +Copyright (C) 2015 Paul Boddie <paul@boddie.org.uk>
    2.12 +
    2.13 +This program is free software; you can redistribute it and/or modify it under
    2.14 +the terms of the GNU General Public License as published by the Free Software
    2.15 +Foundation; either version 3 of the License, or (at your option) any later
    2.16 +version.
    2.17 +
    2.18 +This program is distributed in the hope that it will be useful, but WITHOUT ANY
    2.19 +WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
    2.20 +PARTICULAR PURPOSE.  See the GNU General Public License for more details.
    2.21 +
    2.22 +You should have received a copy of the GNU General Public License along
    2.23 +with this program.  If not, see <http://www.gnu.org/licenses/>.
    2.24 +"""
    2.25 +
    2.26 +from random import random, randrange
    2.27 +
    2.28 +corners = [
    2.29 +    (0, 0, 0), (255, 0, 0), (0, 255, 0), (255, 255, 0),
    2.30 +    (0, 0, 255), (255, 0, 255), (0, 255, 255), (255, 255, 255)
    2.31 +    ]
    2.32 +
    2.33 +# Basic colour operations.
    2.34 +
    2.35 +def within(v, lower, upper):
    2.36 +    return min(max(v, lower), upper)
    2.37 +
    2.38 +def clip(v):
    2.39 +    return int(within(v, 0, 255))
    2.40 +
    2.41 +def restore(srgb):
    2.42 +    r, g, b = srgb
    2.43 +    return int(r * 255.0), int(g * 255.0), int(b * 255.0)
    2.44 +
    2.45 +def scale(rgb):
    2.46 +    r, g, b = rgb
    2.47 +    return r / 255.0, g / 255.0, b / 255.0
    2.48 +
    2.49 +def invert(srgb):
    2.50 +    r, g, b = srgb
    2.51 +    return 1.0 - r, 1.0 - g, 1.0 - b
    2.52 +
    2.53 +scaled_corners = map(scale, corners)
    2.54 +zipped_corners = zip(corners, scaled_corners)
    2.55 +
    2.56 +# Colour distribution functions.
    2.57 +
    2.58 +def combination(rgb):
    2.59 +
    2.60 +    "Return the colour distribution for 'rgb'."
    2.61 +
    2.62 +    # Get the colour with components scaled from 0 to 1, plus the inverted
    2.63 +    # component values.
    2.64 +
    2.65 +    srgb = scale(rgb)
    2.66 +    rgbi = invert(srgb)
    2.67 +    pairs = zip(rgbi, srgb)
    2.68 +
    2.69 +    # For each corner of the colour cube (primary and secondary colours plus
    2.70 +    # black and white), calculate the corner value's contribution to the
    2.71 +    # input colour.
    2.72 +
    2.73 +    d = []
    2.74 +    for corner, scaled in zipped_corners:
    2.75 +        rs, gs, bs = scaled
    2.76 +
    2.77 +        # Obtain inverted channel values where corner channels are low;
    2.78 +        # obtain original channel values where corner channels are high.
    2.79 +
    2.80 +        d.append((pairs[0][int(rs)] * pairs[1][int(gs)] * pairs[2][int(bs)], corner))
    2.81 +
    2.82 +    # Balance the corner contributions.
    2.83 +
    2.84 +    return balance(d)
    2.85 +
    2.86 +def complements(rgb):
    2.87 +
    2.88 +    "Return 'rgb' and its complement."
    2.89 +
    2.90 +    r, g, b = rgb
    2.91 +    return rgb, restore(invert(scale(rgb)))
    2.92 +
    2.93 +bases = [(0, 0, 0), (255, 0, 0), (0, 255, 0), (0, 0, 255)]
    2.94 +base_complements = map(complements, bases)
    2.95 +
    2.96 +def balance(d):
    2.97 +
    2.98 +    """
    2.99 +    Balance distribution 'd', cancelling opposing values and their complements
   2.100 +    and replacing their common contributions with black and white contributions.
   2.101 +    """
   2.102 +
   2.103 +    dd = dict([(value, f) for f, value in d])
   2.104 +    for primary, secondary in base_complements:
   2.105 +        common = min(dd[primary], dd[secondary])
   2.106 +        dd[primary] -= common
   2.107 +        dd[secondary] -= common
   2.108 +    return [(f, value) for value, f in dd.items()]
   2.109 +
   2.110 +def combine(d):
   2.111 +
   2.112 +    "Combine distribution 'd' to get a colour value."
   2.113 +
   2.114 +    out = [0, 0, 0]
   2.115 +    for v, rgb in d:
   2.116 +        out[0] += v * rgb[0]
   2.117 +        out[1] += v * rgb[1]
   2.118 +        out[2] += v * rgb[2]
   2.119 +    return tuple(map(int, out))
   2.120 +
   2.121 +def pattern(rgb, chosen=None):
   2.122 +
   2.123 +    """
   2.124 +    Obtain a sorted colour distribution for 'rgb', optionally limited to any
   2.125 +    specified 'chosen' colours.
   2.126 +    """
   2.127 +
   2.128 +    l = [(f, value) for f, value in combination(rgb) if not chosen or value in chosen]
   2.129 +    l.sort(reverse=True)
   2.130 +    return l
   2.131 +
   2.132 +def get_value(rgb, chosen=None, fail=False):
   2.133 +
   2.134 +    """
   2.135 +    Get an output colour for 'rgb', optionally limited to any specified 'chosen'
   2.136 +    colours. If 'fail' is set to a true value, return None if the colour cannot
   2.137 +    be expressed using any of the chosen colours.
   2.138 +    """
   2.139 +
   2.140 +    l = pattern(rgb, chosen)
   2.141 +    limit = sum([f for f, c in l])
   2.142 +    if not limit:
   2.143 +        if fail:
   2.144 +            return None
   2.145 +        else:
   2.146 +            return l[randrange(0, len(l))][1]
   2.147 +
   2.148 +    choose = random() * limit
   2.149 +    threshold = 0
   2.150 +    for f, c in l:
   2.151 +        threshold += f
   2.152 +        if choose < threshold:
   2.153 +            return c
   2.154 +    return c
   2.155 +
   2.156 +# Colour processing operations.
   2.157 +
   2.158 +def sign(x):
   2.159 +    if x >= 0:
   2.160 +        return 1
   2.161 +    else:
   2.162 +        return -1
   2.163 +
   2.164 +def saturate_rgb(rgb, exp):
   2.165 +    r, g, b = rgb
   2.166 +    return saturate_value(r, exp), saturate_value(g, exp), saturate_value(b, exp)
   2.167 +
   2.168 +def saturate_value(x, exp):
   2.169 +    return int(127.5 + sign(x - 127.5) * 127.5 * pow(abs(x - 127.5) / 127.5, exp))
   2.170 +
   2.171 +def amplify_rgb(rgb, exp):
   2.172 +    r, g, b = rgb
   2.173 +    return amplify_value(r, exp), amplify_value(g, exp), amplify_value(b, exp)
   2.174 +
   2.175 +def amplify_value(x, exp):
   2.176 +    return int(pow(x / 255.0, exp) * 255.0)
   2.177 +
   2.178 +# Exercise functions for Shedskin.
   2.179 +
   2.180 +if __name__ == "__main__":
   2.181 +    rgb = (200, 100, 50)
   2.182 +    saturate_rgb(rgb, 1.0)
   2.183 +    amplify_rgb(rgb, 1.0)
   2.184 +    get_value(rgb)
   2.185 +    get_value(rgb, [(255, 255, 255), (255, 0, 0), (255, 255, 0), (0, 0, 0)])
   2.186 +    combine([(1.0, (255, 0, 0)), (0.0, (0, 0, 0))])
   2.187 +    clip(200.0)
   2.188 +
   2.189 +# vim: tabstop=4 expandtab shiftwidth=4