This isn't at the same level as <http://guard-soft.com/strokes_maker.html> but it has still produced some visually impressive results.
#!/usr/bin/python # -*- coding: utf-8 -*- """Convert a color image to a 1-bit black-and-white almost-engraving. Posted on the web at <http://canonical.org/~kragen/sw/byn>. I wrote an algorithm to produce black-and-white, somewhat abstract versions of photographs, by median-thresholding a high-pass-filtered version of the image. This super-simplistic approach works okay at producing engraving-like or Xerox-like images. In fact, it works far better than it has any right to. The approach used here is not quite simple thresholding: instead we come up with a sort of adaptive threshold for each pixel, based on its neighbors. The result looks a lot like a Xerox photocopy. It does not preserve as much information from the original image as Floyd-Steinberg dithering but produces more stark, engraving-like images. I, the creator of this work, hereby release it into the public domain. This applies worldwide. In case this is not legally possible, I grant any entity the right to use this work for any purpose, without any conditions, unless such conditions are required by law. """ import os import sys import bisect import Image import ImageFilter import Numeric def main(argv): for filename in argv[1:]: convert_to_byn(filename) def convert_to_byn(filename): image = Image.open(filename) basename, ext = os.path.splitext(filename) outname = basename + '-bn.png' assert not os.path.exists(outname), outname gray = image.convert("L") # Convert image to grayscale. blurred = gray.filter(ImageFilter.BLUR) #blurred.save('blurred.png') differences = autostretch(image2array(gray) - 0.95 * image2array(blurred) + 0.5) #array2image(differences).save('differences.png') output = threshold(differences, median(differences)) # optimize=1 still isn’t doing a good job of reducing the image # size; opening it in the Gimp, converting it to grayscale and # then a 1-bit indexed palette, and resaving it, gets it to be # just over half the size. bits=1 breaks the output. array2image(output).convert('1').save(outname, optimize=1) print 'eog', outname def autostretch(pixels): raveled = Numeric.ravel(pixels) min_pixel = min(raveled) max_pixel = max(raveled) return ((pixels - min_pixel).astype(Numeric.Int32) * 256 / (max_pixel + 1 - min_pixel) ).astype(Numeric.UnsignedInt8) def median(pixels): cums = Numeric.cumsum(array2image(pixels).histogram()) return bisect.bisect(cums, cums[-1]/2) def threshold(pixels, threshold): white = Numeric.array(255).astype(Numeric.UnsignedInt8) black = Numeric.array(0).astype(Numeric.UnsignedInt8) return Numeric.where(pixels > threshold, white, black) # From <http://effbot.org/zone/pil-numpy.htm>, 1998. # As explained in <http://effbot.org/zone/pil-changes-116.htm>, # you can now use Image.fromarray and numpy.asarray if you’ve # upgraded from Numeric to NumPy. def image2array(im): if im.mode not in ("L", "F"): raise ValueError, ("can only convert single-layer images", im.mode) if im.mode == "L": a = Numeric.fromstring(im.tostring(), Numeric.UnsignedInt8) else: a = Numeric.fromstring(im.tostring(), Numeric.Float32) a.shape = im.size[1], im.size[0] return a def array2image(a): if a.typecode() == Numeric.UnsignedInt8: mode = "L" elif a.typecode() == Numeric.Float32: mode = "F" else: raise ValueError, "unsupported image mode" return Image.fromstring(mode, (a.shape[1], a.shape[0]), a.tostring()) if __name__ == '__main__': main(sys.argv) -- To unsubscribe: http://lists.canonical.org/mailman/listinfo/kragen-hacks