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