edges.py 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485
  1. """
  2. Augmenters that deal with edge detection.
  3. List of augmenters:
  4. * :class:`Canny`
  5. :class:`~imgaug.augmenters.convolutional.EdgeDetect` and
  6. :class:`~imgaug.augmenters.convolutional.DirectedEdgeDetect` are currently
  7. still in ``convolutional.py``.
  8. """
  9. from __future__ import print_function, division, absolute_import
  10. from abc import ABCMeta, abstractmethod
  11. import numpy as np
  12. import cv2
  13. import six
  14. import imgaug as ia
  15. from imgaug.imgaug import _normalize_cv2_input_arr_
  16. from . import meta
  17. from . import blend
  18. from .. import parameters as iap
  19. from .. import dtypes as iadt
  20. # TODO this should be placed in some other file than edges.py as it could be
  21. # re-used wherever a binary image is the result
  22. @six.add_metaclass(ABCMeta)
  23. class IBinaryImageColorizer(object):
  24. """Interface for classes that convert binary masks to color images."""
  25. @abstractmethod
  26. def colorize(self, image_binary, image_original, nth_image, random_state):
  27. """
  28. Convert a binary image to a colorized one.
  29. Parameters
  30. ----------
  31. image_binary : ndarray
  32. Boolean ``(H,W)`` image.
  33. image_original : ndarray
  34. Original ``(H,W,C)`` input image.
  35. nth_image : int
  36. Index of the image in the batch.
  37. random_state : imgaug.random.RNG
  38. Random state to use.
  39. Returns
  40. -------
  41. ndarray
  42. Colorized form of `image_binary`.
  43. """
  44. # TODO see above, this should be moved to another file
  45. class RandomColorsBinaryImageColorizer(IBinaryImageColorizer):
  46. """
  47. Colorizer using two randomly sampled foreground/background colors.
  48. Parameters
  49. ----------
  50. color_true : int or tuple of int or list of int or imgaug.parameters.StochasticParameter, optional
  51. Color of the foreground, i.e. all pixels in binary images that are
  52. ``True``. This parameter will be queried once per image to
  53. generate ``(3,)`` samples denoting the color. (Note that even for
  54. grayscale images three values will be sampled and converted to
  55. grayscale according to ``0.299*R + 0.587*G + 0.114*B``. This is the
  56. same equation that is also used by OpenCV.)
  57. * If an int, exactly that value will always be used, i.e. every
  58. color will be ``(v, v, v)`` for value ``v``.
  59. * If a tuple ``(a, b)``, three random values from the range
  60. ``a <= x <= b`` will be sampled per image.
  61. * If a list, then three random values will be sampled from that
  62. list per image.
  63. * If a StochasticParameter, three values will be sampled from the
  64. parameter per image.
  65. color_false : int or tuple of int or list of int or imgaug.parameters.StochasticParameter, optional
  66. Analogous to `color_true`, but denotes the color for all pixels that
  67. are ``False`` in the binary input image.
  68. """
  69. def __init__(self, color_true=(0, 255), color_false=(0, 255)):
  70. self.color_true = iap.handle_discrete_param(
  71. color_true,
  72. "color_true",
  73. value_range=(0, 255),
  74. tuple_to_uniform=True,
  75. list_to_choice=True,
  76. allow_floats=False)
  77. self.color_false = iap.handle_discrete_param(
  78. color_false,
  79. "color_false",
  80. value_range=(0, 255),
  81. tuple_to_uniform=True,
  82. list_to_choice=True,
  83. allow_floats=False)
  84. def _draw_samples(self, random_state):
  85. color_true = self.color_true.draw_samples((3,),
  86. random_state=random_state)
  87. color_false = self.color_false.draw_samples((3,),
  88. random_state=random_state)
  89. return color_true, color_false
  90. def colorize(self, image_binary, image_original, nth_image, random_state):
  91. assert image_binary.ndim == 2, (
  92. "Expected binary image to colorize to be 2-dimensional, "
  93. "got %d dimensions." % (image_binary.ndim,))
  94. assert image_binary.dtype.kind == "b", (
  95. "Expected binary image to colorize to be boolean, "
  96. "got dtype kind %s." % (image_binary.dtype.kind,))
  97. assert image_original.ndim == 3, (
  98. "Expected original image to be 3-dimensional, got %d "
  99. "dimensions." % (image_original.ndim,))
  100. assert image_original.shape[-1] in [1, 3, 4], (
  101. "Expected original image to have 1, 3 or 4 channels. "
  102. "Got %d channels." % (image_original.shape[-1],))
  103. assert image_original.dtype.name == "uint8", (
  104. "Expected original image to have dtype uint8, got dtype %s." % (
  105. image_original.dtype.name))
  106. color_true, color_false = self._draw_samples(random_state)
  107. nb_channels = min(image_original.shape[-1], 3)
  108. image_colorized = np.zeros(
  109. (image_original.shape[0], image_original.shape[1], nb_channels),
  110. dtype=image_original.dtype)
  111. if nb_channels == 1:
  112. # single channel input image, convert colors to grayscale
  113. image_colorized[image_binary] = (
  114. 0.299*color_true[0]
  115. + 0.587*color_true[1]
  116. + 0.114*color_true[2])
  117. image_colorized[~image_binary] = (
  118. 0.299*color_false[0]
  119. + 0.587*color_false[1]
  120. + 0.114*color_false[2])
  121. else:
  122. image_colorized[image_binary] = color_true
  123. image_colorized[~image_binary] = color_false
  124. # re-attach alpha channel if it was present in input image
  125. if image_original.shape[-1] == 4:
  126. image_colorized = np.dstack(
  127. [image_colorized, image_original[:, :, 3:4]])
  128. return image_colorized
  129. def __str__(self):
  130. return ("RandomColorsBinaryImageColorizer("
  131. "color_true=%s, color_false=%s)") % (
  132. self.color_true, self.color_false)
  133. class Canny(meta.Augmenter):
  134. """
  135. Apply a canny edge detector to input images.
  136. **Supported dtypes**:
  137. * ``uint8``: yes; fully tested
  138. * ``uint16``: no; not tested
  139. * ``uint32``: no; not tested
  140. * ``uint64``: no; not tested
  141. * ``int8``: no; not tested
  142. * ``int16``: no; not tested
  143. * ``int32``: no; not tested
  144. * ``int64``: no; not tested
  145. * ``float16``: no; not tested
  146. * ``float32``: no; not tested
  147. * ``float64``: no; not tested
  148. * ``float128``: no; not tested
  149. * ``bool``: no; not tested
  150. Parameters
  151. ----------
  152. alpha : number or tuple of number or list of number or imgaug.parameters.StochasticParameter, optional
  153. Blending factor to use in alpha blending.
  154. A value close to 1.0 means that only the edge image is visible.
  155. A value close to 0.0 means that only the original image is visible.
  156. A value close to 0.5 means that the images are merged according to
  157. `0.5*image + 0.5*edge_image`.
  158. If a sample from this parameter is 0, no action will be performed for
  159. the corresponding image.
  160. * If an int or float, exactly that value will be used.
  161. * If a tuple ``(a, b)``, a random value from the range
  162. ``a <= x <= b`` will be sampled per image.
  163. * If a list, then a random value will be sampled from that list
  164. per image.
  165. * If a StochasticParameter, a value will be sampled from the
  166. parameter per image.
  167. hysteresis_thresholds : number or tuple of number or list of number or imgaug.parameters.StochasticParameter or tuple of tuple of number or tuple of list of number or tuple of imgaug.parameters.StochasticParameter, optional
  168. Min and max values to use in hysteresis thresholding.
  169. (This parameter seems to have not very much effect on the results.)
  170. Either a single parameter or a tuple of two parameters.
  171. If a single parameter is provided, the sampling happens once for all
  172. images with `(N,2)` samples being requested from the parameter,
  173. where each first value denotes the hysteresis minimum and each second
  174. the maximum.
  175. If a tuple of two parameters is provided, one sampling of `(N,)` values
  176. is independently performed per parameter (first parameter: hysteresis
  177. minimum, second: hysteresis maximum).
  178. * If this is a single number, both min and max value will always be
  179. exactly that value.
  180. * If this is a tuple of numbers ``(a, b)``, two random values from
  181. the range ``a <= x <= b`` will be sampled per image.
  182. * If this is a list, two random values will be sampled from that
  183. list per image.
  184. * If this is a StochasticParameter, two random values will be
  185. sampled from that parameter per image.
  186. * If this is a tuple ``(min, max)`` with ``min`` and ``max``
  187. both *not* being numbers, they will be treated according to the
  188. rules above (i.e. may be a number, tuple, list or
  189. StochasticParameter). A single value will be sampled per image
  190. and parameter.
  191. sobel_kernel_size : int or tuple of int or list of int or imgaug.parameters.StochasticParameter, optional
  192. Kernel size of the sobel operator initially applied to each image.
  193. This corresponds to ``apertureSize`` in ``cv2.Canny()``.
  194. If a sample from this parameter is ``<=1``, no action will be performed
  195. for the corresponding image.
  196. The maximum for this parameter is ``7`` (inclusive). Higher values are
  197. not accepted by OpenCV.
  198. If an even value ``v`` is sampled, it is automatically changed to
  199. ``v-1``.
  200. * If this is a single integer, the kernel size always matches that
  201. value.
  202. * If this is a tuple of integers ``(a, b)``, a random discrete
  203. value will be sampled from the range ``a <= x <= b`` per image.
  204. * If this is a list, a random value will be sampled from that
  205. list per image.
  206. * If this is a StochasticParameter, a random value will be sampled
  207. from that parameter per image.
  208. colorizer : None or imgaug.augmenters.edges.IBinaryImageColorizer, optional
  209. A strategy to convert binary edge images to color images.
  210. If this is ``None``, an instance of ``RandomColorBinaryImageColorizer``
  211. is created, which means that each edge image is converted into an
  212. ``uint8`` image, where edge and non-edge pixels each have a different
  213. color that was uniformly randomly sampled from the space of all
  214. ``uint8`` colors.
  215. seed : None or int or imgaug.random.RNG or numpy.random.Generator or numpy.random.BitGenerator or numpy.random.SeedSequence or numpy.random.RandomState, optional
  216. See :func:`~imgaug.augmenters.meta.Augmenter.__init__`.
  217. name : None or str, optional
  218. See :func:`~imgaug.augmenters.meta.Augmenter.__init__`.
  219. random_state : None or int or imgaug.random.RNG or numpy.random.Generator or numpy.random.BitGenerator or numpy.random.SeedSequence or numpy.random.RandomState, optional
  220. Old name for parameter `seed`.
  221. Its usage will not yet cause a deprecation warning,
  222. but it is still recommended to use `seed` now.
  223. Outdated since 0.4.0.
  224. deterministic : bool, optional
  225. Deprecated since 0.4.0.
  226. See method ``to_deterministic()`` for an alternative and for
  227. details about what the "deterministic mode" actually does.
  228. Examples
  229. --------
  230. >>> import imgaug.augmenters as iaa
  231. >>> aug = iaa.Canny()
  232. Create an augmenter that generates random blends between images and
  233. their canny edge representations.
  234. >>> aug = iaa.Canny(alpha=(0.0, 0.5))
  235. Create a canny edge augmenter that generates edge images with a blending
  236. factor of max ``50%``, i.e. the original (non-edge) image is always at
  237. least partially visible.
  238. >>> aug = iaa.Canny(
  239. >>> alpha=(0.0, 0.5),
  240. >>> colorizer=iaa.RandomColorsBinaryImageColorizer(
  241. >>> color_true=255,
  242. >>> color_false=0
  243. >>> )
  244. >>> )
  245. Same as in the previous example, but the edge image always uses the
  246. color white for edges and black for the background.
  247. >>> aug = iaa.Canny(alpha=(0.5, 1.0), sobel_kernel_size=[3, 7])
  248. Create a canny edge augmenter that initially preprocesses images using
  249. a sobel filter with kernel size of either ``3x3`` or ``13x13`` and
  250. alpha-blends with result using a strength of ``50%`` (both images
  251. equally visible) to ``100%`` (only edge image visible).
  252. >>> aug = iaa.Alpha(
  253. >>> (0.0, 1.0),
  254. >>> iaa.Canny(alpha=1),
  255. >>> iaa.MedianBlur(13)
  256. >>> )
  257. Create an augmenter that blends a canny edge image with a median-blurred
  258. version of the input image. The median blur uses a fixed kernel size
  259. of ``13x13`` pixels.
  260. """
  261. def __init__(self,
  262. alpha=(0.0, 1.0),
  263. hysteresis_thresholds=((100-40, 100+40), (200-40, 200+40)),
  264. sobel_kernel_size=(3, 7),
  265. colorizer=None,
  266. seed=None, name=None,
  267. random_state="deprecated", deterministic="deprecated"):
  268. super(Canny, self).__init__(
  269. seed=seed, name=name,
  270. random_state=random_state, deterministic=deterministic)
  271. self.alpha = iap.handle_continuous_param(
  272. alpha, "alpha", value_range=(0, 1.0), tuple_to_uniform=True,
  273. list_to_choice=True)
  274. if isinstance(hysteresis_thresholds, tuple) \
  275. and len(hysteresis_thresholds) == 2 \
  276. and not ia.is_single_number(hysteresis_thresholds[0]) \
  277. and not ia.is_single_number(hysteresis_thresholds[1]):
  278. self.hysteresis_thresholds = (
  279. iap.handle_discrete_param(
  280. hysteresis_thresholds[0],
  281. "hysteresis_thresholds[0]",
  282. value_range=(0, 255),
  283. tuple_to_uniform=True,
  284. list_to_choice=True,
  285. allow_floats=True),
  286. iap.handle_discrete_param(
  287. hysteresis_thresholds[1],
  288. "hysteresis_thresholds[1]",
  289. value_range=(0, 255),
  290. tuple_to_uniform=True,
  291. list_to_choice=True,
  292. allow_floats=True)
  293. )
  294. else:
  295. self.hysteresis_thresholds = iap.handle_discrete_param(
  296. hysteresis_thresholds,
  297. "hysteresis_thresholds",
  298. value_range=(0, 255),
  299. tuple_to_uniform=True,
  300. list_to_choice=True,
  301. allow_floats=True)
  302. # we don't use handle_discrete_kernel_size_param() here, because
  303. # cv2.Canny() can't handle independent height/width values, only a
  304. # single kernel size
  305. self.sobel_kernel_size = iap.handle_discrete_param(
  306. sobel_kernel_size,
  307. "sobel_kernel_size",
  308. value_range=(0, 7), # OpenCV only accepts ksize up to 7
  309. tuple_to_uniform=True,
  310. list_to_choice=True,
  311. allow_floats=False)
  312. self.colorizer = (
  313. colorizer
  314. if colorizer is not None
  315. else RandomColorsBinaryImageColorizer()
  316. )
  317. def _draw_samples(self, augmentables, random_state):
  318. nb_images = len(augmentables)
  319. rss = random_state.duplicate(4)
  320. alpha_samples = self.alpha.draw_samples((nb_images,), rss[0])
  321. hthresh = self.hysteresis_thresholds
  322. if isinstance(hthresh, tuple):
  323. min_values = hthresh[0].draw_samples((nb_images,), rss[1])
  324. max_values = hthresh[1].draw_samples((nb_images,), rss[2])
  325. hthresh_samples = np.stack([min_values, max_values], axis=-1)
  326. else:
  327. hthresh_samples = hthresh.draw_samples((nb_images, 2), rss[1])
  328. sobel_samples = self.sobel_kernel_size.draw_samples((nb_images,),
  329. rss[3])
  330. # verify for hysteresis thresholds that min_value < max_value everywhere
  331. invalid = (hthresh_samples[:, 0] > hthresh_samples[:, 1])
  332. if np.any(invalid):
  333. hthresh_samples[invalid, :] = hthresh_samples[invalid, :][:, [1, 0]]
  334. # ensure that sobel kernel sizes are correct
  335. # note that OpenCV accepts only kernel sizes that are (a) even
  336. # and (b) <=7
  337. assert not np.any(sobel_samples < 0), (
  338. "Sampled a sobel kernel size below 0 in Canny. "
  339. "Allowed value range is 0 to 7.")
  340. assert not np.any(sobel_samples > 7), (
  341. "Sampled a sobel kernel size above 7 in Canny. "
  342. "Allowed value range is 0 to 7.")
  343. even_idx = (np.mod(sobel_samples, 2) == 0)
  344. sobel_samples[even_idx] -= 1
  345. return alpha_samples, hthresh_samples, sobel_samples
  346. # Added in 0.4.0.
  347. def _augment_batch_(self, batch, random_state, parents, hooks):
  348. if batch.images is None:
  349. return batch
  350. images = batch.images
  351. iadt.gate_dtypes(images,
  352. allowed=["uint8"],
  353. disallowed=[
  354. "bool",
  355. "uint16", "uint32", "uint64", "uint128",
  356. "uint256",
  357. "int8", "int16", "int32", "int64", "int128",
  358. "int256",
  359. "float32", "float64", "float96", "float128",
  360. "float256"],
  361. augmenter=self)
  362. rss = random_state.duplicate(len(images))
  363. samples = self._draw_samples(images, rss[-1])
  364. alpha_samples = samples[0]
  365. hthresh_samples = samples[1]
  366. sobel_samples = samples[2]
  367. gen = enumerate(zip(images, alpha_samples, hthresh_samples,
  368. sobel_samples))
  369. for i, (image, alpha, hthreshs, sobel) in gen:
  370. assert image.shape[-1] in [1, 3, 4], (
  371. "Canny edge detector can currently only handle images with "
  372. "channel numbers that are 1, 3 or 4. Got %d.") % (
  373. image.shape[-1],)
  374. has_zero_sized_axes = (0 in image.shape[0:2])
  375. if alpha > 0 and sobel > 1 and not has_zero_sized_axes:
  376. image_canny = cv2.Canny(
  377. _normalize_cv2_input_arr_(image[:, :, 0:3]),
  378. threshold1=hthreshs[0],
  379. threshold2=hthreshs[1],
  380. apertureSize=sobel,
  381. L2gradient=True)
  382. image_canny = (image_canny > 0)
  383. # canny returns a boolean (H,W) image, so we change it to
  384. # (H,W,C) and then uint8
  385. image_canny_color = self.colorizer.colorize(
  386. image_canny, image, nth_image=i, random_state=rss[i])
  387. batch.images[i] = blend.blend_alpha(image_canny_color, image,
  388. alpha)
  389. return batch
  390. def get_parameters(self):
  391. """See :func:`~imgaug.augmenters.meta.Augmenter.get_parameters`."""
  392. return [self.alpha, self.hysteresis_thresholds, self.sobel_kernel_size,
  393. self.colorizer]
  394. def __str__(self):
  395. return ("Canny("
  396. "alpha=%s, "
  397. "hysteresis_thresholds=%s, "
  398. "sobel_kernel_size=%s, "
  399. "colorizer=%s, "
  400. "name=%s, "
  401. "deterministic=%s)" % (
  402. self.alpha, self.hysteresis_thresholds,
  403. self.sobel_kernel_size, self.colorizer,
  404. self.name, self.deterministic))