brief.py 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216
  1. import copy
  2. import numpy as np
  3. from packaging.version import Version
  4. from .._shared.filters import gaussian
  5. from .._shared.utils import check_nD
  6. from .brief_cy import _brief_loop
  7. from .util import (
  8. DescriptorExtractor,
  9. _mask_border_keypoints,
  10. _prepare_grayscale_input_2D,
  11. )
  12. np2 = Version(np.__version__) >= Version('2')
  13. class BRIEF(DescriptorExtractor):
  14. """BRIEF binary descriptor extractor.
  15. BRIEF (Binary Robust Independent Elementary Features) is an efficient
  16. feature point descriptor. It is highly discriminative even when using
  17. relatively few bits and is computed using simple intensity difference
  18. tests.
  19. For each keypoint, intensity comparisons are carried out for a specifically
  20. distributed number N of pixel-pairs resulting in a binary descriptor of
  21. length N. For binary descriptors the Hamming distance can be used for
  22. feature matching, which leads to lower computational cost in comparison to
  23. the L2 norm.
  24. Parameters
  25. ----------
  26. descriptor_size : int, optional
  27. Size of BRIEF descriptor for each keypoint. Sizes 128, 256 and 512
  28. recommended by the authors. Default is 256.
  29. patch_size : int, optional
  30. Length of the two dimensional square patch sampling region around
  31. the keypoints. Default is 49.
  32. mode : {'normal', 'uniform'}, optional
  33. Probability distribution for sampling location of decision pixel-pairs
  34. around keypoints.
  35. rng : {`numpy.random.Generator`, int}, optional
  36. Pseudo-random number generator (RNG).
  37. By default, a PCG64 generator is used (see :func:`numpy.random.default_rng`).
  38. If `rng` is an int, it is used to seed the generator.
  39. The PRNG is used for the random sampling of the decision
  40. pixel-pairs. From a square window with length `patch_size`,
  41. pixel pairs are sampled using the `mode` parameter to build
  42. the descriptors using intensity comparison.
  43. For matching across images, the same `rng` should be used to construct
  44. descriptors. To facilitate this:
  45. (a) `rng` defaults to 1
  46. (b) Subsequent calls of the ``extract`` method will use the same rng/seed.
  47. sigma : float, optional
  48. Standard deviation of the Gaussian low-pass filter applied to the image
  49. to alleviate noise sensitivity, which is strongly recommended to obtain
  50. discriminative and good descriptors.
  51. Attributes
  52. ----------
  53. descriptors : (Q, `descriptor_size`) array of dtype bool
  54. 2D ndarray of binary descriptors of size `descriptor_size` for Q
  55. keypoints after filtering out border keypoints with value at an
  56. index ``(i, j)`` either being ``True`` or ``False`` representing
  57. the outcome of the intensity comparison for i-th keypoint on j-th
  58. decision pixel-pair. It is ``Q == np.sum(mask)``.
  59. mask : (N,) array of dtype bool
  60. Mask indicating whether a keypoint has been filtered out
  61. (``False``) or is described in the `descriptors` array (``True``).
  62. Examples
  63. --------
  64. >>> from skimage.feature import (corner_harris, corner_peaks, BRIEF,
  65. ... match_descriptors)
  66. >>> import numpy as np
  67. >>> square1 = np.zeros((8, 8), dtype=np.int32)
  68. >>> square1[2:6, 2:6] = 1
  69. >>> square1
  70. array([[0, 0, 0, 0, 0, 0, 0, 0],
  71. [0, 0, 0, 0, 0, 0, 0, 0],
  72. [0, 0, 1, 1, 1, 1, 0, 0],
  73. [0, 0, 1, 1, 1, 1, 0, 0],
  74. [0, 0, 1, 1, 1, 1, 0, 0],
  75. [0, 0, 1, 1, 1, 1, 0, 0],
  76. [0, 0, 0, 0, 0, 0, 0, 0],
  77. [0, 0, 0, 0, 0, 0, 0, 0]], dtype=int32)
  78. >>> square2 = np.zeros((9, 9), dtype=np.int32)
  79. >>> square2[2:7, 2:7] = 1
  80. >>> square2
  81. array([[0, 0, 0, 0, 0, 0, 0, 0, 0],
  82. [0, 0, 0, 0, 0, 0, 0, 0, 0],
  83. [0, 0, 1, 1, 1, 1, 1, 0, 0],
  84. [0, 0, 1, 1, 1, 1, 1, 0, 0],
  85. [0, 0, 1, 1, 1, 1, 1, 0, 0],
  86. [0, 0, 1, 1, 1, 1, 1, 0, 0],
  87. [0, 0, 1, 1, 1, 1, 1, 0, 0],
  88. [0, 0, 0, 0, 0, 0, 0, 0, 0],
  89. [0, 0, 0, 0, 0, 0, 0, 0, 0]], dtype=int32)
  90. >>> keypoints1 = corner_peaks(corner_harris(square1), min_distance=1)
  91. >>> keypoints2 = corner_peaks(corner_harris(square2), min_distance=1)
  92. >>> extractor = BRIEF(patch_size=5)
  93. >>> extractor.extract(square1, keypoints1)
  94. >>> descriptors1 = extractor.descriptors
  95. >>> extractor.extract(square2, keypoints2)
  96. >>> descriptors2 = extractor.descriptors
  97. >>> matches = match_descriptors(descriptors1, descriptors2)
  98. >>> matches
  99. array([[0, 0],
  100. [1, 1],
  101. [2, 2],
  102. [3, 3]])
  103. >>> keypoints1[matches[:, 0]]
  104. array([[2, 2],
  105. [2, 5],
  106. [5, 2],
  107. [5, 5]])
  108. >>> keypoints2[matches[:, 1]]
  109. array([[2, 2],
  110. [2, 6],
  111. [6, 2],
  112. [6, 6]])
  113. """
  114. def __init__(
  115. self, descriptor_size=256, patch_size=49, mode='normal', sigma=1, rng=1
  116. ):
  117. mode = mode.lower()
  118. if mode not in ('normal', 'uniform'):
  119. raise ValueError("`mode` must be 'normal' or 'uniform'.")
  120. self.descriptor_size = descriptor_size
  121. self.patch_size = patch_size
  122. self.mode = mode
  123. self.sigma = sigma
  124. if isinstance(rng, np.random.Generator):
  125. # Spawn an independent RNG from parent RNG provided by the user.
  126. # This is necessary so that we can safely deepcopy the RNG.
  127. # See https://github.com/scikit-learn/scikit-learn/issues/16988#issuecomment-1518037853
  128. bg = rng._bit_generator
  129. ss = bg._seed_seq
  130. (child_ss,) = ss.spawn(1)
  131. self.rng = np.random.Generator(type(bg)(child_ss))
  132. elif rng is None:
  133. self.rng = np.random.default_rng(np.random.SeedSequence())
  134. else:
  135. self.rng = np.random.default_rng(rng)
  136. self.descriptors = None
  137. self.mask = None
  138. def extract(self, image, keypoints):
  139. """Extract BRIEF binary descriptors for given keypoints in image.
  140. Parameters
  141. ----------
  142. image : 2D array
  143. Input image.
  144. keypoints : (N, 2) array
  145. Keypoint coordinates as ``(row, col)``.
  146. """
  147. check_nD(image, 2)
  148. # Copy RNG so we can repeatedly call extract with the same random values
  149. rng = copy.deepcopy(self.rng)
  150. image = _prepare_grayscale_input_2D(image)
  151. # Gaussian low-pass filtering to alleviate noise sensitivity
  152. image = np.ascontiguousarray(gaussian(image, sigma=self.sigma, mode='reflect'))
  153. # Sampling pairs of decision pixels in patch_size x patch_size window
  154. desc_size = self.descriptor_size
  155. patch_size = self.patch_size
  156. if self.mode == 'normal':
  157. samples = (patch_size / 5.0) * rng.standard_normal(desc_size * 8)
  158. samples = np.array(samples, dtype=np.int32)
  159. samples = samples[
  160. (samples < (patch_size // 2)) & (samples > -(patch_size - 2) // 2)
  161. ]
  162. pos1 = samples[: desc_size * 2].reshape(desc_size, 2)
  163. pos2 = samples[desc_size * 2 : desc_size * 4].reshape(desc_size, 2)
  164. elif self.mode == 'uniform':
  165. samples = rng.integers(
  166. -(patch_size - 2) // 2, (patch_size // 2) + 1, (desc_size * 2, 2)
  167. )
  168. samples = np.array(samples, dtype=np.int32)
  169. pos1, pos2 = np.split(samples, 2)
  170. pos1 = np.ascontiguousarray(pos1)
  171. pos2 = np.ascontiguousarray(pos2)
  172. # Removing keypoints that are within (patch_size / 2) distance from the
  173. # image border
  174. self.mask = _mask_border_keypoints(image.shape, keypoints, patch_size // 2)
  175. keypoints = np.array(
  176. keypoints[self.mask, :],
  177. dtype=np.int64,
  178. order='C',
  179. copy=None if np2 else False,
  180. )
  181. self.descriptors = np.zeros(
  182. (keypoints.shape[0], desc_size), dtype=bool, order='C'
  183. )
  184. _brief_loop(image, self.descriptors.view(np.uint8), keypoints, pos1, pos2)