util.py 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232
  1. import numpy as np
  2. from ..util import img_as_float
  3. from .._shared.utils import (
  4. _supported_float_type,
  5. check_nD,
  6. )
  7. class FeatureDetector:
  8. def __init__(self):
  9. self.keypoints_ = np.array([])
  10. def detect(self, image):
  11. """Detect keypoints in image.
  12. Parameters
  13. ----------
  14. image : 2D array
  15. Input image.
  16. """
  17. raise NotImplementedError()
  18. class DescriptorExtractor:
  19. def __init__(self):
  20. self.descriptors_ = np.array([])
  21. def extract(self, image, keypoints):
  22. """Extract feature descriptors in image for given keypoints.
  23. Parameters
  24. ----------
  25. image : 2D array
  26. Input image.
  27. keypoints : (N, 2) array
  28. Keypoint locations as ``(row, col)``.
  29. """
  30. raise NotImplementedError()
  31. def plot_matched_features(
  32. image0,
  33. image1,
  34. *,
  35. keypoints0,
  36. keypoints1,
  37. matches,
  38. ax,
  39. keypoints_color='k',
  40. matches_color=None,
  41. only_matches=False,
  42. alignment='horizontal',
  43. ):
  44. """Plot matched features between two images.
  45. .. versionadded:: 0.23
  46. Parameters
  47. ----------
  48. image0 : (N, M [, 3]) array
  49. First image.
  50. image1 : (N, M [, 3]) array
  51. Second image.
  52. keypoints0 : (K1, 2) array
  53. First keypoint coordinates as ``(row, col)``.
  54. keypoints1 : (K2, 2) array
  55. Second keypoint coordinates as ``(row, col)``.
  56. matches : (Q, 2) array
  57. Indices of corresponding matches in first and second sets of
  58. descriptors, where `matches[:, 0]` (resp. `matches[:, 1]`) contains
  59. the indices in the first (resp. second) set of descriptors.
  60. ax : matplotlib.axes.Axes
  61. The Axes object where the images and their matched features are drawn.
  62. keypoints_color : matplotlib color, optional
  63. Color for keypoint locations.
  64. matches_color : matplotlib color or sequence thereof, optional
  65. Single color or sequence of colors for each line defined by `matches`,
  66. which connect keypoint matches. See [1]_ for an overview of supported
  67. color formats. By default, colors are picked randomly.
  68. only_matches : bool, optional
  69. Set to True to plot matches only and not the keypoint locations.
  70. alignment : {'horizontal', 'vertical'}, optional
  71. Whether to show the two images side by side (`'horizontal'`), or one above
  72. the other (`'vertical'`).
  73. References
  74. ----------
  75. .. [1] https://matplotlib.org/stable/users/explain/colors/colors.html#specifying-colors
  76. Notes
  77. -----
  78. To make a sequence of colors passed to `matches_color` work for any number of
  79. `matches`, you can wrap that sequence in :func:`itertools.cycle`.
  80. """
  81. image0 = img_as_float(image0)
  82. image1 = img_as_float(image1)
  83. new_shape0 = list(image0.shape)
  84. new_shape1 = list(image1.shape)
  85. if image0.shape[0] < image1.shape[0]:
  86. new_shape0[0] = image1.shape[0]
  87. elif image0.shape[0] > image1.shape[0]:
  88. new_shape1[0] = image0.shape[0]
  89. if image0.shape[1] < image1.shape[1]:
  90. new_shape0[1] = image1.shape[1]
  91. elif image0.shape[1] > image1.shape[1]:
  92. new_shape1[1] = image0.shape[1]
  93. if new_shape0 != image0.shape:
  94. new_image0 = np.zeros(new_shape0, dtype=image0.dtype)
  95. new_image0[: image0.shape[0], : image0.shape[1]] = image0
  96. image0 = new_image0
  97. if new_shape1 != image1.shape:
  98. new_image1 = np.zeros(new_shape1, dtype=image1.dtype)
  99. new_image1[: image1.shape[0], : image1.shape[1]] = image1
  100. image1 = new_image1
  101. offset = np.array(image0.shape)
  102. if alignment == 'horizontal':
  103. image = np.concatenate([image0, image1], axis=1)
  104. offset[0] = 0
  105. elif alignment == 'vertical':
  106. image = np.concatenate([image0, image1], axis=0)
  107. offset[1] = 0
  108. else:
  109. mesg = (
  110. f"`plot_matched_features` accepts either 'horizontal' or 'vertical' for "
  111. f"alignment, but '{alignment}' was given. See "
  112. f"https://scikit-image.org/docs/dev/api/skimage.feature.html#skimage.feature.plot_matched_features "
  113. f"for details."
  114. )
  115. raise ValueError(mesg)
  116. if not only_matches:
  117. ax.scatter(
  118. keypoints0[:, 1],
  119. keypoints0[:, 0],
  120. facecolors='none',
  121. edgecolors=keypoints_color,
  122. )
  123. ax.scatter(
  124. keypoints1[:, 1] + offset[1],
  125. keypoints1[:, 0] + offset[0],
  126. facecolors='none',
  127. edgecolors=keypoints_color,
  128. )
  129. ax.imshow(image, cmap='gray')
  130. ax.axis((0, image0.shape[1] + offset[1], image0.shape[0] + offset[0], 0))
  131. number_of_matches = matches.shape[0]
  132. from matplotlib.colors import is_color_like
  133. if matches_color is None:
  134. rng = np.random.default_rng(seed=0)
  135. colors = [rng.random(3) for _ in range(number_of_matches)]
  136. elif is_color_like(matches_color):
  137. colors = [matches_color for _ in range(number_of_matches)]
  138. elif hasattr(matches_color, "__len__") and len(matches_color) == number_of_matches:
  139. # No need to check each color, matplotlib does so for us
  140. colors = matches_color
  141. else:
  142. error_message = (
  143. '`matches_color` needs to be a single color '
  144. 'or a sequence of length equal to the number of matches.'
  145. )
  146. raise ValueError(error_message)
  147. for i, match in enumerate(matches):
  148. idx0, idx1 = match
  149. ax.plot(
  150. (keypoints0[idx0, 1], keypoints1[idx1, 1] + offset[1]),
  151. (keypoints0[idx0, 0], keypoints1[idx1, 0] + offset[0]),
  152. '-',
  153. color=colors[i],
  154. )
  155. def _prepare_grayscale_input_2D(image):
  156. image = np.squeeze(image)
  157. check_nD(image, 2)
  158. image = img_as_float(image)
  159. float_dtype = _supported_float_type(image.dtype)
  160. return image.astype(float_dtype, copy=False)
  161. def _prepare_grayscale_input_nD(image):
  162. image = np.squeeze(image)
  163. check_nD(image, range(2, 6))
  164. image = img_as_float(image)
  165. float_dtype = _supported_float_type(image.dtype)
  166. return image.astype(float_dtype, copy=False)
  167. def _mask_border_keypoints(image_shape, keypoints, distance):
  168. """Mask coordinates that are within certain distance from the image border.
  169. Parameters
  170. ----------
  171. image_shape : (2,) array_like
  172. Shape of the image as ``(rows, cols)``.
  173. keypoints : (N, 2) array
  174. Keypoint coordinates as ``(rows, cols)``.
  175. distance : int
  176. Image border distance.
  177. Returns
  178. -------
  179. mask : (N,) bool array
  180. Mask indicating if pixels are within the image (``True``) or in the
  181. border region of the image (``False``).
  182. """
  183. rows = image_shape[0]
  184. cols = image_shape[1]
  185. mask = (
  186. ((distance - 1) < keypoints[:, 0])
  187. & (keypoints[:, 0] < (rows - distance + 1))
  188. & ((distance - 1) < keypoints[:, 1])
  189. & (keypoints[:, 1] < (cols - distance + 1))
  190. )
  191. return mask