morphology.py 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582
  1. # LICENSE HEADER MANAGED BY add-license-header
  2. #
  3. # Copyright 2018 Kornia Team
  4. #
  5. # Licensed under the Apache License, Version 2.0 (the "License");
  6. # you may not use this file except in compliance with the License.
  7. # You may obtain a copy of the License at
  8. #
  9. # http://www.apache.org/licenses/LICENSE-2.0
  10. #
  11. # Unless required by applicable law or agreed to in writing, software
  12. # distributed under the License is distributed on an "AS IS" BASIS,
  13. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14. # See the License for the specific language governing permissions and
  15. # limitations under the License.
  16. #
  17. from typing import List, Optional
  18. import torch
  19. import torch.nn.functional as F
  20. __all__ = ["bottom_hat", "closing", "dilation", "erosion", "gradient", "opening", "top_hat"]
  21. def _neight2channels_like_kernel(kernel: torch.Tensor) -> torch.Tensor:
  22. h, w = kernel.size()
  23. kernel = torch.eye(h * w, dtype=kernel.dtype, device=kernel.device)
  24. return kernel.view(h * w, 1, h, w)
  25. def dilation(
  26. tensor: torch.Tensor,
  27. kernel: torch.Tensor,
  28. structuring_element: Optional[torch.Tensor] = None,
  29. origin: Optional[List[int]] = None,
  30. border_type: str = "geodesic",
  31. border_value: float = 0.0,
  32. max_val: float = 1e4,
  33. engine: str = "unfold",
  34. ) -> torch.Tensor:
  35. r"""Return the dilated image applying the same kernel in each channel.
  36. .. image:: _static/img/dilation.png
  37. The kernel must have 2 dimensions.
  38. Args:
  39. tensor: Image with shape :math:`(B, C, H, W)`.
  40. kernel: Positions of non-infinite elements of a flat structuring element. Non-zero values give
  41. the set of neighbors of the center over which the operation is applied. Its shape is :math:`(k_x, k_y)`.
  42. For full structural elements use torch.ones_like(structural_element).
  43. structuring_element: Structuring element used for the grayscale dilation. It may be a non-flat
  44. structuring element.
  45. origin: Origin of the structuring element. Default: ``None`` and uses the center of
  46. the structuring element as origin (rounding towards zero).
  47. border_type: It determines how the image borders are handled, where ``border_value`` is the value
  48. when ``border_type`` is equal to ``constant``. Default: ``geodesic`` which ignores the values that are
  49. outside the image when applying the operation.
  50. border_value: Value to fill past edges of input if ``border_type`` is ``constant``.
  51. max_val: The value of the infinite elements in the kernel.
  52. engine: convolution is faster and less memory hungry, and unfold is more stable numerically
  53. Returns:
  54. Dilated image with shape :math:`(B, C, H, W)`.
  55. .. note::
  56. See a working example `here <https://kornia.github.io/tutorials/nbs/morphology_101.html>`__.
  57. Example:
  58. >>> tensor = torch.rand(1, 3, 5, 5)
  59. >>> kernel = torch.ones(3, 3)
  60. >>> dilated_img = dilation(tensor, kernel)
  61. """
  62. if not isinstance(tensor, torch.Tensor):
  63. raise TypeError(f"Input type is not a torch.Tensor. Got {type(tensor)}")
  64. if len(tensor.shape) != 4:
  65. raise ValueError(f"Input size must have 4 dimensions. Got {tensor.dim()}")
  66. if not isinstance(kernel, torch.Tensor):
  67. raise TypeError(f"Kernel type is not a torch.Tensor. Got {type(kernel)}")
  68. if len(kernel.shape) != 2:
  69. raise ValueError(f"Kernel size must have 2 dimensions. Got {kernel.dim()}")
  70. # origin
  71. se_h, se_w = kernel.shape
  72. if origin is None:
  73. origin = [se_h // 2, se_w // 2]
  74. # pad
  75. pad_e: List[int] = [origin[1], se_w - origin[1] - 1, origin[0], se_h - origin[0] - 1]
  76. if border_type == "geodesic":
  77. border_value = -max_val
  78. border_type = "constant"
  79. output: torch.Tensor = F.pad(tensor, pad_e, mode=border_type, value=border_value)
  80. # computation
  81. if structuring_element is None:
  82. neighborhood = torch.zeros_like(kernel)
  83. neighborhood[kernel == 0] = -max_val
  84. else:
  85. neighborhood = structuring_element.clone()
  86. neighborhood[kernel == 0] = -max_val
  87. if engine == "unfold":
  88. output = output.unfold(2, se_h, 1).unfold(3, se_w, 1)
  89. output, _ = torch.max(output + neighborhood.flip((0, 1)), 4)
  90. output, _ = torch.max(output, 4)
  91. elif engine == "convolution":
  92. B, C, H, W = tensor.size()
  93. h_pad, w_pad = output.shape[-2:]
  94. reshape_kernel = _neight2channels_like_kernel(kernel)
  95. output, _ = F.conv2d(
  96. output.view(B * C, 1, h_pad, w_pad), reshape_kernel, padding=0, bias=neighborhood.view(-1).flip(0)
  97. ).max(dim=1)
  98. output = output.view(B, C, H, W)
  99. else:
  100. raise NotImplementedError(f"engine {engine} is unknown, use 'convolution' or 'unfold'")
  101. return output.view_as(tensor)
  102. def erosion(
  103. tensor: torch.Tensor,
  104. kernel: torch.Tensor,
  105. structuring_element: Optional[torch.Tensor] = None,
  106. origin: Optional[List[int]] = None,
  107. border_type: str = "geodesic",
  108. border_value: float = 0.0,
  109. max_val: float = 1e4,
  110. engine: str = "unfold",
  111. ) -> torch.Tensor:
  112. r"""Return the eroded image applying the same kernel in each channel.
  113. .. image:: _static/img/erosion.png
  114. The kernel must have 2 dimensions.
  115. Args:
  116. tensor: Image with shape :math:`(B, C, H, W)`.
  117. kernel: Positions of non-infinite elements of a flat structuring element. Non-zero values give
  118. the set of neighbors of the center over which the operation is applied. Its shape is :math:`(k_x, k_y)`.
  119. For full structural elements use torch.ones_like(structural_element).
  120. structuring_element (torch.Tensor, optional): Structuring element used for the grayscale dilation.
  121. It may be a non-flat structuring element.
  122. origin: Origin of the structuring element. Default: ``None`` and uses the center of
  123. the structuring element as origin (rounding towards zero).
  124. border_type: It determines how the image borders are handled, where ``border_value`` is the value
  125. when ``border_type`` is equal to ``constant``. Default: ``geodesic`` which ignores the values that are
  126. outside the image when applying the operation.
  127. border_value: Value to fill past edges of input if border_type is ``constant``.
  128. max_val: The value of the infinite elements in the kernel.
  129. engine: ``convolution`` is faster and less memory hungry, and ``unfold`` is more stable numerically
  130. Returns:
  131. Eroded image with shape :math:`(B, C, H, W)`.
  132. .. note::
  133. See a working example `here <https://kornia.github.io/tutorials/nbs/morphology_101.html>`__.
  134. Example:
  135. >>> tensor = torch.rand(1, 3, 5, 5)
  136. >>> kernel = torch.ones(5, 5)
  137. >>> output = erosion(tensor, kernel)
  138. """
  139. if not isinstance(tensor, torch.Tensor):
  140. raise TypeError(f"Input type is not a torch.Tensor. Got {type(tensor)}")
  141. if len(tensor.shape) != 4:
  142. raise ValueError(f"Input size must have 4 dimensions. Got {tensor.dim()}")
  143. if not isinstance(kernel, torch.Tensor):
  144. raise TypeError(f"Kernel type is not a torch.Tensor. Got {type(kernel)}")
  145. if len(kernel.shape) != 2:
  146. raise ValueError(f"Kernel size must have 2 dimensions. Got {kernel.dim()}")
  147. # origin
  148. se_h, se_w = kernel.shape
  149. if origin is None:
  150. origin = [se_h // 2, se_w // 2]
  151. # pad
  152. pad_e: List[int] = [origin[1], se_w - origin[1] - 1, origin[0], se_h - origin[0] - 1]
  153. if border_type == "geodesic":
  154. border_value = max_val
  155. border_type = "constant"
  156. output: torch.Tensor = F.pad(tensor, pad_e, mode=border_type, value=border_value)
  157. # computation
  158. if structuring_element is None:
  159. neighborhood = torch.zeros_like(kernel)
  160. neighborhood[kernel == 0] = -max_val
  161. else:
  162. neighborhood = structuring_element.clone()
  163. neighborhood[kernel == 0] = -max_val
  164. if engine == "unfold":
  165. output = output.unfold(2, se_h, 1).unfold(3, se_w, 1)
  166. output, _ = torch.min(output - neighborhood, 4)
  167. output, _ = torch.min(output, 4)
  168. elif engine == "convolution":
  169. B, C, H, W = tensor.size()
  170. Hpad, Wpad = output.shape[-2:]
  171. reshape_kernel = _neight2channels_like_kernel(kernel)
  172. output, _ = F.conv2d(
  173. output.view(B * C, 1, Hpad, Wpad), reshape_kernel, padding=0, bias=-neighborhood.view(-1)
  174. ).min(dim=1)
  175. output = output.view(B, C, H, W)
  176. else:
  177. raise NotImplementedError(f"engine {engine} is unknown, use 'convolution' or 'unfold'")
  178. return output
  179. def opening(
  180. tensor: torch.Tensor,
  181. kernel: torch.Tensor,
  182. structuring_element: Optional[torch.Tensor] = None,
  183. origin: Optional[List[int]] = None,
  184. border_type: str = "geodesic",
  185. border_value: float = 0.0,
  186. max_val: float = 1e4,
  187. engine: str = "unfold",
  188. ) -> torch.Tensor:
  189. r"""Return the opened image, (that means, dilation after an erosion) applying the same kernel in each channel.
  190. .. image:: _static/img/opening.png
  191. The kernel must have 2 dimensions.
  192. Args:
  193. tensor: Image with shape :math:`(B, C, H, W)`.
  194. kernel: Positions of non-infinite elements of a flat structuring element. Non-zero values give
  195. the set of neighbors of the center over which the operation is applied. Its shape is :math:`(k_x, k_y)`.
  196. For full structural elements use torch.ones_like(structural_element).
  197. structuring_element: Structuring element used for the grayscale dilation. It may be a
  198. non-flat structuring element.
  199. origin: Origin of the structuring element. Default: ``None`` and uses the center of
  200. the structuring element as origin (rounding towards zero).
  201. border_type: It determines how the image borders are handled, where ``border_value`` is the value
  202. when ``border_type`` is equal to ``constant``. Default: ``geodesic`` which ignores the values that are
  203. outside the image when applying the operation.
  204. border_value: Value to fill past edges of input if ``border_type`` is ``constant``.
  205. max_val: The value of the infinite elements in the kernel.
  206. engine: convolution is faster and less memory hungry, and unfold is more stable numerically
  207. Returns:
  208. torch.Tensor: Opened image with shape :math:`(B, C, H, W)`.
  209. .. note::
  210. See a working example `here <https://kornia.github.io/tutorials/nbs/morphology_101.html>`__.
  211. Example:
  212. >>> tensor = torch.rand(1, 3, 5, 5)
  213. >>> kernel = torch.ones(3, 3)
  214. >>> opened_img = opening(tensor, kernel)
  215. """
  216. if not isinstance(tensor, torch.Tensor):
  217. raise TypeError(f"Input type is not a torch.Tensor. Got {type(tensor)}")
  218. if len(tensor.shape) != 4:
  219. raise ValueError(f"Input size must have 4 dimensions. Got {tensor.dim()}")
  220. if not isinstance(kernel, torch.Tensor):
  221. raise TypeError(f"Kernel type is not a torch.Tensor. Got {type(kernel)}")
  222. if len(kernel.shape) != 2:
  223. raise ValueError(f"Kernel size must have 2 dimensions. Got {kernel.dim()}")
  224. return dilation(
  225. erosion(
  226. tensor,
  227. kernel=kernel,
  228. structuring_element=structuring_element,
  229. origin=origin,
  230. border_type=border_type,
  231. border_value=border_value,
  232. max_val=max_val,
  233. engine=engine,
  234. ),
  235. kernel=kernel,
  236. structuring_element=structuring_element,
  237. origin=origin,
  238. border_type=border_type,
  239. border_value=border_value,
  240. max_val=max_val,
  241. engine=engine,
  242. )
  243. def closing(
  244. tensor: torch.Tensor,
  245. kernel: torch.Tensor,
  246. structuring_element: Optional[torch.Tensor] = None,
  247. origin: Optional[List[int]] = None,
  248. border_type: str = "geodesic",
  249. border_value: float = 0.0,
  250. max_val: float = 1e4,
  251. engine: str = "unfold",
  252. ) -> torch.Tensor:
  253. r"""Return the closed image, (that means, erosion after a dilation) applying the same kernel in each channel.
  254. .. image:: _static/img/closing.png
  255. The kernel must have 2 dimensions.
  256. Args:
  257. tensor: Image with shape :math:`(B, C, H, W)`.
  258. kernel: Positions of non-infinite elements of a flat structuring element. Non-zero values give
  259. the set of neighbors of the center over which the operation is applied. Its shape is :math:`(k_x, k_y)`.
  260. For full structural elements use torch.ones_like(structural_element).
  261. structuring_element: Structuring element used for the grayscale dilation. It may be a
  262. non-flat structuring element.
  263. origin: Origin of the structuring element. Default is None and uses the center of
  264. the structuring element as origin (rounding towards zero).
  265. border_type: It determines how the image borders are handled, where ``border_value`` is the value
  266. when ``border_type`` is equal to ``constant``. Default: ``geodesic`` which ignores the values that are
  267. outside the image when applying the operation.
  268. border_value: Value to fill past edges of input if ``border_type`` is ``constant``.
  269. max_val: The value of the infinite elements in the kernel.
  270. engine: convolution is faster and less memory hungry, and unfold is more stable numerically
  271. Returns:
  272. Closed image with shape :math:`(B, C, H, W)`.
  273. .. note::
  274. See a working example `here <https://kornia.github.io/tutorials/nbs/morphology_101.html>`__.
  275. Example:
  276. >>> tensor = torch.rand(1, 3, 5, 5)
  277. >>> kernel = torch.ones(3, 3)
  278. >>> closed_img = closing(tensor, kernel)
  279. """
  280. if not isinstance(tensor, torch.Tensor):
  281. raise TypeError(f"Input type is not a torch.Tensor. Got {type(tensor)}")
  282. if len(tensor.shape) != 4:
  283. raise ValueError(f"Input size must have 4 dimensions. Got {tensor.dim()}")
  284. if not isinstance(kernel, torch.Tensor):
  285. raise TypeError(f"Kernel type is not a torch.Tensor. Got {type(kernel)}")
  286. if len(kernel.shape) != 2:
  287. raise ValueError(f"Kernel size must have 2 dimensions. Got {kernel.dim()}")
  288. return erosion(
  289. dilation(
  290. tensor,
  291. kernel=kernel,
  292. structuring_element=structuring_element,
  293. origin=origin,
  294. border_type=border_type,
  295. border_value=border_value,
  296. max_val=max_val,
  297. engine=engine,
  298. ),
  299. kernel=kernel,
  300. structuring_element=structuring_element,
  301. origin=origin,
  302. border_type=border_type,
  303. border_value=border_value,
  304. max_val=max_val,
  305. engine=engine,
  306. )
  307. # Morphological Gradient
  308. def gradient(
  309. tensor: torch.Tensor,
  310. kernel: torch.Tensor,
  311. structuring_element: Optional[torch.Tensor] = None,
  312. origin: Optional[List[int]] = None,
  313. border_type: str = "geodesic",
  314. border_value: float = 0.0,
  315. max_val: float = 1e4,
  316. engine: str = "unfold",
  317. ) -> torch.Tensor:
  318. r"""Return the morphological gradient of an image.
  319. .. image:: _static/img/gradient.png
  320. That means, (dilation - erosion) applying the same kernel in each channel.
  321. The kernel must have 2 dimensions.
  322. Args:
  323. tensor: Image with shape :math:`(B, C, H, W)`.
  324. kernel: Positions of non-infinite elements of a flat structuring element. Non-zero values give
  325. the set of neighbors of the center over which the operation is applied. Its shape is :math:`(k_x, k_y)`.
  326. For full structural elements use torch.ones_like(structural_element).
  327. structuring_element: Structuring element used for the grayscale dilation. It may be a
  328. non-flat structuring element.
  329. origin: Origin of the structuring element. Default is None and uses the center of
  330. the structuring element as origin (rounding towards zero).
  331. border_type: It determines how the image borders are handled, where ``border_value`` is the value
  332. when ``border_type`` is equal to ``constant``. Default: ``geodesic`` which ignores the values that are
  333. outside the image when applying the operation.
  334. border_value: Value to fill past edges of input if ``border_type`` is ``constant``.
  335. max_val: The value of the infinite elements in the kernel.
  336. engine: convolution is faster and less memory hungry, and unfold is more stable numerically
  337. Returns:
  338. Gradient image with shape :math:`(B, C, H, W)`.
  339. .. note::
  340. See a working example `here <https://kornia.github.io/tutorials/nbs/morphology_101.html>`__.
  341. Example:
  342. >>> tensor = torch.rand(1, 3, 5, 5)
  343. >>> kernel = torch.ones(3, 3)
  344. >>> gradient_img = gradient(tensor, kernel)
  345. """
  346. return dilation(
  347. tensor,
  348. kernel=kernel,
  349. structuring_element=structuring_element,
  350. origin=origin,
  351. border_type=border_type,
  352. border_value=border_value,
  353. max_val=max_val,
  354. engine=engine,
  355. ) - erosion(
  356. tensor,
  357. kernel=kernel,
  358. structuring_element=structuring_element,
  359. origin=origin,
  360. border_type=border_type,
  361. border_value=border_value,
  362. max_val=max_val,
  363. engine=engine,
  364. )
  365. def top_hat(
  366. tensor: torch.Tensor,
  367. kernel: torch.Tensor,
  368. structuring_element: Optional[torch.Tensor] = None,
  369. origin: Optional[List[int]] = None,
  370. border_type: str = "geodesic",
  371. border_value: float = 0.0,
  372. max_val: float = 1e4,
  373. engine: str = "unfold",
  374. ) -> torch.Tensor:
  375. r"""Return the top hat transformation of an image.
  376. .. image:: _static/img/top_hat.png
  377. That means, (image - opened_image) applying the same kernel in each channel.
  378. The kernel must have 2 dimensions.
  379. See :func:`~kornia.morphology.opening` for details.
  380. Args:
  381. tensor: Image with shape :math:`(B, C, H, W)`.
  382. kernel: Positions of non-infinite elements of a flat structuring element. Non-zero values give
  383. the set of neighbors of the center over which the operation is applied. Its shape is :math:`(k_x, k_y)`.
  384. For full structural elements use torch.ones_like(structural_element).
  385. structuring_element: Structuring element used for the grayscale dilation. It may be a
  386. non-flat structuring element.
  387. origin: Origin of the structuring element. Default: ``None`` and uses the center of
  388. the structuring element as origin (rounding towards zero).
  389. border_type: It determines how the image borders are handled, where ``border_value`` is the value
  390. when ``border_type`` is equal to ``constant``. Default: ``geodesic`` which ignores the values that are
  391. outside the image when applying the operation.
  392. border_value: Value to fill past edges of input if ``border_type`` is ``constant``.
  393. max_val: The value of the infinite elements in the kernel.
  394. engine: convolution is faster and less memory hungry, and unfold is more stable numerically
  395. Returns:
  396. Top hat transformed image with shape :math:`(B, C, H, W)`.
  397. .. note::
  398. See a working example `here <https://kornia.github.io/tutorials/nbs/morphology_101.html>`__.
  399. Example:
  400. >>> tensor = torch.rand(1, 3, 5, 5)
  401. >>> kernel = torch.ones(3, 3)
  402. >>> top_hat_img = top_hat(tensor, kernel)
  403. """
  404. if not isinstance(tensor, torch.Tensor):
  405. raise TypeError(f"Input type is not a torch.Tensor. Got {type(tensor)}")
  406. if len(tensor.shape) != 4:
  407. raise ValueError(f"Input size must have 4 dimensions. Got {tensor.dim()}")
  408. if not isinstance(kernel, torch.Tensor):
  409. raise TypeError(f"Kernel type is not a torch.Tensor. Got {type(kernel)}")
  410. if len(kernel.shape) != 2:
  411. raise ValueError(f"Kernel size must have 2 dimensions. Got {kernel.dim()}")
  412. return tensor - opening(
  413. tensor,
  414. kernel=kernel,
  415. structuring_element=structuring_element,
  416. origin=origin,
  417. border_type=border_type,
  418. border_value=border_value,
  419. max_val=max_val,
  420. engine=engine,
  421. )
  422. def bottom_hat(
  423. tensor: torch.Tensor,
  424. kernel: torch.Tensor,
  425. structuring_element: Optional[torch.Tensor] = None,
  426. origin: Optional[List[int]] = None,
  427. border_type: str = "geodesic",
  428. border_value: float = 0.0,
  429. max_val: float = 1e4,
  430. engine: str = "unfold",
  431. ) -> torch.Tensor:
  432. r"""Return the bottom hat transformation of an image.
  433. .. image:: _static/img/bottom_hat.png
  434. That means, (closed_image - image) applying the same kernel in each channel.
  435. The kernel must have 2 dimensions.
  436. See :func:`~kornia.morphology.closing` for details.
  437. Args:
  438. tensor: Image with shape :math:`(B, C, H, W)`.
  439. kernel: Positions of non-infinite elements of a flat structuring element. Non-zero values give
  440. the set of neighbors of the center over which the operation is applied. Its shape is :math:`(k_x, k_y)`.
  441. For full structural elements use torch.ones_like(structural_element).
  442. structuring_element: Structuring element used for the grayscale dilation. It may be a
  443. non-flat structuring element.
  444. origin: Origin of the structuring element. Default: ``None`` and uses the center of
  445. the structuring element as origin (rounding towards zero).
  446. border_type: It determines how the image borders are handled, where ``border_value`` is the value
  447. when ``border_type`` is equal to ``constant``. Default: ``geodesic`` which ignores the values that are
  448. outside the image when applying the operation.
  449. border_value: Value to fill past edges of input if ``border_type`` is ``constant``.
  450. max_val: The value of the infinite elements in the kernel.
  451. engine: convolution is faster and less memory hungry, and unfold is more stable numerically
  452. Returns:
  453. Top hat transformed image with shape :math:`(B, C, H, W)`.
  454. .. note::
  455. See a working example `here <https://kornia.github.io/tutorials/nbs/morphology_101.html>`__.
  456. Example:
  457. >>> tensor = torch.rand(1, 3, 5, 5)
  458. >>> kernel = torch.ones(3, 3)
  459. >>> bottom_hat_img = bottom_hat(tensor, kernel)
  460. """
  461. if not isinstance(tensor, torch.Tensor):
  462. raise TypeError(f"Input type is not a torch.Tensor. Got {type(tensor)}")
  463. if len(tensor.shape) != 4:
  464. raise ValueError(f"Input size must have 4 dimensions. Got {tensor.dim()}")
  465. if not isinstance(kernel, torch.Tensor):
  466. raise TypeError(f"Kernel type is not a torch.Tensor. Got {type(kernel)}")
  467. if len(kernel.shape) != 2:
  468. raise ValueError(f"Kernel size must have 2 dimensions. Got {kernel.dim()}")
  469. return (
  470. closing(
  471. tensor,
  472. kernel=kernel,
  473. structuring_element=structuring_element,
  474. origin=origin,
  475. border_type=border_type,
  476. border_value=border_value,
  477. max_val=max_val,
  478. engine=engine,
  479. )
  480. - tensor
  481. )