Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/PIL/ImageOps.py: 18%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1#
2# The Python Imaging Library.
3# $Id$
4#
5# standard image operations
6#
7# History:
8# 2001-10-20 fl Created
9# 2001-10-23 fl Added autocontrast operator
10# 2001-12-18 fl Added Kevin's fit operator
11# 2004-03-14 fl Fixed potential division by zero in equalize
12# 2005-05-05 fl Fixed equalize for low number of values
13#
14# Copyright (c) 2001-2004 by Secret Labs AB
15# Copyright (c) 2001-2004 by Fredrik Lundh
16#
17# See the README file for information on usage and redistribution.
18#
19from __future__ import annotations
21import functools
22import operator
23import re
24from collections.abc import Sequence
25from typing import Literal, Protocol, cast, overload
27from . import ExifTags, Image, ImagePalette
29#
30# helpers
33def _border(border: int | tuple[int, ...]) -> tuple[int, int, int, int]:
34 if isinstance(border, tuple):
35 if len(border) == 2:
36 left, top = right, bottom = border
37 elif len(border) == 4:
38 left, top, right, bottom = border
39 else:
40 msg = "border must be an integer, or a tuple of two or four elements"
41 raise ValueError(msg)
42 else:
43 left = top = right = bottom = border
44 return left, top, right, bottom
47def _color(color: str | int | tuple[int, ...], mode: str) -> int | tuple[int, ...]:
48 if isinstance(color, str):
49 from . import ImageColor
51 color = ImageColor.getcolor(color, mode)
52 return color
55def _lut(image: Image.Image, lut: list[int]) -> Image.Image:
56 if image.mode == "P":
57 # FIXME: apply to lookup table, not image data
58 msg = "mode P support coming soon"
59 raise NotImplementedError(msg)
60 elif image.mode in ("L", "RGB"):
61 if image.mode == "RGB" and len(lut) == 256:
62 lut = lut + lut + lut
63 return image.point(lut)
64 else:
65 msg = f"not supported for mode {image.mode}"
66 raise OSError(msg)
69#
70# actions
73def autocontrast(
74 image: Image.Image,
75 cutoff: float | tuple[float, float] = 0,
76 ignore: int | Sequence[int] | None = None,
77 mask: Image.Image | None = None,
78 preserve_tone: bool = False,
79) -> Image.Image:
80 """
81 Maximize (normalize) image contrast. This function calculates a
82 histogram of the input image (or mask region), removes ``cutoff`` percent of the
83 lightest and darkest pixels from the histogram, and remaps the image
84 so that the darkest pixel becomes black (0), and the lightest
85 becomes white (255).
87 :param image: The image to process.
88 :param cutoff: The percent to cut off from the histogram on the low and
89 high ends. Either a tuple of (low, high), or a single
90 number for both.
91 :param ignore: The background pixel value (use None for no background).
92 :param mask: Histogram used in contrast operation is computed using pixels
93 within the mask. If no mask is given the entire image is used
94 for histogram computation.
95 :param preserve_tone: Preserve image tone in Photoshop-like style autocontrast.
97 .. versionadded:: 8.2.0
99 :return: An image.
100 """
101 if preserve_tone:
102 histogram = image.convert("L").histogram(mask)
103 else:
104 histogram = image.histogram(mask)
106 lut = []
107 for layer in range(0, len(histogram), 256):
108 h = histogram[layer : layer + 256]
109 if ignore is not None:
110 # get rid of outliers
111 if isinstance(ignore, int):
112 h[ignore] = 0
113 else:
114 for ix in ignore:
115 h[ix] = 0
116 if cutoff:
117 # cut off pixels from both ends of the histogram
118 if not isinstance(cutoff, tuple):
119 cutoff = (cutoff, cutoff)
120 # get number of pixels
121 n = 0
122 for ix in range(256):
123 n = n + h[ix]
124 # remove cutoff% pixels from the low end
125 cut = int(n * cutoff[0] // 100)
126 for lo in range(256):
127 if cut > h[lo]:
128 cut = cut - h[lo]
129 h[lo] = 0
130 else:
131 h[lo] -= cut
132 cut = 0
133 if cut <= 0:
134 break
135 # remove cutoff% samples from the high end
136 cut = int(n * cutoff[1] // 100)
137 for hi in range(255, -1, -1):
138 if cut > h[hi]:
139 cut = cut - h[hi]
140 h[hi] = 0
141 else:
142 h[hi] -= cut
143 cut = 0
144 if cut <= 0:
145 break
146 # find lowest/highest samples after preprocessing
147 for lo in range(256):
148 if h[lo]:
149 break
150 for hi in range(255, -1, -1):
151 if h[hi]:
152 break
153 if hi <= lo:
154 # don't bother
155 lut.extend(list(range(256)))
156 else:
157 scale = 255.0 / (hi - lo)
158 offset = -lo * scale
159 for ix in range(256):
160 ix = int(ix * scale + offset)
161 if ix < 0:
162 ix = 0
163 elif ix > 255:
164 ix = 255
165 lut.append(ix)
166 return _lut(image, lut)
169def colorize(
170 image: Image.Image,
171 black: str | tuple[int, ...],
172 white: str | tuple[int, ...],
173 mid: str | int | tuple[int, ...] | None = None,
174 blackpoint: int = 0,
175 whitepoint: int = 255,
176 midpoint: int = 127,
177) -> Image.Image:
178 """
179 Colorize grayscale image.
180 This function calculates a color wedge which maps all black pixels in
181 the source image to the first color and all white pixels to the
182 second color. If ``mid`` is specified, it uses three-color mapping.
183 The ``black`` and ``white`` arguments should be RGB tuples or color names;
184 optionally you can use three-color mapping by also specifying ``mid``.
185 Mapping positions for any of the colors can be specified
186 (e.g. ``blackpoint``), where these parameters are the integer
187 value corresponding to where the corresponding color should be mapped.
188 These parameters must have logical order, such that
189 ``blackpoint <= midpoint <= whitepoint`` (if ``mid`` is specified).
191 :param image: The image to colorize.
192 :param black: The color to use for black input pixels.
193 :param white: The color to use for white input pixels.
194 :param mid: The color to use for midtone input pixels.
195 :param blackpoint: an int value [0, 255] for the black mapping.
196 :param whitepoint: an int value [0, 255] for the white mapping.
197 :param midpoint: an int value [0, 255] for the midtone mapping.
198 :return: An image.
199 """
201 # Initial asserts
202 assert image.mode == "L"
203 if mid is None:
204 assert 0 <= blackpoint <= whitepoint <= 255
205 else:
206 assert 0 <= blackpoint <= midpoint <= whitepoint <= 255
208 # Define colors from arguments
209 rgb_black = cast(Sequence[int], _color(black, "RGB"))
210 rgb_white = cast(Sequence[int], _color(white, "RGB"))
211 rgb_mid = cast(Sequence[int], _color(mid, "RGB")) if mid is not None else None
213 # Empty lists for the mapping
214 red = []
215 green = []
216 blue = []
218 # Create the low-end values
219 for i in range(blackpoint):
220 red.append(rgb_black[0])
221 green.append(rgb_black[1])
222 blue.append(rgb_black[2])
224 # Create the mapping (2-color)
225 if rgb_mid is None:
226 range_map = range(whitepoint - blackpoint)
228 for i in range_map:
229 red.append(
230 rgb_black[0] + i * (rgb_white[0] - rgb_black[0]) // len(range_map)
231 )
232 green.append(
233 rgb_black[1] + i * (rgb_white[1] - rgb_black[1]) // len(range_map)
234 )
235 blue.append(
236 rgb_black[2] + i * (rgb_white[2] - rgb_black[2]) // len(range_map)
237 )
239 # Create the mapping (3-color)
240 else:
241 range_map1 = range(midpoint - blackpoint)
242 range_map2 = range(whitepoint - midpoint)
244 for i in range_map1:
245 red.append(
246 rgb_black[0] + i * (rgb_mid[0] - rgb_black[0]) // len(range_map1)
247 )
248 green.append(
249 rgb_black[1] + i * (rgb_mid[1] - rgb_black[1]) // len(range_map1)
250 )
251 blue.append(
252 rgb_black[2] + i * (rgb_mid[2] - rgb_black[2]) // len(range_map1)
253 )
254 for i in range_map2:
255 red.append(rgb_mid[0] + i * (rgb_white[0] - rgb_mid[0]) // len(range_map2))
256 green.append(
257 rgb_mid[1] + i * (rgb_white[1] - rgb_mid[1]) // len(range_map2)
258 )
259 blue.append(rgb_mid[2] + i * (rgb_white[2] - rgb_mid[2]) // len(range_map2))
261 # Create the high-end values
262 for i in range(256 - whitepoint):
263 red.append(rgb_white[0])
264 green.append(rgb_white[1])
265 blue.append(rgb_white[2])
267 # Return converted image
268 image = image.convert("RGB")
269 return _lut(image, red + green + blue)
272def contain(
273 image: Image.Image, size: tuple[int, int], method: int = Image.Resampling.BICUBIC
274) -> Image.Image:
275 """
276 Returns a resized version of the image, set to the maximum width and height
277 within the requested size, while maintaining the original aspect ratio.
279 :param image: The image to resize.
280 :param size: The requested output size in pixels, given as a
281 (width, height) tuple.
282 :param method: Resampling method to use. Default is
283 :py:attr:`~PIL.Image.Resampling.BICUBIC`.
284 See :ref:`concept-filters`.
285 :return: An image.
286 """
288 im_ratio = image.width / image.height
289 dest_ratio = size[0] / size[1]
291 if im_ratio != dest_ratio:
292 if im_ratio > dest_ratio:
293 new_height = round(image.height / image.width * size[0])
294 if new_height != size[1]:
295 size = (size[0], new_height)
296 else:
297 new_width = round(image.width / image.height * size[1])
298 if new_width != size[0]:
299 size = (new_width, size[1])
300 return image.resize(size, resample=method)
303def cover(
304 image: Image.Image, size: tuple[int, int], method: int = Image.Resampling.BICUBIC
305) -> Image.Image:
306 """
307 Returns a resized version of the image, so that the requested size is
308 covered, while maintaining the original aspect ratio.
310 :param image: The image to resize.
311 :param size: The requested output size in pixels, given as a
312 (width, height) tuple.
313 :param method: Resampling method to use. Default is
314 :py:attr:`~PIL.Image.Resampling.BICUBIC`.
315 See :ref:`concept-filters`.
316 :return: An image.
317 """
319 im_ratio = image.width / image.height
320 dest_ratio = size[0] / size[1]
322 if im_ratio != dest_ratio:
323 if im_ratio < dest_ratio:
324 new_height = round(image.height / image.width * size[0])
325 if new_height != size[1]:
326 size = (size[0], new_height)
327 else:
328 new_width = round(image.width / image.height * size[1])
329 if new_width != size[0]:
330 size = (new_width, size[1])
331 return image.resize(size, resample=method)
334def pad(
335 image: Image.Image,
336 size: tuple[int, int],
337 method: int = Image.Resampling.BICUBIC,
338 color: str | int | tuple[int, ...] | None = None,
339 centering: tuple[float, float] = (0.5, 0.5),
340) -> Image.Image:
341 """
342 Returns a resized and padded version of the image, expanded to fill the
343 requested aspect ratio and size.
345 :param image: The image to resize and crop.
346 :param size: The requested output size in pixels, given as a
347 (width, height) tuple.
348 :param method: Resampling method to use. Default is
349 :py:attr:`~PIL.Image.Resampling.BICUBIC`.
350 See :ref:`concept-filters`.
351 :param color: The background color of the padded image.
352 :param centering: Control the position of the original image within the
353 padded version.
355 (0.5, 0.5) will keep the image centered
356 (0, 0) will keep the image aligned to the top left
357 (1, 1) will keep the image aligned to the bottom
358 right
359 :return: An image.
360 """
362 resized = contain(image, size, method)
363 if resized.size == size:
364 out = resized
365 else:
366 out = Image.new(image.mode, size, color)
367 if resized.palette:
368 palette = resized.getpalette()
369 if palette is not None:
370 out.putpalette(palette)
371 if resized.width != size[0]:
372 x = round((size[0] - resized.width) * max(0, min(centering[0], 1)))
373 out.paste(resized, (x, 0))
374 else:
375 y = round((size[1] - resized.height) * max(0, min(centering[1], 1)))
376 out.paste(resized, (0, y))
377 return out
380def crop(image: Image.Image, border: int = 0) -> Image.Image:
381 """
382 Remove border from image. The same amount of pixels are removed
383 from all four sides. This function works on all image modes.
385 .. seealso:: :py:meth:`~PIL.Image.Image.crop`
387 :param image: The image to crop.
388 :param border: The number of pixels to remove.
389 :return: An image.
390 """
391 left, top, right, bottom = _border(border)
392 return image.crop((left, top, image.size[0] - right, image.size[1] - bottom))
395def scale(
396 image: Image.Image, factor: float, resample: int = Image.Resampling.BICUBIC
397) -> Image.Image:
398 """
399 Returns a rescaled image by a specific factor given in parameter.
400 A factor greater than 1 expands the image, between 0 and 1 contracts the
401 image.
403 :param image: The image to rescale.
404 :param factor: The expansion factor, as a float.
405 :param resample: Resampling method to use. Default is
406 :py:attr:`~PIL.Image.Resampling.BICUBIC`.
407 See :ref:`concept-filters`.
408 :returns: An :py:class:`~PIL.Image.Image` object.
409 """
410 if factor == 1:
411 return image.copy()
412 elif factor <= 0:
413 msg = "the factor must be greater than 0"
414 raise ValueError(msg)
415 else:
416 size = (round(factor * image.width), round(factor * image.height))
417 return image.resize(size, resample)
420class SupportsGetMesh(Protocol):
421 """
422 An object that supports the ``getmesh`` method, taking an image as an
423 argument, and returning a list of tuples. Each tuple contains two tuples,
424 the source box as a tuple of 4 integers, and a tuple of 8 integers for the
425 final quadrilateral, in order of top left, bottom left, bottom right, top
426 right.
427 """
429 def getmesh(
430 self, image: Image.Image
431 ) -> list[
432 tuple[tuple[int, int, int, int], tuple[int, int, int, int, int, int, int, int]]
433 ]: ...
436def deform(
437 image: Image.Image,
438 deformer: SupportsGetMesh,
439 resample: int = Image.Resampling.BILINEAR,
440) -> Image.Image:
441 """
442 Deform the image.
444 :param image: The image to deform.
445 :param deformer: A deformer object. Any object that implements a
446 ``getmesh`` method can be used.
447 :param resample: An optional resampling filter. Same values possible as
448 in the PIL.Image.transform function.
449 :return: An image.
450 """
451 return image.transform(
452 image.size, Image.Transform.MESH, deformer.getmesh(image), resample
453 )
456def equalize(image: Image.Image, mask: Image.Image | None = None) -> Image.Image:
457 """
458 Equalize the image histogram. This function applies a non-linear
459 mapping to the input image, in order to create a uniform
460 distribution of grayscale values in the output image.
462 :param image: The image to equalize.
463 :param mask: An optional mask. If given, only the pixels selected by
464 the mask are included in the analysis.
465 :return: An image.
466 """
467 if image.mode == "P":
468 image = image.convert("RGB")
469 h = image.histogram(mask)
470 lut = []
471 for b in range(0, len(h), 256):
472 histo = [_f for _f in h[b : b + 256] if _f]
473 if len(histo) <= 1:
474 lut.extend(list(range(256)))
475 else:
476 step = (functools.reduce(operator.add, histo) - histo[-1]) // 255
477 if not step:
478 lut.extend(list(range(256)))
479 else:
480 n = step // 2
481 for i in range(256):
482 lut.append(n // step)
483 n = n + h[i + b]
484 return _lut(image, lut)
487def expand(
488 image: Image.Image,
489 border: int | tuple[int, ...] = 0,
490 fill: str | int | tuple[int, ...] = 0,
491) -> Image.Image:
492 """
493 Add border to the image
495 :param image: The image to expand.
496 :param border: Border width, in pixels.
497 :param fill: Pixel fill value (a color value). Default is 0 (black).
498 :return: An image.
499 """
500 left, top, right, bottom = _border(border)
501 width = left + image.size[0] + right
502 height = top + image.size[1] + bottom
503 color = _color(fill, image.mode)
504 if image.palette:
505 mode = image.palette.mode
506 palette = ImagePalette.ImagePalette(mode, image.getpalette(mode))
507 if isinstance(color, tuple) and (len(color) == 3 or len(color) == 4):
508 color = palette.getcolor(color)
509 else:
510 palette = None
511 out = Image.new(image.mode, (width, height), color)
512 if palette:
513 out.putpalette(palette.palette, mode)
514 out.paste(image, (left, top))
515 return out
518def fit(
519 image: Image.Image,
520 size: tuple[int, int],
521 method: int = Image.Resampling.BICUBIC,
522 bleed: float = 0.0,
523 centering: tuple[float, float] = (0.5, 0.5),
524) -> Image.Image:
525 """
526 Returns a resized and cropped version of the image, cropped to the
527 requested aspect ratio and size.
529 This function was contributed by Kevin Cazabon.
531 :param image: The image to resize and crop.
532 :param size: The requested output size in pixels, given as a
533 (width, height) tuple.
534 :param method: Resampling method to use. Default is
535 :py:attr:`~PIL.Image.Resampling.BICUBIC`.
536 See :ref:`concept-filters`.
537 :param bleed: Remove a border around the outside of the image from all
538 four edges. The value is a decimal percentage (use 0.01 for
539 one percent). The default value is 0 (no border).
540 Cannot be greater than or equal to 0.5.
541 :param centering: Control the cropping position. Use (0.5, 0.5) for
542 center cropping (e.g. if cropping the width, take 50% off
543 of the left side, and therefore 50% off the right side).
544 (0.0, 0.0) will crop from the top left corner (i.e. if
545 cropping the width, take all of the crop off of the right
546 side, and if cropping the height, take all of it off the
547 bottom). (1.0, 0.0) will crop from the bottom left
548 corner, etc. (i.e. if cropping the width, take all of the
549 crop off the left side, and if cropping the height take
550 none from the top, and therefore all off the bottom).
551 :return: An image.
552 """
554 # by Kevin Cazabon, Feb 17/2000
555 # kevin@cazabon.com
556 # https://www.cazabon.com
558 centering_x, centering_y = centering
560 if not 0.0 <= centering_x <= 1.0:
561 centering_x = 0.5
562 if not 0.0 <= centering_y <= 1.0:
563 centering_y = 0.5
565 if not 0.0 <= bleed < 0.5:
566 bleed = 0.0
568 # calculate the area to use for resizing and cropping, subtracting
569 # the 'bleed' around the edges
571 # number of pixels to trim off on Top and Bottom, Left and Right
572 bleed_pixels = (bleed * image.size[0], bleed * image.size[1])
574 live_size = (
575 image.size[0] - bleed_pixels[0] * 2,
576 image.size[1] - bleed_pixels[1] * 2,
577 )
579 # calculate the aspect ratio of the live_size
580 live_size_ratio = live_size[0] / live_size[1]
582 # calculate the aspect ratio of the output image
583 output_ratio = size[0] / size[1]
585 # figure out if the sides or top/bottom will be cropped off
586 if live_size_ratio == output_ratio:
587 # live_size is already the needed ratio
588 crop_width = live_size[0]
589 crop_height = live_size[1]
590 elif live_size_ratio >= output_ratio:
591 # live_size is wider than what's needed, crop the sides
592 crop_width = output_ratio * live_size[1]
593 crop_height = live_size[1]
594 else:
595 # live_size is taller than what's needed, crop the top and bottom
596 crop_width = live_size[0]
597 crop_height = live_size[0] / output_ratio
599 # make the crop
600 crop_left = bleed_pixels[0] + (live_size[0] - crop_width) * centering_x
601 crop_top = bleed_pixels[1] + (live_size[1] - crop_height) * centering_y
603 crop = (crop_left, crop_top, crop_left + crop_width, crop_top + crop_height)
605 # resize the image and return it
606 return image.resize(size, method, box=crop)
609def flip(image: Image.Image) -> Image.Image:
610 """
611 Flip the image vertically (top to bottom).
613 :param image: The image to flip.
614 :return: An image.
615 """
616 return image.transpose(Image.Transpose.FLIP_TOP_BOTTOM)
619def grayscale(image: Image.Image) -> Image.Image:
620 """
621 Convert the image to grayscale.
623 :param image: The image to convert.
624 :return: An image.
625 """
626 return image.convert("L")
629def invert(image: Image.Image) -> Image.Image:
630 """
631 Invert (negate) the image.
633 :param image: The image to invert.
634 :return: An image.
635 """
636 lut = list(range(255, -1, -1))
637 return image.point(lut) if image.mode == "1" else _lut(image, lut)
640def mirror(image: Image.Image) -> Image.Image:
641 """
642 Flip image horizontally (left to right).
644 :param image: The image to mirror.
645 :return: An image.
646 """
647 return image.transpose(Image.Transpose.FLIP_LEFT_RIGHT)
650def posterize(image: Image.Image, bits: int) -> Image.Image:
651 """
652 Reduce the number of bits for each color channel.
654 :param image: The image to posterize.
655 :param bits: The number of bits to keep for each channel (1-8).
656 :return: An image.
657 """
658 mask = ~(2 ** (8 - bits) - 1)
659 lut = [i & mask for i in range(256)]
660 return _lut(image, lut)
663def solarize(image: Image.Image, threshold: int = 128) -> Image.Image:
664 """
665 Invert all pixel values above a threshold.
667 :param image: The image to solarize.
668 :param threshold: All pixels above this grayscale level are inverted.
669 :return: An image.
670 """
671 lut = []
672 for i in range(256):
673 if i < threshold:
674 lut.append(i)
675 else:
676 lut.append(255 - i)
677 return _lut(image, lut)
680@overload
681def exif_transpose(image: Image.Image, *, in_place: Literal[True]) -> None: ...
684@overload
685def exif_transpose(
686 image: Image.Image, *, in_place: Literal[False] = False
687) -> Image.Image: ...
690def exif_transpose(image: Image.Image, *, in_place: bool = False) -> Image.Image | None:
691 """
692 If an image has an EXIF Orientation tag, other than 1, transpose the image
693 accordingly, and remove the orientation data.
695 :param image: The image to transpose.
696 :param in_place: Boolean. Keyword-only argument.
697 If ``True``, the original image is modified in-place, and ``None`` is returned.
698 If ``False`` (default), a new :py:class:`~PIL.Image.Image` object is returned
699 with the transposition applied. If there is no transposition, a copy of the
700 image will be returned.
701 """
702 image.load()
703 image_exif = image.getexif()
704 orientation = image_exif.get(ExifTags.Base.Orientation, 1)
705 method = {
706 2: Image.Transpose.FLIP_LEFT_RIGHT,
707 3: Image.Transpose.ROTATE_180,
708 4: Image.Transpose.FLIP_TOP_BOTTOM,
709 5: Image.Transpose.TRANSPOSE,
710 6: Image.Transpose.ROTATE_270,
711 7: Image.Transpose.TRANSVERSE,
712 8: Image.Transpose.ROTATE_90,
713 }.get(orientation)
714 if method is not None:
715 if in_place:
716 image.im = image.im.transpose(method)
717 image._size = image.im.size
718 else:
719 transposed_image = image.transpose(method)
720 exif_image = image if in_place else transposed_image
722 exif = exif_image.getexif()
723 if ExifTags.Base.Orientation in exif:
724 del exif[ExifTags.Base.Orientation]
725 if "exif" in exif_image.info:
726 exif_image.info["exif"] = exif.tobytes()
727 elif "Raw profile type exif" in exif_image.info:
728 exif_image.info["Raw profile type exif"] = exif.tobytes().hex()
729 for key in ("XML:com.adobe.xmp", "xmp"):
730 if key in exif_image.info:
731 for pattern in (
732 r'tiff:Orientation="([0-9])"',
733 r"<tiff:Orientation>([0-9])</tiff:Orientation>",
734 ):
735 value = exif_image.info[key]
736 if isinstance(value, str):
737 value = re.sub(pattern, "", value)
738 elif isinstance(value, tuple):
739 value = tuple(
740 re.sub(pattern.encode(), b"", v) for v in value
741 )
742 else:
743 value = re.sub(pattern.encode(), b"", value)
744 exif_image.info[key] = value
745 if not in_place:
746 return transposed_image
747 elif not in_place:
748 return image.copy()
749 return None