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

Reply via email to