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
20
21import functools
22import operator
23import re
24from collections.abc import Sequence
25from typing import Protocol, cast
26
27from . import ExifTags, Image, ImagePalette
28
29#
30# helpers
31
32
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
42
43
44def _color(color: str | int | tuple[int, ...], mode: str) -> int | tuple[int, ...]:
45 if isinstance(color, str):
46 from . import ImageColor
47
48 color = ImageColor.getcolor(color, mode)
49 return color
50
51
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)
64
65
66#
67# actions
68
69
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).
83
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.
93
94 .. versionadded:: 8.2.0
95
96 :return: An image.
97 """
98 if preserve_tone:
99 histogram = image.convert("L").histogram(mask)
100 else:
101 histogram = image.histogram(mask)
102
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)
164
165
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).
187
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 """
197
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
204
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
209
210 # Empty lists for the mapping
211 red = []
212 green = []
213 blue = []
214
215 # Create the low-end values
216 for i in range(0, blackpoint):
217 red.append(rgb_black[0])
218 green.append(rgb_black[1])
219 blue.append(rgb_black[2])
220
221 # Create the mapping (2-color)
222 if rgb_mid is None:
223 range_map = range(0, whitepoint - blackpoint)
224
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 )
235
236 # Create the mapping (3-color)
237 else:
238 range_map1 = range(0, midpoint - blackpoint)
239 range_map2 = range(0, whitepoint - midpoint)
240
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))
257
258 # Create the high-end values
259 for i in range(0, 256 - whitepoint):
260 red.append(rgb_white[0])
261 green.append(rgb_white[1])
262 blue.append(rgb_white[2])
263
264 # Return converted image
265 image = image.convert("RGB")
266 return _lut(image, red + green + blue)
267
268
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.
275
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 """
284
285 im_ratio = image.width / image.height
286 dest_ratio = size[0] / size[1]
287
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)
298
299
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.
306
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 """
315
316 im_ratio = image.width / image.height
317 dest_ratio = size[0] / size[1]
318
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)
329
330
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.
341
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.
351
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 """
358
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
375
376
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.
381
382 .. seealso:: :py:meth:`~PIL.Image.Image.crop`
383
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))
390
391
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.
399
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)
415
416
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 """
425
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 ]: ...
431
432
433def deform(
434 image: Image.Image,
435 deformer: SupportsGetMesh,
436 resample: int = Image.Resampling.BILINEAR,
437) -> Image.Image:
438 """
439 Deform the image.
440
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 )
451
452
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.
458
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)
482
483
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
491
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
512
513
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.
524
525 This function was contributed by Kevin Cazabon.
526
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 """
549
550 # by Kevin Cazabon, Feb 17/2000
551 # kevin@cazabon.com
552 # https://www.cazabon.com
553
554 centering_x, centering_y = centering
555
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
560
561 if not 0.0 <= bleed < 0.5:
562 bleed = 0.0
563
564 # calculate the area to use for resizing and cropping, subtracting
565 # the 'bleed' around the edges
566
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])
569
570 live_size = (
571 image.size[0] - bleed_pixels[0] * 2,
572 image.size[1] - bleed_pixels[1] * 2,
573 )
574
575 # calculate the aspect ratio of the live_size
576 live_size_ratio = live_size[0] / live_size[1]
577
578 # calculate the aspect ratio of the output image
579 output_ratio = size[0] / size[1]
580
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
594
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
598
599 crop = (crop_left, crop_top, crop_left + crop_width, crop_top + crop_height)
600
601 # resize the image and return it
602 return image.resize(size, method, box=crop)
603
604
605def flip(image: Image.Image) -> Image.Image:
606 """
607 Flip the image vertically (top to bottom).
608
609 :param image: The image to flip.
610 :return: An image.
611 """
612 return image.transpose(Image.Transpose.FLIP_TOP_BOTTOM)
613
614
615def grayscale(image: Image.Image) -> Image.Image:
616 """
617 Convert the image to grayscale.
618
619 :param image: The image to convert.
620 :return: An image.
621 """
622 return image.convert("L")
623
624
625def invert(image: Image.Image) -> Image.Image:
626 """
627 Invert (negate) the image.
628
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)
634
635
636def mirror(image: Image.Image) -> Image.Image:
637 """
638 Flip image horizontally (left to right).
639
640 :param image: The image to mirror.
641 :return: An image.
642 """
643 return image.transpose(Image.Transpose.FLIP_LEFT_RIGHT)
644
645
646def posterize(image: Image.Image, bits: int) -> Image.Image:
647 """
648 Reduce the number of bits for each color channel.
649
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)
657
658
659def solarize(image: Image.Image, threshold: int = 128) -> Image.Image:
660 """
661 Invert all pixel values above a threshold.
662
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)
674
675
676def exif_transpose(image: Image.Image, *, in_place: bool = False) -> Image.Image | None:
677 """
678 If an image has an EXIF Orientation tag, other than 1, transpose the image
679 accordingly, and remove the orientation data.
680
681 :param image: The image to transpose.
682 :param in_place: Boolean. Keyword-only argument.
683 If ``True``, the original image is modified in-place, and ``None`` is returned.
684 If ``False`` (default), a new :py:class:`~PIL.Image.Image` object is returned
685 with the transposition applied. If there is no transposition, a copy of the
686 image will be returned.
687 """
688 image.load()
689 image_exif = image.getexif()
690 orientation = image_exif.get(ExifTags.Base.Orientation, 1)
691 method = {
692 2: Image.Transpose.FLIP_LEFT_RIGHT,
693 3: Image.Transpose.ROTATE_180,
694 4: Image.Transpose.FLIP_TOP_BOTTOM,
695 5: Image.Transpose.TRANSPOSE,
696 6: Image.Transpose.ROTATE_270,
697 7: Image.Transpose.TRANSVERSE,
698 8: Image.Transpose.ROTATE_90,
699 }.get(orientation)
700 if method is not None:
701 transposed_image = image.transpose(method)
702 if in_place:
703 image.im = transposed_image.im
704 image._size = transposed_image._size
705 exif_image = image if in_place else transposed_image
706
707 exif = exif_image.getexif()
708 if ExifTags.Base.Orientation in exif:
709 del exif[ExifTags.Base.Orientation]
710 if "exif" in exif_image.info:
711 exif_image.info["exif"] = exif.tobytes()
712 elif "Raw profile type exif" in exif_image.info:
713 exif_image.info["Raw profile type exif"] = exif.tobytes().hex()
714 for key in ("XML:com.adobe.xmp", "xmp"):
715 if key in exif_image.info:
716 for pattern in (
717 r'tiff:Orientation="([0-9])"',
718 r"<tiff:Orientation>([0-9])</tiff:Orientation>",
719 ):
720 value = exif_image.info[key]
721 exif_image.info[key] = (
722 re.sub(pattern, "", value)
723 if isinstance(value, str)
724 else re.sub(pattern.encode(), b"", value)
725 )
726 if not in_place:
727 return transposed_image
728 elif not in_place:
729 return image.copy()
730 return None