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 left = top = right = bottom = border
41 return left, top, right, bottom
44def _color(color: str | int | tuple[int, ...], mode: str) -> int | tuple[int, ...]:
45 if isinstance(color, str):
46 from . import ImageColor
48 color = ImageColor.getcolor(color, mode)
49 return color
52def _lut(image: Image.Image, lut: list[int]) -> Image.Image:
53 if image.mode == "P":
54 # FIXME: apply to lookup table, not image data
55 msg = "mode P support coming soon"
56 raise NotImplementedError(msg)
57 elif image.mode in ("L", "RGB"):
58 if image.mode == "RGB" and len(lut) == 256:
59 lut = lut + lut + lut
60 return image.point(lut)
61 else:
62 msg = f"not supported for mode {image.mode}"
63 raise OSError(msg)
66#
67# actions
70def autocontrast(
71 image: Image.Image,
72 cutoff: float | tuple[float, float] = 0,
73 ignore: int | Sequence[int] | None = None,
74 mask: Image.Image | None = None,
75 preserve_tone: bool = False,
76) -> Image.Image:
77 """
78 Maximize (normalize) image contrast. This function calculates a
79 histogram of the input image (or mask region), removes ``cutoff`` percent of the
80 lightest and darkest pixels from the histogram, and remaps the image
81 so that the darkest pixel becomes black (0), and the lightest
82 becomes white (255).
84 :param image: The image to process.
85 :param cutoff: The percent to cut off from the histogram on the low and
86 high ends. Either a tuple of (low, high), or a single
87 number for both.
88 :param ignore: The background pixel value (use None for no background).
89 :param mask: Histogram used in contrast operation is computed using pixels
90 within the mask. If no mask is given the entire image is used
91 for histogram computation.
92 :param preserve_tone: Preserve image tone in Photoshop-like style autocontrast.
94 .. versionadded:: 8.2.0
96 :return: An image.
97 """
98 if preserve_tone:
99 histogram = image.convert("L").histogram(mask)
100 else:
101 histogram = image.histogram(mask)
103 lut = []
104 for layer in range(0, len(histogram), 256):
105 h = histogram[layer : layer + 256]
106 if ignore is not None:
107 # get rid of outliers
108 if isinstance(ignore, int):
109 h[ignore] = 0
110 else:
111 for ix in ignore:
112 h[ix] = 0
113 if cutoff:
114 # cut off pixels from both ends of the histogram
115 if not isinstance(cutoff, tuple):
116 cutoff = (cutoff, cutoff)
117 # get number of pixels
118 n = 0
119 for ix in range(256):
120 n = n + h[ix]
121 # remove cutoff% pixels from the low end
122 cut = int(n * cutoff[0] // 100)
123 for lo in range(256):
124 if cut > h[lo]:
125 cut = cut - h[lo]
126 h[lo] = 0
127 else:
128 h[lo] -= cut
129 cut = 0
130 if cut <= 0:
131 break
132 # remove cutoff% samples from the high end
133 cut = int(n * cutoff[1] // 100)
134 for hi in range(255, -1, -1):
135 if cut > h[hi]:
136 cut = cut - h[hi]
137 h[hi] = 0
138 else:
139 h[hi] -= cut
140 cut = 0
141 if cut <= 0:
142 break
143 # find lowest/highest samples after preprocessing
144 for lo in range(256):
145 if h[lo]:
146 break
147 for hi in range(255, -1, -1):
148 if h[hi]:
149 break
150 if hi <= lo:
151 # don't bother
152 lut.extend(list(range(256)))
153 else:
154 scale = 255.0 / (hi - lo)
155 offset = -lo * scale
156 for ix in range(256):
157 ix = int(ix * scale + offset)
158 if ix < 0:
159 ix = 0
160 elif ix > 255:
161 ix = 255
162 lut.append(ix)
163 return _lut(image, lut)
166def colorize(
167 image: Image.Image,
168 black: str | tuple[int, ...],
169 white: str | tuple[int, ...],
170 mid: str | int | tuple[int, ...] | None = None,
171 blackpoint: int = 0,
172 whitepoint: int = 255,
173 midpoint: int = 127,
174) -> Image.Image:
175 """
176 Colorize grayscale image.
177 This function calculates a color wedge which maps all black pixels in
178 the source image to the first color and all white pixels to the
179 second color. If ``mid`` is specified, it uses three-color mapping.
180 The ``black`` and ``white`` arguments should be RGB tuples or color names;
181 optionally you can use three-color mapping by also specifying ``mid``.
182 Mapping positions for any of the colors can be specified
183 (e.g. ``blackpoint``), where these parameters are the integer
184 value corresponding to where the corresponding color should be mapped.
185 These parameters must have logical order, such that
186 ``blackpoint <= midpoint <= whitepoint`` (if ``mid`` is specified).
188 :param image: The image to colorize.
189 :param black: The color to use for black input pixels.
190 :param white: The color to use for white input pixels.
191 :param mid: The color to use for midtone input pixels.
192 :param blackpoint: an int value [0, 255] for the black mapping.
193 :param whitepoint: an int value [0, 255] for the white mapping.
194 :param midpoint: an int value [0, 255] for the midtone mapping.
195 :return: An image.
196 """
198 # Initial asserts
199 assert image.mode == "L"
200 if mid is None:
201 assert 0 <= blackpoint <= whitepoint <= 255
202 else:
203 assert 0 <= blackpoint <= midpoint <= whitepoint <= 255
205 # Define colors from arguments
206 rgb_black = cast(Sequence[int], _color(black, "RGB"))
207 rgb_white = cast(Sequence[int], _color(white, "RGB"))
208 rgb_mid = cast(Sequence[int], _color(mid, "RGB")) if mid is not None else None
210 # Empty lists for the mapping
211 red = []
212 green = []
213 blue = []
215 # Create the low-end values
216 for i in range(blackpoint):
217 red.append(rgb_black[0])
218 green.append(rgb_black[1])
219 blue.append(rgb_black[2])
221 # Create the mapping (2-color)
222 if rgb_mid is None:
223 range_map = range(whitepoint - blackpoint)
225 for i in range_map:
226 red.append(
227 rgb_black[0] + i * (rgb_white[0] - rgb_black[0]) // len(range_map)
228 )
229 green.append(
230 rgb_black[1] + i * (rgb_white[1] - rgb_black[1]) // len(range_map)
231 )
232 blue.append(
233 rgb_black[2] + i * (rgb_white[2] - rgb_black[2]) // len(range_map)
234 )
236 # Create the mapping (3-color)
237 else:
238 range_map1 = range(midpoint - blackpoint)
239 range_map2 = range(whitepoint - midpoint)
241 for i in range_map1:
242 red.append(
243 rgb_black[0] + i * (rgb_mid[0] - rgb_black[0]) // len(range_map1)
244 )
245 green.append(
246 rgb_black[1] + i * (rgb_mid[1] - rgb_black[1]) // len(range_map1)
247 )
248 blue.append(
249 rgb_black[2] + i * (rgb_mid[2] - rgb_black[2]) // len(range_map1)
250 )
251 for i in range_map2:
252 red.append(rgb_mid[0] + i * (rgb_white[0] - rgb_mid[0]) // len(range_map2))
253 green.append(
254 rgb_mid[1] + i * (rgb_white[1] - rgb_mid[1]) // len(range_map2)
255 )
256 blue.append(rgb_mid[2] + i * (rgb_white[2] - rgb_mid[2]) // len(range_map2))
258 # Create the high-end values
259 for i in range(256 - whitepoint):
260 red.append(rgb_white[0])
261 green.append(rgb_white[1])
262 blue.append(rgb_white[2])
264 # Return converted image
265 image = image.convert("RGB")
266 return _lut(image, red + green + blue)
269def contain(
270 image: Image.Image, size: tuple[int, int], method: int = Image.Resampling.BICUBIC
271) -> Image.Image:
272 """
273 Returns a resized version of the image, set to the maximum width and height
274 within the requested size, while maintaining the original aspect ratio.
276 :param image: The image to resize.
277 :param size: The requested output size in pixels, given as a
278 (width, height) tuple.
279 :param method: Resampling method to use. Default is
280 :py:attr:`~PIL.Image.Resampling.BICUBIC`.
281 See :ref:`concept-filters`.
282 :return: An image.
283 """
285 im_ratio = image.width / image.height
286 dest_ratio = size[0] / size[1]
288 if im_ratio != dest_ratio:
289 if im_ratio > dest_ratio:
290 new_height = round(image.height / image.width * size[0])
291 if new_height != size[1]:
292 size = (size[0], new_height)
293 else:
294 new_width = round(image.width / image.height * size[1])
295 if new_width != size[0]:
296 size = (new_width, size[1])
297 return image.resize(size, resample=method)
300def cover(
301 image: Image.Image, size: tuple[int, int], method: int = Image.Resampling.BICUBIC
302) -> Image.Image:
303 """
304 Returns a resized version of the image, so that the requested size is
305 covered, while maintaining the original aspect ratio.
307 :param image: The image to resize.
308 :param size: The requested output size in pixels, given as a
309 (width, height) tuple.
310 :param method: Resampling method to use. Default is
311 :py:attr:`~PIL.Image.Resampling.BICUBIC`.
312 See :ref:`concept-filters`.
313 :return: An image.
314 """
316 im_ratio = image.width / image.height
317 dest_ratio = size[0] / size[1]
319 if im_ratio != dest_ratio:
320 if im_ratio < dest_ratio:
321 new_height = round(image.height / image.width * size[0])
322 if new_height != size[1]:
323 size = (size[0], new_height)
324 else:
325 new_width = round(image.width / image.height * size[1])
326 if new_width != size[0]:
327 size = (new_width, size[1])
328 return image.resize(size, resample=method)
331def pad(
332 image: Image.Image,
333 size: tuple[int, int],
334 method: int = Image.Resampling.BICUBIC,
335 color: str | int | tuple[int, ...] | None = None,
336 centering: tuple[float, float] = (0.5, 0.5),
337) -> Image.Image:
338 """
339 Returns a resized and padded version of the image, expanded to fill the
340 requested aspect ratio and size.
342 :param image: The image to resize and crop.
343 :param size: The requested output size in pixels, given as a
344 (width, height) tuple.
345 :param method: Resampling method to use. Default is
346 :py:attr:`~PIL.Image.Resampling.BICUBIC`.
347 See :ref:`concept-filters`.
348 :param color: The background color of the padded image.
349 :param centering: Control the position of the original image within the
350 padded version.
352 (0.5, 0.5) will keep the image centered
353 (0, 0) will keep the image aligned to the top left
354 (1, 1) will keep the image aligned to the bottom
355 right
356 :return: An image.
357 """
359 resized = contain(image, size, method)
360 if resized.size == size:
361 out = resized
362 else:
363 out = Image.new(image.mode, size, color)
364 if resized.palette:
365 palette = resized.getpalette()
366 if palette is not None:
367 out.putpalette(palette)
368 if resized.width != size[0]:
369 x = round((size[0] - resized.width) * max(0, min(centering[0], 1)))
370 out.paste(resized, (x, 0))
371 else:
372 y = round((size[1] - resized.height) * max(0, min(centering[1], 1)))
373 out.paste(resized, (0, y))
374 return out
377def crop(image: Image.Image, border: int = 0) -> Image.Image:
378 """
379 Remove border from image. The same amount of pixels are removed
380 from all four sides. This function works on all image modes.
382 .. seealso:: :py:meth:`~PIL.Image.Image.crop`
384 :param image: The image to crop.
385 :param border: The number of pixels to remove.
386 :return: An image.
387 """
388 left, top, right, bottom = _border(border)
389 return image.crop((left, top, image.size[0] - right, image.size[1] - bottom))
392def scale(
393 image: Image.Image, factor: float, resample: int = Image.Resampling.BICUBIC
394) -> Image.Image:
395 """
396 Returns a rescaled image by a specific factor given in parameter.
397 A factor greater than 1 expands the image, between 0 and 1 contracts the
398 image.
400 :param image: The image to rescale.
401 :param factor: The expansion factor, as a float.
402 :param resample: Resampling method to use. Default is
403 :py:attr:`~PIL.Image.Resampling.BICUBIC`.
404 See :ref:`concept-filters`.
405 :returns: An :py:class:`~PIL.Image.Image` object.
406 """
407 if factor == 1:
408 return image.copy()
409 elif factor <= 0:
410 msg = "the factor must be greater than 0"
411 raise ValueError(msg)
412 else:
413 size = (round(factor * image.width), round(factor * image.height))
414 return image.resize(size, resample)
417class SupportsGetMesh(Protocol):
418 """
419 An object that supports the ``getmesh`` method, taking an image as an
420 argument, and returning a list of tuples. Each tuple contains two tuples,
421 the source box as a tuple of 4 integers, and a tuple of 8 integers for the
422 final quadrilateral, in order of top left, bottom left, bottom right, top
423 right.
424 """
426 def getmesh(
427 self, image: Image.Image
428 ) -> list[
429 tuple[tuple[int, int, int, int], tuple[int, int, int, int, int, int, int, int]]
430 ]: ...
433def deform(
434 image: Image.Image,
435 deformer: SupportsGetMesh,
436 resample: int = Image.Resampling.BILINEAR,
437) -> Image.Image:
438 """
439 Deform the image.
441 :param image: The image to deform.
442 :param deformer: A deformer object. Any object that implements a
443 ``getmesh`` method can be used.
444 :param resample: An optional resampling filter. Same values possible as
445 in the PIL.Image.transform function.
446 :return: An image.
447 """
448 return image.transform(
449 image.size, Image.Transform.MESH, deformer.getmesh(image), resample
450 )
453def equalize(image: Image.Image, mask: Image.Image | None = None) -> Image.Image:
454 """
455 Equalize the image histogram. This function applies a non-linear
456 mapping to the input image, in order to create a uniform
457 distribution of grayscale values in the output image.
459 :param image: The image to equalize.
460 :param mask: An optional mask. If given, only the pixels selected by
461 the mask are included in the analysis.
462 :return: An image.
463 """
464 if image.mode == "P":
465 image = image.convert("RGB")
466 h = image.histogram(mask)
467 lut = []
468 for b in range(0, len(h), 256):
469 histo = [_f for _f in h[b : b + 256] if _f]
470 if len(histo) <= 1:
471 lut.extend(list(range(256)))
472 else:
473 step = (functools.reduce(operator.add, histo) - histo[-1]) // 255
474 if not step:
475 lut.extend(list(range(256)))
476 else:
477 n = step // 2
478 for i in range(256):
479 lut.append(n // step)
480 n = n + h[i + b]
481 return _lut(image, lut)
484def expand(
485 image: Image.Image,
486 border: int | tuple[int, ...] = 0,
487 fill: str | int | tuple[int, ...] = 0,
488) -> Image.Image:
489 """
490 Add border to the image
492 :param image: The image to expand.
493 :param border: Border width, in pixels.
494 :param fill: Pixel fill value (a color value). Default is 0 (black).
495 :return: An image.
496 """
497 left, top, right, bottom = _border(border)
498 width = left + image.size[0] + right
499 height = top + image.size[1] + bottom
500 color = _color(fill, image.mode)
501 if image.palette:
502 palette = ImagePalette.ImagePalette(palette=image.getpalette())
503 if isinstance(color, tuple) and (len(color) == 3 or len(color) == 4):
504 color = palette.getcolor(color)
505 else:
506 palette = None
507 out = Image.new(image.mode, (width, height), color)
508 if palette:
509 out.putpalette(palette.palette)
510 out.paste(image, (left, top))
511 return out
514def fit(
515 image: Image.Image,
516 size: tuple[int, int],
517 method: int = Image.Resampling.BICUBIC,
518 bleed: float = 0.0,
519 centering: tuple[float, float] = (0.5, 0.5),
520) -> Image.Image:
521 """
522 Returns a resized and cropped version of the image, cropped to the
523 requested aspect ratio and size.
525 This function was contributed by Kevin Cazabon.
527 :param image: The image to resize and crop.
528 :param size: The requested output size in pixels, given as a
529 (width, height) tuple.
530 :param method: Resampling method to use. Default is
531 :py:attr:`~PIL.Image.Resampling.BICUBIC`.
532 See :ref:`concept-filters`.
533 :param bleed: Remove a border around the outside of the image from all
534 four edges. The value is a decimal percentage (use 0.01 for
535 one percent). The default value is 0 (no border).
536 Cannot be greater than or equal to 0.5.
537 :param centering: Control the cropping position. Use (0.5, 0.5) for
538 center cropping (e.g. if cropping the width, take 50% off
539 of the left side, and therefore 50% off the right side).
540 (0.0, 0.0) will crop from the top left corner (i.e. if
541 cropping the width, take all of the crop off of the right
542 side, and if cropping the height, take all of it off the
543 bottom). (1.0, 0.0) will crop from the bottom left
544 corner, etc. (i.e. if cropping the width, take all of the
545 crop off the left side, and if cropping the height take
546 none from the top, and therefore all off the bottom).
547 :return: An image.
548 """
550 # by Kevin Cazabon, Feb 17/2000
551 # kevin@cazabon.com
552 # https://www.cazabon.com
554 centering_x, centering_y = centering
556 if not 0.0 <= centering_x <= 1.0:
557 centering_x = 0.5
558 if not 0.0 <= centering_y <= 1.0:
559 centering_y = 0.5
561 if not 0.0 <= bleed < 0.5:
562 bleed = 0.0
564 # calculate the area to use for resizing and cropping, subtracting
565 # the 'bleed' around the edges
567 # number of pixels to trim off on Top and Bottom, Left and Right
568 bleed_pixels = (bleed * image.size[0], bleed * image.size[1])
570 live_size = (
571 image.size[0] - bleed_pixels[0] * 2,
572 image.size[1] - bleed_pixels[1] * 2,
573 )
575 # calculate the aspect ratio of the live_size
576 live_size_ratio = live_size[0] / live_size[1]
578 # calculate the aspect ratio of the output image
579 output_ratio = size[0] / size[1]
581 # figure out if the sides or top/bottom will be cropped off
582 if live_size_ratio == output_ratio:
583 # live_size is already the needed ratio
584 crop_width = live_size[0]
585 crop_height = live_size[1]
586 elif live_size_ratio >= output_ratio:
587 # live_size is wider than what's needed, crop the sides
588 crop_width = output_ratio * live_size[1]
589 crop_height = live_size[1]
590 else:
591 # live_size is taller than what's needed, crop the top and bottom
592 crop_width = live_size[0]
593 crop_height = live_size[0] / output_ratio
595 # make the crop
596 crop_left = bleed_pixels[0] + (live_size[0] - crop_width) * centering_x
597 crop_top = bleed_pixels[1] + (live_size[1] - crop_height) * centering_y
599 crop = (crop_left, crop_top, crop_left + crop_width, crop_top + crop_height)
601 # resize the image and return it
602 return image.resize(size, method, box=crop)
605def flip(image: Image.Image) -> Image.Image:
606 """
607 Flip the image vertically (top to bottom).
609 :param image: The image to flip.
610 :return: An image.
611 """
612 return image.transpose(Image.Transpose.FLIP_TOP_BOTTOM)
615def grayscale(image: Image.Image) -> Image.Image:
616 """
617 Convert the image to grayscale.
619 :param image: The image to convert.
620 :return: An image.
621 """
622 return image.convert("L")
625def invert(image: Image.Image) -> Image.Image:
626 """
627 Invert (negate) the image.
629 :param image: The image to invert.
630 :return: An image.
631 """
632 lut = list(range(255, -1, -1))
633 return image.point(lut) if image.mode == "1" else _lut(image, lut)
636def mirror(image: Image.Image) -> Image.Image:
637 """
638 Flip image horizontally (left to right).
640 :param image: The image to mirror.
641 :return: An image.
642 """
643 return image.transpose(Image.Transpose.FLIP_LEFT_RIGHT)
646def posterize(image: Image.Image, bits: int) -> Image.Image:
647 """
648 Reduce the number of bits for each color channel.
650 :param image: The image to posterize.
651 :param bits: The number of bits to keep for each channel (1-8).
652 :return: An image.
653 """
654 mask = ~(2 ** (8 - bits) - 1)
655 lut = [i & mask for i in range(256)]
656 return _lut(image, lut)
659def solarize(image: Image.Image, threshold: int = 128) -> Image.Image:
660 """
661 Invert all pixel values above a threshold.
663 :param image: The image to solarize.
664 :param threshold: All pixels above this grayscale level are inverted.
665 :return: An image.
666 """
667 lut = []
668 for i in range(256):
669 if i < threshold:
670 lut.append(i)
671 else:
672 lut.append(255 - i)
673 return _lut(image, lut)
676@overload
677def exif_transpose(image: Image.Image, *, in_place: Literal[True]) -> None: ...
680@overload
681def exif_transpose(
682 image: Image.Image, *, in_place: Literal[False] = False
683) -> Image.Image: ...
686def exif_transpose(image: Image.Image, *, in_place: bool = False) -> Image.Image | None:
687 """
688 If an image has an EXIF Orientation tag, other than 1, transpose the image
689 accordingly, and remove the orientation data.
691 :param image: The image to transpose.
692 :param in_place: Boolean. Keyword-only argument.
693 If ``True``, the original image is modified in-place, and ``None`` is returned.
694 If ``False`` (default), a new :py:class:`~PIL.Image.Image` object is returned
695 with the transposition applied. If there is no transposition, a copy of the
696 image will be returned.
697 """
698 image.load()
699 image_exif = image.getexif()
700 orientation = image_exif.get(ExifTags.Base.Orientation, 1)
701 method = {
702 2: Image.Transpose.FLIP_LEFT_RIGHT,
703 3: Image.Transpose.ROTATE_180,
704 4: Image.Transpose.FLIP_TOP_BOTTOM,
705 5: Image.Transpose.TRANSPOSE,
706 6: Image.Transpose.ROTATE_270,
707 7: Image.Transpose.TRANSVERSE,
708 8: Image.Transpose.ROTATE_90,
709 }.get(orientation)
710 if method is not None:
711 if in_place:
712 image.im = image.im.transpose(method)
713 image._size = image.im.size
714 else:
715 transposed_image = image.transpose(method)
716 exif_image = image if in_place else transposed_image
718 exif = exif_image.getexif()
719 if ExifTags.Base.Orientation in exif:
720 del exif[ExifTags.Base.Orientation]
721 if "exif" in exif_image.info:
722 exif_image.info["exif"] = exif.tobytes()
723 elif "Raw profile type exif" in exif_image.info:
724 exif_image.info["Raw profile type exif"] = exif.tobytes().hex()
725 for key in ("XML:com.adobe.xmp", "xmp"):
726 if key in exif_image.info:
727 for pattern in (
728 r'tiff:Orientation="([0-9])"',
729 r"<tiff:Orientation>([0-9])</tiff:Orientation>",
730 ):
731 value = exif_image.info[key]
732 if isinstance(value, str):
733 value = re.sub(pattern, "", value)
734 elif isinstance(value, tuple):
735 value = tuple(
736 re.sub(pattern.encode(), b"", v) for v in value
737 )
738 else:
739 value = re.sub(pattern.encode(), b"", value)
740 exif_image.info[key] = value
741 if not in_place:
742 return transposed_image
743 elif not in_place:
744 return image.copy()
745 return None