├── 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 | ![cat](images/cat.jpg) 14 | 15 | Command: 16 | 17 | hyperdither.py -1 cat.jpg 18 | 19 | Output: 20 | 21 | ![out cat](images/cat_out.png) 22 | 23 | Reduced contrast: 24 | 25 | hyperdither.py -1 cat.jpg -c .85 26 | 27 | ![out cat](images/cat_out2.png) 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 | --------------------------------------------------------------------------------