diff --git a/skimage/feature/__init__.py b/skimage/feature/__init__.py index 50217f53..c7a5a42f 100644 --- a/skimage/feature/__init__.py +++ b/skimage/feature/__init__.py @@ -9,9 +9,9 @@ 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 descriptor_brief +from ._brief import BRIEF from .match import match_binary_descriptors -from .util import pairwise_hamming_distance, create_keypoint_recarray +from .util import pairwise_hamming_distance from .censure import keypoints_censure from .orb import keypoints_orb, descriptor_orb @@ -29,9 +29,8 @@ __all__ = ['daisy', 'corner_peaks', 'corner_moravec', 'match_template', - 'descriptor_brief', + 'BRIEF', 'pairwise_hamming_distance', - 'create_keypoint_recarray', 'match_binary_descriptors', 'keypoints_censure', 'corner_fast', diff --git a/skimage/feature/_brief.py b/skimage/feature/_brief.py index 6f90c39e..048cd7bb 100644 --- a/skimage/feature/_brief.py +++ b/skimage/feature/_brief.py @@ -1,15 +1,15 @@ import numpy as np from scipy.ndimage.filters import gaussian_filter -from .util import (_mask_border_keypoints, pairwise_hamming_distance, +from .util import (DescriptorExtractor, _mask_border_keypoints, _prepare_grayscale_input_2D) from ._brief_cy import _brief_loop -def descriptor_brief(image, keypoints, descriptor_size=256, mode='normal', - patch_size=49, sample_seed=1, variance=2): - """Extract BRIEF binary descriptors for given keypoints in an image. +class BRIEF(DescriptorExtractor): + + """BRIEF binary descriptor extractor. BRIEF (Binary Robust Independent Elementary Features) is an efficient feature point descriptor. It it is highly discriminative even when using @@ -24,58 +24,34 @@ def descriptor_brief(image, keypoints, descriptor_size=256, mode='normal', Parameters ---------- - image : 2D ndarray - Input image. - keypoints : (P, ...) recarray - Record array as returned by `skimage.feature.create_keypoint_recarray` - with the fields: `row`, `col`, `scale`, `orientation` and `response`. descriptor_size : int Size of BRIEF descriptor for each keypoint. Sizes 128, 256 and 512 recommended by the authors. Default is 256. - mode : {'normal', 'uniform'} - Probability distribution for sampling location of decision pixel-pairs - around keypoints. patch_size : int Length of the two dimensional square patch sampling region around the keypoints. Default is 49. + mode : {'normal', 'uniform'} + Probability distribution for sampling location of decision pixel-pairs + around keypoints. sample_seed : int Seed for the random sampling of the decision pixel-pairs. From a square window with length patch_size, pixel pairs are sampled using the `mode` parameter to build the descriptors using intensity comparison. The value of `sample_seed` must be the same for the images to be matched while building the descriptors. - variance : float - Variance of the Gaussian low pass filter applied to the image to - alleviate noise sensitivity, which is strongly recommended to obtain + sigma : float + Standard deviation of the Gaussian low pass filter applied to the image + to alleviate noise sensitivity, which is strongly recommended to obtain discriminative and good descriptors. - Returns - ------- - descriptors : (Q, `descriptor_size`) ndarray of dtype bool - 2D ndarray of binary descriptors of size `descriptor_size` for Q - keypoints after filtering out border keypoints with value at an index - ``(i, j)`` either being `True` or `False` representing the outcome - of the intensity comparison for i-th keypoint on j-th decision - pixel-pair. - keypoints : (Q, ...) recarray - Record array as returned by `skimage.feature.create_keypoint_recarray` - with the fields: `row`, `col`, `scale`, `orientation` and `response`. - - References - ---------- - .. [1] Michael Calonder, Vincent Lepetit, Christoph Strecha and Pascal Fua - "BRIEF : Binary robust independent elementary features", 2010 - http://cvlabwww.epfl.ch/~lepetit/papers/calonder_eccv10.pdf - Examples -------- - >> from skimage.feature.corner import corner_peaks, corner_harris - >> from skimage.feature import (pairwise_hamming_distance, descriptor_brief, - ... match_binary_descriptors, - ... create_keypoint_recarray) - >> square1 = np.zeros([8, 8], dtype=np.int32) - >> square1[2:6, 2:6] = 1 - >> square1 + >>> from skimage.feature import (corner_harris, corner_peaks, BRIEF, + ... match_binary_descriptors) + >>> import numpy as np + >>> square1 = np.zeros((8, 8), dtype=np.int32) + >>> square1[2:6, 2:6] = 1 + >>> square1 array([[0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 1, 1, 1, 1, 0, 0], @@ -84,17 +60,9 @@ def descriptor_brief(image, keypoints, descriptor_size=256, mode='normal', [0, 0, 1, 1, 1, 1, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0]], dtype=int32) - >> keypoints1 = corner_peaks(corner_harris(square1), min_distance=1) - >> keypoints1 = create_keypoint_recarray(keypoints1[:, 0], keypoints1[:, 1]) - >> descriptors1, keypoints1 = descriptor_brief(square1, keypoints1, patch_size=5) - >> keypoints1 - rec.array([(2.0, 2.0, nan, nan, nan), (2.0, 5.0, nan, nan, nan), - (5.0, 2.0, nan, nan, nan), (5.0, 5.0, nan, nan, nan)], - dtype=[('row', '> square2 = np.zeros([9, 9], dtype=np.int32) - >> square2[2:7, 2:7] = 1 - >> square2 + >>> square2 = np.zeros((9, 9), dtype=np.int32) + >>> square2[2:7, 2:7] = 1 + >>> square2 array([[0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 1, 1, 1, 1, 1, 0, 0], @@ -104,24 +72,14 @@ def descriptor_brief(image, keypoints, descriptor_size=256, mode='normal', [0, 0, 1, 1, 1, 1, 1, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0]], dtype=int32) - >> keypoints2 = corner_peaks(corner_harris(square2), min_distance=1) - >> keypoints2 = create_keypoint_recarray(keypoints2[:, 0], keypoints2[:, 1]) - >> keypoints2 - rec.array([(2.0, 2.0, nan, nan, nan), (2.0, 6.0, nan, nan, nan), - (6.0, 2.0, nan, nan, nan), (6.0, 6.0, nan, nan, nan)], - dtype=[('row', '> descriptors2, keypoints2 = descriptor_brief(square2, keypoints2, patch_size=5) - >> pairwise_hamming_distance(descriptors1, descriptors2) - array([[ 0.03125 , 0.3203125, 0.3671875, 0.6171875], - [ 0.3203125, 0.03125 , 0.640625 , 0.375 ], - [ 0.375 , 0.6328125, 0.0390625, 0.328125 ], - [ 0.625 , 0.3671875, 0.34375 , 0.0234375]]) - >> matched_kpts, mask1, mask2 = match_binary_descriptors(keypoints1, - ... descriptors1, - ... keypoints2, - ... descriptors2) - >> matched_kpts + >>> keypoints1 = corner_peaks(corner_harris(square1), min_distance=1) + >>> keypoints2 = corner_peaks(corner_harris(square2), min_distance=1) + >>> extractor = BRIEF(patch_size=5) + >>> descs1, _ = extractor.extract(square1, keypoints1) + >>> descs2, _ = extractor.extract(square2, keypoints2) + >>> matches, idxs1, idxs2 = match_binary_descriptors(keypoints1, descs1, + ... keypoints2, descs2) + >>> matches array([[[2, 2], [2, 2]], @@ -133,53 +91,87 @@ def descriptor_brief(image, keypoints, descriptor_size=256, mode='normal', [[5, 5], [6, 6]]]) + >>> mask1 + array([0, 1, 2, 3]) + >>> mask2 + array([0, 1, 2, 3]) """ - if mode not in ('normal', 'uniform'): - raise ValueError("`mode` must be one of 'normal' or 'uniform'.") + def __init__(self, descriptor_size=256, patch_size=49, + mode='normal', sigma=1, sample_seed=1): - np.random.seed(sample_seed) + if mode not in ('normal', 'uniform'): + raise ValueError("`mode` must be 'normal' or 'uniform'.") - image = _prepare_grayscale_input_2D(image) + self.descriptor_size = descriptor_size + self.patch_size = patch_size + self.mode = mode + self.sigma = sigma + self.sample_seed = sample_seed - # Gaussian Low pass filtering to alleviate noise sensitivity - image = gaussian_filter(image, variance) - image = np.ascontiguousarray(image) + def extract(self, image, keypoints): + """Extract BRIEF binary descriptors for given keypoints in image. - # Sampling pairs of decision pixels in patch_size x patch_size window - if mode == 'normal': - samples = (patch_size / 5.0) * np.random.randn(descriptor_size * 8) - samples = np.array(samples, dtype=np.int32) - samples = samples[(samples < (patch_size // 2)) - & (samples > - (patch_size - 2) // 2)] + Parameters + ---------- + image : 2D array + Input image. + keypoints : (N, 2) array + Keypoint coordinates as ``(row, col)``. - pos1 = samples[:descriptor_size * 2] - pos1 = pos1.reshape(descriptor_size, 2) - pos2 = samples[descriptor_size * 2:descriptor_size * 4] - pos2 = pos2.reshape(descriptor_size, 2) - elif mode == 'uniform': - samples = np.random.randint(-(patch_size - 2) // 2, - (patch_size // 2) + 1, - (descriptor_size * 2, 2)) - samples = np.array(samples, dtype=np.int32) - pos1, pos2 = np.split(samples, 2) + Returns + ------- + descriptors : (Q, `descriptor_size`) array of dtype bool + 2D ndarray of binary descriptors of size `descriptor_size` for Q + keypoints after filtering out border keypoints with value at an + index ``(i, j)`` either being ``True`` or ``False`` representing + the outcome of the intensity comparison for i-th keypoint on j-th + decision pixel-pair. It is ``Q == np.sum(mask)``. + mask : (N, ) array of dtype bool + Mask indicating whether a keypoint has been filtered out + (``False``) or is described in the `descriptors` array (``True``). - pos1 = np.ascontiguousarray(pos1) - pos2 = np.ascontiguousarray(pos2) + """ - # Removing keypoints that are within (patch_size / 2) distance from the - # image border - border_mask = _mask_border_keypoints(image.shape, keypoints.row, - keypoints.col, patch_size // 2) + np.random.seed(self.sample_seed) - keypoints_row = keypoints.row[border_mask].astype(np.intp) - keypoints_col = keypoints.col[border_mask].astype(np.intp) + image = _prepare_grayscale_input_2D(image) - descriptors = np.zeros((keypoints_row.shape[0], descriptor_size), - dtype=bool, order='C') + # Gaussian Low pass filtering to alleviate noise sensitivity + image = np.ascontiguousarray(gaussian_filter(image, self.sigma)) - _brief_loop(image, descriptors.view(np.uint8), - keypoints_row, keypoints_col, pos1, pos2) + # Sampling pairs of decision pixels in patch_size x patch_size window + desc_size = self.descriptor_size + patch_size = self.patch_size + if self.mode == 'normal': + samples = (patch_size / 5.0) * np.random.randn(desc_size * 8) + samples = np.array(samples, dtype=np.int32) + samples = samples[(samples < (patch_size // 2)) + & (samples > - (patch_size - 2) // 2)] - return descriptors, keypoints + pos1 = samples[:desc_size * 2].reshape(desc_size, 2) + pos2 = samples[desc_size * 2:desc_size * 4].reshape(desc_size, 2) + elif self.mode == 'uniform': + samples = np.random.randint(-(patch_size - 2) // 2, + (patch_size // 2) + 1, + (desc_size * 2, 2)) + samples = np.array(samples, dtype=np.int32) + pos1, pos2 = np.split(samples, 2) + + pos1 = np.ascontiguousarray(pos1) + pos2 = np.ascontiguousarray(pos2) + + # Removing keypoints that are within (patch_size / 2) distance from the + # image border + mask = _mask_border_keypoints(image.shape, keypoints, patch_size // 2) + + keypoints = np.array(keypoints[mask, :], dtype=np.intp, order='C', + copy=False) + + descriptors = np.zeros((keypoints.shape[0], desc_size), + dtype=bool, order='C') + + _brief_loop(image, descriptors.view(np.uint8), keypoints, pos1, pos2) + + return descriptors, mask diff --git a/skimage/feature/_brief_cy.pyx b/skimage/feature/_brief_cy.pyx index 5c027c57..8cd1afa7 100644 --- a/skimage/feature/_brief_cy.pyx +++ b/skimage/feature/_brief_cy.pyx @@ -7,7 +7,7 @@ cimport numpy as cnp def _brief_loop(double[:, ::1] image, unsigned char[:, ::1] descriptors, - Py_ssize_t[::1] keypoints_row, Py_ssize_t[::1] keypoints_col, + Py_ssize_t[:, ::1] keypoints, int[:, ::1] pos0, int[:, ::1] pos1): cdef Py_ssize_t k, d, kr, kc, pr0, pr1, pc0, pc1 @@ -17,8 +17,8 @@ def _brief_loop(double[:, ::1] image, unsigned char[:, ::1] descriptors, pc0 = pos0[p, 1] pr1 = pos1[p, 0] pc1 = pos1[p, 1] - for k in range(keypoints_row.shape[0]): - kr = keypoints_row[k] - kc = keypoints_col[k] + for k in range(keypoints.shape[0]): + kr = keypoints[k, 0] + kc = keypoints[k, 1] if image[kr + pr0, kc + pc0] < image[kr + pr1, kc + pc1]: descriptors[k, p] = True diff --git a/skimage/feature/match.py b/skimage/feature/match.py index 405fb86b..86d32eb8 100644 --- a/skimage/feature/match.py +++ b/skimage/feature/match.py @@ -34,9 +34,9 @@ def match_binary_descriptors(keypoints1, descriptors1, keypoints2, ------- matches : (Q, 2, 2) ndarray Location of Q matched keypoint pairs from two images. - mask1 : (Q,) ndarray + idxs1 : (Q,) ndarray Indices of keypoints in keypoints1 that have been matched. - mask2 : (Q,) ndarray + idxs2 : (Q,) ndarray Indices of keypoints in keypoints2 that have been matched. """ @@ -51,33 +51,31 @@ def match_binary_descriptors(keypoints1, descriptors1, keypoints2, # Get hamming distances between keypoints1 and keypoints2 distance = pairwise_hamming_distance(descriptors1, descriptors2) - kp1 = np.squeeze(np.dstack((keypoints1.row, keypoints1.col))) - kp2 = np.squeeze(np.dstack((keypoints2.row, keypoints2.col))) if cross_check: matched_keypoints1_index = np.argmin(distance, axis=1) matched_keypoints2_index = np.argmin(distance, axis=0) - matched_index = _binary_cross_check_loop(matched_keypoints1_index, + matched_idxs = _binary_cross_check_loop(matched_keypoints1_index, matched_keypoints2_index, distance, threshold) - matches = np.zeros((matched_index.shape[0], 2, 2), + matches = np.zeros((matched_idxs.shape[0], 2, 2), dtype=np.intp) - mask1 = matched_index[:, 0] - mask2 = matched_index[:, 1] - matches[:, 0, :] = kp1[mask1] - matches[:, 1, :] = kp2[mask2] + idxs1 = matched_idxs[:, 0] + idxs2 = matched_idxs[:, 1] + matches[:, 0, :] = keypoints1[idxs1] + matches[:, 1, :] = keypoints2[idxs2] else: temp = distance > threshold row_check = np.any(~temp, axis=1) - matched_keypoints2 = kp2[np.argmin(distance, axis=1)] + matched_keypoints2 = keypoints2[np.argmin(distance, axis=1)] matches = np.zeros((np.sum(row_check), 2, 2), dtype=np.intp) - matches[:, 0, :] = kp1[row_check] + matches[:, 0, :] = keypoints1[row_check] matches[:, 1, :] = matched_keypoints2[row_check] - mask1 = np.where(row_check == True)[0] - mask2 = np.argmin(distance, axis=1)[row_check] + idxs1 = np.where(row_check == True)[0] + idxs2 = np.argmin(distance, axis=1)[row_check] return matches, mask1, mask2 diff --git a/skimage/feature/tests/_test_brief.py b/skimage/feature/tests/_test_brief.py index 40a0ac38..61042119 100644 --- a/skimage/feature/tests/_test_brief.py +++ b/skimage/feature/tests/_test_brief.py @@ -3,30 +3,26 @@ from numpy.testing import assert_array_equal, assert_raises from skimage import data from skimage import transform as tf from skimage.color import rgb2gray -from skimage.feature import (descriptor_brief, match_binary_descriptors, - corner_peaks, corner_harris, - create_keypoint_recarray) +from skimage.feature import (BRIEF, match_binary_descriptors, + corner_peaks, corner_harris) def test_descriptor_brief_color_image_unsupported_error(): """Brief descriptors can be evaluated on gray-scale images only.""" img = np.zeros((20, 20, 3)) - keypoints_loc = np.asarray([[7, 5], [11, 13]]) - keypoints = create_keypoint_recarray(keypoints_loc[:, 0], - keypoints_loc[:, 1]) - assert_raises(ValueError, descriptor_brief, img, keypoints) + keypoints = np.asarray([[7, 5], [11, 13]]) + assert_raises(ValueError, BRIEF().extract, img, keypoints) def test_descriptor_brief_normal_mode(): """Verify the computed BRIEF descriptors with expected for normal mode.""" - img = data.lena() - img = rgb2gray(img) - keypoints_loc = corner_peaks(corner_harris(img), min_distance=5) - keypoints = create_keypoint_recarray(keypoints_loc[:, 0], - keypoints_loc[:, 1]) + img = rgb2gray(data.lena()) - descriptors, keypoints = descriptor_brief(img, keypoints[:8], - descriptor_size=8) + keypoints = corner_peaks(corner_harris(img), min_distance=5) + + extractor = BRIEF(descriptor_size=8, sigma=2) + + descriptors, mask = extractor.extract(img, keypoints[:8]) expected = np.array([[ True, False, True, False, True, True, False, False], [False, False, False, False, True, False, False, False], @@ -42,14 +38,13 @@ def test_descriptor_brief_normal_mode(): def test_descriptor_brief_uniform_mode(): """Verify the computed BRIEF descriptors with expected for uniform mode.""" - img = data.lena() - img = rgb2gray(img) - keypoints_loc = corner_peaks(corner_harris(img), min_distance=5) - keypoints = create_keypoint_recarray(keypoints_loc[:, 0], - keypoints_loc[:, 1]) - descriptors, keypoints = descriptor_brief(img, keypoints[:8], - descriptor_size=8, - mode='uniform') + img = rgb2gray(data.lena()) + + keypoints = corner_peaks(corner_harris(img), min_distance=5) + + extractor = BRIEF(descriptor_size=8, sigma=2, mode='uniform') + + descriptors, mask = extractor.extract(img, keypoints[:8]) expected = np.array([[ True, False, True, False, False, True, False, False], [False, True, False, False, True, True, True, True], diff --git a/skimage/feature/util.py b/skimage/feature/util.py index b2ddcb7e..1da4b613 100644 --- a/skimage/feature/util.py +++ b/skimage/feature/util.py @@ -3,43 +3,40 @@ import numpy as np from skimage.util import img_as_float -def create_keypoint_recarray(rows, cols, scales=None, orientations=None, - responses=None): - """Create keypoint array that allows field access through attributes. +class FeatureDetector(object): - Parameters - ---------- - rows : (N, ) array - Row coordinates of keypoints. - cols : (N, ) array - Column coordinates of keypoints. - scales : (N, ) array - Scales in which the keypoints have been detected. - orientations : (N, ) array - Orientations of the keypoints. - responses : (N, ) array - Detector response (strength) of the keypoints. + def __init__(self): + raise NotImplementedError() - Returns - ------- - recarray : (N, ...) recarray - Array with the fields: `row`, `col`, `scale`, `orientation` and - `response`. + def detect(self, image): + """Detect keypoints in image. - """ + Parameters + ---------- + image : 2D array + Input image. - dtype = [('row', np.double), - ('col', np.double), - ('scale', np.double), - ('orientation', np.double), - ('response', np.double)] - keypoints = np.zeros(rows.shape[0], dtype=dtype) - keypoints['row'] = rows - keypoints['col'] = cols - keypoints['scale'] = scales - keypoints['orientation'] = orientations - keypoints['response'] = responses - return keypoints.view(np.recarray) + """ + raise NotImplementedError() + + +class DescriptorExtractor(object): + + def __init__(self): + raise NotImplementedError() + + def extract(self, image, keypoints): + """Extract feature descriptors in image for given keypoints. + + Parameters + ---------- + image : 2D array + Input image. + keypoints : (N, 2) array + Keypoint locations as ``(row, col)``. + + """ + raise NotImplementedError() def _prepare_grayscale_input_2D(image): @@ -50,17 +47,15 @@ def _prepare_grayscale_input_2D(image): return img_as_float(image) -def _mask_border_keypoints(shape, rr, cc, distance): +def _mask_border_keypoints(image_shape, keypoints, distance): """Mask coordinates that are within certain distance from the image border. Parameters ---------- - shape : (2, ) array_like + image_shape : (2, ) array_like Shape of the image as ``(rows, cols)``. - rr : (N, ) array - Row coordinates. - cc : (N, ) array - Column coordinates. + coords : (N, 2) array + Keypoint coordinates as ``(rows, cols)``. distance : int Image border distance. @@ -72,13 +67,13 @@ def _mask_border_keypoints(shape, rr, cc, distance): """ - rows = shape[0] - cols = shape[1] + rows = image_shape[0] + cols = image_shape[1] - mask = (((distance - 1) < rr) - & (rr < (rows - distance + 1)) - & ((distance - 1) < cc) - & (cc < (cols - distance + 1))) + mask = (((distance - 1) < keypoints[:, 0]) + & (keypoints[:, 0] < (rows - distance + 1)) + & ((distance - 1) < keypoints[:, 1]) + & (keypoints[:, 1] < (cols - distance + 1))) return mask