123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658 |
- #
- # The Python Imaging Library.
- # $Id$
- #
- # 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 functools
- import operator
- import re
- from . import ExifTags, Image, ImagePalette
- #
- # helpers
- def _border(border):
- if isinstance(border, tuple):
- 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 isinstance(color, str):
- from . 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
- msg = "mode P support coming soon"
- raise NotImplementedError(msg)
- elif image.mode in ("L", "RGB"):
- if image.mode == "RGB" and len(lut) == 256:
- lut = lut + lut + lut
- return image.point(lut)
- else:
- msg = "not supported for this image mode"
- raise OSError(msg)
- #
- # actions
- def autocontrast(image, cutoff=0, ignore=None, mask=None, preserve_tone=False):
- """
- Maximize (normalize) image contrast. This function calculates a
- histogram of the input image (or mask region), removes ``cutoff`` 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: The percent to cut off from the histogram on the low and
- high ends. Either a tuple of (low, high), or a single
- number for both.
- :param ignore: The background pixel value (use None for no background).
- :param mask: Histogram used in contrast operation is computed using pixels
- within the mask. If no mask is given the entire image is used
- for histogram computation.
- :param preserve_tone: Preserve image tone in Photoshop-like style autocontrast.
- .. versionadded:: 8.2.0
- :return: An image.
- """
- if preserve_tone:
- histogram = image.convert("L").histogram(mask)
- else:
- histogram = image.histogram(mask)
- 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
- if not isinstance(cutoff, tuple):
- cutoff = (cutoff, cutoff)
- # 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[0] // 100
- for lo in range(256):
- if cut > h[lo]:
- cut = cut - h[lo]
- h[lo] = 0
- else:
- h[lo] -= cut
- cut = 0
- if cut <= 0:
- break
- # remove cutoff% samples from the high end
- cut = n * cutoff[1] // 100
- for hi in range(255, -1, -1):
- if cut > h[hi]:
- cut = cut - h[hi]
- h[hi] = 0
- else:
- 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(list(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)
- def colorize(image, black, white, mid=None, blackpoint=0, whitepoint=255, midpoint=127):
- """
- Colorize grayscale image.
- This function calculates a color wedge which maps all black pixels in
- the source image to the first color and all white pixels to the
- second color. If ``mid`` is specified, it uses three-color mapping.
- The ``black`` and ``white`` arguments should be RGB tuples or color names;
- optionally you can use three-color mapping by also specifying ``mid``.
- Mapping positions for any of the colors can be specified
- (e.g. ``blackpoint``), where these parameters are the integer
- value corresponding to where the corresponding color should be mapped.
- These parameters must have logical order, such that
- ``blackpoint <= midpoint <= whitepoint`` (if ``mid`` is specified).
- :param image: The image to colorize.
- :param black: The color to use for black input pixels.
- :param white: The color to use for white input pixels.
- :param mid: The color to use for midtone input pixels.
- :param blackpoint: an int value [0, 255] for the black mapping.
- :param whitepoint: an int value [0, 255] for the white mapping.
- :param midpoint: an int value [0, 255] for the midtone mapping.
- :return: An image.
- """
- # Initial asserts
- assert image.mode == "L"
- if mid is None:
- assert 0 <= blackpoint <= whitepoint <= 255
- else:
- assert 0 <= blackpoint <= midpoint <= whitepoint <= 255
- # Define colors from arguments
- black = _color(black, "RGB")
- white = _color(white, "RGB")
- if mid is not None:
- mid = _color(mid, "RGB")
- # Empty lists for the mapping
- red = []
- green = []
- blue = []
- # Create the low-end values
- for i in range(0, blackpoint):
- red.append(black[0])
- green.append(black[1])
- blue.append(black[2])
- # Create the mapping (2-color)
- if mid is None:
- range_map = range(0, whitepoint - blackpoint)
- for i in range_map:
- red.append(black[0] + i * (white[0] - black[0]) // len(range_map))
- green.append(black[1] + i * (white[1] - black[1]) // len(range_map))
- blue.append(black[2] + i * (white[2] - black[2]) // len(range_map))
- # Create the mapping (3-color)
- else:
- range_map1 = range(0, midpoint - blackpoint)
- range_map2 = range(0, whitepoint - midpoint)
- for i in range_map1:
- red.append(black[0] + i * (mid[0] - black[0]) // len(range_map1))
- green.append(black[1] + i * (mid[1] - black[1]) // len(range_map1))
- blue.append(black[2] + i * (mid[2] - black[2]) // len(range_map1))
- for i in range_map2:
- red.append(mid[0] + i * (white[0] - mid[0]) // len(range_map2))
- green.append(mid[1] + i * (white[1] - mid[1]) // len(range_map2))
- blue.append(mid[2] + i * (white[2] - mid[2]) // len(range_map2))
- # Create the high-end values
- for i in range(0, 256 - whitepoint):
- red.append(white[0])
- green.append(white[1])
- blue.append(white[2])
- # Return converted image
- image = image.convert("RGB")
- return _lut(image, red + green + blue)
- def contain(image, size, method=Image.Resampling.BICUBIC):
- """
- Returns a resized version of the image, set to the maximum width and height
- within the requested size, while maintaining the original aspect ratio.
- :param image: The image to resize.
- :param size: The requested output size in pixels, given as a
- (width, height) tuple.
- :param method: Resampling method to use. Default is
- :py:attr:`~PIL.Image.Resampling.BICUBIC`.
- See :ref:`concept-filters`.
- :return: An image.
- """
- im_ratio = image.width / image.height
- dest_ratio = size[0] / size[1]
- if im_ratio != dest_ratio:
- if im_ratio > dest_ratio:
- new_height = round(image.height / image.width * size[0])
- if new_height != size[1]:
- size = (size[0], new_height)
- else:
- new_width = round(image.width / image.height * size[1])
- if new_width != size[0]:
- size = (new_width, size[1])
- return image.resize(size, resample=method)
- def cover(image, size, method=Image.Resampling.BICUBIC):
- """
- Returns a resized version of the image, so that the requested size is
- covered, while maintaining the original aspect ratio.
- :param image: The image to resize.
- :param size: The requested output size in pixels, given as a
- (width, height) tuple.
- :param method: Resampling method to use. Default is
- :py:attr:`~PIL.Image.Resampling.BICUBIC`.
- See :ref:`concept-filters`.
- :return: An image.
- """
- im_ratio = image.width / image.height
- dest_ratio = size[0] / size[1]
- if im_ratio != dest_ratio:
- if im_ratio < dest_ratio:
- new_height = round(image.height / image.width * size[0])
- if new_height != size[1]:
- size = (size[0], new_height)
- else:
- new_width = round(image.width / image.height * size[1])
- if new_width != size[0]:
- size = (new_width, size[1])
- return image.resize(size, resample=method)
- def pad(image, size, method=Image.Resampling.BICUBIC, color=None, centering=(0.5, 0.5)):
- """
- Returns a resized and padded version of the image, expanded to fill the
- requested aspect ratio and size.
- :param image: The image to resize and crop.
- :param size: The requested output size in pixels, given as a
- (width, height) tuple.
- :param method: Resampling method to use. Default is
- :py:attr:`~PIL.Image.Resampling.BICUBIC`.
- See :ref:`concept-filters`.
- :param color: The background color of the padded image.
- :param centering: Control the position of the original image within the
- padded version.
- (0.5, 0.5) will keep the image centered
- (0, 0) will keep the image aligned to the top left
- (1, 1) will keep the image aligned to the bottom
- right
- :return: An image.
- """
- resized = contain(image, size, method)
- if resized.size == size:
- out = resized
- else:
- out = Image.new(image.mode, size, color)
- if resized.palette:
- out.putpalette(resized.getpalette())
- if resized.width != size[0]:
- x = round((size[0] - resized.width) * max(0, min(centering[0], 1)))
- out.paste(resized, (x, 0))
- else:
- y = round((size[1] - resized.height) * max(0, min(centering[1], 1)))
- out.paste(resized, (0, y))
- return out
- def crop(image, border=0):
- """
- Remove border from image. The same amount of pixels are removed
- from all four sides. This function works on all image modes.
- .. seealso:: :py:meth:`~PIL.Image.Image.crop`
- :param image: The image to crop.
- :param border: The number of pixels to remove.
- :return: An image.
- """
- left, top, right, bottom = _border(border)
- return image.crop((left, top, image.size[0] - right, image.size[1] - bottom))
- def scale(image, factor, resample=Image.Resampling.BICUBIC):
- """
- Returns a rescaled image by a specific factor given in parameter.
- A factor greater than 1 expands the image, between 0 and 1 contracts the
- image.
- :param image: The image to rescale.
- :param factor: The expansion factor, as a float.
- :param resample: Resampling method to use. Default is
- :py:attr:`~PIL.Image.Resampling.BICUBIC`.
- See :ref:`concept-filters`.
- :returns: An :py:class:`~PIL.Image.Image` object.
- """
- if factor == 1:
- return image.copy()
- elif factor <= 0:
- msg = "the factor must be greater than 0"
- raise ValueError(msg)
- else:
- size = (round(factor * image.width), round(factor * image.height))
- return image.resize(size, resample)
- def deform(image, deformer, resample=Image.Resampling.BILINEAR):
- """
- Deform the image.
- :param image: The image to deform.
- :param deformer: A deformer object. Any object that implements a
- ``getmesh`` method can be used.
- :param resample: An optional resampling filter. Same values possible as
- in the PIL.Image.transform function.
- :return: An image.
- """
- return image.transform(
- image.size, Image.Transform.MESH, deformer.getmesh(image), resample
- )
- def equalize(image, mask=None):
- """
- 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.
- """
- if image.mode == "P":
- image = image.convert("RGB")
- h = image.histogram(mask)
- lut = []
- for b in range(0, len(h), 256):
- histo = [_f for _f in h[b : b + 256] if _f]
- if len(histo) <= 1:
- lut.extend(list(range(256)))
- else:
- step = (functools.reduce(operator.add, histo) - histo[-1]) // 255
- if not step:
- lut.extend(list(range(256)))
- else:
- n = step // 2
- for i in range(256):
- lut.append(n // step)
- n = n + h[i + b]
- return _lut(image, lut)
- def expand(image, border=0, fill=0):
- """
- Add border to the image
- :param image: The image to expand.
- :param border: Border width, in pixels.
- :param fill: Pixel fill value (a color value). Default is 0 (black).
- :return: An image.
- """
- left, top, right, bottom = _border(border)
- width = left + image.size[0] + right
- height = top + image.size[1] + bottom
- color = _color(fill, image.mode)
- if image.palette:
- palette = ImagePalette.ImagePalette(palette=image.getpalette())
- if isinstance(color, tuple):
- color = palette.getcolor(color)
- else:
- palette = None
- out = Image.new(image.mode, (width, height), color)
- if palette:
- out.putpalette(palette.palette)
- out.paste(image, (left, top))
- return out
- def fit(image, size, method=Image.Resampling.BICUBIC, bleed=0.0, centering=(0.5, 0.5)):
- """
- Returns a resized and cropped version of the image, cropped to the
- requested aspect ratio and size.
- This function was contributed by Kevin Cazabon.
- :param image: The image to resize and crop.
- :param size: The requested output size in pixels, given as a
- (width, height) tuple.
- :param method: Resampling method to use. Default is
- :py:attr:`~PIL.Image.Resampling.BICUBIC`.
- See :ref:`concept-filters`.
- :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).
- Cannot be greater than or equal to 0.5.
- :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.
- """
- # by Kevin Cazabon, Feb 17/2000
- # kevin@cazabon.com
- # https://www.cazabon.com
- # ensure centering is mutable
- centering = list(centering)
- if not 0.0 <= centering[0] <= 1.0:
- centering[0] = 0.5
- if not 0.0 <= centering[1] <= 1.0:
- centering[1] = 0.5
- if not 0.0 <= bleed < 0.5:
- 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
- bleed_pixels = (bleed * image.size[0], bleed * image.size[1])
- live_size = (
- image.size[0] - bleed_pixels[0] * 2,
- image.size[1] - bleed_pixels[1] * 2,
- )
- # calculate the aspect ratio of the live_size
- live_size_ratio = live_size[0] / live_size[1]
- # calculate the aspect ratio of the output image
- output_ratio = size[0] / size[1]
- # figure out if the sides or top/bottom will be cropped off
- if live_size_ratio == output_ratio:
- # live_size is already the needed ratio
- crop_width = live_size[0]
- crop_height = live_size[1]
- elif live_size_ratio >= output_ratio:
- # live_size is wider than what's needed, crop the sides
- crop_width = output_ratio * live_size[1]
- crop_height = live_size[1]
- else:
- # live_size is taller than what's needed, crop the top and bottom
- crop_width = live_size[0]
- crop_height = live_size[0] / output_ratio
- # make the crop
- crop_left = bleed_pixels[0] + (live_size[0] - crop_width) * centering[0]
- crop_top = bleed_pixels[1] + (live_size[1] - crop_height) * centering[1]
- crop = (crop_left, crop_top, crop_left + crop_width, crop_top + crop_height)
- # resize the image and return it
- return image.resize(size, method, box=crop)
- def flip(image):
- """
- Flip the image vertically (top to bottom).
- :param image: The image to flip.
- :return: An image.
- """
- return image.transpose(Image.Transpose.FLIP_TOP_BOTTOM)
- def grayscale(image):
- """
- Convert the image to grayscale.
- :param image: The image to convert.
- :return: An image.
- """
- return image.convert("L")
- def invert(image):
- """
- Invert (negate) the image.
- :param image: The image to invert.
- :return: An image.
- """
- lut = []
- for i in range(256):
- lut.append(255 - i)
- return image.point(lut) if image.mode == "1" else _lut(image, lut)
- def mirror(image):
- """
- Flip image horizontally (left to right).
- :param image: The image to mirror.
- :return: An image.
- """
- return image.transpose(Image.Transpose.FLIP_LEFT_RIGHT)
- def posterize(image, bits):
- """
- Reduce the number of bits for each color channel.
- :param image: The image to posterize.
- :param bits: The number of bits to keep for each channel (1-8).
- :return: An image.
- """
- lut = []
- mask = ~(2 ** (8 - bits) - 1)
- for i in range(256):
- lut.append(i & mask)
- return _lut(image, lut)
- def solarize(image, threshold=128):
- """
- Invert all pixel values above a threshold.
- :param image: The image to solarize.
- :param threshold: All pixels above this greyscale level are inverted.
- :return: An image.
- """
- lut = []
- for i in range(256):
- if i < threshold:
- lut.append(i)
- else:
- lut.append(255 - i)
- return _lut(image, lut)
- def exif_transpose(image, *, in_place=False):
- """
- If an image has an EXIF Orientation tag, other than 1, transpose the image
- accordingly, and remove the orientation data.
- :param image: The image to transpose.
- :param in_place: Boolean. Keyword-only argument.
- If ``True``, the original image is modified in-place, and ``None`` is returned.
- If ``False`` (default), a new :py:class:`~PIL.Image.Image` object is returned
- with the transposition applied. If there is no transposition, a copy of the
- image will be returned.
- """
- image.load()
- image_exif = image.getexif()
- orientation = image_exif.get(ExifTags.Base.Orientation)
- method = {
- 2: Image.Transpose.FLIP_LEFT_RIGHT,
- 3: Image.Transpose.ROTATE_180,
- 4: Image.Transpose.FLIP_TOP_BOTTOM,
- 5: Image.Transpose.TRANSPOSE,
- 6: Image.Transpose.ROTATE_270,
- 7: Image.Transpose.TRANSVERSE,
- 8: Image.Transpose.ROTATE_90,
- }.get(orientation)
- if method is not None:
- transposed_image = image.transpose(method)
- if in_place:
- image.im = transposed_image.im
- image.pyaccess = None
- image._size = transposed_image._size
- exif_image = image if in_place else transposed_image
- exif = exif_image.getexif()
- if ExifTags.Base.Orientation in exif:
- del exif[ExifTags.Base.Orientation]
- if "exif" in exif_image.info:
- exif_image.info["exif"] = exif.tobytes()
- elif "Raw profile type exif" in exif_image.info:
- exif_image.info["Raw profile type exif"] = exif.tobytes().hex()
- elif "XML:com.adobe.xmp" in exif_image.info:
- for pattern in (
- r'tiff:Orientation="([0-9])"',
- r"<tiff:Orientation>([0-9])</tiff:Orientation>",
- ):
- exif_image.info["XML:com.adobe.xmp"] = re.sub(
- pattern, "", exif_image.info["XML:com.adobe.xmp"]
- )
- if not in_place:
- return transposed_image
- elif not in_place:
- return image.copy()
|