diff options
Diffstat (limited to 'PIL/ImageOps.py')
-rw-r--r-- | PIL/ImageOps.py | 408 |
1 files changed, 408 insertions, 0 deletions
diff --git a/PIL/ImageOps.py b/PIL/ImageOps.py new file mode 100644 index 0000000..89b5e72 --- /dev/null +++ b/PIL/ImageOps.py @@ -0,0 +1,408 @@ +# +# The Python Imaging Library. +# $Id: ImageOps.py 2760 2006-06-19 13:31:40Z fredrik $ +# +# standard image operations +# +# History: +# 2001-10-20 fl Created +# 2001-10-23 fl Added autocontrast operator +# 2001-12-18 fl Added Kevin's fit operator +# 2004-03-14 fl Fixed potential division by zero in equalize +# 2005-05-05 fl Fixed equalize for low number of values +# +# Copyright (c) 2001-2004 by Secret Labs AB +# Copyright (c) 2001-2004 by Fredrik Lundh +# +# See the README file for information on usage and redistribution. +# + +import Image +import operator + +## +# (New in 1.1.3) The <b>ImageOps</b> module contains a number of +# 'ready-made' image processing operations. This module is somewhat +# experimental, and most operators only work on L and RGB images. +# +# @since 1.1.3 +## + +# +# helpers + +def _border(border): + if type(border) is type(()): + if len(border) == 2: + left, top = right, bottom = border + elif len(border) == 4: + left, top, right, bottom = border + else: + left = top = right = bottom = border + return left, top, right, bottom + +def _color(color, mode): + if Image.isStringType(color): + import ImageColor + color = ImageColor.getcolor(color, mode) + return color + +def _lut(image, lut): + if image.mode == "P": + # FIXME: apply to lookup table, not image data + raise NotImplementedError("mode P support coming soon") + elif image.mode in ("L", "RGB"): + if image.mode == "RGB" and len(lut) == 256: + lut = lut + lut + lut + return image.point(lut) + else: + raise IOError, "not supported for this image mode" + +# +# actions + +## +# Maximize (normalize) image contrast. This function calculates a +# histogram of the input image, removes <i>cutoff</i> percent of the +# lightest and darkest pixels from the histogram, and remaps the image +# so that the darkest pixel becomes black (0), and the lightest +# becomes white (255). +# +# @param image The image to process. +# @param cutoff How many percent to cut off from the histogram. +# @param ignore The background pixel value (use None for no background). +# @return An image. + +def autocontrast(image, cutoff=0, ignore=None): + "Maximize image contrast, based on histogram" + histogram = image.histogram() + lut = [] + for layer in range(0, len(histogram), 256): + h = histogram[layer:layer+256] + if ignore is not None: + # get rid of outliers + try: + h[ignore] = 0 + except TypeError: + # assume sequence + for ix in ignore: + h[ix] = 0 + if cutoff: + # cut off pixels from both ends of the histogram + # get number of pixels + n = 0 + for ix in range(256): + n = n + h[ix] + # remove cutoff% pixels from the low end + cut = n * cutoff / 100 + for lo in range(256): + if cut > h[lo]: + cut = cut - h[lo] + h[lo] = 0 + else: + h[lo] = h[lo] - cut + cut = 0 + if cut <= 0: + break + # remove cutoff% samples from the hi end + cut = n * cutoff / 100 + for hi in range(255, -1, -1): + if cut > h[hi]: + cut = cut - h[hi] + h[hi] = 0 + else: + h[hi] = h[hi] - cut + cut = 0 + if cut <= 0: + break + # find lowest/highest samples after preprocessing + for lo in range(256): + if h[lo]: + break + for hi in range(255, -1, -1): + if h[hi]: + break + if hi <= lo: + # don't bother + lut.extend(range(256)) + else: + scale = 255.0 / (hi - lo) + offset = -lo * scale + for ix in range(256): + ix = int(ix * scale + offset) + if ix < 0: + ix = 0 + elif ix > 255: + ix = 255 + lut.append(ix) + return _lut(image, lut) + +## +# Colorize grayscale image. The <i>black</i> and <i>white</i> +# arguments should be RGB tuples; this function calculates a colour +# wedge mapping all black pixels in the source image to the first +# colour, and all white pixels to the second colour. +# +# @param image The image to colourize. +# @param black The colour to use for black input pixels. +# @param white The colour to use for white input pixels. +# @return An image. + +def colorize(image, black, white): + "Colorize a grayscale image" + assert image.mode == "L" + black = _color(black, "RGB") + white = _color(white, "RGB") + red = []; green = []; blue = [] + for i in range(256): + red.append(black[0]+i*(white[0]-black[0])/255) + green.append(black[1]+i*(white[1]-black[1])/255) + blue.append(black[2]+i*(white[2]-black[2])/255) + image = image.convert("RGB") + return _lut(image, red + green + blue) + +## +# Remove border from image. The same amount of pixels are removed +# from all four sides. This function works on all image modes. +# +# @param image The image to crop. +# @param border The number of pixels to remove. +# @return An image. +# @see Image#Image.crop + +def crop(image, border=0): + "Crop border off image" + left, top, right, bottom = _border(border) + return image.crop( + (left, top, image.size[0]-right, image.size[1]-bottom) + ) + +## +# Deform the image. +# +# @param image The image to deform. +# @param deformer A deformer object. Any object that implements a +# <b>getmesh</b> method can be used. +# @param resample What resampling filter to use. +# @return An image. + +def deform(image, deformer, resample=Image.BILINEAR): + "Deform image using the given deformer" + return image.transform( + image.size, Image.MESH, deformer.getmesh(image), resample + ) + +## +# Equalize the image histogram. This function applies a non-linear +# mapping to the input image, in order to create a uniform +# distribution of grayscale values in the output image. +# +# @param image The image to equalize. +# @param mask An optional mask. If given, only the pixels selected by +# the mask are included in the analysis. +# @return An image. + +def equalize(image, mask=None): + "Equalize image histogram" + if image.mode == "P": + image = image.convert("RGB") + h = image.histogram(mask) + lut = [] + for b in range(0, len(h), 256): + histo = filter(None, h[b:b+256]) + if len(histo) <= 1: + lut.extend(range(256)) + else: + step = (reduce(operator.add, histo) - histo[-1]) / 255 + if not step: + lut.extend(range(256)) + else: + n = step / 2 + for i in range(256): + lut.append(n / step) + n = n + h[i+b] + return _lut(image, lut) + +## +# Add border to the image +# +# @param image The image to expand. +# @param border Border width, in pixels. +# @param fill Pixel fill value (a colour value). Default is 0 (black). +# @return An image. + +def expand(image, border=0, fill=0): + "Add border to image" + left, top, right, bottom = _border(border) + width = left + image.size[0] + right + height = top + image.size[1] + bottom + out = Image.new(image.mode, (width, height), _color(fill, image.mode)) + out.paste(image, (left, top)) + return out + +## +# Returns a sized and cropped version of the image, cropped to the +# requested aspect ratio and size. +# <p> +# The <b>fit</b> function was contributed by Kevin Cazabon. +# +# @param size The requested output size in pixels, given as a +# (width, height) tuple. +# @param method What resampling method to use. Default is Image.NEAREST. +# @param bleed Remove a border around the outside of the image (from all +# four edges. The value is a decimal percentage (use 0.01 for one +# percent). The default value is 0 (no border). +# @param centering Control the cropping position. Use (0.5, 0.5) for +# center cropping (e.g. if cropping the width, take 50% off of the +# left side, and therefore 50% off the right side). (0.0, 0.0) +# will crop from the top left corner (i.e. if cropping the width, +# take all of the crop off of the right side, and if cropping the +# height, take all of it off the bottom). (1.0, 0.0) will crop +# from the bottom left corner, etc. (i.e. if cropping the width, +# take all of the crop off the left side, and if cropping the height +# take none from the top, and therefore all off the bottom). +# @return An image. + +def fit(image, size, method=Image.NEAREST, bleed=0.0, centering=(0.5, 0.5)): + """ + This method returns a sized and cropped version of the image, + cropped to the aspect ratio and size that you request. + """ + + # by Kevin Cazabon, Feb 17/2000 + # kevin@cazabon.com + # http://www.cazabon.com + + # ensure inputs are valid + if type(centering) != type([]): + centering = [centering[0], centering[1]] + + if centering[0] > 1.0 or centering[0] < 0.0: + centering [0] = 0.50 + if centering[1] > 1.0 or centering[1] < 0.0: + centering[1] = 0.50 + + if bleed > 0.49999 or bleed < 0.0: + bleed = 0.0 + + # calculate the area to use for resizing and cropping, subtracting + # the 'bleed' around the edges + + # number of pixels to trim off on Top and Bottom, Left and Right + bleedPixels = ( + int((float(bleed) * float(image.size[0])) + 0.5), + int((float(bleed) * float(image.size[1])) + 0.5) + ) + + liveArea = ( + bleedPixels[0], bleedPixels[1], image.size[0] - bleedPixels[0] - 1, + image.size[1] - bleedPixels[1] - 1 + ) + + liveSize = (liveArea[2] - liveArea[0], liveArea[3] - liveArea[1]) + + # calculate the aspect ratio of the liveArea + liveAreaAspectRatio = float(liveSize[0])/float(liveSize[1]) + + # calculate the aspect ratio of the output image + aspectRatio = float(size[0]) / float(size[1]) + + # figure out if the sides or top/bottom will be cropped off + if liveAreaAspectRatio >= aspectRatio: + # liveArea is wider than what's needed, crop the sides + cropWidth = int((aspectRatio * float(liveSize[1])) + 0.5) + cropHeight = liveSize[1] + else: + # liveArea is taller than what's needed, crop the top and bottom + cropWidth = liveSize[0] + cropHeight = int((float(liveSize[0])/aspectRatio) + 0.5) + + # make the crop + leftSide = int(liveArea[0] + (float(liveSize[0]-cropWidth) * centering[0])) + if leftSide < 0: + leftSide = 0 + topSide = int(liveArea[1] + (float(liveSize[1]-cropHeight) * centering[1])) + if topSide < 0: + topSide = 0 + + out = image.crop( + (leftSide, topSide, leftSide + cropWidth, topSide + cropHeight) + ) + + # resize the image and return it + return out.resize(size, method) + +## +# Flip the image vertically (top to bottom). +# +# @param image The image to flip. +# @return An image. + +def flip(image): + "Flip image vertically" + return image.transpose(Image.FLIP_TOP_BOTTOM) + +## +# Convert the image to grayscale. +# +# @param image The image to convert. +# @return An image. + +def grayscale(image): + "Convert to grayscale" + return image.convert("L") + +## +# Invert (negate) the image. +# +# @param image The image to invert. +# @return An image. + +def invert(image): + "Invert image (negate)" + lut = [] + for i in range(256): + lut.append(255-i) + return _lut(image, lut) + +## +# Flip image horizontally (left to right). +# +# @param image The image to mirror. +# @return An image. + +def mirror(image): + "Flip image horizontally" + return image.transpose(Image.FLIP_LEFT_RIGHT) + +## +# Reduce the number of bits for each colour channel. +# +# @param image The image to posterize. +# @param bits The number of bits to keep for each channel (1-8). +# @return An image. + +def posterize(image, bits): + "Reduce the number of bits per color channel" + lut = [] + mask = ~(2**(8-bits)-1) + for i in range(256): + lut.append(i & mask) + return _lut(image, lut) + +## +# Invert all pixel values above a threshold. +# +# @param image The image to posterize. +# @param threshold All pixels above this greyscale level are inverted. +# @return An image. + +def solarize(image, threshold=128): + "Invert all values above threshold" + lut = [] + for i in range(256): + if i < threshold: + lut.append(i) + else: + lut.append(255-i) + return _lut(image, lut) |