CoolFace
Apppublic

Aluode/PerceptionLabPortable

sourceHugging Faceupdated 9mo agoView on Hugging Face
0likes
ImageOps.py747 linesDownload Raw Back to PIL
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 Literal, Protocol, cast, overload
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(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(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(midpoint - blackpoint)
239        range_map2 = range(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(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        mode = image.palette.mode
503        palette = ImagePalette.ImagePalette(mode, image.getpalette(mode))
504        if isinstance(color, tuple) and (len(color) == 3 or len(color) == 4):
505            color = palette.getcolor(color)
506    else:
507        palette = None
508    out = Image.new(image.mode, (width, height), color)
509    if palette:
510        out.putpalette(palette.palette, mode)
511    out.paste(image, (left, top))
512    return out
513
514
515def fit(
516    image: Image.Image,
517    size: tuple[int, int],
518    method: int = Image.Resampling.BICUBIC,
519    bleed: float = 0.0,
520    centering: tuple[float, float] = (0.5, 0.5),
521) -> Image.Image:
522    """
523    Returns a resized and cropped version of the image, cropped to the
524    requested aspect ratio and size.
525
526    This function was contributed by Kevin Cazabon.
527
528    :param image: The image to resize and crop.
529    :param size: The requested output size in pixels, given as a
530                 (width, height) tuple.
531    :param method: Resampling method to use. Default is
532                   :py:attr:`~PIL.Image.Resampling.BICUBIC`.
533                   See :ref:`concept-filters`.
534    :param bleed: Remove a border around the outside of the image from all
535                  four edges. The value is a decimal percentage (use 0.01 for
536                  one percent). The default value is 0 (no border).
537                  Cannot be greater than or equal to 0.5.
538    :param centering: Control the cropping position.  Use (0.5, 0.5) for
539                      center cropping (e.g. if cropping the width, take 50% off
540                      of the left side, and therefore 50% off the right side).
541                      (0.0, 0.0) will crop from the top left corner (i.e. if
542                      cropping the width, take all of the crop off of the right
543                      side, and if cropping the height, take all of it off the
544                      bottom).  (1.0, 0.0) will crop from the bottom left
545                      corner, etc. (i.e. if cropping the width, take all of the
546                      crop off the left side, and if cropping the height take
547                      none from the top, and therefore all off the bottom).
548    :return: An image.
549    """
550
551    # by Kevin Cazabon, Feb 17/2000
552    # kevin@cazabon.com
553    # https://www.cazabon.com
554
555    centering_x, centering_y = centering
556
557    if not 0.0 <= centering_x <= 1.0:
558        centering_x = 0.5
559    if not 0.0 <= centering_y <= 1.0:
560        centering_y = 0.5
561
562    if not 0.0 <= bleed < 0.5:
563        bleed = 0.0
564
565    # calculate the area to use for resizing and cropping, subtracting
566    # the 'bleed' around the edges
567
568    # number of pixels to trim off on Top and Bottom, Left and Right
569    bleed_pixels = (bleed * image.size[0], bleed * image.size[1])
570
571    live_size = (
572        image.size[0] - bleed_pixels[0] * 2,
573        image.size[1] - bleed_pixels[1] * 2,
574    )
575
576    # calculate the aspect ratio of the live_size
577    live_size_ratio = live_size[0] / live_size[1]
578
579    # calculate the aspect ratio of the output image
580    output_ratio = size[0] / size[1]
581
582    # figure out if the sides or top/bottom will be cropped off
583    if live_size_ratio == output_ratio:
584        # live_size is already the needed ratio
585        crop_width = live_size[0]
586        crop_height = live_size[1]
587    elif live_size_ratio >= output_ratio:
588        # live_size is wider than what's needed, crop the sides
589        crop_width = output_ratio * live_size[1]
590        crop_height = live_size[1]
591    else:
592        # live_size is taller than what's needed, crop the top and bottom
593        crop_width = live_size[0]
594        crop_height = live_size[0] / output_ratio
595
596    # make the crop
597    crop_left = bleed_pixels[0] + (live_size[0] - crop_width) * centering_x
598    crop_top = bleed_pixels[1] + (live_size[1] - crop_height) * centering_y
599
600    crop = (crop_left, crop_top, crop_left + crop_width, crop_top + crop_height)
601
602    # resize the image and return it
603    return image.resize(size, method, box=crop)
604
605
606def flip(image: Image.Image) -> Image.Image:
607    """
608    Flip the image vertically (top to bottom).
609
610    :param image: The image to flip.
611    :return: An image.
612    """
613    return image.transpose(Image.Transpose.FLIP_TOP_BOTTOM)
614
615
616def grayscale(image: Image.Image) -> Image.Image:
617    """
618    Convert the image to grayscale.
619
620    :param image: The image to convert.
621    :return: An image.
622    """
623    return image.convert("L")
624
625
626def invert(image: Image.Image) -> Image.Image:
627    """
628    Invert (negate) the image.
629
630    :param image: The image to invert.
631    :return: An image.
632    """
633    lut = list(range(255, -1, -1))
634    return image.point(lut) if image.mode == "1" else _lut(image, lut)
635
636
637def mirror(image: Image.Image) -> Image.Image:
638    """
639    Flip image horizontally (left to right).
640
641    :param image: The image to mirror.
642    :return: An image.
643    """
644    return image.transpose(Image.Transpose.FLIP_LEFT_RIGHT)
645
646
647def posterize(image: Image.Image, bits: int) -> Image.Image:
648    """
649    Reduce the number of bits for each color channel.
650
651    :param image: The image to posterize.
652    :param bits: The number of bits to keep for each channel (1-8).
653    :return: An image.
654    """
655    mask = ~(2 ** (8 - bits) - 1)
656    lut = [i & mask for i in range(256)]
657    return _lut(image, lut)
658
659
660def solarize(image: Image.Image, threshold: int = 128) -> Image.Image:
661    """
662    Invert all pixel values above a threshold.
663
664    :param image: The image to solarize.
665    :param threshold: All pixels above this grayscale level are inverted.
666    :return: An image.
667    """
668    lut = []
669    for i in range(256):
670        if i < threshold:
671            lut.append(i)
672        else:
673            lut.append(255 - i)
674    return _lut(image, lut)
675
676
677@overload
678def exif_transpose(image: Image.Image, *, in_place: Literal[True]) -> None: ...
679
680
681@overload
682def exif_transpose(
683    image: Image.Image, *, in_place: Literal[False] = False
684) -> Image.Image: ...
685
686
687def exif_transpose(image: Image.Image, *, in_place: bool = False) -> Image.Image | None:
688    """
689    If an image has an EXIF Orientation tag, other than 1, transpose the image
690    accordingly, and remove the orientation data.
691
692    :param image: The image to transpose.
693    :param in_place: Boolean. Keyword-only argument.
694        If ``True``, the original image is modified in-place, and ``None`` is returned.
695        If ``False`` (default), a new :py:class:`~PIL.Image.Image` object is returned
696        with the transposition applied. If there is no transposition, a copy of the
697        image will be returned.
698    """
699    image.load()
700    image_exif = image.getexif()
701    orientation = image_exif.get(ExifTags.Base.Orientation, 1)
702    method = {
703        2: Image.Transpose.FLIP_LEFT_RIGHT,
704        3: Image.Transpose.ROTATE_180,
705        4: Image.Transpose.FLIP_TOP_BOTTOM,
706        5: Image.Transpose.TRANSPOSE,
707        6: Image.Transpose.ROTATE_270,
708        7: Image.Transpose.TRANSVERSE,
709        8: Image.Transpose.ROTATE_90,
710    }.get(orientation)
711    if method is not None:
712        if in_place:
713            image.im = image.im.transpose(method)
714            image._size = image.im.size
715        else:
716            transposed_image = image.transpose(method)
717        exif_image = image if in_place else transposed_image
718
719        exif = exif_image.getexif()
720        if ExifTags.Base.Orientation in exif:
721            del exif[ExifTags.Base.Orientation]
722            if "exif" in exif_image.info:
723                exif_image.info["exif"] = exif.tobytes()
724            elif "Raw profile type exif" in exif_image.info:
725                exif_image.info["Raw profile type exif"] = exif.tobytes().hex()
726            for key in ("XML:com.adobe.xmp", "xmp"):
727                if key in exif_image.info:
728                    for pattern in (
729                        r'tiff:Orientation="([0-9])"',
730                        r"<tiff:Orientation>([0-9])</tiff:Orientation>",
731                    ):
732                        value = exif_image.info[key]
733                        if isinstance(value, str):
734                            value = re.sub(pattern, "", value)
735                        elif isinstance(value, tuple):
736                            value = tuple(
737                                re.sub(pattern.encode(), b"", v) for v in value
738                            )
739                        else:
740                            value = re.sub(pattern.encode(), b"", value)
741                        exif_image.info[key] = value
742        if not in_place:
743            return transposed_image
744    elif not in_place:
745        return image.copy()
746    return None
747