pipdither 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182
  1. #!/usr/bin/env python
  2. # $URL: http://pypng.googlecode.com/svn/trunk/code/pipdither $
  3. # $Rev: 150 $
  4. # pipdither
  5. # Error Diffusing image dithering.
  6. # Now with serpentine scanning.
  7. # See http://www.efg2.com/Lab/Library/ImageProcessing/DHALF.TXT
  8. # http://www.python.org/doc/2.4.4/lib/module-bisect.html
  9. from bisect import bisect_left
  10. import png
  11. def dither(out, inp,
  12. bitdepth=1, linear=False, defaultgamma=1.0, targetgamma=None,
  13. cutoff=0.75):
  14. """Dither the input PNG `inp` into an image with a smaller bit depth
  15. and write the result image onto `out`. `bitdepth` specifies the bit
  16. depth of the new image.
  17. Normally the source image gamma is honoured (the image is
  18. converted into a linear light space before being dithered), but
  19. if the `linear` argument is true then the image is treated as
  20. being linear already: no gamma conversion is done (this is
  21. quicker, and if you don't care much about accuracy, it won't
  22. matter much).
  23. Images with no gamma indication (no ``gAMA`` chunk) are normally
  24. treated as linear (gamma = 1.0), but often it can be better
  25. to assume a different gamma value: For example continuous tone
  26. photographs intended for presentation on the web often carry
  27. an implicit assumption of being encoded with a gamma of about
  28. 0.45 (because that's what you get if you just "blat the pixels"
  29. onto a PC framebuffer), so ``defaultgamma=0.45`` might be a
  30. good idea. `defaultgamma` does not override a gamma value
  31. specified in the file itself: It is only used when the file
  32. does not specify a gamma.
  33. If you (pointlessly) specify both `linear` and `defaultgamma`,
  34. `linear` wins.
  35. The gamma of the output image is, by default, the same as the input
  36. image. The `targetgamma` argument can be used to specify a
  37. different gamma for the output image. This effectively recodes the
  38. image to a different gamma, dithering as we go. The gamma specified
  39. is the exponent used to encode the output file (and appears in the
  40. output PNG's ``gAMA`` chunk); it is usually less than 1.
  41. """
  42. # Encoding is what happened when the PNG was made (and also what
  43. # happens when we output the PNG). Decoding is what we do to the
  44. # source PNG in order to process it.
  45. # The dithering algorithm is not completely general; it
  46. # can only do bit depth reduction, not arbitrary palette changes.
  47. import operator
  48. maxval = 2**bitdepth - 1
  49. r = png.Reader(file=inp)
  50. # If image gamma is 1 or gamma is not present and we are assuming a
  51. # value of 1, then it is faster to pass a maxval parameter to
  52. # asFloat (the multiplications get combined). With gamma, we have
  53. # to have the pixel values from 0.0 to 1.0 (as long as we are doing
  54. # gamma correction here).
  55. # Slightly annoyingly, we don't know the image gamma until we've
  56. # called asFloat().
  57. _,_,pixels,info = r.asDirect()
  58. planes = info['planes']
  59. assert planes == 1
  60. width = info['size'][0]
  61. sourcemaxval = 2**info['bitdepth'] - 1
  62. if linear:
  63. gamma = 1
  64. else:
  65. gamma = info.get('gamma') or defaultgamma
  66. # Convert gamma from encoding gamma to the required power for
  67. # decoding.
  68. decode = 1.0/gamma
  69. # Build a lookup table for decoding; convert from pixel values to linear
  70. # space:
  71. sourcef = 1.0/sourcemaxval
  72. incode = map(sourcef.__mul__, range(sourcemaxval+1))
  73. if decode != 1.0:
  74. incode = map(decode.__rpow__, incode)
  75. # Could be different, later on. targetdecode is the assumed gamma
  76. # that is going to be used to decoding the target PNG. It is the
  77. # reciprocal of the exponent that we use to encode the target PNG.
  78. # This is the value that we need to build our table that we use for
  79. # converting from linear to target colour space.
  80. if targetgamma is None:
  81. targetdecode = decode
  82. else:
  83. targetdecode = 1.0/targetgamma
  84. # The table we use for encoding (creating the target PNG), still
  85. # maps from pixel value to linear space, but we use it inverted, by
  86. # searching through it with bisect.
  87. targetf = 1.0/maxval
  88. outcode = map(targetf.__mul__, range(maxval+1))
  89. if targetdecode != 1.0:
  90. outcode = map(targetdecode.__rpow__, outcode)
  91. # The table used for choosing output codes. These values represent
  92. # the cutoff points between two adjacent output codes.
  93. choosecode = zip(outcode[1:], outcode)
  94. p = cutoff
  95. choosecode = map(lambda x: x[0]*p+x[1]*(1.0-p), choosecode)
  96. def iterdither():
  97. # Errors diffused downwards (into next row)
  98. ed = [0.0]*width
  99. flipped = False
  100. for row in pixels:
  101. row = map(incode.__getitem__, row)
  102. row = map(operator.add, ed, row)
  103. if flipped:
  104. row = row[::-1]
  105. targetrow = [0] * width
  106. for i,v in enumerate(row):
  107. # Clamp. Necessary because previously added errors may take
  108. # v out of range.
  109. v = max(0.0, min(v, 1.0))
  110. # `it` will be the index of the chosen target colour;
  111. it = bisect_left(choosecode, v)
  112. t = outcode[it]
  113. targetrow[i] = it
  114. # err is the error that needs distributing.
  115. err = v - t
  116. # Sierra "Filter Lite" distributes * 2
  117. # as per this diagram. 1 1
  118. ef = err/2.0
  119. # :todo: consider making rows one wider at each end and
  120. # removing "if"s
  121. if i+1 < width:
  122. row[i+1] += ef
  123. ef /= 2.0
  124. ed[i] = ef
  125. if i:
  126. ed[i-1] += ef
  127. if flipped:
  128. ed = ed[::-1]
  129. targetrow = targetrow[::-1]
  130. yield targetrow
  131. flipped = not flipped
  132. info['bitdepth'] = bitdepth
  133. info['gamma'] = 1.0/targetdecode
  134. w = png.Writer(**info)
  135. w.write(out, iterdither())
  136. def main(argv=None):
  137. # http://www.python.org/doc/2.4.4/lib/module-getopt.html
  138. from getopt import getopt
  139. import sys
  140. if argv is None:
  141. argv = sys.argv
  142. opt,argv = getopt(argv[1:], 'b:c:g:lo:')
  143. k = {}
  144. for o,v in opt:
  145. if o == '-b':
  146. k['bitdepth'] = int(v)
  147. if o == '-c':
  148. k['cutoff'] = float(v)
  149. if o == '-g':
  150. k['defaultgamma'] = float(v)
  151. if o == '-l':
  152. k['linear'] = True
  153. if o == '-o':
  154. k['targetgamma'] = float(v)
  155. if o == '-?':
  156. print >>sys.stderr, "pipdither [-b bits] [-c cutoff] [-g assumed-gamma] [-l] [in.png]"
  157. if len(argv) > 0:
  158. f = open(argv[0], 'rb')
  159. else:
  160. f = sys.stdin
  161. return dither(sys.stdout, f, **k)
  162. if __name__ == '__main__':
  163. main()