PaletteOptimiser

Annotated optimiserlib.py

83:3f96e51bfacb
2015-10-10 Paul Boddie Made a new module for Shedskin to compile as an extension module. simpleimage-shedskin
paul@83 1
#!/usr/bin/env python
paul@83 2
paul@83 3
"""
paul@83 4
Convert and optimise images for display in an Acorn Electron MODE 1 variant
paul@83 5
with four colours per line but eight colours available for selection on each
paul@83 6
line.
paul@83 7
paul@83 8
Copyright (C) 2015 Paul Boddie <paul@boddie.org.uk>
paul@83 9
paul@83 10
This program is free software; you can redistribute it and/or modify it under
paul@83 11
the terms of the GNU General Public License as published by the Free Software
paul@83 12
Foundation; either version 3 of the License, or (at your option) any later
paul@83 13
version.
paul@83 14
paul@83 15
This program is distributed in the hope that it will be useful, but WITHOUT ANY
paul@83 16
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
paul@83 17
PARTICULAR PURPOSE.  See the GNU General Public License for more details.
paul@83 18
paul@83 19
You should have received a copy of the GNU General Public License along
paul@83 20
with this program.  If not, see <http://www.gnu.org/licenses/>.
paul@83 21
"""
paul@83 22
paul@83 23
from random import random, randrange
paul@83 24
paul@83 25
corners = [
paul@83 26
    (0, 0, 0), (255, 0, 0), (0, 255, 0), (255, 255, 0),
paul@83 27
    (0, 0, 255), (255, 0, 255), (0, 255, 255), (255, 255, 255)
paul@83 28
    ]
paul@83 29
paul@83 30
# Basic colour operations.
paul@83 31
paul@83 32
def within(v, lower, upper):
paul@83 33
    return min(max(v, lower), upper)
paul@83 34
paul@83 35
def clip(v):
paul@83 36
    return int(within(v, 0, 255))
paul@83 37
paul@83 38
def restore(srgb):
paul@83 39
    r, g, b = srgb
paul@83 40
    return int(r * 255.0), int(g * 255.0), int(b * 255.0)
paul@83 41
paul@83 42
def scale(rgb):
paul@83 43
    r, g, b = rgb
paul@83 44
    return r / 255.0, g / 255.0, b / 255.0
paul@83 45
paul@83 46
def invert(srgb):
paul@83 47
    r, g, b = srgb
paul@83 48
    return 1.0 - r, 1.0 - g, 1.0 - b
paul@83 49
paul@83 50
scaled_corners = map(scale, corners)
paul@83 51
zipped_corners = zip(corners, scaled_corners)
paul@83 52
paul@83 53
# Colour distribution functions.
paul@83 54
paul@83 55
def combination(rgb):
paul@83 56
paul@83 57
    "Return the colour distribution for 'rgb'."
paul@83 58
paul@83 59
    # Get the colour with components scaled from 0 to 1, plus the inverted
paul@83 60
    # component values.
paul@83 61
paul@83 62
    srgb = scale(rgb)
paul@83 63
    rgbi = invert(srgb)
paul@83 64
    pairs = zip(rgbi, srgb)
paul@83 65
paul@83 66
    # For each corner of the colour cube (primary and secondary colours plus
paul@83 67
    # black and white), calculate the corner value's contribution to the
paul@83 68
    # input colour.
paul@83 69
paul@83 70
    d = []
paul@83 71
    for corner, scaled in zipped_corners:
paul@83 72
        rs, gs, bs = scaled
paul@83 73
paul@83 74
        # Obtain inverted channel values where corner channels are low;
paul@83 75
        # obtain original channel values where corner channels are high.
paul@83 76
paul@83 77
        d.append((pairs[0][int(rs)] * pairs[1][int(gs)] * pairs[2][int(bs)], corner))
paul@83 78
paul@83 79
    # Balance the corner contributions.
paul@83 80
paul@83 81
    return balance(d)
paul@83 82
paul@83 83
def complements(rgb):
paul@83 84
paul@83 85
    "Return 'rgb' and its complement."
paul@83 86
paul@83 87
    r, g, b = rgb
paul@83 88
    return rgb, restore(invert(scale(rgb)))
paul@83 89
paul@83 90
bases = [(0, 0, 0), (255, 0, 0), (0, 255, 0), (0, 0, 255)]
paul@83 91
base_complements = map(complements, bases)
paul@83 92
paul@83 93
def balance(d):
paul@83 94
paul@83 95
    """
paul@83 96
    Balance distribution 'd', cancelling opposing values and their complements
paul@83 97
    and replacing their common contributions with black and white contributions.
paul@83 98
    """
paul@83 99
paul@83 100
    dd = dict([(value, f) for f, value in d])
paul@83 101
    for primary, secondary in base_complements:
paul@83 102
        common = min(dd[primary], dd[secondary])
paul@83 103
        dd[primary] -= common
paul@83 104
        dd[secondary] -= common
paul@83 105
    return [(f, value) for value, f in dd.items()]
paul@83 106
paul@83 107
def combine(d):
paul@83 108
paul@83 109
    "Combine distribution 'd' to get a colour value."
paul@83 110
paul@83 111
    out = [0, 0, 0]
paul@83 112
    for v, rgb in d:
paul@83 113
        out[0] += v * rgb[0]
paul@83 114
        out[1] += v * rgb[1]
paul@83 115
        out[2] += v * rgb[2]
paul@83 116
    return tuple(map(int, out))
paul@83 117
paul@83 118
def pattern(rgb, chosen=None):
paul@83 119
paul@83 120
    """
paul@83 121
    Obtain a sorted colour distribution for 'rgb', optionally limited to any
paul@83 122
    specified 'chosen' colours.
paul@83 123
    """
paul@83 124
paul@83 125
    l = [(f, value) for f, value in combination(rgb) if not chosen or value in chosen]
paul@83 126
    l.sort(reverse=True)
paul@83 127
    return l
paul@83 128
paul@83 129
def get_value(rgb, chosen=None, fail=False):
paul@83 130
paul@83 131
    """
paul@83 132
    Get an output colour for 'rgb', optionally limited to any specified 'chosen'
paul@83 133
    colours. If 'fail' is set to a true value, return None if the colour cannot
paul@83 134
    be expressed using any of the chosen colours.
paul@83 135
    """
paul@83 136
paul@83 137
    l = pattern(rgb, chosen)
paul@83 138
    limit = sum([f for f, c in l])
paul@83 139
    if not limit:
paul@83 140
        if fail:
paul@83 141
            return None
paul@83 142
        else:
paul@83 143
            return l[randrange(0, len(l))][1]
paul@83 144
paul@83 145
    choose = random() * limit
paul@83 146
    threshold = 0
paul@83 147
    for f, c in l:
paul@83 148
        threshold += f
paul@83 149
        if choose < threshold:
paul@83 150
            return c
paul@83 151
    return c
paul@83 152
paul@83 153
# Colour processing operations.
paul@83 154
paul@83 155
def sign(x):
paul@83 156
    if x >= 0:
paul@83 157
        return 1
paul@83 158
    else:
paul@83 159
        return -1
paul@83 160
paul@83 161
def saturate_rgb(rgb, exp):
paul@83 162
    r, g, b = rgb
paul@83 163
    return saturate_value(r, exp), saturate_value(g, exp), saturate_value(b, exp)
paul@83 164
paul@83 165
def saturate_value(x, exp):
paul@83 166
    return int(127.5 + sign(x - 127.5) * 127.5 * pow(abs(x - 127.5) / 127.5, exp))
paul@83 167
paul@83 168
def amplify_rgb(rgb, exp):
paul@83 169
    r, g, b = rgb
paul@83 170
    return amplify_value(r, exp), amplify_value(g, exp), amplify_value(b, exp)
paul@83 171
paul@83 172
def amplify_value(x, exp):
paul@83 173
    return int(pow(x / 255.0, exp) * 255.0)
paul@83 174
paul@83 175
# Exercise functions for Shedskin.
paul@83 176
paul@83 177
if __name__ == "__main__":
paul@83 178
    rgb = (200, 100, 50)
paul@83 179
    saturate_rgb(rgb, 1.0)
paul@83 180
    amplify_rgb(rgb, 1.0)
paul@83 181
    get_value(rgb)
paul@83 182
    get_value(rgb, [(255, 255, 255), (255, 0, 0), (255, 255, 0), (0, 0, 0)])
paul@83 183
    combine([(1.0, (255, 0, 0)), (0.0, (0, 0, 0))])
paul@83 184
    clip(200.0)
paul@83 185
paul@83 186
# vim: tabstop=4 expandtab shiftwidth=4