├── hyperdither.py
├── images
├── cat.jpg
├── cat_out.png
└── cat_out2.png
└── readme.md
/hyperdither.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 | import sys, os
4 | import argparse
5 | from PIL import Image, ImageEnhance
6 | import numpy as np
7 | import numba
8 | from numba import cuda
9 |
10 | @numba.jit
11 | def dither(num, thresh = 127):
12 | derr = np.zeros(num.shape, dtype=int)
13 |
14 | div = 8
15 | for y in xrange(num.shape[0]):
16 | for x in xrange(num.shape[1]):
17 | newval = derr[y,x] + num[y,x]
18 | if newval >= thresh:
19 | errval = newval - 255
20 | num[y,x] = 1.
21 | else:
22 | errval = newval
23 | num[y,x] = 0.
24 | if x + 1 < num.shape[1]:
25 | derr[y, x + 1] += errval / div
26 | if x + 2 < num.shape[1]:
27 | derr[y, x + 2] += errval / div
28 | if y + 1 < num.shape[0]:
29 | derr[y + 1, x - 1] += errval / div
30 | derr[y + 1, x] += errval / div
31 | if y + 2< num.shape[0]:
32 | derr[y + 2, x] += errval / div
33 | if x + 1 < num.shape[1]:
34 | derr[y + 1, x + 1] += errval / div
35 | return num[::-1,:] * 255
36 |
37 | def main(argv = None):
38 | if argv is None:
39 | argv = sys.argv
40 | programName = os.path.basename(argv[0])
41 |
42 | parser = argparse.ArgumentParser(
43 | description="Dither image an image using Atkinson 'hyperdither'.")
44 | parser.add_argument('filename')
45 | parser.add_argument('-1', '--mono', dest = 'mono', action='store_true',
46 | default = False, help = '1-bit black and white file')
47 | parser.add_argument('-t', '--threshold', dest = 'threshold', type = int,
48 | action='store', default = 127, help = 'black/white threshold')
49 | parser.add_argument('-d', '--dpi', dest = 'dpi', type = int,
50 | action='store', default = 72, help = 'dpi out output file')
51 | parser.add_argument('-e', '--ext', dest = 'ext', action='store',
52 | default = 'png', help = 'output filetype')
53 | parser.add_argument('-b', '--bottom', dest = 'bottom',
54 | action='store_true', default = False,
55 | help = "start at bottom left instead of top left")
56 | parser.add_argument('-c', '--contrast', dest = 'contrast',
57 | action='store', default = 0, type = float,
58 | help = "boost contrast by specified factor (default 1)")
59 | parser.add_argument('-s', '--sharpness', dest = 'sharpness',
60 | action='store', default = 0, type = float,
61 | help = "boost sharpness by specified factor (default 1)")
62 | parser.add_argument('-r', '--resize', dest = 'resize', action='store',
63 | default = None, type = int,
64 | help = """resize pre-dither image on longest dimension""")
65 | parser.add_argument('-2', '--double', dest = 'double',
66 | action='store_true', default = False,
67 | help = "double post-dither image using nearest neighbors")
68 |
69 | args = parser.parse_args()
70 |
71 | if not os.path.isfile(args.filename):
72 | print("Must supply a valid file.")
73 | sys.exit(1)
74 | img = Image.open(os.path.expanduser(args.filename)).convert('L')
75 | if args.contrast:
76 | # img = ImageEnhance.Contrast(img).enhance(1 + args.contrast/100)
77 | img = ImageEnhance.Contrast(img).enhance(args.contrast)
78 | if args.resize:
79 | img.thumbnail((args.resize,) * 2, 3)
80 | if args.sharpness:
81 | # img = ImageEnhance.Sharpness(img).enhance(1 + args.sharpness/100)
82 | img = ImageEnhance.Sharpness(img).enhance(args.sharpness)
83 |
84 | if args.bottom:
85 | m = np.array(img)[::-1,:]
86 | m2 = dither(m, thresh = args.threshold)
87 | out = Image.fromarray(m2[:,:])
88 | else:
89 | m = np.array(img)[:,:]
90 | m2 = dither(m, thresh = args.threshold)
91 | out = Image.fromarray(m2[::-1,:])
92 | basename, ext = os.path.splitext(args.filename)
93 | outfn = basename + '_out.' + args.ext
94 |
95 | if args.double:
96 | out = out.resize((out.width *2, out.height*2))
97 | if args.mono:
98 | out.convert('1').save(outfn, dpi=(args.dpi,)*2)
99 | else:
100 | out.save(outfn, dpi=(args.dpi,)*2)
101 |
102 | if __name__ == "__main__":
103 | sys.exit(main())
104 |
--------------------------------------------------------------------------------
/images/cat.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tgray/hyperdither/54367eee9b651e0e5b156d82c711c4def5b76463/images/cat.jpg
--------------------------------------------------------------------------------
/images/cat_out.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tgray/hyperdither/54367eee9b651e0e5b156d82c711c4def5b76463/images/cat_out.png
--------------------------------------------------------------------------------
/images/cat_out2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tgray/hyperdither/54367eee9b651e0e5b156d82c711c4def5b76463/images/cat_out2.png
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 | # Hyperdither.py
2 |
3 | Simple Atkinson style dithering using Python.
4 |
5 | To speed up the processing, this script requires numpy and numba. I'm sure there are easy ways to do the same using pure C, but I don't know them.
6 |
7 | My main dither function is a little ratty looking. I was surprised though that it was more efficient than some other PIL/PILLOW implementations I found. Once I sped it up with [numba][], it became significantly faster.
8 |
9 | ## Example
10 |
11 | Input image:
12 |
13 | 
14 |
15 | Command:
16 |
17 | hyperdither.py -1 cat.jpg
18 |
19 | Output:
20 |
21 | 
22 |
23 | Reduced contrast:
24 |
25 | hyperdither.py -1 cat.jpg -c .85
26 |
27 | 
28 |
29 | ## Options
30 |
31 | - `-1`: 1-bit black and white file. Otherwise the output is encoded as an RGB file.
32 | - `-t`: the black/white threshold. Everything above is black. The maximum is 255.
33 | - `-d`: dpi out output file. Only for printing purposes. Default is 72 dpi.
34 | - `-e`: output filetype. Must be accepted by PILLOW or it will throw an exception. Default is png.
35 | - `-b`: start at bottom left instead of top left with the dithering.
36 | - `-c`: boost contrast by specified amount (in terms of [pillow's][pillow] [ImageEnhancers][enhance] factor: 1 is no change).
37 | - `-s`: boost sharpness by specified amount (in terms of [pillow's][pillow] [ImageEnhancers][enhance] factor: 1 is no change).
38 | - `-r`: resize pre-dither image on longest dimension. Makes the image smaller only, pre-dithering.
39 | - `-2`: double post-dither image using nearest neighbors. Makes output image twice as large as the pre-dither image. If image is scaled from the resized pre-dither image if the `-r` option was specified.
40 |
41 | For contrast and sharpness, 1 is default and does not need to be specified. Numbers larger than 1 increase sharpness/contrast, numbers below 1 reduce it. Negative numbers for contrast invert the image. If you want to go negative, I found that specifying the option like `-c"-1"` will give you minus 1. Remove the space and surround the argument by quotes.
42 |
43 | I found values of 0.8 to 1.2 are good for contrast if you want lighten or darken specific areas.
44 |
45 | ## Requirements
46 |
47 | - [pillow][]
48 | - [numpy](http://www.numpy.org)
49 | - [numba][]
50 |
51 | [numba]: http://numba.pydata.org
52 | [pillow]: http://python-pillow.org
53 | [enhance]: https://pillow.readthedocs.io/en/4.0.x/reference/ImageEnhance.html
54 |
55 | ## Links
56 |
57 | -
58 | -
59 |
--------------------------------------------------------------------------------