Implement CenSurE detector in object oriented interface

This commit is contained in:
Johannes Schönberger
2013-11-29 23:32:50 +01:00
parent 29026e7231
commit 79616815ff
8 changed files with 231 additions and 200 deletions
+3 -3
View File
@@ -9,10 +9,10 @@ from .corner import (corner_kitchen_rosenfeld, corner_harris,
hessian_matrix_eigvals)
from .corner_cy import corner_moravec, corner_orientations
from .template import match_template
from ._brief import BRIEF
from .brief import BRIEF
from .censure import CenSurE
from .match import match_binary_descriptors
from .util import pairwise_hamming_distance
from .censure import keypoints_censure
from .orb import keypoints_orb, descriptor_orb
__all__ = ['daisy',
@@ -30,9 +30,9 @@ __all__ = ['daisy',
'corner_moravec',
'match_template',
'BRIEF',
'CenSurE',
'pairwise_hamming_distance',
'match_binary_descriptors',
'keypoints_censure',
'corner_fast',
'corner_orientations',
'structure_tensor',
@@ -101,6 +101,7 @@ class BRIEF(DescriptorExtractor):
def __init__(self, descriptor_size=256, patch_size=49,
mode='normal', sigma=1, sample_seed=1):
mode = mode.lower()
if mode not in ('normal', 'uniform'):
raise ValueError("`mode` must be 'normal' or 'uniform'.")
+95 -76
View File
@@ -1,7 +1,7 @@
import numpy as np
from scipy.ndimage.filters import maximum_filter, minimum_filter, convolve
from .util import _prepare_grayscale_input_2D
from skimage.feature.util import FeatureDetector, _prepare_grayscale_input_2D
from skimage.transform import integral_image
from skimage.feature import structure_tensor
@@ -66,19 +66,19 @@ def _filter_image(image, min_scale, max_scale, mode):
mo, no = OCTAGON_OUTER_SHAPE[min_scale + i - 1]
mi, ni = OCTAGON_INNER_SHAPE[min_scale + i - 1]
response[:, :, i] = convolve(image,
_octagon_filter_kernel(mo, no, mi, ni))
_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_filter_kernel(m, n))
response[:, :, i] = convolve(image, _star_kernel(m, n))
return response
def _octagon_filter_kernel(mo, no, mi, ni):
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)
@@ -92,7 +92,7 @@ def _octagon_filter_kernel(mo, no, mi, ni):
return bfilter
def _star_filter_kernel(m, n):
def _star_kernel(m, n):
c = m + m // 2 - n - n // 2
outer_star = star(m)
inner_star = np.zeros_like(outer_star)
@@ -106,21 +106,15 @@ def _star_filter_kernel(m, n):
def _suppress_lines(feature_mask, image, sigma, line_threshold):
Axx, Axy, Ayy = structure_tensor(image, sigma)
feature_mask[(Axx + Ayy) * (Axx + Ayy)
> line_threshold * (Axx * Ayy - Axy * Axy)] = False
feature_mask[(Axx + Ayy) ** 2
> line_threshold * (Axx * Ayy - Axy ** 2)] = False
def keypoints_censure(image, min_scale=1, max_scale=7, mode='DoB',
non_max_threshold=0.15, line_threshold=10):
"""**Experimental function**.
Extracts CenSurE keypoints along with the corresponding scale using
either Difference of Boxes, Octagon or STAR bi-level filter.
class CenSurE(FeatureDetector):
"""CenSurE keypoint detector.
Parameters
----------
image : 2D ndarray
Input image.
min_scale : int
Minimum scale to extract keypoints from.
max_scale : int
@@ -145,13 +139,6 @@ def keypoints_censure(image, min_scale=1, max_scale=7, mode='DoB',
Threshold for rejecting interest points which have ratio of principal
curvatures greater than this value.
Returns
-------
keypoints : (N, 2) array
Location of the extracted keypoints in the ``(row, col)`` format.
scales : (N, 1) array
The corresponding scale of the N extracted keypoints.
References
----------
.. [1] Motilal Agrawal, Kurt Konolige and Morten Rufus Blas
@@ -166,69 +153,101 @@ def keypoints_censure(image, min_scale=1, max_scale=7, mode='DoB',
"""
# (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 neighbourhood. 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.
def __init__(self, min_scale=1, max_scale=7, mode='DoB',
non_max_threshold=0.15, line_threshold=10):
image = _prepare_grayscale_input_2D(image)
mode = mode.lower()
if mode not in ('dob', 'octagon', 'star'):
raise ValueError("`mode` must be one of 'DoB', 'Octagon', 'STAR'.")
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.')
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
image = np.ascontiguousarray(image)
def detect(self, image):
"""Detect CenSurE keypoints along with the corresponding scale.
# Generating all the scales
filter_response = _filter_image(image, min_scale, max_scale, mode)
Parameters
----------
image : 2D ndarray
Input image.
# Suppressing points that are neither minima or maxima in their 3 x 3 x 3
# neighbourhood to zero
minimas = minimum_filter(filter_response, (3, 3, 3)) == filter_response
maximas = maximum_filter(filter_response, (3, 3, 3)) == filter_response
Returns
-------
keypoints : (N, 2) array
Keypoint coordinates as ``(row, col)``.
scales : (N, 1) array
Corresponding scales of the N extracted keypoints.
feature_mask = minimas | maximas
feature_mask[filter_response < non_max_threshold] = False
"""
for i in range(1, max_scale - min_scale):
# 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 + (min_scale + i - 1) / 3.0), line_threshold)
# (1) First we generate the required scales on the input grayscale
# image using a bi-level filter and stack them up in `filter_response`.
rows, cols, scales = np.nonzero(feature_mask[..., 1:max_scale - min_scale])
keypoints = np.column_stack([rows, cols])
scales = scales + min_scale + 1
# (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 neighbourhood. 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.
if mode == 'dob':
return keypoints, scales
num_scales = self.max_scale - self.min_scale
cumulative_mask = np.zeros(keypoints.shape[0], dtype=np.bool)
image = np.ascontiguousarray(_prepare_grayscale_input_2D(image))
if mode == 'octagon':
for i in range(min_scale + 1, max_scale):
c = (OCTAGON_OUTER_SHAPE[i - 1][0] - 1) // 2 \
+ OCTAGON_OUTER_SHAPE[i - 1][1]
cumulative_mask |= _mask_border_keypoints(image, keypoints, c) \
& (scales == i)
elif mode == 'star':
for i in range(min_scale + 1, 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, keypoints, c) \
& (scales == i)
# Generating all the scales
filter_response = _filter_image(image, self.min_scale, self.max_scale,
self.mode)
return keypoints[cumulative_mask], scales[cumulative_mask]
# 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':
return keypoints, scales
cumulative_mask = np.zeros(keypoints.shape[0], dtype=np.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))
return keypoints[cumulative_mask], scales[cumulative_mask]
+41 -30
View File
@@ -1,8 +1,7 @@
import numpy as np
from skimage.feature.util import (_mask_border_keypoints,
_prepare_grayscale_input_2D,
create_keypoint_recarray)
_prepare_grayscale_input_2D)
from skimage.feature import (corner_fast, corner_orientations, corner_peaks,
corner_harris)
@@ -12,36 +11,39 @@ from .orb_cy import _orb_loop
OFAST_MASK = np.zeros((31, 31))
umax = [15, 15, 15, 15, 14, 14, 14, 13, 13, 12, 11, 10, 9, 8, 6, 3]
OFAST_UMAX = [15, 15, 15, 15, 14, 14, 14, 13, 13, 12, 11, 10, 9, 8, 6, 3]
for i in range(-15, 16):
for j in range(-umax[np.abs(i)], umax[np.abs(i)] + 1):
for j in range(-OFAST_UMAX[abs(i)], OFAST_UMAX[abs(i)] + 1):
OFAST_MASK[15 + j, 15 + i] = 1
def keypoints_orb(image, n_keypoints=500, fast_n=9, fast_threshold=0.08,
harris_k=0.04, downscale=1.2, n_scales=8):
"""Detect Oriented Fast keypoints.
"""Detect oriented FAST keypoints.
Parameters
----------
image : 2D ndarray
Input grayscale image.
Input image.
n_keypoints : int
Number of keypoints to be returned from this function. The function
will return best ``n_keypoints`` if more than n_keypoints are detected
based on the values of other parameters. If not, then all the detected
keypoints are returned.
Number of keypoints to be returned. The function will return the best
``n_keypoints`` according to the Harris corner response if more than
``n_keypoints`` are detected. If not, then all the detected keypoints
are returned.
fast_n : int
The ``n`` parameter in ``feature.corner_fast``. Minimum number of
consecutive pixels out of 16 pixels on the circle that should all be
either brighter or darker w.r.t testpixel. A point c on the circle is
either brighter or darker w.r.t test-pixel. A point c on the circle is
darker w.r.t test pixel p if ``Ic < Ip - threshold`` and brighter if
``Ic > Ip + threshold``. Also stands for the n in ``FAST-n`` corner
detector.
fast_threshold : float
The ``threshold`` parameter in ``feature.corner_fast``. Threshold used to
decide whether the pixels on the circle are brighter, darker or
The ``threshold`` parameter in ``feature.corner_fast``. Threshold used
to decide whether the pixels on the circle are brighter, darker or
similar w.r.t. the test pixel. Decrease the threshold when more
corners are desired and vice-versa.
harris_k : float
@@ -50,15 +52,17 @@ def keypoints_orb(image, n_keypoints=500, fast_n=9, fast_threshold=0.08,
values of k result in detection of sharp corners.
downscale : float
Downscale factor for the image pyramid. Default value 1.2 is chosen so
that we have more dense scales that enable robust scale invariance.
that there are more dense scales which enable robust scale invariance
for a subsequent feature description.
n_scales : int
Number of scales from the bottom of the image pyramid to extract
the features from.
Maximum number of scales from the bottom of the image pyramid to
extract the features from.
Returns
-------
keypoints : record array
Record array with fields row, col, octave, orientation, response.
keypoints : (N, ...) recarray
Record array as returned by `skimage.feature.create_keypoint_recarray`
with the fields: `row`, `col`, `scale`, `orientation` and `response`.
References
----------
@@ -97,46 +101,53 @@ def keypoints_orb(image, n_keypoints=500, fast_n=9, fast_threshold=0.08,
scales_list = []
harris_response_list = []
for scale in range(n_scales):
for octave in range(len(pyramid)):
corners = corner_peaks(corner_fast(pyramid[scale], fast_n,
# Extract keypoints for current octave
corners = corner_peaks(corner_fast(pyramid[octave], fast_n,
fast_threshold), min_distance=1)
keypoints_list.append(corners * downscale ** scale)
orientations_list.append(corner_orientations(pyramid[scale], corners,
# Scale keypoint coordinates so they correspond to the original
# image shape
keypoints_list.append(corners * downscale ** octave)
orientations_list.append(corner_orientations(pyramid[octave], corners,
OFAST_MASK))
scales_list.append(scale * np.ones(corners.shape[0], dtype=np.intp))
scales_list.append(octave * np.ones(corners.shape[0], dtype=np.intp))
harris_response = corner_harris(pyramid[scale], method='k', k=harris_k)
harris_response = corner_harris(pyramid[octave], method='k',
k=harris_k)
harris_response_list.append(harris_response[corners[:, 0],
corners[:, 1]])
keypoints_array = np.vstack(keypoints_list)
orientations = np.hstack(orientations_list)
octaves = downscale ** np.hstack(scales_list)
scales = downscale ** np.hstack(scales_list)
harris_measure = np.hstack(harris_response_list)
keypoints = create_keypoint_recarray(keypoints_array[:, 0],
keypoints_array[:, 1],
octaves, orientations,
scales, orientations,
harris_measure)
if keypoints.shape[0] < n_keypoints:
return keypoints
else:
# Choose best n_keypoints according to Harris corner response
best_indices = harris_measure.argsort()[::-1][:n_keypoints]
return keypoints[best_indices]
def descriptor_orb(image, keypoints, downscale=1.2, n_scales=8):
"""Compute rBRIEF descriptors of input keypoints.
"""Compute rBRIEF descriptors for keypoints.
Parameters
----------
image : 2D ndarray
Input grayscale image.
keypoints : record array
Record array with fields row, col, octave, orientation, response.
Input image.
keypoints : (N, ...) recarray
Record array as returned by `skimage.feature.create_keypoint_recarray`
with the fields: `row`, `col`, `scale`, `orientation` and `response`.
downscale : float
Downscale factor for the image pyramid. Should be the same as that
used in ``keypoints_orb``.
+2 -2
View File
@@ -15,7 +15,7 @@ def configuration(parent_package='', top_path=None):
cython(['corner_cy.pyx'], working_path=base_path)
cython(['censure_cy.pyx'], working_path=base_path)
cython(['orb_cy.pyx'], working_path=base_path)
cython(['_brief_cy.pyx'], working_path=base_path)
cython(['brief_cy.pyx'], working_path=base_path)
cython(['match_cy.pyx'], working_path=base_path)
cython(['_texture.pyx'], working_path=base_path)
cython(['_template.pyx'], working_path=base_path)
@@ -26,7 +26,7 @@ def configuration(parent_package='', top_path=None):
include_dirs=[get_numpy_include_dirs()])
config.add_extension('orb_cy', sources=['orb_cy.c'],
include_dirs=[get_numpy_include_dirs()])
config.add_extension('_brief_cy', sources=['_brief_cy.c'],
config.add_extension('brief_cy', sources=['brief_cy.c'],
include_dirs=[get_numpy_include_dirs()])
config.add_extension('match_cy', sources=['match_cy.c'],
include_dirs=[get_numpy_include_dirs()])
-89
View File
@@ -1,89 +0,0 @@
import numpy as np
from numpy.testing import assert_array_equal, assert_raises
from skimage.data import moon
from skimage.feature import keypoints_censure
def test_keypoints_censure_color_image_unsupported_error():
"""Censure keypoints can be extracted from gray-scale images only."""
img = np.zeros((20, 20, 3))
assert_raises(ValueError, keypoints_censure, img)
def test_keypoints_censure_mode_validity_error():
"""Mode argument in keypoints_censure can be either DoB, Octagon or
STAR."""
img = np.zeros((20, 20))
assert_raises(ValueError, keypoints_censure, img, mode='dummy')
def test_keypoints_censure_scale_range_error():
"""Difference between the the max_scale and min_scale parameters in
keypoints_censure should be greater than or equal to two."""
img = np.zeros((20, 20))
assert_raises(ValueError, keypoints_censure, img, min_scale=1, max_scale=2)
def test_keypoints_censure_moon_image_dob():
"""Verify the actual Censure keypoints and their corresponding scale with
the expected values for DoB filter."""
img = moon()
actual_kp_dob, actual_scale = keypoints_censure(img, 1, 7, 'DoB', 0.15)
expected_kp_dob = np.array([[ 21, 497],
[ 36, 46],
[119, 350],
[185, 177],
[287, 250],
[357, 239],
[463, 116],
[464, 132],
[467, 260]])
expected_scale = np.array([3, 4, 4, 2, 2, 3, 2, 2, 2])
assert_array_equal(expected_kp_dob, actual_kp_dob)
assert_array_equal(expected_scale, actual_scale)
def test_keypoints_censure_moon_image_octagon():
"""Verify the actual Censure keypoints and their corresponding scale with
the expected values for Octagon filter."""
img = moon()
actual_kp_octagon, actual_scale = keypoints_censure(img, 1, 7, 'Octagon',
0.15)
expected_kp_octagon = np.array([[ 21, 496],
[ 35, 46],
[287, 250],
[356, 239],
[463, 116]])
expected_scale = np.array([3, 4, 2, 2, 2])
assert_array_equal(expected_kp_octagon, actual_kp_octagon)
assert_array_equal(expected_scale, actual_scale)
def test_keypoints_censure_moon_image_star():
"""Verify the actual Censure keypoints and their corresponding scale with
the expected values for STAR filter."""
img = moon()
actual_kp_star, actual_scale = keypoints_censure(img, 1, 7, 'STAR', 0.15)
expected_kp_star = np.array([[ 21, 497],
[ 36, 46],
[117, 356],
[185, 177],
[260, 227],
[287, 250],
[357, 239],
[451, 281],
[463, 116],
[467, 260]])
expected_scale = np.array([3, 3, 6, 2, 3, 2, 3, 5, 2, 2])
assert_array_equal(expected_kp_star, actual_kp_star)
assert_array_equal(expected_scale, actual_scale)
if __name__ == '__main__':
from numpy import testing
testing.run_module_suite()
+89
View File
@@ -0,0 +1,89 @@
import numpy as np
from numpy.testing import assert_array_equal, assert_raises
from skimage.data import moon
from skimage.feature import CenSurE
img = moon()
def test_keypoints_censure_color_image_unsupported_error():
"""Censure keypoints can be extracted from gray-scale images only."""
assert_raises(ValueError, CenSurE().detect, np.zeros((20, 20, 3)))
def test_keypoints_censure_mode_validity_error():
"""Mode argument in keypoints_censure can be either DoB, Octagon or
STAR."""
assert_raises(ValueError, CenSurE, mode='dummy')
def test_keypoints_censure_scale_range_error():
"""Difference between the the max_scale and min_scale parameters in
keypoints_censure should be greater than or equal to two."""
assert_raises(ValueError, CenSurE, min_scale=1, max_scale=2)
def test_keypoints_censure_moon_image_dob():
"""Verify the actual Censure keypoints and their corresponding scale with
the expected values for DoB filter."""
detector = CenSurE()
keypoints, scales = detector.detect(img)
expected_keypoints = np.array([[ 21, 497],
[ 36, 46],
[119, 350],
[185, 177],
[287, 250],
[357, 239],
[463, 116],
[464, 132],
[467, 260]])
expected_scales = np.array([3, 4, 4, 2, 2, 3, 2, 2, 2])
assert_array_equal(expected_keypoints, keypoints)
assert_array_equal(expected_scales, scales)
def test_keypoints_censure_moon_image_octagon():
"""Verify the actual Censure keypoints and their corresponding scale with
the expected values for Octagon filter."""
detector = CenSurE(mode='octagon')
keypoints, scales = detector.detect(img)
expected_keypoints = np.array([[ 21, 496],
[ 35, 46],
[287, 250],
[356, 239],
[463, 116]])
expected_scales = np.array([3, 4, 2, 2, 2])
assert_array_equal(expected_keypoints, keypoints)
assert_array_equal(expected_scales, scales)
def test_keypoints_censure_moon_image_star():
"""Verify the actual Censure keypoints and their corresponding scale with
the expected values for STAR filter."""
detector = CenSurE(mode='star')
keypoints, scales = detector.detect(img)
expected_keypoints = np.array([[ 21, 497],
[ 36, 46],
[117, 356],
[185, 177],
[260, 227],
[287, 250],
[357, 239],
[451, 281],
[463, 116],
[467, 260]])
expected_scale = np.array([3, 3, 6, 2, 3, 2, 3, 5, 2, 2])
assert_array_equal(expected_keypoints, keypoints)
assert_array_equal(expected_scale, scales)
if __name__ == '__main__':
from numpy import testing
testing.run_module_suite()