| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346 |
- import numpy as np
- from scipy.ndimage import maximum_filter, minimum_filter, convolve
- from ..transform import integral_image
- from .corner import structure_tensor
- from ..morphology import octagon, star
- from .censure_cy import _censure_dob_loop
- from ..feature.util import (
- FeatureDetector,
- _prepare_grayscale_input_2D,
- _mask_border_keypoints,
- )
- from .._shared.utils import check_nD
- # The paper(Reference [1]) mentions the sizes of the Octagon shaped filter
- # kernel for the first seven scales only. The sizes of the later scales
- # have been extrapolated based on the following statement in the paper.
- # "These octagons scale linearly and were experimentally chosen to correspond
- # to the seven DOBs described in the previous section."
- OCTAGON_OUTER_SHAPE = [
- (5, 2),
- (5, 3),
- (7, 3),
- (9, 4),
- (9, 7),
- (13, 7),
- (15, 10),
- (15, 11),
- (15, 12),
- (17, 13),
- (17, 14),
- ]
- OCTAGON_INNER_SHAPE = [
- (3, 0),
- (3, 1),
- (3, 2),
- (5, 2),
- (5, 3),
- (5, 4),
- (5, 5),
- (7, 5),
- (7, 6),
- (9, 6),
- (9, 7),
- ]
- # The sizes for the STAR shaped filter kernel for different scales have been
- # taken from the OpenCV implementation.
- STAR_SHAPE = [1, 2, 3, 4, 6, 8, 11, 12, 16, 22, 23, 32, 45, 46, 64, 90, 128]
- STAR_FILTER_SHAPE = [
- (1, 0),
- (3, 1),
- (4, 2),
- (5, 3),
- (7, 4),
- (8, 5),
- (9, 6),
- (11, 8),
- (13, 10),
- (14, 11),
- (15, 12),
- (16, 14),
- ]
- def _filter_image(image, min_scale, max_scale, mode):
- response = np.zeros(
- (image.shape[0], image.shape[1], max_scale - min_scale + 1), dtype=np.float64
- )
- if mode == 'dob':
- # make response[:, :, i] contiguous memory block
- item_size = response.itemsize
- response = np.lib.stride_tricks.as_strided(
- response,
- strides=(
- item_size * response.shape[1],
- item_size,
- item_size * response.shape[0] * response.shape[1],
- ),
- )
- integral_img = integral_image(image)
- for i in range(max_scale - min_scale + 1):
- n = min_scale + i
- # Constant multipliers for the outer region and the inner region
- # of the bi-level filters with the constraint of keeping the
- # DC bias 0.
- inner_weight = 1.0 / (2 * n + 1) ** 2
- outer_weight = 1.0 / (12 * n**2 + 4 * n)
- _censure_dob_loop(
- n, integral_img, response[:, :, i], inner_weight, outer_weight
- )
- # NOTE : For the Octagon shaped filter, we implemented and evaluated the
- # slanted integral image based image filtering but the performance was
- # more or less equal to image filtering using
- # scipy.ndimage.filters.convolve(). Hence we have decided to use the
- # later for a much cleaner implementation.
- elif mode == 'octagon':
- # TODO : Decide the shapes of Octagon filters for scales > 7
- for i in range(max_scale - min_scale + 1):
- mo, no = OCTAGON_OUTER_SHAPE[min_scale + i - 1]
- mi, ni = OCTAGON_INNER_SHAPE[min_scale + i - 1]
- response[:, :, i] = convolve(image, _octagon_kernel(mo, no, mi, ni))
- elif mode == 'star':
- for i in range(max_scale - min_scale + 1):
- m = STAR_SHAPE[STAR_FILTER_SHAPE[min_scale + i - 1][0]]
- n = STAR_SHAPE[STAR_FILTER_SHAPE[min_scale + i - 1][1]]
- response[:, :, i] = convolve(image, _star_kernel(m, n))
- return response
- def _octagon_kernel(mo, no, mi, ni):
- outer = (mo + 2 * no) ** 2 - 2 * no * (no + 1)
- inner = (mi + 2 * ni) ** 2 - 2 * ni * (ni + 1)
- outer_weight = 1.0 / (outer - inner)
- inner_weight = 1.0 / inner
- c = ((mo + 2 * no) - (mi + 2 * ni)) // 2
- outer_oct = octagon(mo, no)
- inner_oct = np.zeros((mo + 2 * no, mo + 2 * no))
- inner_oct[c:-c, c:-c] = octagon(mi, ni)
- bfilter = outer_weight * outer_oct - (outer_weight + inner_weight) * inner_oct
- return bfilter
- def _star_kernel(m, n):
- c = m + m // 2 - n - n // 2
- outer_star = star(m)
- inner_star = np.zeros_like(outer_star)
- inner_star[c:-c, c:-c] = star(n)
- outer_weight = 1.0 / (np.sum(outer_star - inner_star))
- inner_weight = 1.0 / np.sum(inner_star)
- bfilter = outer_weight * outer_star - (outer_weight + inner_weight) * inner_star
- return bfilter
- def _suppress_lines(feature_mask, image, sigma, line_threshold):
- Arr, Arc, Acc = structure_tensor(image, sigma, order='rc')
- feature_mask[(Arr + Acc) ** 2 > line_threshold * (Arr * Acc - Arc**2)] = False
- class CENSURE(FeatureDetector):
- """CENSURE keypoint detector.
- min_scale : int, optional
- Minimum scale to extract keypoints from.
- max_scale : int, optional
- Maximum scale to extract keypoints from. The keypoints will be
- extracted from all the scales except the first and the last i.e.
- from the scales in the range [min_scale + 1, max_scale - 1]. The filter
- sizes for different scales is such that the two adjacent scales
- comprise of an octave.
- mode : {'DoB', 'Octagon', 'STAR'}, optional
- Type of bi-level filter used to get the scales of the input image.
- Possible values are 'DoB', 'Octagon' and 'STAR'. The three modes
- represent the shape of the bi-level filters i.e. box(square), octagon
- and star respectively. For instance, a bi-level octagon filter consists
- of a smaller inner octagon and a larger outer octagon with the filter
- weights being uniformly negative in both the inner octagon while
- uniformly positive in the difference region. Use STAR and Octagon for
- better features and DoB for better performance.
- non_max_threshold : float, optional
- Threshold value used to suppress maximas and minimas with a weak
- magnitude response obtained after Non-Maximal Suppression.
- line_threshold : float, optional
- Threshold for rejecting interest points which have ratio of principal
- curvatures greater than this value.
- Attributes
- ----------
- keypoints : (N, 2) array
- Keypoint coordinates as ``(row, col)``.
- scales : (N,) array
- Corresponding scales.
- References
- ----------
- .. [1] Motilal Agrawal, Kurt Konolige and Morten Rufus Blas
- "CENSURE: Center Surround Extremas for Realtime Feature
- Detection and Matching",
- https://link.springer.com/chapter/10.1007/978-3-540-88693-8_8
- :DOI:`10.1007/978-3-540-88693-8_8`
- .. [2] Adam Schmidt, Marek Kraft, Michal Fularz and Zuzanna Domagala
- "Comparative Assessment of Point Feature Detectors and
- Descriptors in the Context of Robot Navigation"
- http://yadda.icm.edu.pl/yadda/element/bwmeta1.element.baztech-268aaf28-0faf-4872-a4df-7e2e61cb364c/c/Schmidt_comparative.pdf
- :DOI:`10.1.1.465.1117`
- Examples
- --------
- >>> from skimage.data import astronaut
- >>> from skimage.color import rgb2gray
- >>> from skimage.feature import CENSURE
- >>> img = rgb2gray(astronaut()[100:300, 100:300])
- >>> censure = CENSURE()
- >>> censure.detect(img)
- >>> censure.keypoints
- array([[ 4, 148],
- [ 12, 73],
- [ 21, 176],
- [ 91, 22],
- [ 93, 56],
- [ 94, 22],
- [ 95, 54],
- [100, 51],
- [103, 51],
- [106, 67],
- [108, 15],
- [117, 20],
- [122, 60],
- [125, 37],
- [129, 37],
- [133, 76],
- [145, 44],
- [146, 94],
- [150, 114],
- [153, 33],
- [154, 156],
- [155, 151],
- [184, 63]])
- >>> censure.scales
- array([2, 6, 6, 2, 4, 3, 2, 3, 2, 6, 3, 2, 2, 3, 2, 2, 2, 3, 2, 2, 4, 2,
- 2])
- """
- def __init__(
- self,
- min_scale=1,
- max_scale=7,
- mode='DoB',
- non_max_threshold=0.15,
- line_threshold=10,
- ):
- mode = mode.lower()
- if mode not in ('dob', 'octagon', 'star'):
- raise ValueError("`mode` must be one of 'DoB', 'Octagon', 'STAR'.")
- if min_scale < 1 or max_scale < 1 or max_scale - min_scale < 2:
- raise ValueError(
- 'The scales must be >= 1 and the number of ' 'scales should be >= 3.'
- )
- self.min_scale = min_scale
- self.max_scale = max_scale
- self.mode = mode
- self.non_max_threshold = non_max_threshold
- self.line_threshold = line_threshold
- self.keypoints = None
- self.scales = None
- def detect(self, image):
- """Detect CENSURE keypoints along with the corresponding scale.
- Parameters
- ----------
- image : 2D ndarray
- Input image.
- """
- # (1) First we generate the required scales on the input grayscale
- # image using a bi-level filter and stack them up in `filter_response`.
- # (2) We then perform Non-Maximal suppression in 3 x 3 x 3 window on
- # the filter_response to suppress points that are neither minima or
- # maxima in 3 x 3 x 3 neighborhood. We obtain a boolean ndarray
- # `feature_mask` containing all the minimas and maximas in
- # `filter_response` as True.
- # (3) Then we suppress all the points in the `feature_mask` for which
- # the corresponding point in the image at a particular scale has the
- # ratio of principal curvatures greater than `line_threshold`.
- # (4) Finally, we remove the border keypoints and return the keypoints
- # along with its corresponding scale.
- check_nD(image, 2)
- num_scales = self.max_scale - self.min_scale
- image = np.ascontiguousarray(_prepare_grayscale_input_2D(image))
- # Generating all the scales
- filter_response = _filter_image(
- image, self.min_scale, self.max_scale, self.mode
- )
- # Suppressing points that are neither minima or maxima in their
- # 3 x 3 x 3 neighborhood to zero
- minimas = minimum_filter(filter_response, (3, 3, 3)) == filter_response
- maximas = maximum_filter(filter_response, (3, 3, 3)) == filter_response
- feature_mask = minimas | maximas
- feature_mask[filter_response < self.non_max_threshold] = False
- for i in range(1, num_scales):
- # sigma = (window_size - 1) / 6.0, so the window covers > 99% of
- # the kernel's distribution
- # window_size = 7 + 2 * (min_scale - 1 + i)
- # Hence sigma = 1 + (min_scale - 1 + i)/ 3.0
- _suppress_lines(
- feature_mask[:, :, i],
- image,
- (1 + (self.min_scale + i - 1) / 3.0),
- self.line_threshold,
- )
- rows, cols, scales = np.nonzero(feature_mask[..., 1:num_scales])
- keypoints = np.column_stack([rows, cols])
- scales = scales + self.min_scale + 1
- if self.mode == 'dob':
- self.keypoints = keypoints
- self.scales = scales
- return
- cumulative_mask = np.zeros(keypoints.shape[0], dtype=bool)
- if self.mode == 'octagon':
- for i in range(self.min_scale + 1, self.max_scale):
- c = (OCTAGON_OUTER_SHAPE[i - 1][0] - 1) // 2 + OCTAGON_OUTER_SHAPE[
- i - 1
- ][1]
- cumulative_mask |= _mask_border_keypoints(image.shape, keypoints, c) & (
- scales == i
- )
- elif self.mode == 'star':
- for i in range(self.min_scale + 1, self.max_scale):
- c = (
- STAR_SHAPE[STAR_FILTER_SHAPE[i - 1][0]]
- + STAR_SHAPE[STAR_FILTER_SHAPE[i - 1][0]] // 2
- )
- cumulative_mask |= _mask_border_keypoints(image.shape, keypoints, c) & (
- scales == i
- )
- self.keypoints = keypoints[cumulative_mask]
- self.scales = scales[cumulative_mask]
|