1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
1.2 +++ b/optimiserlib.py Sun Oct 11 20:10:50 2015 +0200
1.3 @@ -0,0 +1,322 @@
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 +import itertools
1.28 +
1.29 +corners = [
1.30 + (0, 0, 0), (255, 0, 0), (0, 255, 0), (255, 255, 0),
1.31 + (0, 0, 255), (255, 0, 255), (0, 255, 255), (255, 255, 255)
1.32 + ]
1.33 +
1.34 +# Basic colour operations.
1.35 +
1.36 +def within(v, lower, upper):
1.37 + return min(max(v, lower), upper)
1.38 +
1.39 +def clip(v):
1.40 + return int(within(v, 0, 255))
1.41 +
1.42 +def restore(srgb):
1.43 + r, g, b = srgb
1.44 + return int(r * 255.0), int(g * 255.0), int(b * 255.0)
1.45 +
1.46 +def scale(rgb):
1.47 + r, g, b = rgb
1.48 + return r / 255.0, g / 255.0, b / 255.0
1.49 +
1.50 +def invert(srgb):
1.51 + r, g, b = srgb
1.52 + return 1.0 - r, 1.0 - g, 1.0 - b
1.53 +
1.54 +scaled_corners = map(scale, corners)
1.55 +zipped_corners = zip(corners, scaled_corners)
1.56 +
1.57 +# Colour distribution functions.
1.58 +
1.59 +def combination(rgb):
1.60 +
1.61 + "Return the colour distribution for 'rgb'."
1.62 +
1.63 + # Get the colour with components scaled from 0 to 1, plus the inverted
1.64 + # component values.
1.65 +
1.66 + srgb = scale(rgb)
1.67 + rgbi = invert(srgb)
1.68 + pairs = zip(rgbi, srgb)
1.69 +
1.70 + # For each corner of the colour cube (primary and secondary colours plus
1.71 + # black and white), calculate the corner value's contribution to the
1.72 + # input colour.
1.73 +
1.74 + d = []
1.75 + for corner, scaled in zipped_corners:
1.76 + rs, gs, bs = scaled
1.77 +
1.78 + # Obtain inverted channel values where corner channels are low;
1.79 + # obtain original channel values where corner channels are high.
1.80 +
1.81 + d.append((pairs[0][int(rs)] * pairs[1][int(gs)] * pairs[2][int(bs)], corner))
1.82 +
1.83 + # Balance the corner contributions.
1.84 +
1.85 + return balance(d)
1.86 +
1.87 +def complements(rgb):
1.88 +
1.89 + "Return 'rgb' and its complement."
1.90 +
1.91 + r, g, b = rgb
1.92 + return rgb, restore(invert(scale(rgb)))
1.93 +
1.94 +bases = [(0, 0, 0), (255, 0, 0), (0, 255, 0), (0, 0, 255)]
1.95 +base_complements = map(complements, bases)
1.96 +
1.97 +def balance(d):
1.98 +
1.99 + """
1.100 + Balance distribution 'd', cancelling opposing values and their complements
1.101 + and replacing their common contributions with black and white contributions.
1.102 + """
1.103 +
1.104 + d = dict([(value, f) for f, value in d])
1.105 + for primary, secondary in base_complements:
1.106 + common = min(d[primary], d[secondary])
1.107 + d[primary] -= common
1.108 + d[secondary] -= common
1.109 + return [(f, value) for value, f in d.items()]
1.110 +
1.111 +def combine(d):
1.112 +
1.113 + "Combine distribution 'd' to get a colour value."
1.114 +
1.115 + out = [0, 0, 0]
1.116 + for v, rgb in d:
1.117 + out[0] += v * rgb[0]
1.118 + out[1] += v * rgb[1]
1.119 + out[2] += v * rgb[2]
1.120 + return tuple(map(int, out))
1.121 +
1.122 +def pattern(rgb, chosen=None):
1.123 +
1.124 + """
1.125 + Obtain a sorted colour distribution for 'rgb', optionally limited to any
1.126 + specified 'chosen' colours.
1.127 + """
1.128 +
1.129 + l = [(f, value) for f, value in combination(rgb) if not chosen or value in chosen]
1.130 + l.sort(reverse=True)
1.131 + return l
1.132 +
1.133 +def get_value(rgb, chosen=None, fail=False):
1.134 +
1.135 + """
1.136 + Get an output colour for 'rgb', optionally limited to any specified 'chosen'
1.137 + colours. If 'fail' is set to a true value, return None if the colour cannot
1.138 + be expressed using any of the chosen colours.
1.139 + """
1.140 +
1.141 + l = pattern(rgb, chosen)
1.142 + limit = sum([f for f, c in l])
1.143 + if not limit:
1.144 + if fail:
1.145 + return None
1.146 + else:
1.147 + return l[randrange(0, len(l))][1]
1.148 +
1.149 + choose = random() * limit
1.150 + threshold = 0
1.151 + for f, c in l:
1.152 + threshold += f
1.153 + if choose < threshold:
1.154 + return c
1.155 + return c
1.156 +
1.157 +# Colour processing operations.
1.158 +
1.159 +def sign(x):
1.160 + return x >= 0 and 1 or -1
1.161 +
1.162 +def saturate_rgb(rgb, exp):
1.163 + r, g, b = rgb
1.164 + return saturate_value(r, exp), saturate_value(g, exp), saturate_value(b, exp)
1.165 +
1.166 +def saturate_value(x, exp):
1.167 + return int(127.5 + sign(x - 127.5) * 127.5 * pow(abs(x - 127.5) / 127.5, exp))
1.168 +
1.169 +def amplify_rgb(rgb, exp):
1.170 + r, g, b = rgb
1.171 + return amplify_value(r, exp), amplify_value(g, exp), amplify_value(b, exp)
1.172 +
1.173 +def amplify_value(x, exp):
1.174 + return int(pow(x / 255.0, exp) * 255.0)
1.175 +
1.176 +# Image operations.
1.177 +
1.178 +def get_colours(im, y):
1.179 +
1.180 + "Get a colour distribution from image 'im' for the row 'y'."
1.181 +
1.182 + width, height = im.size
1.183 + c = {}
1.184 + x = 0
1.185 + while x < width:
1.186 + rgb = im.getpixel((x, y))
1.187 +
1.188 + # Sum the colour probabilities.
1.189 +
1.190 + for f, value in combination(rgb):
1.191 + if not c.has_key(value):
1.192 + c[value] = f
1.193 + else:
1.194 + c[value] += f
1.195 +
1.196 + x += 1
1.197 +
1.198 + d = [(n/width, value) for value, n in c.items()]
1.199 + d.sort(reverse=True)
1.200 + return d
1.201 +
1.202 +def get_combinations(c, n):
1.203 +
1.204 + """
1.205 + Get combinations of colours from 'c' of size 'n' in decreasing order of
1.206 + probability.
1.207 + """
1.208 +
1.209 + all = []
1.210 + for l in itertools.combinations(c, n):
1.211 + total = 0
1.212 + for f, value in l:
1.213 + total += f
1.214 + all.append((total, l))
1.215 + all.sort(reverse=True)
1.216 + return [l for total, l in all]
1.217 +
1.218 +def count_colours(im, colours):
1.219 +
1.220 + """
1.221 + Count colours on each row of image 'im', returning a tuple indicating the
1.222 + first row with more than the given number of 'colours' together with the
1.223 + found colours; otherwise returning None.
1.224 + """
1.225 +
1.226 + width, height = im.size
1.227 +
1.228 + y = 0
1.229 + while y < height:
1.230 + l = set()
1.231 + x = 0
1.232 + while x < width:
1.233 + l.add(im.getpixel((x, y)))
1.234 + x += 1
1.235 + if len(l) > colours:
1.236 + return (y, l)
1.237 + y += 1
1.238 + return None
1.239 +
1.240 +def process_image(im, saturate, desaturate, darken, brighten):
1.241 +
1.242 + """
1.243 + Process image 'im' using the given options: 'saturate', 'desaturate',
1.244 + 'darken', 'brighten'.
1.245 + """
1.246 +
1.247 + width, height = im.size
1.248 +
1.249 + if saturate or desaturate or darken or brighten:
1.250 + y = 0
1.251 + while y < height:
1.252 + x = 0
1.253 + while x < width:
1.254 + rgb = im.getpixel((x, y))
1.255 + if saturate or desaturate:
1.256 + rgb = saturate_rgb(rgb, saturate and 0.5 / saturate or 2 * desaturate)
1.257 + if darken or brighten:
1.258 + rgb = amplify_rgb(rgb, brighten and 0.5 / brighten or 2 * darken)
1.259 + im.putpixel((x, y), rgb)
1.260 + x += 1
1.261 + y += 1
1.262 +
1.263 +def convert_image(im, colours):
1.264 +
1.265 + "Convert image 'im' to an appropriate output representation."
1.266 +
1.267 + width, height = im.size
1.268 +
1.269 + y = 0
1.270 + while y < height:
1.271 + c = get_colours(im, y)
1.272 +
1.273 + suggestions = []
1.274 +
1.275 + for l in get_combinations(c, colours):
1.276 + most = [value for f, value in l]
1.277 + missing = 0
1.278 +
1.279 + x = 0
1.280 + while x < width:
1.281 + rgb = im.getpixel((x, y))
1.282 + value = get_value(rgb, most, True)
1.283 + if value is None:
1.284 + missing += 1
1.285 + x += 1
1.286 +
1.287 + if not missing:
1.288 + break # use this combination
1.289 + suggestions.append((missing, l))
1.290 +
1.291 + # Find the most accurate suggestion.
1.292 +
1.293 + else:
1.294 + suggestions.sort()
1.295 + most = [value for f, value in suggestions[0][1]] # get the combination
1.296 +
1.297 + x = 0
1.298 + while x < width:
1.299 + rgb = im.getpixel((x, y))
1.300 + value = get_value(rgb, most)
1.301 + im.putpixel((x, y), value)
1.302 +
1.303 + if x < width - 1:
1.304 + rgbn = im.getpixel((x+1, y))
1.305 + rgbn = (
1.306 + clip(rgbn[0] + (rgb[0] - value[0]) / 4.0),
1.307 + clip(rgbn[1] + (rgb[1] - value[1]) / 4.0),
1.308 + clip(rgbn[2] + (rgb[2] - value[2]) / 4.0)
1.309 + )
1.310 + im.putpixel((x+1, y), rgbn)
1.311 +
1.312 + if y < height - 1:
1.313 + rgbn = im.getpixel((x, y+1))
1.314 + rgbn = (
1.315 + clip(rgbn[0] + (rgb[0] - value[0]) / 2.0),
1.316 + clip(rgbn[1] + (rgb[1] - value[1]) / 2.0),
1.317 + clip(rgbn[2] + (rgb[2] - value[2]) / 2.0)
1.318 + )
1.319 + im.putpixel((x, y+1), rgbn)
1.320 +
1.321 + x += 1
1.322 +
1.323 + y += 1
1.324 +
1.325 +# vim: tabstop=4 expandtab shiftwidth=4