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

356 statements  

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 

21__lazy_modules__ = {"re"} 

22 

23import re 

24from typing import Protocol, cast, overload 

25 

26from . import ExifTags, Image, ImageFilter, ImagePalette 

27 

28TYPE_CHECKING = False 

29if TYPE_CHECKING: 

30 from collections.abc import Sequence 

31 from typing import Literal 

32 

33# 

34# helpers 

35 

36 

37def _border(border: int | tuple[int, ...]) -> tuple[int, int, int, int]: 

38 if isinstance(border, tuple): 

39 if len(border) == 2: 

40 left, top = right, bottom = border 

41 elif len(border) == 4: 

42 left, top, right, bottom = border 

43 else: 

44 msg = "border must be an integer, or a tuple of two or four elements" 

45 raise ValueError(msg) 

46 else: 

47 left = top = right = bottom = border 

48 return left, top, right, bottom 

49 

50 

51def _color( 

52 color: str | int | tuple[int, ...] | None, mode: str 

53) -> int | tuple[int, ...] | None: 

54 if isinstance(color, str): 

55 from . import ImageColor 

56 

57 color = ImageColor.getcolor(color, mode) 

58 return color 

59 

60 

61def _lut(image: Image.Image, lut: list[int], palette: bool = False) -> Image.Image: 

62 if image.mode == "P": 

63 if not palette: 

64 # The lookup table was built from the palette indexes, not colors 

65 msg = "mode P support coming soon" 

66 raise NotImplementedError(msg) 

67 

68 # The lookup table can be applied to the palette colors 

69 im = image.copy() 

70 assert im.palette is not None 

71 palette_mode = im.palette.mode 

72 channels = len(palette_mode) 

73 colors = im.getpalette(None) 

74 assert colors is not None 

75 im.putpalette( 

76 [ 

77 v if palette_mode[i % channels] == "A" else lut[v] 

78 for i, v in enumerate(colors) 

79 ], 

80 palette_mode, 

81 ) 

82 return im 

83 elif image.mode in ("L", "RGB"): 

84 if image.mode == "RGB" and len(lut) == 256: 

85 lut = lut + lut + lut 

86 return image.point(lut) 

87 else: 

88 msg = f"not supported for mode {image.mode}" 

89 raise OSError(msg) 

90 

91 

92# 

93# actions 

94 

95 

96def autocontrast( 

97 image: Image.Image, 

98 cutoff: float | tuple[float, float] = 0, 

99 ignore: int | Sequence[int] | None = None, 

100 mask: Image.Image | None = None, 

101 preserve_tone: bool = False, 

102) -> Image.Image: 

103 """ 

104 Maximize (normalize) image contrast. This function calculates a 

105 histogram of the input image (or mask region), removes ``cutoff`` percent of the 

106 lightest and darkest pixels from the histogram, and remaps the image 

107 so that the darkest pixel becomes black (0), and the lightest 

108 becomes white (255). 

109 

110 :param image: The image to process. 

111 :param cutoff: The percent to cut off from the histogram on the low and 

112 high ends. Either a tuple of (low, high), or a single 

113 number for both. 

114 :param ignore: The background pixel value (use None for no background). 

115 :param mask: Histogram used in contrast operation is computed using pixels 

116 within the mask. If no mask is given the entire image is used 

117 for histogram computation. 

118 :param preserve_tone: Preserve image tone in Photoshop-like style autocontrast. 

119 

120 .. versionadded:: 8.2.0 

121 

122 :return: An image. 

123 """ 

124 if ignore is not None: 

125 if isinstance(ignore, int): 

126 ignore = (ignore,) 

127 if not all(0 <= ix <= 255 for ix in ignore): 

128 msg = "ignore must be between or equal to 0 and 255" 

129 raise ValueError(msg) 

130 

131 if preserve_tone: 

132 histogram = image.convert("L").histogram(mask) 

133 else: 

134 histogram = image.histogram(mask) 

135 

136 lut = [] 

137 for layer in range(0, len(histogram), 256): 

138 h = histogram[layer : layer + 256] 

139 if ignore is not None: 

140 # get rid of outliers 

141 for ix in ignore: 

142 h[ix] = 0 

143 if cutoff: 

144 # cut off pixels from both ends of the histogram 

145 if not isinstance(cutoff, tuple): 

146 cutoff = (cutoff, cutoff) 

147 # get number of pixels 

148 n = sum(h) 

149 # remove cutoff% pixels from the low end 

150 cut = int(n * cutoff[0] // 100) 

151 for lo in range(256): 

152 if cut > h[lo]: 

153 cut = cut - h[lo] 

154 h[lo] = 0 

155 else: 

156 h[lo] -= cut 

157 cut = 0 

158 if cut <= 0: 

159 break 

160 # remove cutoff% samples from the high end 

161 cut = int(n * cutoff[1] // 100) 

162 for hi in range(255, -1, -1): 

163 if cut > h[hi]: 

164 cut = cut - h[hi] 

165 h[hi] = 0 

166 else: 

167 h[hi] -= cut 

168 cut = 0 

169 if cut <= 0: 

170 break 

171 # find lowest/highest samples after preprocessing 

172 for lo in range(256): 

173 if h[lo]: 

174 break 

175 for hi in range(255, -1, -1): 

176 if h[hi]: 

177 break 

178 if hi <= lo: 

179 # don't bother 

180 lut.extend(list(range(256))) 

181 else: 

182 for ix in range(256): 

183 ix = (ix - lo) * 255 // (hi - lo) 

184 if ix < 0: 

185 ix = 0 

186 elif ix > 255: 

187 ix = 255 

188 lut.append(ix) 

189 return _lut(image, lut) 

190 

191 

192def colorize( 

193 image: Image.Image, 

194 black: str | tuple[int, ...], 

195 white: str | tuple[int, ...], 

196 mid: str | int | tuple[int, ...] | None = None, 

197 blackpoint: int = 0, 

198 whitepoint: int = 255, 

199 midpoint: int = 127, 

200) -> Image.Image: 

201 """ 

202 Colorize grayscale image. 

203 This function calculates a color wedge which maps all black pixels in 

204 the source image to the first color and all white pixels to the 

205 second color. If ``mid`` is specified, it uses three-color mapping. 

206 The ``black`` and ``white`` arguments should be RGB tuples or color names; 

207 optionally you can use three-color mapping by also specifying ``mid``. 

208 Mapping positions for any of the colors can be specified 

209 (e.g. ``blackpoint``), where these parameters are the integer 

210 value corresponding to where the corresponding color should be mapped. 

211 These parameters must have logical order, such that 

212 ``blackpoint <= midpoint <= whitepoint`` (if ``mid`` is specified). 

213 

214 :param image: The image to colorize. 

215 :param black: The color to use for black input pixels. 

216 :param white: The color to use for white input pixels. 

217 :param mid: The color to use for midtone input pixels. 

218 :param blackpoint: an int value [0, 255] for the black mapping. 

219 :param whitepoint: an int value [0, 255] for the white mapping. 

220 :param midpoint: an int value [0, 255] for the midtone mapping. 

221 :return: An image. 

222 """ 

223 

224 if image.mode != "L": 

225 msg = f"mode must be L, not {image.mode}" 

226 raise ValueError(msg) 

227 if not 0 <= blackpoint <= whitepoint <= 255: 

228 msg = ( 

229 "blackpoint and whitepoint must each be between or equal to 0 and 255, " 

230 "with blackpoint less than or equal to whitepoint" 

231 ) 

232 raise ValueError(msg) 

233 if mid is not None and not blackpoint <= midpoint <= whitepoint: 

234 msg = "midpoint must be between or equal to blackpoint and whitepoint" 

235 raise ValueError(msg) 

236 

237 # Define colors from arguments 

238 rgb_black = cast("Sequence[int]", _color(black, "RGB")) 

239 rgb_white = cast("Sequence[int]", _color(white, "RGB")) 

240 rgb_mid = cast("Sequence[int]", _color(mid, "RGB")) if mid is not None else None 

241 

242 # Empty lists for the mapping 

243 red = [] 

244 green = [] 

245 blue = [] 

246 

247 # Create the low-end values 

248 for i in range(blackpoint): 

249 red.append(rgb_black[0]) 

250 green.append(rgb_black[1]) 

251 blue.append(rgb_black[2]) 

252 

253 # Create the mapping (2-color) 

254 if rgb_mid is None: 

255 range_map = range(whitepoint - blackpoint) 

256 

257 for i in range_map: 

258 red.append( 

259 rgb_black[0] + i * (rgb_white[0] - rgb_black[0]) // len(range_map) 

260 ) 

261 green.append( 

262 rgb_black[1] + i * (rgb_white[1] - rgb_black[1]) // len(range_map) 

263 ) 

264 blue.append( 

265 rgb_black[2] + i * (rgb_white[2] - rgb_black[2]) // len(range_map) 

266 ) 

267 

268 # Create the mapping (3-color) 

269 else: 

270 range_map1 = range(midpoint - blackpoint) 

271 range_map2 = range(whitepoint - midpoint) 

272 

273 for i in range_map1: 

274 red.append( 

275 rgb_black[0] + i * (rgb_mid[0] - rgb_black[0]) // len(range_map1) 

276 ) 

277 green.append( 

278 rgb_black[1] + i * (rgb_mid[1] - rgb_black[1]) // len(range_map1) 

279 ) 

280 blue.append( 

281 rgb_black[2] + i * (rgb_mid[2] - rgb_black[2]) // len(range_map1) 

282 ) 

283 for i in range_map2: 

284 red.append(rgb_mid[0] + i * (rgb_white[0] - rgb_mid[0]) // len(range_map2)) 

285 green.append( 

286 rgb_mid[1] + i * (rgb_white[1] - rgb_mid[1]) // len(range_map2) 

287 ) 

288 blue.append(rgb_mid[2] + i * (rgb_white[2] - rgb_mid[2]) // len(range_map2)) 

289 

290 # Create the high-end values 

291 for i in range(256 - whitepoint): 

292 red.append(rgb_white[0]) 

293 green.append(rgb_white[1]) 

294 blue.append(rgb_white[2]) 

295 

296 # Return converted image 

297 image = image.convert("RGB") 

298 return _lut(image, red + green + blue) 

299 

300 

301def contain( 

302 image: Image.Image, size: tuple[int, int], method: int = Image.Resampling.BICUBIC 

303) -> Image.Image: 

304 """ 

305 Returns a resized version of the image, set to the maximum width and height 

306 within the requested size, while maintaining the original aspect ratio. 

307 

308 :param image: The image to resize. 

309 :param size: The requested output size in pixels, given as a 

310 (width, height) tuple. 

311 :param method: Resampling method to use. Default is 

312 :py:attr:`~PIL.Image.Resampling.BICUBIC`. 

313 See :ref:`concept-filters`. 

314 :return: An image. 

315 """ 

316 

317 if image.height == 0: 

318 return image.crop((0, 0, size[0], 0)) 

319 im_ratio = image.width / image.height 

320 if size[1] == 0: 

321 return image.crop((0, 0, 0, 0)) 

322 dest_ratio = size[0] / size[1] 

323 

324 if im_ratio != dest_ratio: 

325 if im_ratio > dest_ratio: 

326 new_height = round(image.height / image.width * size[0]) 

327 if new_height != size[1]: 

328 size = (size[0], new_height) 

329 else: 

330 new_width = round(image.width / image.height * size[1]) 

331 if new_width != size[0]: 

332 size = (new_width, size[1]) 

333 if min(size) == 0: 

334 return image.crop((0, 0, *size)) 

335 return image.resize(size, resample=method) 

336 

337 

338def cover( 

339 image: Image.Image, size: tuple[int, int], method: int = Image.Resampling.BICUBIC 

340) -> Image.Image: 

341 """ 

342 Returns a resized version of the image, so that the requested size is 

343 covered, while maintaining the original aspect ratio. 

344 

345 :param image: The image to resize. 

346 :param size: The requested output size in pixels, given as a 

347 (width, height) tuple. 

348 :param method: Resampling method to use. Default is 

349 :py:attr:`~PIL.Image.Resampling.BICUBIC`. 

350 See :ref:`concept-filters`. 

351 :return: An image. 

352 """ 

353 

354 if image.width == 0: 

355 return image.crop((0, 0, 0, size[1])) 

356 if image.height == 0: 

357 return image.crop((0, 0, size[0], 0)) 

358 im_ratio = image.width / image.height 

359 if size[1] == 0: 

360 return image.crop((0, 0, 0, 0)) 

361 dest_ratio = size[0] / size[1] 

362 

363 if im_ratio != dest_ratio: 

364 if im_ratio < dest_ratio: 

365 new_height = round(image.height / image.width * size[0]) 

366 if new_height != size[1]: 

367 size = (size[0], new_height) 

368 else: 

369 new_width = round(image.width / image.height * size[1]) 

370 if new_width != size[0]: 

371 size = (new_width, size[1]) 

372 return image.resize(size, resample=method) 

373 

374 

375def _new_with_fill( 

376 image: Image.Image, size: tuple[int, int], fill: str | int | tuple[int, ...] | None 

377) -> Image.Image: 

378 color = _color(fill, image.mode) 

379 if image.palette: 

380 mode = image.palette.mode 

381 palette = ImagePalette.ImagePalette(mode, image.getpalette(mode)) 

382 if isinstance(color, tuple) and len(color) in (3, 4): 

383 if image.mode == "PA": 

384 alpha = color[3] if len(color) == 4 else 255 

385 color = (palette.getcolor(color[:3], image), alpha) 

386 else: 

387 color = palette.getcolor(color, image) 

388 else: 

389 palette = None 

390 out = Image.new(image.mode, size, color) 

391 if palette: 

392 out.putpalette(palette.palette, mode) 

393 if "transparency" in image.info: 

394 out.info["transparency"] = image.info["transparency"] 

395 return out 

396 

397 

398def pad( 

399 image: Image.Image, 

400 size: tuple[int, int], 

401 method: int = Image.Resampling.BICUBIC, 

402 color: str | int | tuple[int, ...] | None = None, 

403 centering: tuple[float, float] = (0.5, 0.5), 

404) -> Image.Image: 

405 """ 

406 Returns a resized and padded version of the image, expanded to fill the 

407 requested aspect ratio and size. 

408 

409 :param image: The image to resize and crop. 

410 :param size: The requested output size in pixels, given as a 

411 (width, height) tuple. 

412 :param method: Resampling method to use. Default is 

413 :py:attr:`~PIL.Image.Resampling.BICUBIC`. 

414 See :ref:`concept-filters`. 

415 :param color: The background color of the padded image. 

416 :param centering: Control the position of the original image within the 

417 padded version. 

418 

419 (0.5, 0.5) will keep the image centered 

420 (0, 0) will keep the image aligned to the top left 

421 (1, 1) will keep the image aligned to the bottom 

422 right 

423 :return: An image. 

424 """ 

425 

426 resized = contain(image, size, method) 

427 if resized.size == size: 

428 out = resized 

429 else: 

430 out = _new_with_fill(resized, size, color) 

431 if resized.width != size[0]: 

432 x = round((size[0] - resized.width) * max(0, min(centering[0], 1))) 

433 out.paste(resized, (x, 0)) 

434 else: 

435 y = round((size[1] - resized.height) * max(0, min(centering[1], 1))) 

436 out.paste(resized, (0, y)) 

437 return out 

438 

439 

440def crop(image: Image.Image, border: int | tuple[int, ...] = 0) -> Image.Image: 

441 """ 

442 Remove border from image. This function works on all image modes. 

443 

444 .. seealso:: :py:meth:`~PIL.Image.Image.crop` 

445 

446 :param image: The image to crop. 

447 :param border: The number of pixels to remove. An integer removes the same 

448 number of pixels from all four sides. A 2-tuple specifies 

449 the number of pixels to remove horizontally and vertically. 

450 A 4-tuple specifies the number of pixels to remove from the 

451 left, top, right and bottom of the image. 

452 :return: An image. 

453 """ 

454 left, top, right, bottom = _border(border) 

455 return image.crop((left, top, image.size[0] - right, image.size[1] - bottom)) 

456 

457 

458def scale( 

459 image: Image.Image, factor: float, resample: int = Image.Resampling.BICUBIC 

460) -> Image.Image: 

461 """ 

462 Returns a rescaled image by a specific factor given in parameter. 

463 A factor greater than 1 expands the image, between 0 and 1 contracts the 

464 image. 

465 

466 :param image: The image to rescale. 

467 :param factor: The expansion factor, as a float. 

468 :param resample: Resampling method to use. Default is 

469 :py:attr:`~PIL.Image.Resampling.BICUBIC`. 

470 See :ref:`concept-filters`. 

471 :returns: An :py:class:`~PIL.Image.Image` object. 

472 """ 

473 if factor == 1: 

474 return image.copy() 

475 elif factor <= 0: 

476 msg = "the factor must be greater than 0" 

477 raise ValueError(msg) 

478 else: 

479 size = (round(factor * image.width), round(factor * image.height)) 

480 if min(size) == 0: 

481 return image.crop((0, 0, *size)) 

482 return image.resize(size, resample) 

483 

484 

485class SupportsGetMesh(Protocol): 

486 """ 

487 An object that supports the ``getmesh`` method, taking an image as an 

488 argument, and returning a list of tuples. Each tuple contains two tuples, 

489 the source box as a tuple of 4 integers, and a tuple of 8 integers for the 

490 final quadrilateral, in order of top left, bottom left, bottom right, top 

491 right. 

492 """ 

493 

494 def getmesh( 

495 self, image: Image.Image 

496 ) -> list[ 

497 tuple[tuple[int, int, int, int], tuple[int, int, int, int, int, int, int, int]] 

498 ]: ... 

499 

500 

501def deform( 

502 image: Image.Image, 

503 deformer: SupportsGetMesh, 

504 resample: int = Image.Resampling.BILINEAR, 

505) -> Image.Image: 

506 """ 

507 Deform the image. 

508 

509 :param image: The image to deform. 

510 :param deformer: A deformer object. Any object that implements a 

511 ``getmesh`` method can be used. 

512 :param resample: An optional resampling filter. Same values possible as 

513 in the PIL.Image.transform function. 

514 :return: An image. 

515 """ 

516 return image.transform( 

517 image.size, Image.Transform.MESH, deformer.getmesh(image), resample 

518 ) 

519 

520 

521def equalize(image: Image.Image, mask: Image.Image | None = None) -> Image.Image: 

522 """ 

523 Equalize the image histogram. This function applies a non-linear 

524 mapping to the input image, in order to create a uniform 

525 distribution of grayscale values in the output image. 

526 

527 :param image: The image to equalize. 

528 :param mask: An optional mask. If given, only the pixels selected by 

529 the mask are included in the analysis. 

530 :return: An image. 

531 """ 

532 if image.mode == "P": 

533 image = image.convert("RGB") 

534 h = image.histogram(mask) 

535 lut = [] 

536 for b in range(0, len(h), 256): 

537 histo = [_f for _f in h[b : b + 256] if _f] 

538 if len(histo) <= 1: 

539 lut.extend(list(range(256))) 

540 else: 

541 step = (sum(histo) - histo[-1]) // 255 

542 if not step: 

543 lut.extend(list(range(256))) 

544 else: 

545 n = step // 2 

546 for i in range(256): 

547 lut.append(n // step) 

548 n = n + h[i + b] 

549 return _lut(image, lut) 

550 

551 

552def expand( 

553 image: Image.Image, 

554 border: int | tuple[int, ...] = 0, 

555 fill: str | int | tuple[int, ...] = 0, 

556) -> Image.Image: 

557 """ 

558 Add border to the image 

559 

560 :param image: The image to expand. 

561 :param border: Border width, in pixels. An integer adds the same width to 

562 all four sides. A 2-tuple specifies the horizontal and 

563 vertical border widths. A 4-tuple specifies the widths of 

564 the left, top, right and bottom borders. 

565 :param fill: Pixel fill value (a color value). Default is 0 (black). 

566 :return: An image. 

567 """ 

568 left, top, right, bottom = _border(border) 

569 width = left + image.size[0] + right 

570 height = top + image.size[1] + bottom 

571 out = _new_with_fill(image, (width, height), fill) 

572 out.paste(image, (left, top)) 

573 return out 

574 

575 

576def fit( 

577 image: Image.Image, 

578 size: tuple[int, int], 

579 method: int = Image.Resampling.BICUBIC, 

580 bleed: float = 0.0, 

581 centering: tuple[float, float] = (0.5, 0.5), 

582) -> Image.Image: 

583 """ 

584 Returns a resized and cropped version of the image, cropped to the 

585 requested aspect ratio and size. 

586 

587 This function was contributed by Kevin Cazabon. 

588 

589 :param image: The image to resize and crop. 

590 :param size: The requested output size in pixels, given as a 

591 (width, height) tuple. 

592 :param method: Resampling method to use. Default is 

593 :py:attr:`~PIL.Image.Resampling.BICUBIC`. 

594 See :ref:`concept-filters`. 

595 :param bleed: Remove a border around the outside of the image from all 

596 four edges. The value is a decimal percentage (use 0.01 for 

597 one percent). The default value is 0 (no border). 

598 Cannot be greater than or equal to 0.5. 

599 :param centering: Control the cropping position. Use (0.5, 0.5) for 

600 center cropping (e.g. if cropping the width, take 50% off 

601 of the left side, and therefore 50% off the right side). 

602 (0.0, 0.0) will crop from the top left corner (i.e. if 

603 cropping the width, take all of the crop off of the right 

604 side, and if cropping the height, take all of it off the 

605 bottom). (1.0, 1.0) will crop from the bottom right 

606 corner, etc. (i.e. if cropping the width, take all of the 

607 crop off the left side, and if cropping the height take 

608 none from the bottom, and therefore all off the top). 

609 :return: An image. 

610 """ 

611 

612 # by Kevin Cazabon, Feb 17/2000 

613 # kevin@cazabon.com 

614 # https://www.cazabon.com 

615 

616 if min(size) == 0: 

617 return image.crop((0, 0, *size)) 

618 centering_x, centering_y = centering 

619 

620 if not 0.0 <= centering_x <= 1.0: 

621 centering_x = 0.5 

622 if not 0.0 <= centering_y <= 1.0: 

623 centering_y = 0.5 

624 

625 if not 0.0 <= bleed < 0.5: 

626 bleed = 0.0 

627 

628 # calculate the area to use for resizing and cropping, subtracting 

629 # the 'bleed' around the edges 

630 

631 # number of pixels to trim off on Top and Bottom, Left and Right 

632 bleed_pixels = (bleed * image.size[0], bleed * image.size[1]) 

633 

634 live_size = ( 

635 image.size[0] - bleed_pixels[0] * 2, 

636 image.size[1] - bleed_pixels[1] * 2, 

637 ) 

638 

639 # calculate the aspect ratio of the live_size 

640 if live_size[1] == 0: 

641 return image.crop((0, 0, *size)) 

642 live_size_ratio = live_size[0] / live_size[1] 

643 

644 # calculate the aspect ratio of the output image 

645 output_ratio = size[0] / size[1] 

646 

647 # figure out if the sides or top/bottom will be cropped off 

648 if live_size_ratio == output_ratio: 

649 # live_size is already the needed ratio 

650 crop_width = live_size[0] 

651 crop_height = live_size[1] 

652 elif live_size_ratio >= output_ratio: 

653 # live_size is wider than what's needed, crop the sides 

654 crop_width = output_ratio * live_size[1] 

655 crop_height = live_size[1] 

656 else: 

657 # live_size is taller than what's needed, crop the top and bottom 

658 crop_width = live_size[0] 

659 crop_height = live_size[0] / output_ratio 

660 

661 # make the crop 

662 crop_left = bleed_pixels[0] + (live_size[0] - crop_width) * centering_x 

663 crop_top = bleed_pixels[1] + (live_size[1] - crop_height) * centering_y 

664 

665 crop = (crop_left, crop_top, crop_left + crop_width, crop_top + crop_height) 

666 

667 # resize the image and return it 

668 return image.resize(size, method, box=crop) 

669 

670 

671def flip(image: Image.Image) -> Image.Image: 

672 """ 

673 Flip the image vertically (top to bottom). 

674 

675 :param image: The image to flip. 

676 :return: An image. 

677 """ 

678 return image.transpose(Image.Transpose.FLIP_TOP_BOTTOM) 

679 

680 

681def grayscale(image: Image.Image) -> Image.Image: 

682 """ 

683 Convert the image to grayscale. 

684 

685 :param image: The image to convert. 

686 :return: An image. 

687 """ 

688 return image.convert("L") 

689 

690 

691def sepia(image: Image.Image) -> Image.Image: 

692 """ 

693 Apply a sepia tone to the image. 

694 

695 :param image: The image to modify. 

696 :return: An image. 

697 

698 .. versionadded:: 13.1.0 

699 """ 

700 if image.mode != "RGB": 

701 image = image.convert("RGB") 

702 

703 return image.convert( 

704 "RGB", 

705 matrix=( 

706 0.393, 

707 0.769, 

708 0.189, 

709 0, 

710 0.349, 

711 0.686, 

712 0.168, 

713 0, 

714 0.272, 

715 0.534, 

716 0.131, 

717 0, 

718 ), 

719 ) 

720 

721 

722def _sobel(image: Image.Image) -> Image.Image: 

723 if image.mode != "L": 

724 image = image.convert("L") 

725 

726 Kx = [[-1, 0, 1], [-2, 0, 2], [-1, 0, 1]] 

727 Ky = [[1, 2, 1], [0, 0, 0], [-1, -2, -1]] 

728 

729 out = Image.new("L", image.size) 

730 

731 for x in range(1, image.width - 1): 

732 for y in range(1, image.height - 1): 

733 

734 gx = gy = 0.0 

735 

736 for dy in (-1, 0, 1): 

737 for dx in (-1, 0, 1): 

738 v = image.getpixel((x + dx, y + dy)) 

739 assert isinstance(v, (int, float)) 

740 

741 gx += v * Kx[dy + 1][dx + 1] 

742 gy += v * Ky[dy + 1][dx + 1] 

743 

744 # Approximate gradient magnitude and clamp to [0, 255] 

745 mag = int(min(255, abs(gx) + abs(gy))) 

746 out.putpixel((x, y), mag) 

747 

748 return out 

749 

750 

751def neon( 

752 image: Image.Image, color: tuple[int, int, int] = (255, 0, 255), alpha: float = 0.2 

753) -> Image.Image: 

754 """ 

755 Apply a neon glow effect to an image using edge detection, 

756 blur-based glow generation, colorization, and alpha blending. 

757 

758 :param image: The image to be used for the effect 

759 :param color: RGB color to be used for the neon effect 

760 :param alpha: The intensity of the neon effect. If alpha is 0.0, a copy of the 

761 image is returned unaltered. 

762 :return: An image. 

763 

764 .. versionadded:: 13.1.0 

765 """ 

766 edges = _sobel(image).filter(ImageFilter.GaussianBlur(2)) 

767 

768 # Apply a glow-enhancing mask transformation 

769 glow = edges.point(lambda value: 255 - ((255 - value) ** 2 // 255)) 

770 

771 # Apply a color tint to the intensity mask 

772 neon = Image.merge( 

773 "RGB", 

774 tuple(glow.point(lambda value: min(255, int(value * c / 255))) for c in color), 

775 ) 

776 

777 return Image.blend(image.convert("RGB"), neon, alpha) 

778 

779 

780def invert(image: Image.Image) -> Image.Image: 

781 """ 

782 Invert (negate) the image. 

783 

784 :param image: The image to invert. 

785 :return: An image. 

786 """ 

787 lut = list(range(255, -1, -1)) 

788 return image.point(lut) if image.mode == "1" else _lut(image, lut, palette=True) 

789 

790 

791def mirror(image: Image.Image) -> Image.Image: 

792 """ 

793 Flip image horizontally (left to right). 

794 

795 :param image: The image to mirror. 

796 :return: An image. 

797 """ 

798 return image.transpose(Image.Transpose.FLIP_LEFT_RIGHT) 

799 

800 

801def posterize(image: Image.Image, bits: int) -> Image.Image: 

802 """ 

803 Reduce the number of bits for each color channel. 

804 

805 :param image: The image to posterize. 

806 :param bits: The number of bits to keep for each channel (1-8). 

807 :return: An image. 

808 """ 

809 if not 1 <= bits <= 8: 

810 msg = "bits must be between or equal to 1 and 8" 

811 raise ValueError(msg) 

812 mask = ~(2 ** (8 - bits) - 1) 

813 lut = [i & mask for i in range(256)] 

814 return _lut(image, lut, palette=True) 

815 

816 

817def solarize(image: Image.Image, threshold: int = 128) -> Image.Image: 

818 """ 

819 Invert all pixel values above a threshold. 

820 

821 :param image: The image to solarize. 

822 :param threshold: All pixels above this grayscale level are inverted. 

823 :return: An image. 

824 """ 

825 lut = [] 

826 for i in range(256): 

827 if i < threshold: 

828 lut.append(i) 

829 else: 

830 lut.append(255 - i) 

831 return _lut(image, lut, palette=True) 

832 

833 

834@overload 

835def exif_transpose(image: Image.Image, *, in_place: Literal[True]) -> None: ... 

836 

837 

838@overload 

839def exif_transpose( 

840 image: Image.Image, *, in_place: Literal[False] = False 

841) -> Image.Image: ... 

842 

843 

844def exif_transpose(image: Image.Image, *, in_place: bool = False) -> Image.Image | None: 

845 """ 

846 If an image has an EXIF Orientation tag, other than 1, transpose the image 

847 accordingly, and remove the orientation data. 

848 

849 :param image: The image to transpose. 

850 :param in_place: Boolean. Keyword-only argument. 

851 If ``True``, the original image is modified in-place, and ``None`` is returned. 

852 If ``False`` (default), a new :py:class:`~PIL.Image.Image` object is returned 

853 with the transposition applied. If there is no transposition, a copy of the 

854 image will be returned. 

855 """ 

856 image.load() 

857 image_exif = image.getexif() 

858 orientation = image_exif.get(ExifTags.Base.Orientation, 1) 

859 method = { 

860 2: Image.Transpose.FLIP_LEFT_RIGHT, 

861 3: Image.Transpose.ROTATE_180, 

862 4: Image.Transpose.FLIP_TOP_BOTTOM, 

863 5: Image.Transpose.TRANSPOSE, 

864 6: Image.Transpose.ROTATE_270, 

865 7: Image.Transpose.TRANSVERSE, 

866 8: Image.Transpose.ROTATE_90, 

867 }.get(orientation) 

868 if method is not None: 

869 if in_place: 

870 image.im = image.im.transpose(method) 

871 image._size = image.im.size 

872 else: 

873 transposed_image = image.transpose(method) 

874 exif_image = image if in_place else transposed_image 

875 

876 exif = exif_image.getexif() 

877 if ExifTags.Base.Orientation in exif: 

878 del exif[ExifTags.Base.Orientation] 

879 if "exif" in exif_image.info: 

880 exif_image.info["exif"] = exif.tobytes() 

881 elif "Raw profile type exif" in exif_image.info: 

882 data = exif.tobytes() 

883 exif_image.info["Raw profile type exif"] = ( 

884 f"\nexif\n{len(data):8}\n{data.hex()}\n" 

885 ) 

886 for key in ("XML:com.adobe.xmp", "xmp"): 

887 if key in exif_image.info: 

888 for pattern in ( 

889 r'tiff:Orientation="([0-9])"', 

890 r"<tiff:Orientation>([0-9])</tiff:Orientation>", 

891 ): 

892 value = exif_image.info[key] 

893 exif_image.info[key] = ( 

894 re.sub(pattern, "", value) 

895 if isinstance(value, str) 

896 else re.sub(pattern.encode(), b"", value) 

897 ) 

898 if not in_place: 

899 return transposed_image 

900 elif not in_place: 

901 return image.copy() 

902 return None