# HG changeset patch # User Paul Boddie # Date 1444226152 -7200 # Node ID 8fd84d0ba366a57caad776fff3b120d16fb92441 # Parent 075e5f1f12e316c69e5f2f4c83bcbbf5c3d10fbd Added documentation and copyright/licensing details. diff -r 075e5f1f12e3 -r 8fd84d0ba366 optimiser.py --- a/optimiser.py Wed Oct 07 00:49:40 2015 +0200 +++ b/optimiser.py Wed Oct 07 15:55:52 2015 +0200 @@ -1,10 +1,29 @@ #!/usr/bin/env python -from random import random, randrange -from os.path import splitext +""" +Convert and optimise images for display in an Acorn Electron MODE 1 variant +with four colours per line but eight colours available for selection for each +line. + +Copyright (C) 2015 Paul Boddie + +This program is free software; you can redistribute it and/or modify it under +the terms of the GNU General Public License as published by the Free Software +Foundation; either version 3 of the License, or (at your option) any later +version. + +This program is distributed in the hope that it will be useful, but WITHOUT ANY +WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +PARTICULAR PURPOSE. See the GNU General Public License for more details. + +You should have received a copy of the GNU General Public License along +with this program. If not, see . +""" + +from random import random +from os.path import split, splitext import EXIF import PIL.Image -import itertools import math import sys @@ -13,6 +32,8 @@ (0, 0, 255), (255, 0, 255), (0, 255, 255), (255, 255, 255) ] +# Basic colour operations. + def within(v, lower, upper): return min(max(v, lower), upper) @@ -41,25 +62,56 @@ def invert(srgb): return tuple(map(lambda x: 1.0 - x, srgb)) +# Colour distribution functions. + cache = {} def combination(rgb): + + "Return the colour distribution for 'rgb'." + if not cache.has_key(rgb): + + # Get the colour with components scaled from 0 to 1, plus the inverted + # component values. + rgb = square(scale(rgb)) rgbi = invert(rgb) pairs = zip(rgbi, rgb) + + # For each corner of the colour cube (primary and secondary colours plus + # black and white), calculate the corner value's contribution to the + # input colour. + d = [] for corner in corners: rs, gs, bs = scale(corner) + + # Obtain inverted channel values where corner channels are low; + # obtain original channel values where corner channels are high. + d.append((pairs[0][int(rs)] * pairs[1][int(gs)] * pairs[2][int(bs)], corner)) + + # Balance the corner contributions. + cache[rgb] = balance(d) + return cache[rgb] def complements(rgb): + + "Return 'rgb' and its complement." + r, g, b = rgb return rgb, restore(invert(scale(rgb))) def balance(d): + + """ + Balance distribution 'd', cancelling opposing values and their complements + and replacing their common contributions with black and white contributions. + """ + d = dict([(value, f) for f, value in d]) for primary, secondary in map(complements, [(255, 0, 0), (0, 255, 0), (0, 0, 255)]): common = min(d[primary], d[secondary]) @@ -70,6 +122,12 @@ return [(f, value) for value, f in d.items()] def compensate(d, chosen): + + """ + Compensate distribution 'd' for the given 'chosen' colours, reducing chosen + colour contributions where their complements are not part of the chosen set. + """ + dd = dict([(value, f) for f, value in d]) for f, value in d: if value in chosen: @@ -80,6 +138,9 @@ return [(f, value) for value, f in dd.items() if value in chosen] def combine(d): + + "Combine distribution 'd' to get a colour value." + out = [0, 0, 0] for v, rgb in d: out[0] += v * rgb[0] @@ -88,6 +149,12 @@ return out def pattern(rgb, chosen=None): + + """ + Obtain a sorted colour distribution for 'rgb', optionally limited to any + specified 'chosen' colours. + """ + l = combination(rgb) if chosen: l = compensate(l, chosen) @@ -95,6 +162,12 @@ return l def get_value(rgb, chosen=None): + + """ + Get an output colour for 'rgb', optionally limited to any specified 'chosen' + colours. + """ + l = pattern(rgb, chosen) limit = sum([f for f, c in l]) choose = random() * limit @@ -105,6 +178,8 @@ return c return c +# Colour processing operations. + def sign(x): return x >= 0 and 1 or -1 @@ -120,7 +195,12 @@ def amplify_value(x, exp): return int(pow(x / 255.0, exp) * 255.0) +# Image operations. + def get_colours(im, y): + + "Get a colour distribution from image 'im' for the row 'y'." + width, height = im.size c = {} for x in range(0, width): @@ -139,6 +219,9 @@ return c def test(): + + "Generate slices of the colour cube." + size = 512 for r in (0, 63, 127, 191, 255): im = PIL.Image.new("RGB", (size, size)) @@ -149,6 +232,9 @@ im.save("rgb%d.png" % r) def test_flat(rgb): + + "Generate a flat image for the colour 'rgb'." + size = 64 im = PIL.Image.new("RGB", (size, size)) for y in range(0, size): @@ -157,6 +243,14 @@ im.save("rgb%02d%02d%02d.png" % rgb) def rotate_and_scale(exif, im, width, height, rotate): + + """ + Using the given 'exif' information, rotate and scale image 'im' given the + indicated 'width' and 'height' constraints and any explicit 'rotate' + indication. The returned image will be within the given 'width' and + 'height', filling either or both, and preserve its original aspect ratio. + """ + if rotate or exif and exif["Image Orientation"].values == [6L]: im = im.rotate(270) @@ -169,6 +263,13 @@ return im.resize((width, height)) def count_colours(im, colours): + + """ + Count colours on each row of image 'im', returning a tuple indicating the + first row with more than the given number of 'colours' together with the + found colours; otherwise returning None. + """ + width, height = im.size for y in range(0, height): l = set() @@ -178,13 +279,37 @@ return (y, l) return None +# Main program. + if __name__ == "__main__": + + # Test options. + if "--test" in sys.argv: test() sys.exit(0) elif "--test-flat" in sys.argv: test_flat((120, 40, 60)) sys.exit(0) + elif "--help" in sys.argv: + print >>sys.stderr, """\ +Usage: %s [ ] + +Options are... + +-s - Saturate the input image (can be repeated) +-d - Desaturate the input image (can be repeated) +-D - Darken the input image (can be repeated) +-B - Brighten the input image (can be repeated) +-2 - Square/diminish the bright corner colour contributions (experimental) + +-r - Rotate the input image clockwise +-p - Generate a separate preview image +-h - Make the preview image with half horizontal resolution (MODE 2) +-v - Verify the output image (loaded if -n is given) +-n - Generate no output image +""" % split(sys.argv[0])[1] + sys.exit(1) width = 320 height = 256 @@ -195,18 +320,28 @@ options = sys.argv[3:] - rotate = "-r" in options + # Preprocessing options that can be repeated for extra effect. + saturate = options.count("-s") desaturate = options.count("-d") darken = options.count("-D") brighten = options.count("-B") + + # Experimental colour distribution modification. + square = "-2" in options and square or (lambda x: x) + + # General output options. + + rotate = "-r" in options preview = "-p" in options half_resolution_preview = "-h" in options verify = "-v" in options no_normal_output = "-n" in options make_image = not no_normal_output + # Load the input image if requested. + if make_image or preview: exif = EXIF.process_file(open(input_filename)) im = PIL.Image.open(input_filename).convert("RGB") @@ -224,6 +359,8 @@ rgb = amplify_rgb(rgb, brighten and math.pow(0.5, brighten) or math.pow(2, darken)) im.putpixel((x, y), rgb) + # Generate a preview if requested. + if preview: imp = im.copy() step = half_resolution_preview and 2 or 1 @@ -237,6 +374,8 @@ imp.save(preview_filename) + # Generate an output image if requested. + if make_image: for y in range(0, height): c = get_colours(im, y) @@ -255,6 +394,8 @@ im.save(output_filename) + # Verify the output image (which may be loaded) if requested. + if verify: if no_normal_output: im = PIL.Image.open(output_filename).convert("RGB") @@ -262,6 +403,6 @@ result = count_colours(im, 4) if result is not None: y, colours = result - print "Row %d has the following colours: %s" % (y, "; ".join([repr(c) for c in colours])) + print "Image %s: row %d has the following colours: %s" % (output_filename, y, "; ".join([repr(c) for c in colours])) # vim: tabstop=4 expandtab shiftwidth=4