Coverage for /var/srv/projects/api.amasfac.comuna18.com/tmp/venv/lib/python3.9/site-packages/PIL/ImageOps.py: 6%
251 statements
« prev ^ index » next coverage.py v6.4.4, created at 2023-07-17 14:22 -0600
« prev ^ index » next coverage.py v6.4.4, created at 2023-07-17 14:22 -0600
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#
20import functools
21import operator
22import re
24from . import Image
26#
27# helpers
30def _border(border):
31 if isinstance(border, tuple):
32 if len(border) == 2:
33 left, top = right, bottom = border
34 elif len(border) == 4:
35 left, top, right, bottom = border
36 else:
37 left = top = right = bottom = border
38 return left, top, right, bottom
41def _color(color, mode):
42 if isinstance(color, str):
43 from . import ImageColor
45 color = ImageColor.getcolor(color, mode)
46 return color
49def _lut(image, lut):
50 if image.mode == "P":
51 # FIXME: apply to lookup table, not image data
52 raise NotImplementedError("mode P support coming soon")
53 elif image.mode in ("L", "RGB"):
54 if image.mode == "RGB" and len(lut) == 256:
55 lut = lut + lut + lut
56 return image.point(lut)
57 else:
58 raise OSError("not supported for this image mode")
61#
62# actions
65def autocontrast(image, cutoff=0, ignore=None, mask=None, preserve_tone=False):
66 """
67 Maximize (normalize) image contrast. This function calculates a
68 histogram of the input image (or mask region), removes ``cutoff`` percent of the
69 lightest and darkest pixels from the histogram, and remaps the image
70 so that the darkest pixel becomes black (0), and the lightest
71 becomes white (255).
73 :param image: The image to process.
74 :param cutoff: The percent to cut off from the histogram on the low and
75 high ends. Either a tuple of (low, high), or a single
76 number for both.
77 :param ignore: The background pixel value (use None for no background).
78 :param mask: Histogram used in contrast operation is computed using pixels
79 within the mask. If no mask is given the entire image is used
80 for histogram computation.
81 :param preserve_tone: Preserve image tone in Photoshop-like style autocontrast.
83 .. versionadded:: 8.2.0
85 :return: An image.
86 """
87 if preserve_tone:
88 histogram = image.convert("L").histogram(mask)
89 else:
90 histogram = image.histogram(mask)
92 lut = []
93 for layer in range(0, len(histogram), 256):
94 h = histogram[layer : layer + 256]
95 if ignore is not None:
96 # get rid of outliers
97 try:
98 h[ignore] = 0
99 except TypeError:
100 # assume sequence
101 for ix in ignore:
102 h[ix] = 0
103 if cutoff:
104 # cut off pixels from both ends of the histogram
105 if not isinstance(cutoff, tuple):
106 cutoff = (cutoff, cutoff)
107 # get number of pixels
108 n = 0
109 for ix in range(256):
110 n = n + h[ix]
111 # remove cutoff% pixels from the low end
112 cut = n * cutoff[0] // 100
113 for lo in range(256):
114 if cut > h[lo]:
115 cut = cut - h[lo]
116 h[lo] = 0
117 else:
118 h[lo] -= cut
119 cut = 0
120 if cut <= 0:
121 break
122 # remove cutoff% samples from the high end
123 cut = n * cutoff[1] // 100
124 for hi in range(255, -1, -1):
125 if cut > h[hi]:
126 cut = cut - h[hi]
127 h[hi] = 0
128 else:
129 h[hi] -= cut
130 cut = 0
131 if cut <= 0:
132 break
133 # find lowest/highest samples after preprocessing
134 for lo in range(256):
135 if h[lo]:
136 break
137 for hi in range(255, -1, -1):
138 if h[hi]:
139 break
140 if hi <= lo:
141 # don't bother
142 lut.extend(list(range(256)))
143 else:
144 scale = 255.0 / (hi - lo)
145 offset = -lo * scale
146 for ix in range(256):
147 ix = int(ix * scale + offset)
148 if ix < 0:
149 ix = 0
150 elif ix > 255:
151 ix = 255
152 lut.append(ix)
153 return _lut(image, lut)
156def colorize(image, black, white, mid=None, blackpoint=0, whitepoint=255, midpoint=127):
157 """
158 Colorize grayscale image.
159 This function calculates a color wedge which maps all black pixels in
160 the source image to the first color and all white pixels to the
161 second color. If ``mid`` is specified, it uses three-color mapping.
162 The ``black`` and ``white`` arguments should be RGB tuples or color names;
163 optionally you can use three-color mapping by also specifying ``mid``.
164 Mapping positions for any of the colors can be specified
165 (e.g. ``blackpoint``), where these parameters are the integer
166 value corresponding to where the corresponding color should be mapped.
167 These parameters must have logical order, such that
168 ``blackpoint <= midpoint <= whitepoint`` (if ``mid`` is specified).
170 :param image: The image to colorize.
171 :param black: The color to use for black input pixels.
172 :param white: The color to use for white input pixels.
173 :param mid: The color to use for midtone input pixels.
174 :param blackpoint: an int value [0, 255] for the black mapping.
175 :param whitepoint: an int value [0, 255] for the white mapping.
176 :param midpoint: an int value [0, 255] for the midtone mapping.
177 :return: An image.
178 """
180 # Initial asserts
181 assert image.mode == "L"
182 if mid is None:
183 assert 0 <= blackpoint <= whitepoint <= 255
184 else:
185 assert 0 <= blackpoint <= midpoint <= whitepoint <= 255
187 # Define colors from arguments
188 black = _color(black, "RGB")
189 white = _color(white, "RGB")
190 if mid is not None:
191 mid = _color(mid, "RGB")
193 # Empty lists for the mapping
194 red = []
195 green = []
196 blue = []
198 # Create the low-end values
199 for i in range(0, blackpoint):
200 red.append(black[0])
201 green.append(black[1])
202 blue.append(black[2])
204 # Create the mapping (2-color)
205 if mid is None:
207 range_map = range(0, whitepoint - blackpoint)
209 for i in range_map:
210 red.append(black[0] + i * (white[0] - black[0]) // len(range_map))
211 green.append(black[1] + i * (white[1] - black[1]) // len(range_map))
212 blue.append(black[2] + i * (white[2] - black[2]) // len(range_map))
214 # Create the mapping (3-color)
215 else:
217 range_map1 = range(0, midpoint - blackpoint)
218 range_map2 = range(0, whitepoint - midpoint)
220 for i in range_map1:
221 red.append(black[0] + i * (mid[0] - black[0]) // len(range_map1))
222 green.append(black[1] + i * (mid[1] - black[1]) // len(range_map1))
223 blue.append(black[2] + i * (mid[2] - black[2]) // len(range_map1))
224 for i in range_map2:
225 red.append(mid[0] + i * (white[0] - mid[0]) // len(range_map2))
226 green.append(mid[1] + i * (white[1] - mid[1]) // len(range_map2))
227 blue.append(mid[2] + i * (white[2] - mid[2]) // len(range_map2))
229 # Create the high-end values
230 for i in range(0, 256 - whitepoint):
231 red.append(white[0])
232 green.append(white[1])
233 blue.append(white[2])
235 # Return converted image
236 image = image.convert("RGB")
237 return _lut(image, red + green + blue)
240def contain(image, size, method=Image.Resampling.BICUBIC):
241 """
242 Returns a resized version of the image, set to the maximum width and height
243 within the requested size, while maintaining the original aspect ratio.
245 :param image: The image to resize and crop.
246 :param size: The requested output size in pixels, given as a
247 (width, height) tuple.
248 :param method: Resampling method to use. Default is
249 :py:attr:`PIL.Image.BICUBIC`. See :ref:`concept-filters`.
250 :return: An image.
251 """
253 im_ratio = image.width / image.height
254 dest_ratio = size[0] / size[1]
256 if im_ratio != dest_ratio:
257 if im_ratio > dest_ratio:
258 new_height = int(image.height / image.width * size[0])
259 if new_height != size[1]:
260 size = (size[0], new_height)
261 else:
262 new_width = int(image.width / image.height * size[1])
263 if new_width != size[0]:
264 size = (new_width, size[1])
265 return image.resize(size, resample=method)
268def pad(image, size, method=Image.Resampling.BICUBIC, color=None, centering=(0.5, 0.5)):
269 """
270 Returns a resized and padded version of the image, expanded to fill the
271 requested aspect ratio and size.
273 :param image: The image to resize and crop.
274 :param size: The requested output size in pixels, given as a
275 (width, height) tuple.
276 :param method: Resampling method to use. Default is
277 :py:attr:`PIL.Image.BICUBIC`. See :ref:`concept-filters`.
278 :param color: The background color of the padded image.
279 :param centering: Control the position of the original image within the
280 padded version.
282 (0.5, 0.5) will keep the image centered
283 (0, 0) will keep the image aligned to the top left
284 (1, 1) will keep the image aligned to the bottom
285 right
286 :return: An image.
287 """
289 resized = contain(image, size, method)
290 if resized.size == size:
291 out = resized
292 else:
293 out = Image.new(image.mode, size, color)
294 if resized.width != size[0]:
295 x = int((size[0] - resized.width) * max(0, min(centering[0], 1)))
296 out.paste(resized, (x, 0))
297 else:
298 y = int((size[1] - resized.height) * max(0, min(centering[1], 1)))
299 out.paste(resized, (0, y))
300 return out
303def crop(image, border=0):
304 """
305 Remove border from image. The same amount of pixels are removed
306 from all four sides. This function works on all image modes.
308 .. seealso:: :py:meth:`~PIL.Image.Image.crop`
310 :param image: The image to crop.
311 :param border: The number of pixels to remove.
312 :return: An image.
313 """
314 left, top, right, bottom = _border(border)
315 return image.crop((left, top, image.size[0] - right, image.size[1] - bottom))
318def scale(image, factor, resample=Image.Resampling.BICUBIC):
319 """
320 Returns a rescaled image by a specific factor given in parameter.
321 A factor greater than 1 expands the image, between 0 and 1 contracts the
322 image.
324 :param image: The image to rescale.
325 :param factor: The expansion factor, as a float.
326 :param resample: Resampling method to use. Default is
327 :py:attr:`PIL.Image.BICUBIC`. See :ref:`concept-filters`.
328 :returns: An :py:class:`~PIL.Image.Image` object.
329 """
330 if factor == 1:
331 return image.copy()
332 elif factor <= 0:
333 raise ValueError("the factor must be greater than 0")
334 else:
335 size = (round(factor * image.width), round(factor * image.height))
336 return image.resize(size, resample)
339def deform(image, deformer, resample=Image.Resampling.BILINEAR):
340 """
341 Deform the image.
343 :param image: The image to deform.
344 :param deformer: A deformer object. Any object that implements a
345 ``getmesh`` method can be used.
346 :param resample: An optional resampling filter. Same values possible as
347 in the PIL.Image.transform function.
348 :return: An image.
349 """
350 return image.transform(
351 image.size, Image.Transform.MESH, deformer.getmesh(image), resample
352 )
355def equalize(image, mask=None):
356 """
357 Equalize the image histogram. This function applies a non-linear
358 mapping to the input image, in order to create a uniform
359 distribution of grayscale values in the output image.
361 :param image: The image to equalize.
362 :param mask: An optional mask. If given, only the pixels selected by
363 the mask are included in the analysis.
364 :return: An image.
365 """
366 if image.mode == "P":
367 image = image.convert("RGB")
368 h = image.histogram(mask)
369 lut = []
370 for b in range(0, len(h), 256):
371 histo = [_f for _f in h[b : b + 256] if _f]
372 if len(histo) <= 1:
373 lut.extend(list(range(256)))
374 else:
375 step = (functools.reduce(operator.add, histo) - histo[-1]) // 255
376 if not step:
377 lut.extend(list(range(256)))
378 else:
379 n = step // 2
380 for i in range(256):
381 lut.append(n // step)
382 n = n + h[i + b]
383 return _lut(image, lut)
386def expand(image, border=0, fill=0):
387 """
388 Add border to the image
390 :param image: The image to expand.
391 :param border: Border width, in pixels.
392 :param fill: Pixel fill value (a color value). Default is 0 (black).
393 :return: An image.
394 """
395 left, top, right, bottom = _border(border)
396 width = left + image.size[0] + right
397 height = top + image.size[1] + bottom
398 color = _color(fill, image.mode)
399 if image.mode == "P" and image.palette:
400 image.load()
401 palette = image.palette.copy()
402 if isinstance(color, tuple):
403 color = palette.getcolor(color)
404 else:
405 palette = None
406 out = Image.new(image.mode, (width, height), color)
407 if palette:
408 out.putpalette(palette.palette)
409 out.paste(image, (left, top))
410 return out
413def fit(image, size, method=Image.Resampling.BICUBIC, bleed=0.0, centering=(0.5, 0.5)):
414 """
415 Returns a resized and cropped version of the image, cropped to the
416 requested aspect ratio and size.
418 This function was contributed by Kevin Cazabon.
420 :param image: The image to resize and crop.
421 :param size: The requested output size in pixels, given as a
422 (width, height) tuple.
423 :param method: Resampling method to use. Default is
424 :py:attr:`PIL.Image.BICUBIC`. See :ref:`concept-filters`.
425 :param bleed: Remove a border around the outside of the image from all
426 four edges. The value is a decimal percentage (use 0.01 for
427 one percent). The default value is 0 (no border).
428 Cannot be greater than or equal to 0.5.
429 :param centering: Control the cropping position. Use (0.5, 0.5) for
430 center cropping (e.g. if cropping the width, take 50% off
431 of the left side, and therefore 50% off the right side).
432 (0.0, 0.0) will crop from the top left corner (i.e. if
433 cropping the width, take all of the crop off of the right
434 side, and if cropping the height, take all of it off the
435 bottom). (1.0, 0.0) will crop from the bottom left
436 corner, etc. (i.e. if cropping the width, take all of the
437 crop off the left side, and if cropping the height take
438 none from the top, and therefore all off the bottom).
439 :return: An image.
440 """
442 # by Kevin Cazabon, Feb 17/2000
443 # kevin@cazabon.com
444 # https://www.cazabon.com
446 # ensure centering is mutable
447 centering = list(centering)
449 if not 0.0 <= centering[0] <= 1.0:
450 centering[0] = 0.5
451 if not 0.0 <= centering[1] <= 1.0:
452 centering[1] = 0.5
454 if not 0.0 <= bleed < 0.5:
455 bleed = 0.0
457 # calculate the area to use for resizing and cropping, subtracting
458 # the 'bleed' around the edges
460 # number of pixels to trim off on Top and Bottom, Left and Right
461 bleed_pixels = (bleed * image.size[0], bleed * image.size[1])
463 live_size = (
464 image.size[0] - bleed_pixels[0] * 2,
465 image.size[1] - bleed_pixels[1] * 2,
466 )
468 # calculate the aspect ratio of the live_size
469 live_size_ratio = live_size[0] / live_size[1]
471 # calculate the aspect ratio of the output image
472 output_ratio = size[0] / size[1]
474 # figure out if the sides or top/bottom will be cropped off
475 if live_size_ratio == output_ratio:
476 # live_size is already the needed ratio
477 crop_width = live_size[0]
478 crop_height = live_size[1]
479 elif live_size_ratio >= output_ratio:
480 # live_size is wider than what's needed, crop the sides
481 crop_width = output_ratio * live_size[1]
482 crop_height = live_size[1]
483 else:
484 # live_size is taller than what's needed, crop the top and bottom
485 crop_width = live_size[0]
486 crop_height = live_size[0] / output_ratio
488 # make the crop
489 crop_left = bleed_pixels[0] + (live_size[0] - crop_width) * centering[0]
490 crop_top = bleed_pixels[1] + (live_size[1] - crop_height) * centering[1]
492 crop = (crop_left, crop_top, crop_left + crop_width, crop_top + crop_height)
494 # resize the image and return it
495 return image.resize(size, method, box=crop)
498def flip(image):
499 """
500 Flip the image vertically (top to bottom).
502 :param image: The image to flip.
503 :return: An image.
504 """
505 return image.transpose(Image.Transpose.FLIP_TOP_BOTTOM)
508def grayscale(image):
509 """
510 Convert the image to grayscale.
512 :param image: The image to convert.
513 :return: An image.
514 """
515 return image.convert("L")
518def invert(image):
519 """
520 Invert (negate) the image.
522 :param image: The image to invert.
523 :return: An image.
524 """
525 lut = []
526 for i in range(256):
527 lut.append(255 - i)
528 return image.point(lut) if image.mode == "1" else _lut(image, lut)
531def mirror(image):
532 """
533 Flip image horizontally (left to right).
535 :param image: The image to mirror.
536 :return: An image.
537 """
538 return image.transpose(Image.Transpose.FLIP_LEFT_RIGHT)
541def posterize(image, bits):
542 """
543 Reduce the number of bits for each color channel.
545 :param image: The image to posterize.
546 :param bits: The number of bits to keep for each channel (1-8).
547 :return: An image.
548 """
549 lut = []
550 mask = ~(2 ** (8 - bits) - 1)
551 for i in range(256):
552 lut.append(i & mask)
553 return _lut(image, lut)
556def solarize(image, threshold=128):
557 """
558 Invert all pixel values above a threshold.
560 :param image: The image to solarize.
561 :param threshold: All pixels above this greyscale level are inverted.
562 :return: An image.
563 """
564 lut = []
565 for i in range(256):
566 if i < threshold:
567 lut.append(i)
568 else:
569 lut.append(255 - i)
570 return _lut(image, lut)
573def exif_transpose(image):
574 """
575 If an image has an EXIF Orientation tag, return a new image that is
576 transposed accordingly. Otherwise, return a copy of the image.
578 :param image: The image to transpose.
579 :return: An image.
580 """
581 exif = image.getexif()
582 orientation = exif.get(0x0112)
583 method = {
584 2: Image.Transpose.FLIP_LEFT_RIGHT,
585 3: Image.Transpose.ROTATE_180,
586 4: Image.Transpose.FLIP_TOP_BOTTOM,
587 5: Image.Transpose.TRANSPOSE,
588 6: Image.Transpose.ROTATE_270,
589 7: Image.Transpose.TRANSVERSE,
590 8: Image.Transpose.ROTATE_90,
591 }.get(orientation)
592 if method is not None:
593 transposed_image = image.transpose(method)
594 transposed_exif = transposed_image.getexif()
595 if 0x0112 in transposed_exif:
596 del transposed_exif[0x0112]
597 if "exif" in transposed_image.info:
598 transposed_image.info["exif"] = transposed_exif.tobytes()
599 elif "Raw profile type exif" in transposed_image.info:
600 transposed_image.info[
601 "Raw profile type exif"
602 ] = transposed_exif.tobytes().hex()
603 elif "XML:com.adobe.xmp" in transposed_image.info:
604 transposed_image.info["XML:com.adobe.xmp"] = re.sub(
605 r'tiff:Orientation="([0-9])"',
606 "",
607 transposed_image.info["XML:com.adobe.xmp"],
608 )
609 return transposed_image
610 return image.copy()