gooderp18绿色标准版
Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.

255 lines
7.6KB

  1. #!C:\odoobuild\WinPy64\python-3.12.3.amd64\python.exe
  2. # pipdither
  3. # Error Diffusing image dithering.
  4. # Now with serpentine scanning.
  5. # See http://www.efg2.com/Lab/Library/ImageProcessing/DHALF.TXT
  6. # http://www.python.org/doc/2.4.4/lib/module-bisect.html
  7. from bisect import bisect_left
  8. import png
  9. def dither(
  10. out,
  11. input,
  12. bitdepth=1,
  13. linear=False,
  14. defaultgamma=1.0,
  15. targetgamma=None,
  16. cutoff=0.5, # see :cutoff:default
  17. ):
  18. """Dither the input PNG `inp` into an image with a smaller bit depth
  19. and write the result image onto `out`. `bitdepth` specifies the bit
  20. depth of the new image.
  21. Normally the source image gamma is honoured (the image is
  22. converted into a linear light space before being dithered), but
  23. if the `linear` argument is true then the image is treated as
  24. being linear already: no gamma conversion is done (this is
  25. quicker, and if you don't care much about accuracy, it won't
  26. matter much).
  27. Images with no gamma indication (no ``gAMA`` chunk) are normally
  28. treated as linear (gamma = 1.0), but often it can be better
  29. to assume a different gamma value: For example continuous tone
  30. photographs intended for presentation on the web often carry
  31. an implicit assumption of being encoded with a gamma of about
  32. 0.45 (because that's what you get if you just "blat the pixels"
  33. onto a PC framebuffer), so ``defaultgamma=0.45`` might be a
  34. good idea. `defaultgamma` does not override a gamma value
  35. specified in the file itself: It is only used when the file
  36. does not specify a gamma.
  37. If you (pointlessly) specify both `linear` and `defaultgamma`,
  38. `linear` wins.
  39. The gamma of the output image is, by default, the same as the input
  40. image. The `targetgamma` argument can be used to specify a
  41. different gamma for the output image. This effectively recodes the
  42. image to a different gamma, dithering as we go. The gamma specified
  43. is the exponent used to encode the output file (and appears in the
  44. output PNG's ``gAMA`` chunk); it is usually less than 1.
  45. """
  46. # Encoding is what happened when the PNG was made (and also what
  47. # happens when we output the PNG). Decoding is what we do to the
  48. # source PNG in order to process it.
  49. # The dithering algorithm is not completely general; it
  50. # can only do bit depth reduction, not arbitrary palette changes.
  51. import operator
  52. maxval = 2 ** bitdepth - 1
  53. r = png.Reader(file=input)
  54. _, _, pixels, info = r.asDirect()
  55. planes = info["planes"]
  56. # :todo: make an Exception
  57. assert planes == 1
  58. width = info["size"][0]
  59. sourcemaxval = 2 ** info["bitdepth"] - 1
  60. if linear:
  61. gamma = 1
  62. else:
  63. gamma = info.get("gamma") or defaultgamma
  64. # Calculate an effective gamma for input and output;
  65. # then build tables using those.
  66. # `gamma` (whether it was obtained from the input file or an
  67. # assumed value) is the encoding gamma.
  68. # We need the decoding gamma, which is the reciprocal.
  69. decode = 1.0 / gamma
  70. # `targetdecode` is the assumed gamma that is going to be used
  71. # to decoding the target PNG.
  72. # Note that even though we will _encode_ the target PNG we
  73. # still need the decoding gamma, because
  74. # the table we use maps from PNG pixel value to linear light level.
  75. if targetgamma is None:
  76. targetdecode = decode
  77. else:
  78. targetdecode = 1.0 / targetgamma
  79. incode = build_decode_table(sourcemaxval, decode)
  80. # For encoding, we still build a decode table, because we
  81. # use it inverted (searching with bisect).
  82. outcode = build_decode_table(maxval, targetdecode)
  83. # The table used for choosing output codes. These values represent
  84. # the cutoff points between two adjacent output codes.
  85. # The cutoff parameter can be varied between 0 and 1 to
  86. # preferentially choose lighter (when cutoff > 0.5) or
  87. # darker (when cutoff < 0.5) values.
  88. # :cutoff:default: The default for this used to be 0.75, but
  89. # testing by drj on 2021-07-30 showed that this produces
  90. # banding when dithering left-to-right gradients;
  91. # test with:
  92. # priforgepng grl | priditherpng | kitty icat
  93. choosecode = list(zip(outcode[1:], outcode))
  94. p = cutoff
  95. choosecode = [x[0] * p + x[1] * (1.0 - p) for x in choosecode]
  96. rows = repeat_header(pixels)
  97. dithered_rows = run_dither(incode, choosecode, outcode, width, rows)
  98. dithered_rows = remove_header(dithered_rows)
  99. info["bitdepth"] = bitdepth
  100. info["gamma"] = 1.0 / targetdecode
  101. w = png.Writer(**info)
  102. w.write(out, dithered_rows)
  103. def build_decode_table(maxval, gamma):
  104. """Build a lookup table for decoding;
  105. table converts from pixel values to linear space.
  106. """
  107. assert maxval == int(maxval)
  108. assert maxval > 0
  109. f = 1.0 / maxval
  110. table = [f * v for v in range(maxval + 1)]
  111. if gamma != 1.0:
  112. table = [v ** gamma for v in table]
  113. return table
  114. def run_dither(incode, choosecode, outcode, width, rows):
  115. """
  116. Run an serpentine dither.
  117. Using the incode and choosecode tables.
  118. """
  119. # Errors diffused downwards (into next row)
  120. ed = [0.0] * width
  121. flipped = False
  122. for row in rows:
  123. # Convert to linear...
  124. row = [incode[v] for v in row]
  125. # Add errors...
  126. row = [e + v for e, v in zip(ed, row)]
  127. if flipped:
  128. row = row[::-1]
  129. targetrow = [0] * width
  130. for i, v in enumerate(row):
  131. # `it` will be the index of the chosen target colour;
  132. it = bisect_left(choosecode, v)
  133. targetrow[i] = it
  134. t = outcode[it]
  135. # err is the error that needs distributing.
  136. err = v - t
  137. # Sierra "Filter Lite" distributes * 2
  138. # as per this diagram. 1 1
  139. ef = err * 0.5
  140. # :todo: consider making rows one wider at each end and
  141. # removing "if"s
  142. if i + 1 < width:
  143. row[i + 1] += ef
  144. ef *= 0.5
  145. ed[i] = ef
  146. if i:
  147. ed[i - 1] += ef
  148. if flipped:
  149. ed = ed[::-1]
  150. targetrow = targetrow[::-1]
  151. yield targetrow
  152. flipped = not flipped
  153. WARMUP_ROWS = 32
  154. def repeat_header(rows):
  155. """Repeat the first row, to "warm up" the error register."""
  156. for row in rows:
  157. yield row
  158. for _ in range(WARMUP_ROWS):
  159. yield row
  160. break
  161. yield from rows
  162. def remove_header(rows):
  163. """Remove the same number of rows that repeat_header added."""
  164. for _ in range(WARMUP_ROWS):
  165. next(rows)
  166. yield from rows
  167. def main(argv=None):
  168. import sys
  169. # https://docs.python.org/3.5/library/argparse.html
  170. import argparse
  171. parser = argparse.ArgumentParser()
  172. if argv is None:
  173. argv = sys.argv
  174. progname, *args = argv
  175. parser.add_argument("--bitdepth", type=int, default=1, help="bitdepth of output")
  176. parser.add_argument(
  177. "--cutoff",
  178. type=float,
  179. default=0.5,
  180. help="cutoff to select adjacent output values",
  181. )
  182. parser.add_argument(
  183. "--defaultgamma",
  184. type=float,
  185. default=1.0,
  186. help="gamma value to use when no gamma in input",
  187. )
  188. parser.add_argument("--linear", action="store_true", help="force linear input")
  189. parser.add_argument(
  190. "--targetgamma",
  191. type=float,
  192. help="gamma to use in output (target), defaults to input gamma",
  193. )
  194. parser.add_argument(
  195. "input", nargs="?", default="-", type=png.cli_open, metavar="PNG"
  196. )
  197. ns = parser.parse_args(args)
  198. return dither(png.binary_stdout(), **vars(ns))
  199. if __name__ == "__main__":
  200. main()
上海开阖软件有限公司 沪ICP备12045867号-1