You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
181 lines
6.7 KiB
181 lines
6.7 KiB
#!/usr/bin/env python |
|
# $URL: http://pypng.googlecode.com/svn/trunk/code/pipdither $ |
|
# $Rev: 150 $ |
|
|
|
# pipdither |
|
# Error Diffusing image dithering. |
|
# Now with serpentine scanning. |
|
|
|
# See http://www.efg2.com/Lab/Library/ImageProcessing/DHALF.TXT |
|
|
|
# http://www.python.org/doc/2.4.4/lib/module-bisect.html |
|
from bisect import bisect_left |
|
|
|
import png |
|
|
|
def dither(out, inp, |
|
bitdepth=1, linear=False, defaultgamma=1.0, targetgamma=None, |
|
cutoff=0.75): |
|
"""Dither the input PNG `inp` into an image with a smaller bit depth |
|
and write the result image onto `out`. `bitdepth` specifies the bit |
|
depth of the new image. |
|
|
|
Normally the source image gamma is honoured (the image is |
|
converted into a linear light space before being dithered), but |
|
if the `linear` argument is true then the image is treated as |
|
being linear already: no gamma conversion is done (this is |
|
quicker, and if you don't care much about accuracy, it won't |
|
matter much). |
|
|
|
Images with no gamma indication (no ``gAMA`` chunk) are normally |
|
treated as linear (gamma = 1.0), but often it can be better |
|
to assume a different gamma value: For example continuous tone |
|
photographs intended for presentation on the web often carry |
|
an implicit assumption of being encoded with a gamma of about |
|
0.45 (because that's what you get if you just "blat the pixels" |
|
onto a PC framebuffer), so ``defaultgamma=0.45`` might be a |
|
good idea. `defaultgamma` does not override a gamma value |
|
specified in the file itself: It is only used when the file |
|
does not specify a gamma. |
|
|
|
If you (pointlessly) specify both `linear` and `defaultgamma`, |
|
`linear` wins. |
|
|
|
The gamma of the output image is, by default, the same as the input |
|
image. The `targetgamma` argument can be used to specify a |
|
different gamma for the output image. This effectively recodes the |
|
image to a different gamma, dithering as we go. The gamma specified |
|
is the exponent used to encode the output file (and appears in the |
|
output PNG's ``gAMA`` chunk); it is usually less than 1. |
|
|
|
""" |
|
|
|
# Encoding is what happened when the PNG was made (and also what |
|
# happens when we output the PNG). Decoding is what we do to the |
|
# source PNG in order to process it. |
|
|
|
# The dithering algorithm is not completely general; it |
|
# can only do bit depth reduction, not arbitrary palette changes. |
|
import operator |
|
maxval = 2**bitdepth - 1 |
|
r = png.Reader(file=inp) |
|
# If image gamma is 1 or gamma is not present and we are assuming a |
|
# value of 1, then it is faster to pass a maxval parameter to |
|
# asFloat (the multiplications get combined). With gamma, we have |
|
# to have the pixel values from 0.0 to 1.0 (as long as we are doing |
|
# gamma correction here). |
|
# Slightly annoyingly, we don't know the image gamma until we've |
|
# called asFloat(). |
|
_,_,pixels,info = r.asDirect() |
|
planes = info['planes'] |
|
assert planes == 1 |
|
width = info['size'][0] |
|
sourcemaxval = 2**info['bitdepth'] - 1 |
|
if linear: |
|
gamma = 1 |
|
else: |
|
gamma = info.get('gamma') or defaultgamma |
|
# Convert gamma from encoding gamma to the required power for |
|
# decoding. |
|
decode = 1.0/gamma |
|
# Build a lookup table for decoding; convert from pixel values to linear |
|
# space: |
|
sourcef = 1.0/sourcemaxval |
|
incode = map(sourcef.__mul__, range(sourcemaxval+1)) |
|
if decode != 1.0: |
|
incode = map(decode.__rpow__, incode) |
|
# Could be different, later on. targetdecode is the assumed gamma |
|
# that is going to be used to decoding the target PNG. It is the |
|
# reciprocal of the exponent that we use to encode the target PNG. |
|
# This is the value that we need to build our table that we use for |
|
# converting from linear to target colour space. |
|
if targetgamma is None: |
|
targetdecode = decode |
|
else: |
|
targetdecode = 1.0/targetgamma |
|
# The table we use for encoding (creating the target PNG), still |
|
# maps from pixel value to linear space, but we use it inverted, by |
|
# searching through it with bisect. |
|
targetf = 1.0/maxval |
|
outcode = map(targetf.__mul__, range(maxval+1)) |
|
if targetdecode != 1.0: |
|
outcode = map(targetdecode.__rpow__, outcode) |
|
# The table used for choosing output codes. These values represent |
|
# the cutoff points between two adjacent output codes. |
|
choosecode = zip(outcode[1:], outcode) |
|
p = cutoff |
|
choosecode = map(lambda x: x[0]*p+x[1]*(1.0-p), choosecode) |
|
def iterdither(): |
|
# Errors diffused downwards (into next row) |
|
ed = [0.0]*width |
|
flipped = False |
|
for row in pixels: |
|
row = map(incode.__getitem__, row) |
|
row = map(operator.add, ed, row) |
|
if flipped: |
|
row = row[::-1] |
|
targetrow = [0] * width |
|
for i,v in enumerate(row): |
|
# Clamp. Necessary because previously added errors may take |
|
# v out of range. |
|
v = max(0.0, min(v, 1.0)) |
|
# `it` will be the index of the chosen target colour; |
|
it = bisect_left(choosecode, v) |
|
t = outcode[it] |
|
targetrow[i] = it |
|
# err is the error that needs distributing. |
|
err = v - t |
|
# Sierra "Filter Lite" distributes * 2 |
|
# as per this diagram. 1 1 |
|
ef = err/2.0 |
|
# :todo: consider making rows one wider at each end and |
|
# removing "if"s |
|
if i+1 < width: |
|
row[i+1] += ef |
|
ef /= 2.0 |
|
ed[i] = ef |
|
if i: |
|
ed[i-1] += ef |
|
if flipped: |
|
ed = ed[::-1] |
|
targetrow = targetrow[::-1] |
|
yield targetrow |
|
flipped = not flipped |
|
info['bitdepth'] = bitdepth |
|
info['gamma'] = 1.0/targetdecode |
|
w = png.Writer(**info) |
|
w.write(out, iterdither()) |
|
|
|
|
|
def main(argv=None): |
|
# http://www.python.org/doc/2.4.4/lib/module-getopt.html |
|
from getopt import getopt |
|
import sys |
|
if argv is None: |
|
argv = sys.argv |
|
opt,argv = getopt(argv[1:], 'b:c:g:lo:') |
|
k = {} |
|
for o,v in opt: |
|
if o == '-b': |
|
k['bitdepth'] = int(v) |
|
if o == '-c': |
|
k['cutoff'] = float(v) |
|
if o == '-g': |
|
k['defaultgamma'] = float(v) |
|
if o == '-l': |
|
k['linear'] = True |
|
if o == '-o': |
|
k['targetgamma'] = float(v) |
|
if o == '-?': |
|
print >>sys.stderr, "pipdither [-b bits] [-c cutoff] [-g assumed-gamma] [-l] [in.png]" |
|
|
|
if len(argv) > 0: |
|
f = open(argv[0], 'rb') |
|
else: |
|
f = sys.stdin |
|
|
|
return dither(sys.stdout, f, **k) |
|
|
|
|
|
if __name__ == '__main__': |
|
main()
|
|
|