ImageFilter.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607
  1. #
  2. # The Python Imaging Library.
  3. # $Id$
  4. #
  5. # standard filters
  6. #
  7. # History:
  8. # 1995-11-27 fl Created
  9. # 2002-06-08 fl Added rank and mode filters
  10. # 2003-09-15 fl Fixed rank calculation in rank filter; added expand call
  11. #
  12. # Copyright (c) 1997-2003 by Secret Labs AB.
  13. # Copyright (c) 1995-2002 by Fredrik Lundh.
  14. #
  15. # See the README file for information on usage and redistribution.
  16. #
  17. from __future__ import annotations
  18. import abc
  19. import functools
  20. from collections.abc import Sequence
  21. from typing import cast
  22. TYPE_CHECKING = False
  23. if TYPE_CHECKING:
  24. from collections.abc import Callable
  25. from types import ModuleType
  26. from typing import Any
  27. from . import _imaging
  28. from ._typing import NumpyArray
  29. class Filter(abc.ABC):
  30. @abc.abstractmethod
  31. def filter(self, image: _imaging.ImagingCore) -> _imaging.ImagingCore:
  32. pass
  33. class MultibandFilter(Filter):
  34. pass
  35. class BuiltinFilter(MultibandFilter):
  36. filterargs: tuple[Any, ...]
  37. def filter(self, image: _imaging.ImagingCore) -> _imaging.ImagingCore:
  38. if image.mode == "P":
  39. msg = "cannot filter palette images"
  40. raise ValueError(msg)
  41. return image.filter(*self.filterargs)
  42. class Kernel(BuiltinFilter):
  43. """
  44. Create a convolution kernel. This only supports 3x3 and 5x5 integer and floating
  45. point kernels.
  46. Kernels can only be applied to "L" and "RGB" images.
  47. :param size: Kernel size, given as (width, height). This must be (3,3) or (5,5).
  48. :param kernel: A sequence containing kernel weights. The kernel will be flipped
  49. vertically before being applied to the image.
  50. :param scale: Scale factor. If given, the result for each pixel is divided by this
  51. value. The default is the sum of the kernel weights.
  52. :param offset: Offset. If given, this value is added to the result, after it has
  53. been divided by the scale factor.
  54. """
  55. name = "Kernel"
  56. def __init__(
  57. self,
  58. size: tuple[int, int],
  59. kernel: Sequence[float],
  60. scale: float | None = None,
  61. offset: float = 0,
  62. ) -> None:
  63. if scale is None:
  64. # default scale is sum of kernel
  65. scale = functools.reduce(lambda a, b: a + b, kernel)
  66. if size[0] * size[1] != len(kernel):
  67. msg = "not enough coefficients in kernel"
  68. raise ValueError(msg)
  69. self.filterargs = size, scale, offset, kernel
  70. class RankFilter(Filter):
  71. """
  72. Create a rank filter. The rank filter sorts all pixels in
  73. a window of the given size, and returns the ``rank``'th value.
  74. :param size: The kernel size, in pixels.
  75. :param rank: What pixel value to pick. Use 0 for a min filter,
  76. ``size * size / 2`` for a median filter, ``size * size - 1``
  77. for a max filter, etc.
  78. """
  79. name = "Rank"
  80. def __init__(self, size: int, rank: int) -> None:
  81. self.size = size
  82. self.rank = rank
  83. def filter(self, image: _imaging.ImagingCore) -> _imaging.ImagingCore:
  84. if image.mode == "P":
  85. msg = "cannot filter palette images"
  86. raise ValueError(msg)
  87. image = image.expand(self.size // 2, self.size // 2)
  88. return image.rankfilter(self.size, self.rank)
  89. class MedianFilter(RankFilter):
  90. """
  91. Create a median filter. Picks the median pixel value in a window with the
  92. given size.
  93. :param size: The kernel size, in pixels.
  94. """
  95. name = "Median"
  96. def __init__(self, size: int = 3) -> None:
  97. self.size = size
  98. self.rank = size * size // 2
  99. class MinFilter(RankFilter):
  100. """
  101. Create a min filter. Picks the lowest pixel value in a window with the
  102. given size.
  103. :param size: The kernel size, in pixels.
  104. """
  105. name = "Min"
  106. def __init__(self, size: int = 3) -> None:
  107. self.size = size
  108. self.rank = 0
  109. class MaxFilter(RankFilter):
  110. """
  111. Create a max filter. Picks the largest pixel value in a window with the
  112. given size.
  113. :param size: The kernel size, in pixels.
  114. """
  115. name = "Max"
  116. def __init__(self, size: int = 3) -> None:
  117. self.size = size
  118. self.rank = size * size - 1
  119. class ModeFilter(Filter):
  120. """
  121. Create a mode filter. Picks the most frequent pixel value in a box with the
  122. given size. Pixel values that occur only once or twice are ignored; if no
  123. pixel value occurs more than twice, the original pixel value is preserved.
  124. :param size: The kernel size, in pixels.
  125. """
  126. name = "Mode"
  127. def __init__(self, size: int = 3) -> None:
  128. self.size = size
  129. def filter(self, image: _imaging.ImagingCore) -> _imaging.ImagingCore:
  130. return image.modefilter(self.size)
  131. class GaussianBlur(MultibandFilter):
  132. """Blurs the image with a sequence of extended box filters, which
  133. approximates a Gaussian kernel. For details on accuracy see
  134. <https://www.mia.uni-saarland.de/Publications/gwosdek-ssvm11.pdf>
  135. :param radius: Standard deviation of the Gaussian kernel. Either a sequence of two
  136. numbers for x and y, or a single number for both.
  137. """
  138. name = "GaussianBlur"
  139. def __init__(self, radius: float | Sequence[float] = 2) -> None:
  140. self.radius = radius
  141. def filter(self, image: _imaging.ImagingCore) -> _imaging.ImagingCore:
  142. xy = self.radius
  143. if isinstance(xy, (int, float)):
  144. xy = (xy, xy)
  145. if xy == (0, 0):
  146. return image.copy()
  147. return image.gaussian_blur(xy)
  148. class BoxBlur(MultibandFilter):
  149. """Blurs the image by setting each pixel to the average value of the pixels
  150. in a square box extending radius pixels in each direction.
  151. Supports float radius of arbitrary size. Uses an optimized implementation
  152. which runs in linear time relative to the size of the image
  153. for any radius value.
  154. :param radius: Size of the box in a direction. Either a sequence of two numbers for
  155. x and y, or a single number for both.
  156. Radius 0 does not blur, returns an identical image.
  157. Radius 1 takes 1 pixel in each direction, i.e. 9 pixels in total.
  158. """
  159. name = "BoxBlur"
  160. def __init__(self, radius: float | Sequence[float]) -> None:
  161. xy = radius if isinstance(radius, (tuple, list)) else (radius, radius)
  162. if xy[0] < 0 or xy[1] < 0:
  163. msg = "radius must be >= 0"
  164. raise ValueError(msg)
  165. self.radius = radius
  166. def filter(self, image: _imaging.ImagingCore) -> _imaging.ImagingCore:
  167. xy = self.radius
  168. if isinstance(xy, (int, float)):
  169. xy = (xy, xy)
  170. if xy == (0, 0):
  171. return image.copy()
  172. return image.box_blur(xy)
  173. class UnsharpMask(MultibandFilter):
  174. """Unsharp mask filter.
  175. See Wikipedia's entry on `digital unsharp masking`_ for an explanation of
  176. the parameters.
  177. :param radius: Blur Radius
  178. :param percent: Unsharp strength, in percent
  179. :param threshold: Threshold controls the minimum brightness change that
  180. will be sharpened
  181. .. _digital unsharp masking: https://en.wikipedia.org/wiki/Unsharp_masking#Digital_unsharp_masking
  182. """
  183. name = "UnsharpMask"
  184. def __init__(
  185. self, radius: float = 2, percent: int = 150, threshold: int = 3
  186. ) -> None:
  187. self.radius = radius
  188. self.percent = percent
  189. self.threshold = threshold
  190. def filter(self, image: _imaging.ImagingCore) -> _imaging.ImagingCore:
  191. return image.unsharp_mask(self.radius, self.percent, self.threshold)
  192. class BLUR(BuiltinFilter):
  193. name = "Blur"
  194. # fmt: off
  195. filterargs = (5, 5), 16, 0, (
  196. 1, 1, 1, 1, 1,
  197. 1, 0, 0, 0, 1,
  198. 1, 0, 0, 0, 1,
  199. 1, 0, 0, 0, 1,
  200. 1, 1, 1, 1, 1,
  201. )
  202. # fmt: on
  203. class CONTOUR(BuiltinFilter):
  204. name = "Contour"
  205. # fmt: off
  206. filterargs = (3, 3), 1, 255, (
  207. -1, -1, -1,
  208. -1, 8, -1,
  209. -1, -1, -1,
  210. )
  211. # fmt: on
  212. class DETAIL(BuiltinFilter):
  213. name = "Detail"
  214. # fmt: off
  215. filterargs = (3, 3), 6, 0, (
  216. 0, -1, 0,
  217. -1, 10, -1,
  218. 0, -1, 0,
  219. )
  220. # fmt: on
  221. class EDGE_ENHANCE(BuiltinFilter):
  222. name = "Edge-enhance"
  223. # fmt: off
  224. filterargs = (3, 3), 2, 0, (
  225. -1, -1, -1,
  226. -1, 10, -1,
  227. -1, -1, -1,
  228. )
  229. # fmt: on
  230. class EDGE_ENHANCE_MORE(BuiltinFilter):
  231. name = "Edge-enhance More"
  232. # fmt: off
  233. filterargs = (3, 3), 1, 0, (
  234. -1, -1, -1,
  235. -1, 9, -1,
  236. -1, -1, -1,
  237. )
  238. # fmt: on
  239. class EMBOSS(BuiltinFilter):
  240. name = "Emboss"
  241. # fmt: off
  242. filterargs = (3, 3), 1, 128, (
  243. -1, 0, 0,
  244. 0, 1, 0,
  245. 0, 0, 0,
  246. )
  247. # fmt: on
  248. class FIND_EDGES(BuiltinFilter):
  249. name = "Find Edges"
  250. # fmt: off
  251. filterargs = (3, 3), 1, 0, (
  252. -1, -1, -1,
  253. -1, 8, -1,
  254. -1, -1, -1,
  255. )
  256. # fmt: on
  257. class SHARPEN(BuiltinFilter):
  258. name = "Sharpen"
  259. # fmt: off
  260. filterargs = (3, 3), 16, 0, (
  261. -2, -2, -2,
  262. -2, 32, -2,
  263. -2, -2, -2,
  264. )
  265. # fmt: on
  266. class SMOOTH(BuiltinFilter):
  267. name = "Smooth"
  268. # fmt: off
  269. filterargs = (3, 3), 13, 0, (
  270. 1, 1, 1,
  271. 1, 5, 1,
  272. 1, 1, 1,
  273. )
  274. # fmt: on
  275. class SMOOTH_MORE(BuiltinFilter):
  276. name = "Smooth More"
  277. # fmt: off
  278. filterargs = (5, 5), 100, 0, (
  279. 1, 1, 1, 1, 1,
  280. 1, 5, 5, 5, 1,
  281. 1, 5, 44, 5, 1,
  282. 1, 5, 5, 5, 1,
  283. 1, 1, 1, 1, 1,
  284. )
  285. # fmt: on
  286. class Color3DLUT(MultibandFilter):
  287. """Three-dimensional color lookup table.
  288. Transforms 3-channel pixels using the values of the channels as coordinates
  289. in the 3D lookup table and interpolating the nearest elements.
  290. This method allows you to apply almost any color transformation
  291. in constant time by using pre-calculated decimated tables.
  292. .. versionadded:: 5.2.0
  293. :param size: Size of the table. One int or tuple of (int, int, int).
  294. Minimal size in any dimension is 2, maximum is 65.
  295. :param table: Flat lookup table. A list of ``channels * size**3``
  296. float elements or a list of ``size**3`` channels-sized
  297. tuples with floats. Channels are changed first,
  298. then first dimension, then second, then third.
  299. Value 0.0 corresponds lowest value of output, 1.0 highest.
  300. :param channels: Number of channels in the table. Could be 3 or 4.
  301. Default is 3.
  302. :param target_mode: A mode for the result image. Should have not less
  303. than ``channels`` channels. Default is ``None``,
  304. which means that mode wouldn't be changed.
  305. """
  306. name = "Color 3D LUT"
  307. def __init__(
  308. self,
  309. size: int | tuple[int, int, int],
  310. table: Sequence[float] | Sequence[Sequence[int]] | NumpyArray,
  311. channels: int = 3,
  312. target_mode: str | None = None,
  313. **kwargs: bool,
  314. ) -> None:
  315. if channels not in (3, 4):
  316. msg = "Only 3 or 4 output channels are supported"
  317. raise ValueError(msg)
  318. self.size = size = self._check_size(size)
  319. self.channels = channels
  320. self.mode = target_mode
  321. # Hidden flag `_copy_table=False` could be used to avoid extra copying
  322. # of the table if the table is specially made for the constructor.
  323. copy_table = kwargs.get("_copy_table", True)
  324. items = size[0] * size[1] * size[2]
  325. wrong_size = False
  326. numpy: ModuleType | None = None
  327. if hasattr(table, "shape"):
  328. try:
  329. import numpy
  330. except ImportError:
  331. pass
  332. if numpy and isinstance(table, numpy.ndarray):
  333. numpy_table: NumpyArray = table
  334. if copy_table:
  335. numpy_table = numpy_table.copy()
  336. if numpy_table.shape in [
  337. (items * channels,),
  338. (items, channels),
  339. (size[2], size[1], size[0], channels),
  340. ]:
  341. table = numpy_table.reshape(items * channels)
  342. else:
  343. wrong_size = True
  344. else:
  345. if copy_table:
  346. table = list(table)
  347. # Convert to a flat list
  348. if table and isinstance(table[0], (list, tuple)):
  349. raw_table = cast(Sequence[Sequence[int]], table)
  350. flat_table: list[int] = []
  351. for pixel in raw_table:
  352. if len(pixel) != channels:
  353. msg = (
  354. "The elements of the table should "
  355. f"have a length of {channels}."
  356. )
  357. raise ValueError(msg)
  358. flat_table.extend(pixel)
  359. table = flat_table
  360. if wrong_size or len(table) != items * channels:
  361. msg = (
  362. "The table should have either channels * size**3 float items "
  363. "or size**3 items of channels-sized tuples with floats. "
  364. f"Table should be: {channels}x{size[0]}x{size[1]}x{size[2]}. "
  365. f"Actual length: {len(table)}"
  366. )
  367. raise ValueError(msg)
  368. self.table = table
  369. @staticmethod
  370. def _check_size(size: Any) -> tuple[int, int, int]:
  371. try:
  372. _, _, _ = size
  373. except ValueError as e:
  374. msg = "Size should be either an integer or a tuple of three integers."
  375. raise ValueError(msg) from e
  376. except TypeError:
  377. size = (size, size, size)
  378. size = tuple(int(x) for x in size)
  379. for size_1d in size:
  380. if not 2 <= size_1d <= 65:
  381. msg = "Size should be in [2, 65] range."
  382. raise ValueError(msg)
  383. return size
  384. @classmethod
  385. def generate(
  386. cls,
  387. size: int | tuple[int, int, int],
  388. callback: Callable[[float, float, float], tuple[float, ...]],
  389. channels: int = 3,
  390. target_mode: str | None = None,
  391. ) -> Color3DLUT:
  392. """Generates new LUT using provided callback.
  393. :param size: Size of the table. Passed to the constructor.
  394. :param callback: Function with three parameters which correspond
  395. three color channels. Will be called ``size**3``
  396. times with values from 0.0 to 1.0 and should return
  397. a tuple with ``channels`` elements.
  398. :param channels: The number of channels which should return callback.
  399. :param target_mode: Passed to the constructor of the resulting
  400. lookup table.
  401. """
  402. size_1d, size_2d, size_3d = cls._check_size(size)
  403. if channels not in (3, 4):
  404. msg = "Only 3 or 4 output channels are supported"
  405. raise ValueError(msg)
  406. table: list[float] = [0] * (size_1d * size_2d * size_3d * channels)
  407. idx_out = 0
  408. for b in range(size_3d):
  409. for g in range(size_2d):
  410. for r in range(size_1d):
  411. table[idx_out : idx_out + channels] = callback(
  412. r / (size_1d - 1), g / (size_2d - 1), b / (size_3d - 1)
  413. )
  414. idx_out += channels
  415. return cls(
  416. (size_1d, size_2d, size_3d),
  417. table,
  418. channels=channels,
  419. target_mode=target_mode,
  420. _copy_table=False,
  421. )
  422. def transform(
  423. self,
  424. callback: Callable[..., tuple[float, ...]],
  425. with_normals: bool = False,
  426. channels: int | None = None,
  427. target_mode: str | None = None,
  428. ) -> Color3DLUT:
  429. """Transforms the table values using provided callback and returns
  430. a new LUT with altered values.
  431. :param callback: A function which takes old lookup table values
  432. and returns a new set of values. The number
  433. of arguments which function should take is
  434. ``self.channels`` or ``3 + self.channels``
  435. if ``with_normals`` flag is set.
  436. Should return a tuple of ``self.channels`` or
  437. ``channels`` elements if it is set.
  438. :param with_normals: If true, ``callback`` will be called with
  439. coordinates in the color cube as the first
  440. three arguments. Otherwise, ``callback``
  441. will be called only with actual color values.
  442. :param channels: The number of channels in the resulting lookup table.
  443. :param target_mode: Passed to the constructor of the resulting
  444. lookup table.
  445. """
  446. if channels not in (None, 3, 4):
  447. msg = "Only 3 or 4 output channels are supported"
  448. raise ValueError(msg)
  449. ch_in = self.channels
  450. ch_out = channels or ch_in
  451. size_1d, size_2d, size_3d = self.size
  452. table: list[float] = [0] * (size_1d * size_2d * size_3d * ch_out)
  453. idx_in = 0
  454. idx_out = 0
  455. for b in range(size_3d):
  456. for g in range(size_2d):
  457. for r in range(size_1d):
  458. values = self.table[idx_in : idx_in + ch_in]
  459. if with_normals:
  460. values = callback(
  461. r / (size_1d - 1),
  462. g / (size_2d - 1),
  463. b / (size_3d - 1),
  464. *values,
  465. )
  466. else:
  467. values = callback(*values)
  468. table[idx_out : idx_out + ch_out] = values
  469. idx_in += ch_in
  470. idx_out += ch_out
  471. return type(self)(
  472. self.size,
  473. table,
  474. channels=ch_out,
  475. target_mode=target_mode or self.mode,
  476. _copy_table=False,
  477. )
  478. def __repr__(self) -> str:
  479. r = [
  480. f"{self.__class__.__name__} from {self.table.__class__.__name__}",
  481. "size={:d}x{:d}x{:d}".format(*self.size),
  482. f"channels={self.channels:d}",
  483. ]
  484. if self.mode:
  485. r.append(f"target_mode={self.mode}")
  486. return "<{}>".format(" ".join(r))
  487. def filter(self, image: _imaging.ImagingCore) -> _imaging.ImagingCore:
  488. from . import Image
  489. return image.color_lut_3d(
  490. self.mode or image.mode,
  491. Image.Resampling.BILINEAR,
  492. self.channels,
  493. self.size,
  494. self.table,
  495. )