PaletteOptimiser

Changeset

71:ba632fd74cbb
2015-10-09 Paul Boddie raw files shortlog changelog graph Introduced a separate main program for potential Shedskin analysis. shedskin
main.py (file) optimiser.py (file)
     1.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.2 +++ b/main.py	Fri Oct 09 23:48:21 2015 +0200
     1.3 @@ -0,0 +1,174 @@
     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 optimiser import *
    1.27 +from os.path import split, splitext
    1.28 +import EXIF
    1.29 +import PIL.Image
    1.30 +import sys
    1.31 +
    1.32 +def rotate_and_scale(exif, im, width, height, rotate):
    1.33 +
    1.34 +    """
    1.35 +    Using the given 'exif' information, rotate and scale image 'im' given the
    1.36 +    indicated 'width' and 'height' constraints and any explicit 'rotate'
    1.37 +    indication. The returned image will be within the given 'width' and
    1.38 +    'height', filling either or both, and preserve its original aspect ratio.
    1.39 +    """
    1.40 +
    1.41 +    if rotate or exif and exif["Image Orientation"].values == [6L]:
    1.42 +        im = im.rotate(270)
    1.43 +
    1.44 +    w, h = im.size
    1.45 +    if w > h:
    1.46 +        height = (width * h) / w
    1.47 +    else:
    1.48 +        width = (height * w) / h
    1.49 +
    1.50 +    return im.resize((width, height))
    1.51 +
    1.52 +def test():
    1.53 +
    1.54 +    "Generate slices of the colour cube."
    1.55 +
    1.56 +    size = 64
    1.57 +    for r in (0, 63, 127, 191, 255):
    1.58 +        pim = PIL.Image.new("RGB", (size, size))
    1.59 +        im = SimpleImage(list(pim.getdata()), pim.size)
    1.60 +        test_slice(im, size, r)
    1.61 +        pim.putdata(im.getdata())
    1.62 +        pim.save("rgb%d.png" % r)
    1.63 +
    1.64 +def test_flat(rgb):
    1.65 +
    1.66 +    "Generate a flat image for the colour 'rgb'."
    1.67 +
    1.68 +    size = 64
    1.69 +    pim = PIL.Image.new("RGB", (size, size))
    1.70 +    im = SimpleImage(list(pim.getdata()), pim.size)
    1.71 +    test_flat_slice(im, size, rgb)
    1.72 +    pim.putdata(im.getdata())
    1.73 +    pim.save("rgb%02d%02d%02d.png" % rgb)
    1.74 +
    1.75 +def get_float(options, flag):
    1.76 +    try:
    1.77 +        i = options.index(flag)
    1.78 +        if i+1 < len(options) and options[i+1].isdigit():
    1.79 +            return float(options[i+1])
    1.80 +        else:
    1.81 +            return 1.0
    1.82 +    except ValueError:
    1.83 +        return 0.0
    1.84 +
    1.85 +# Main program.
    1.86 +
    1.87 +if __name__ == "__main__":
    1.88 +
    1.89 +    # Test options.
    1.90 +
    1.91 +    if "--test" in sys.argv:
    1.92 +        test()
    1.93 +        sys.exit(0)
    1.94 +    elif "--test-flat" in sys.argv:
    1.95 +        test_flat((120, 40, 60))
    1.96 +        sys.exit(0)
    1.97 +    elif "--help" in sys.argv:
    1.98 +        print >>sys.stderr, """\
    1.99 +Usage: %s <input filename> <output filename> [ <options> ]
   1.100 +
   1.101 +Options are...
   1.102 +
   1.103 +-s - Saturate the input image (can be followed by a float, default 1.0)
   1.104 +-d - Desaturate the input image (can be followed by a float, default 1.0)
   1.105 +-D - Darken the input image (can be followed by a float, default 1.0)
   1.106 +-B - Brighten the input image (can be followed by a float, default 1.0)
   1.107 +
   1.108 +-r - Rotate the input image clockwise
   1.109 +-p - Generate a separate preview image
   1.110 +-h - Make the preview image with half horizontal resolution (MODE 2)
   1.111 +-v - Verify the output image (loaded if -n is given)
   1.112 +-n - Generate no output image
   1.113 +""" % split(sys.argv[0])[1]
   1.114 +        sys.exit(1)
   1.115 +
   1.116 +    width = 320
   1.117 +    height = 256
   1.118 +
   1.119 +    input_filename, output_filename = sys.argv[1:3]
   1.120 +    basename, ext = splitext(output_filename)
   1.121 +    preview_filename = "".join([basename + "_preview", ext])
   1.122 +
   1.123 +    options = sys.argv[3:]
   1.124 +
   1.125 +    # Preprocessing options that can be repeated for extra effect.
   1.126 +
   1.127 +    saturate = get_float(options, "-s")
   1.128 +    desaturate = get_float(options, "-d")
   1.129 +    darken = get_float(options, "-D")
   1.130 +    brighten = get_float(options, "-B")
   1.131 +
   1.132 +    # General output options.
   1.133 +
   1.134 +    rotate = "-r" in options
   1.135 +    preview = "-p" in options
   1.136 +    half_resolution_preview = "-h" in options
   1.137 +    verify = "-v" in options
   1.138 +    no_normal_output = "-n" in options
   1.139 +    make_image = not no_normal_output
   1.140 +
   1.141 +    # Load the input image if requested.
   1.142 +
   1.143 +    if make_image or preview:
   1.144 +        exif = EXIF.process_file(open(input_filename))
   1.145 +        pim = PIL.Image.open(input_filename).convert("RGB")
   1.146 +        pim = rotate_and_scale(exif, pim, width, height, rotate)
   1.147 +        im = SimpleImage(list(pim.getdata()), pim.size)
   1.148 +        process_image(im, saturate, desaturate, darken, brighten)
   1.149 +
   1.150 +    # Generate a preview if requested.
   1.151 +
   1.152 +    if preview:
   1.153 +        imp = preview_image(im, half_resolution_preview)
   1.154 +        pimp = pim.copy()
   1.155 +        pimp.putdata(imp.getdata())
   1.156 +        pimp.save(preview_filename)
   1.157 +
   1.158 +    # Generate an output image if requested.
   1.159 +
   1.160 +    if make_image:
   1.161 +        convert_image(im)
   1.162 +        pim.putdata(im.getdata())
   1.163 +        pim.save(output_filename)
   1.164 +
   1.165 +    # Verify the output image (which may be loaded) if requested.
   1.166 +
   1.167 +    if verify:
   1.168 +        if no_normal_output:
   1.169 +            pim = PIL.Image.open(output_filename).convert("RGB")
   1.170 +            im = SimpleImage(list(pim.getdata()), pim.size)
   1.171 +
   1.172 +        result = count_colours(im, 4)
   1.173 +        if result is not None:
   1.174 +            y, colours = result
   1.175 +            print "Image %s: row %d has the following colours: %s" % (output_filename, y, "; ".join([repr(c) for c in colours]))
   1.176 +
   1.177 +# vim: tabstop=4 expandtab shiftwidth=4
     2.1 --- a/optimiser.py	Fri Oct 09 22:16:49 2015 +0200
     2.2 +++ b/optimiser.py	Fri Oct 09 23:48:21 2015 +0200
     2.3 @@ -21,9 +21,6 @@
     2.4  """
     2.5  
     2.6  from random import random, randrange
     2.7 -from os.path import split, splitext
     2.8 -import EXIF
     2.9 -import PIL.Image
    2.10  import itertools
    2.11  import math
    2.12  import sys
    2.13 @@ -208,49 +205,16 @@
    2.14      all.sort(reverse=True)
    2.15      return [l for total, l in all]
    2.16  
    2.17 -def test():
    2.18 -
    2.19 -    "Generate slices of the colour cube."
    2.20 +def test_slice(im, size, r):
    2.21 +    for g in range(0, size):
    2.22 +        for b in range(0, size):
    2.23 +            value = get_value((r, (g * 256) / size, (b * 256 / size)))
    2.24 +            im.putpixel((g, b), value)
    2.25  
    2.26 -    size = 512
    2.27 -    for r in (0, 63, 127, 191, 255):
    2.28 -        im = PIL.Image.new("RGB", (size, size))
    2.29 -        for g in range(0, size):
    2.30 -            for b in range(0, size):
    2.31 -                value = get_value((r, (g * 256) / size, (b * 256 / size)))
    2.32 -                im.putpixel((g, b), value)
    2.33 -        im.save("rgb%d.png" % r)
    2.34 -
    2.35 -def test_flat(rgb):
    2.36 -
    2.37 -    "Generate a flat image for the colour 'rgb'."
    2.38 -
    2.39 -    size = 64
    2.40 -    im = PIL.Image.new("RGB", (size, size))
    2.41 +def test_flat_slice(im, size, rgb):
    2.42      for y in range(0, size):
    2.43          for x in range(0, size):
    2.44              im.putpixel((x, y), get_value(rgb))
    2.45 -    im.save("rgb%02d%02d%02d.png" % rgb)
    2.46 -
    2.47 -def rotate_and_scale(exif, im, width, height, rotate):
    2.48 -
    2.49 -    """
    2.50 -    Using the given 'exif' information, rotate and scale image 'im' given the
    2.51 -    indicated 'width' and 'height' constraints and any explicit 'rotate'
    2.52 -    indication. The returned image will be within the given 'width' and
    2.53 -    'height', filling either or both, and preserve its original aspect ratio.
    2.54 -    """
    2.55 -
    2.56 -    if rotate or exif and exif["Image Orientation"].values == [6L]:
    2.57 -        im = im.rotate(270)
    2.58 -
    2.59 -    w, h = im.size
    2.60 -    if w > h:
    2.61 -        height = (width * h) / w
    2.62 -    else:
    2.63 -        width = (height * w) / h
    2.64 -
    2.65 -    return im.resize((width, height))
    2.66  
    2.67  def count_colours(im, colours):
    2.68  
    2.69 @@ -263,9 +227,7 @@
    2.70      width, height = im.size
    2.71  
    2.72      for y in range(0, height):
    2.73 -        l = set()
    2.74 -        for x in range(0, width):
    2.75 -            l.add(im.getpixel((x, y)))
    2.76 +        l = set(im.getdata()[y * width:(y+1) * width])
    2.77          if len(l) > colours:
    2.78              return (y, l)
    2.79      return None
    2.80 @@ -344,101 +306,44 @@
    2.81                  rgbn = tuple(map(lambda i: clip(i[0] + (i[1] - i[2]) / 2.0), zip(rgbn, rgb, value)))
    2.82                  im.putpixel((x, y+1), rgbn)
    2.83  
    2.84 -def get_float(options, flag):
    2.85 -    try:
    2.86 -        i = options.index(flag)
    2.87 -        if i+1 < len(options) and options[i+1].isdigit():
    2.88 -            return float(options[i+1])
    2.89 -        else:
    2.90 -            return 1.0
    2.91 -    except ValueError:
    2.92 -        return 0.0
    2.93 +class SimpleImage:
    2.94 +
    2.95 +    "An image behaving like PIL.Image."
    2.96 +
    2.97 +    def __init__(self, data, size):
    2.98 +        self.data = data
    2.99 +        self.width, self.height = self.size = size
   2.100 +
   2.101 +    def copy(self):
   2.102 +        return SimpleImage(self.data[:], self.size)
   2.103  
   2.104 -# Main program.
   2.105 +    def getpixel(self, xy):
   2.106 +        x, y = xy
   2.107 +        return self.data[y * self.width + x]
   2.108 +
   2.109 +    def putpixel(self, xy, value):
   2.110 +        x, y = xy
   2.111 +        self.data[y * self.width + x] = value
   2.112 +
   2.113 +    def getdata(self):
   2.114 +        return self.data
   2.115 +
   2.116 +# Test program.
   2.117  
   2.118  if __name__ == "__main__":
   2.119 -
   2.120 -    # Test options.
   2.121 -
   2.122 -    if "--test" in sys.argv:
   2.123 -        test()
   2.124 -        sys.exit(0)
   2.125 -    elif "--test-flat" in sys.argv:
   2.126 -        test_flat((120, 40, 60))
   2.127 -        sys.exit(0)
   2.128 -    elif "--help" in sys.argv:
   2.129 -        print >>sys.stderr, """\
   2.130 -Usage: %s <input filename> <output filename> [ <options> ]
   2.131 -
   2.132 -Options are...
   2.133 +    data = [(0, 0, 0)] * 1024
   2.134 +    size = (32, 32)
   2.135  
   2.136 --s - Saturate the input image (can be followed by a float, default 1.0)
   2.137 --d - Desaturate the input image (can be followed by a float, default 1.0)
   2.138 --D - Darken the input image (can be followed by a float, default 1.0)
   2.139 --B - Brighten the input image (can be followed by a float, default 1.0)
   2.140 -
   2.141 --r - Rotate the input image clockwise
   2.142 --p - Generate a separate preview image
   2.143 --h - Make the preview image with half horizontal resolution (MODE 2)
   2.144 --v - Verify the output image (loaded if -n is given)
   2.145 --n - Generate no output image
   2.146 -""" % split(sys.argv[0])[1]
   2.147 -        sys.exit(1)
   2.148 -
   2.149 -    width = 320
   2.150 -    height = 256
   2.151 -
   2.152 -    input_filename, output_filename = sys.argv[1:3]
   2.153 -    basename, ext = splitext(output_filename)
   2.154 -    preview_filename = "".join([basename + "_preview", ext])
   2.155 -
   2.156 -    options = sys.argv[3:]
   2.157 -
   2.158 -    # Preprocessing options that can be repeated for extra effect.
   2.159 +    im = SimpleImage(data, size)
   2.160  
   2.161 -    saturate = get_float(options, "-s")
   2.162 -    desaturate = get_float(options, "-d")
   2.163 -    darken = get_float(options, "-D")
   2.164 -    brighten = get_float(options, "-B")
   2.165 -
   2.166 -    # General output options.
   2.167 -
   2.168 -    rotate = "-r" in options
   2.169 -    preview = "-p" in options
   2.170 -    half_resolution_preview = "-h" in options
   2.171 -    verify = "-v" in options
   2.172 -    no_normal_output = "-n" in options
   2.173 -    make_image = not no_normal_output
   2.174 -
   2.175 -    # Load the input image if requested.
   2.176 -
   2.177 -    if make_image or preview:
   2.178 -        exif = EXIF.process_file(open(input_filename))
   2.179 -        im = PIL.Image.open(input_filename).convert("RGB")
   2.180 -        im = rotate_and_scale(exif, im, width, height, rotate)
   2.181 +    process_image(im, 1.0, 0.0, 1.0, 0.0)
   2.182 +    imp = preview_image(im, False)
   2.183 +    convert_image(im)
   2.184  
   2.185 -        process_image(im, saturate, desaturate, darken, brighten)
   2.186 -
   2.187 -    # Generate a preview if requested.
   2.188 -
   2.189 -    if preview:
   2.190 -        preview_image(im, half_resolution_preview).save(preview_filename)
   2.191 -
   2.192 -    # Generate an output image if requested.
   2.193 +    test_im = SimpleImage(data, size)
   2.194 +    test_slice(test_im, 32, 0)
   2.195  
   2.196 -    if make_image:
   2.197 -        convert_image(im)
   2.198 -        im.save(output_filename)
   2.199 -
   2.200 -    # Verify the output image (which may be loaded) if requested.
   2.201 -
   2.202 -    if verify:
   2.203 -        if no_normal_output:
   2.204 -            im = PIL.Image.open(output_filename).convert("RGB")
   2.205 -
   2.206 -        result = count_colours(im, 4)
   2.207 -        if result is not None:
   2.208 -            y, colours = result
   2.209 -            print "Image %s: row %d has the following colours: %s" % (output_filename, y, "; ".join([repr(c) for c in colours]))
   2.210 +    test_flat_im = SimpleImage(data, size)
   2.211 +    test_flat_slice(test_flat_im, 32, (200, 100, 50))
   2.212  
   2.213  # vim: tabstop=4 expandtab shiftwidth=4