From 2705907270549a4b6990e5da005ae453a7ad560c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Sat, 30 Nov 2013 01:46:16 +0100 Subject: [PATCH] Refactor ORB --- skimage/feature/__init__.py | 20 +- skimage/feature/censure.py | 4 +- skimage/feature/orb.py | 334 +++++++++++++++++------------- skimage/feature/orb_cy.pyx | 12 +- skimage/feature/tests/test_orb.py | 115 ++++++---- 5 files changed, 284 insertions(+), 201 deletions(-) diff --git a/skimage/feature/__init__.py b/skimage/feature/__init__.py index 6c9c0df5..4f541227 100644 --- a/skimage/feature/__init__.py +++ b/skimage/feature/__init__.py @@ -11,9 +11,10 @@ from .corner_cy import corner_moravec, corner_orientations from .template import match_template from .brief import BRIEF from .censure import CenSurE +from .orb import ORB from .match import match_binary_descriptors from .util import pairwise_hamming_distance -from .orb import keypoints_orb, descriptor_orb + __all__ = ['daisy', 'hog', @@ -21,6 +22,10 @@ __all__ = ['daisy', 'greycoprops', 'local_binary_pattern', 'peak_local_max', + 'structure_tensor', + 'structure_tensor_eigvals', + 'hessian_matrix', + 'hessian_matrix_eigvals', 'corner_kitchen_rosenfeld', 'corner_harris', 'corner_shi_tomasi', @@ -28,16 +33,11 @@ __all__ = ['daisy', 'corner_subpix', 'corner_peaks', 'corner_moravec', + 'corner_fast', + 'corner_orientations', 'match_template', 'BRIEF', 'CenSurE', + 'ORB', 'pairwise_hamming_distance', - 'match_binary_descriptors', - 'corner_fast', - 'corner_orientations', - 'structure_tensor', - 'structure_tensor_eigvals', - 'hessian_matrix', - 'hessian_matrix_eigvals', - 'keypoints_orb', - 'descriptor_orb'] + 'match_binary_descriptors'] diff --git a/skimage/feature/censure.py b/skimage/feature/censure.py index dc9cd58c..a2830033 100644 --- a/skimage/feature/censure.py +++ b/skimage/feature/censure.py @@ -182,8 +182,8 @@ class CenSurE(FeatureDetector): ------- keypoints : (N, 2) array Keypoint coordinates as ``(row, col)``. - scales : (N, 1) array - Corresponding scales of the N extracted keypoints. + scales : (N, ) array + Corresponding scales. """ diff --git a/skimage/feature/orb.py b/skimage/feature/orb.py index 74414465..62bf257f 100644 --- a/skimage/feature/orb.py +++ b/skimage/feature/orb.py @@ -1,6 +1,7 @@ import numpy as np -from skimage.feature.util import (_mask_border_keypoints, +from skimage.feature.util import (FeatureDetector, DescriptorExtractor, + _mask_border_keypoints, _prepare_grayscale_input_2D) from skimage.feature import (corner_fast, corner_orientations, corner_peaks, @@ -17,18 +18,13 @@ for i in range(-15, 16): OFAST_MASK[15 + j, 15 + i] = 1 +class ORB(FeatureDetector, DescriptorExtractor): - - -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. + """Oriented FAST and rotated BRIEF feature detector and binary descriptor + extractor. Parameters ---------- - image : 2D ndarray - Input image. n_keypoints : int Number of keypoints to be returned. The function will return the best ``n_keypoints`` according to the Harris corner response if more than @@ -58,165 +54,223 @@ def keypoints_orb(image, n_keypoints=500, fast_n=9, fast_threshold=0.08, Maximum number of scales from the bottom of the image pyramid to extract the features from. - Returns - ------- - keypoints : (N, ...) recarray - Record array as returned by `skimage.feature.create_keypoint_recarray` - with the fields: `row`, `col`, `scale`, `orientation` and `response`. - References ---------- .. [1] Ethan Rublee, Vincent Rabaud, Kurt Konolige and Gary Bradski - "ORB : An efficient alternative to SIFT and SURF" + "ORB: An efficient alternative to SIFT and SURF" http://www.vision.cs.chubu.ac.jp/CV-R/pdf/Rublee_iccv2011.pdf - Examples - -------- - >>> from skimage.feature import keypoints_orb, descriptor_orb - >>> square = np.zeros((50, 50)) - >>> square[20:30, 20:30] = 1 - >>> keypoints = keypoints_orb(square, n_keypoints=8, n_scales=2) - >>> keypoints.shape - (8,) - >>> keypoints.row - array([ 29. , 29. , 20. , 20. , 20.4, 20.4, 28.8, 28.8]) - >>> keypoints.col - array([ 29. , 20. , 29. , 20. , 28.8, 20.4, 28.8, 20.4]) - >>> keypoints.octave - array([ 1. , 1. , 1. , 1. , 1.2, 1.2, 1.2, 1.2]) - >>> np.rad2deg(keypoints.orientation) - array([-135., -45., 135., 45., 135., 45., -135., -45.]) - >>> keypoints.response - array([ 21.4776577 , 21.4776577 , 21.4776577 , 21.4776577 , - 14.03845308, 14.03845308, 14.03845308, 14.03845308]) - """ - image = _prepare_grayscale_input_2D(image) + def __init__(self, downscale=1.2, n_scales=8, + n_keypoints=500, fast_n=9, fast_threshold=0.08, + harris_k=0.04): + self.downscale = downscale + self.n_scales = n_scales + self.n_keypoints = n_keypoints + self.fast_n = fast_n + self.fast_threshold = fast_threshold + self.harris_k = harris_k - pyramid = list(pyramid_gaussian(image, n_scales - 1, downscale)) - - keypoints_list = [] - orientations_list = [] - scales_list = [] - harris_response_list = [] - - for octave in range(len(pyramid)): + def _build_pyramid(self, image): + image = _prepare_grayscale_input_2D(image) + return list(pyramid_gaussian(image, self.n_scales - 1, self.downscale)) + def _detect_octave(self, octave_image): # Extract keypoints for current octave - corners = corner_peaks(corner_fast(pyramid[octave], fast_n, - fast_threshold), min_distance=1) + fast_response = corner_fast(octave_image, self.fast_n, + self.fast_threshold) + keypoints = corner_peaks(fast_response, min_distance=1) - # Scale keypoint coordinates so they correspond to the original - # image shape - keypoints_list.append(corners * downscale ** octave) + mask = _mask_border_keypoints(octave_image.shape, keypoints, + distance=16) + keypoints = keypoints[mask] - orientations_list.append(corner_orientations(pyramid[octave], corners, - OFAST_MASK)) + orientations = corner_orientations(octave_image, keypoints, + OFAST_MASK) - scales_list.append(octave * np.ones(corners.shape[0], dtype=np.intp)) + harris_response = corner_harris(octave_image, method='k', + k=self.harris_k) + responses = harris_response[keypoints[:, 0], keypoints[:, 1]] - harris_response = corner_harris(pyramid[octave], method='k', - k=harris_k) - harris_response_list.append(harris_response[corners[:, 0], - corners[:, 1]]) + return keypoints, orientations, responses - keypoints_array = np.vstack(keypoints_list) - orientations = np.hstack(orientations_list) - scales = downscale ** np.hstack(scales_list) - harris_measure = np.hstack(harris_response_list) - keypoints = create_keypoint_recarray(keypoints_array[:, 0], - keypoints_array[:, 1], - scales, orientations, - harris_measure) + def detect(self, image): + """Detect oriented FAST keypoints along with the corresponding scale. - 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] + Parameters + ---------- + image : 2D array + Input image. + Returns + ------- + keypoints : (N, 2) array + Keypoint coordinates as ``(row, col)``. + scales : (N, ) array + Corresponding scales. + orientations : (N, ) array + Corresponding orientations in radians. + responses : (N, ) array + Corresponding Harris corner responses. -def descriptor_orb(image, keypoints, downscale=1.2, n_scales=8): - """Compute rBRIEF descriptors for keypoints. + """ - Parameters - ---------- - image : 2D ndarray - 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``. - n_scales : int - Number of scales from the bottom of the image pyramid to extract - the features from. + pyramid = self._build_pyramid(image) - Returns - ------- - descriptors : (P, 256) bool ndarray - 2darray of type bool describing the P keypoints obtained after - filtering out those near the image border. Size of each descriptor - is 32 bytes or 256 bits. - filtered_keypoints : (P, 2) ndarray - Record array with fields row, col, octave, orientation, response for - P keypoints obtained after removing out those that are near the - border. + keypoints_list = [] + orientations_list = [] + octave_list = [] + responses_list = [] - References - ---------- - .. [1] Ethan Rublee, Vincent Rabaud, Kurt Konolige and Gary Bradski - "ORB : An efficient alternative to SIFT and SURF" - http://www.vision.cs.chubu.ac.jp/CV-R/pdf/Rublee_iccv2011.pdf + for octave in range(len(pyramid)): - Examples - -------- - >>> import numpy as np - >>> from skimage.feature import keypoints_orb, descriptor_orb - >>> square = np.zeros((50, 50)) - >>> square[20:30, 20:30] = 1 - >>> keypoints = keypoints_orb(square, n_keypoints=8, n_scales=2) - >>> keypoints.shape - (8,) - >>> descriptors, filtered_keypoints = descriptor_orb(square, keypoints, n_scales=2) - >>> filtered_keypoints.shape - (8,) - >>> descriptors.shape - (8, 256) + octave_image = np.ascontiguousarray(pyramid[octave]) - """ - image = _prepare_grayscale_input_2D(image) + keypoints, orientations, responses = \ + self._detect_octave(octave_image) - pyramid = list(pyramid_gaussian(image, n_scales - 1, downscale)) + keypoints_list.append(keypoints * self.downscale ** octave) + orientations_list.append(orientations) + octave_list.append(self.downscale ** octave + * np.ones(keypoints.shape[0], dtype=np.intp)) + responses_list.append(responses) - descriptors_list = [] - keypoints_list = [] + keypoints = np.vstack(keypoints_list) + orientations = np.hstack(orientations_list) + scales = np.hstack(octave_list) + responses = np.hstack(responses_list) - for scale in range(n_scales): - curr_image = np.ascontiguousarray(pyramid[scale]) + if keypoints.shape[0] < self.n_keypoints: + return keypoints, scales, orientations, responses + else: + # Choose best n_keypoints according to Harris corner response + best_indices = responses.argsort()[::-1][:self.n_keypoints] + return (keypoints[best_indices], scales[best_indices], + orientations[best_indices], responses[best_indices]) - curr_scale_mask = (np.log(keypoints.octave) / - np.log(downscale)).astype(np.intp) == scale - if np.sum(curr_scale_mask) > 0: - curr_keypoints = keypoints[curr_scale_mask] - curr_scale_kpts = np.squeeze(np.dstack((curr_keypoints.row / curr_keypoints.octave, - curr_keypoints.col / curr_keypoints.octave))) - border_mask = _mask_border_keypoints(curr_image, - curr_scale_kpts, - dist=16) + def _extract_octave(self, octave_image, keypoints, orientations): + mask = _mask_border_keypoints(octave_image.shape, keypoints, + distance=16) + keypoints = np.array(keypoints[mask], dtype=np.intp, order='C', + copy=False) + orientations = np.array(orientations[mask], dtype=np.double, order='C', + copy=False) - curr_keypoints = curr_keypoints[border_mask] + descriptors = _orb_loop(octave_image, keypoints, orientations) - curr_scale_kpts = np.ascontiguousarray(np.round(curr_scale_kpts[border_mask]).astype(np.intp)) - curr_scale_orientation = np.ascontiguousarray(curr_keypoints.orientation) - curr_scale_descriptors = _orb_loop(curr_image, curr_scale) + return descriptors, mask - descriptors_list.append(curr_scale_descriptors) - keypoints_list.append(curr_keypoints) + def extract(self, image, keypoints, scales, orientations): + """Extract rBRIEF binary descriptors for given keypoints in image. - descriptors = np.vstack(descriptors_list).view(np.bool) - filtered_keypoints = np.hstack(keypoints_list) - return descriptors, filtered_keypoints.view(np.recarray) + Parameters + ---------- + image : 2D array + Input image. + keypoints : (N, 2) array + Keypoint coordinates as ``(row, col)``. + scales : (N, ) array + Corresponding scales. + orientations : (N, ) array + Corresponding orientations in radians. + + Returns + ------- + descriptors : (Q, `descriptor_size`) array of dtype bool + 2D array 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``). + + """ + + pyramid = self._build_pyramid(image) + + descriptors_list = [] + mask_list = [] + + # Determine octaves from scales + octaves = (np.log(scales) / np.log(self.downscale)).astype(np.intp) + + for octave in range(len(pyramid)): + + # Mask for all keypoints in current octave + octave_mask = octaves == octave + + if np.sum(octave_mask) > 0: + + octave_image = np.ascontiguousarray(pyramid[octave]) + + octave_keypoints = keypoints[octave_mask] + octave_keypoints /= self.downscale ** octave + + octave_orientations = orientations[octave_mask] + + descriptors, mask = self._extract_octave(octave_image, + octave_keypoints, + octave_orientations) + + descriptors_list.append(descriptors) + mask_list.append(mask) + + descriptors = np.vstack(descriptors_list).view(np.bool) + mask = np.hstack(mask_list) + + return descriptors, mask + + def detect_and_extract(self, image): + """Detect oriented FAST keypoints and extract rBRIEF descriptors. + + Parameters + ---------- + image : 2D array + Input image. + + Returns + ------- + keypoints : (Q, 2) array + Keypoint coordinates as ``(row, col)``. + descriptors : (Q, `descriptor_size`) array of dtype bool + 2D array 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)``. + + """ + + pyramid = self._build_pyramid(image) + + keypoints_list = [] + responses_list = [] + descriptors_list = [] + + for octave in range(len(pyramid)): + + octave_image = np.ascontiguousarray(pyramid[octave]) + + keypoints, orientations, responses = \ + self._detect_octave(octave_image) + + descriptors, mask = self._extract_octave(octave_image, keypoints, + orientations) + + keypoints_list.append(keypoints * self.downscale ** octave) + responses_list.append(responses) + descriptors_list.append(descriptors) + + keypoints = np.vstack(keypoints_list) + responses = np.hstack(responses_list) + descriptors = np.vstack(descriptors_list).view(np.bool) + + if keypoints.shape[0] < self.n_keypoints: + return keypoints, descriptors + else: + # Choose best n_keypoints according to Harris corner response + best_indices = responses.argsort()[::-1][:self.n_keypoints] + return (keypoints[best_indices], descriptors[best_indices]) diff --git a/skimage/feature/orb_cy.pyx b/skimage/feature/orb_cy.pyx index 68911bb9..b497c74d 100644 --- a/skimage/feature/orb_cy.pyx +++ b/skimage/feature/orb_cy.pyx @@ -13,21 +13,23 @@ from libc.math cimport sin, cos, round POS = np.loadtxt(os.path.join(data_dir, "orb_descriptor_positions.txt"), dtype=np.int8) +POS0 = np.ascontiguousarray(POS[:, :2]) +POS1 = np.ascontiguousarray(POS[:, 2:]) def _orb_loop(double[:, ::1] image, Py_ssize_t[:, ::1] keypoints, - double[:] orientations, pos): + double[:] orientations): cdef Py_ssize_t i, d, kr, kc, pr0, pr1, pc0, pc1, spr0, spc0, spr1, spc1 cdef int[:, ::1] steered_pos0, steered_pos1 cdef double angle cdef char[:, ::1] descriptors = np.zeros((keypoints.shape[0], - pos.shape[0]), dtype=np.uint8) - cdef char[:, ::1] cpos0 = pos[:, :2] - cdef char[:, ::1] cpos1 = pos[:, 2:] - + POS.shape[0]), dtype=np.uint8) + cdef char[:, ::1] cpos0 = POS0 + cdef char[:, ::1] cpos1 = POS1 for i in range(descriptors.shape[0]): + angle = orientations[i] sin_a = sin(angle) cos_a = cos(angle) diff --git a/skimage/feature/tests/test_orb.py b/skimage/feature/tests/test_orb.py index 956ff4b4..f857ca75 100644 --- a/skimage/feature/tests/test_orb.py +++ b/skimage/feature/tests/test_orb.py @@ -1,21 +1,24 @@ import numpy as np from numpy.testing import assert_array_equal, assert_almost_equal -from skimage.feature import keypoints_orb, descriptor_orb +from skimage.feature import ORB from skimage.data import lena from skimage.color import rgb2gray -def test_keypoints_orb_desired_no_of_keypoints(): - img = rgb2gray(lena()) - keypoints = keypoints_orb(img, n_keypoints=10, fast_n=12, - fast_threshold=0.20) - exp_row = np.array([ 435. , 435.6 , 376. , 455. , 434.88, 269. , - 375.6 , 310.8 , 413. , 311.04]) - exp_col = np.array([ 180. , 180. , 156. , 176. , 180. , 111. , - 156. , 172.8, 70. , 172.8]) +img = rgb2gray(lena()) - exp_octaves = np.array([ 1. , 1.2 , 1. , 1. , 1.44 , 1. , - 1.2 , 1.2 , 1. , 1.728]) + +def test_keypoints_orb_desired_no_of_keypoints(): + detector_extractor = ORB(n_keypoints=10, fast_n=12, fast_threshold=0.20) + keypoints, scales, orientations, responses = detector_extractor.detect(img) + + exp_rows = np.array([ 435. , 435.6 , 376. , 455. , 434.88, 269. , + 375.6 , 310.8 , 413. , 311.04]) + exp_cols = np.array([ 180. , 180. , 156. , 176. , 180. , 111. , + 156. , 172.8, 70. , 172.8]) + + exp_scales = np.array([ 1. , 1.2 , 1. , 1. , 1.44 , 1. , + 1.2 , 1.2 , 1. , 1.728]) exp_orientations = np.array([-175.64733392, -167.94842949, -148.98350192, -142.03599837, -176.08535837, -53.08162354, @@ -25,24 +28,30 @@ def test_keypoints_orb_desired_no_of_keypoints(): 0.5626413 , 0.5097993 , 0.44351774, 0.39154173, 0.39084861, 0.39063076, 0.37602487]) - assert_almost_equal(exp_row, keypoints.row) - assert_almost_equal(exp_col, keypoints.col) - assert_almost_equal(exp_octaves, keypoints.octave) - assert_almost_equal(exp_response, keypoints.response) - assert_almost_equal(exp_orientations, np.rad2deg(keypoints.orientation)) + + assert_almost_equal(exp_rows, keypoints[:, 0]) + assert_almost_equal(exp_cols, keypoints[:, 1]) + assert_almost_equal(exp_scales, scales) + assert_almost_equal(exp_response, responses) + assert_almost_equal(exp_orientations, np.rad2deg(orientations), 5) + + keypoints, _ = detector_extractor.detect_and_extract(img) + assert_almost_equal(exp_rows, keypoints[:, 0]) + assert_almost_equal(exp_cols, keypoints[:, 1]) def test_keypoints_orb_less_than_desired_no_of_keypoints(): img = rgb2gray(lena()) - keypoints = keypoints_orb(img, n_keypoints=15, fast_n=12, - fast_threshold=0.33, downscale=2, n_scales=2) + detector_extractor = ORB(n_keypoints=15, fast_n=12, + fast_threshold=0.33, downscale=2, n_scales=2) + keypoints, scales, orientations, responses = detector_extractor.detect(img) - exp_row = np.array([ 67., 247., 269., 413., 435., 230., 264., - 330., 372.]) - exp_col = np.array([ 157., 146., 111., 70., 180., 136., 336., - 148., 156.]) + exp_rows = np.array([ 67., 247., 269., 413., 435., 230., 264., + 330., 372.]) + exp_cols = np.array([ 157., 146., 111., 70., 180., 136., 336., + 148., 156.]) - exp_octaves = np.array([ 1., 1., 1., 1., 1., 2., 2., 2., 2.]) + exp_scales = np.array([ 1., 1., 1., 1., 1., 2., 2., 2., 2.]) exp_orientations = np.array([-105.76503839, -96.28973044, -53.08162354, -173.4479964 , -175.64733392, -106.07927215, @@ -52,33 +61,51 @@ def test_keypoints_orb_less_than_desired_no_of_keypoints(): 0.39063076, 0.96770745, 0.04935129, 0.21431068, 0.15826555, 0.42403573]) - assert_almost_equal(exp_row, keypoints.row) - assert_almost_equal(exp_col, keypoints.col) - assert_almost_equal(exp_octaves, keypoints.octave) - assert_almost_equal(exp_response, keypoints.response) - assert_almost_equal(exp_orientations, np.rad2deg(keypoints.orientation)) + assert_almost_equal(exp_rows, keypoints[:, 0]) + assert_almost_equal(exp_cols, keypoints[:, 1]) + assert_almost_equal(exp_scales, scales) + assert_almost_equal(exp_response, responses) + assert_almost_equal(exp_orientations, np.rad2deg(orientations), 5) + + keypoints, _ = detector_extractor.detect_and_extract(img) + assert_almost_equal(exp_rows, keypoints[:, 0]) + assert_almost_equal(exp_cols, keypoints[:, 1]) def test_descriptor_orb(): - img = rgb2gray(lena()) - keypoints = keypoints_orb(img, n_keypoints=10, fast_n=12, - fast_threshold=0.20) - descriptors, filtered_keypoints = descriptor_orb(img, keypoints) + detector_extractor = ORB(fast_n=12, fast_threshold=0.20) - descriptors_120_129 = np.array([[ True, False, False, True, False, False, False, False, False, False], - [ True, True, False, False, True, False, False, True, False, True], - [False, True, True, False, True, False, True, True, True, True], - [False, False, False, True, True, False, True, False, True, False], - [False, True, True, True, True, False, True, True, True, False], - [ True, False, True, True, True, False, False, False, True, False], - [ True, False, True, False, True, False, True, True, False, True], - [ True, True, True, True, True, True, False, True, True, True], - [ True, True, True, False, True, False, True, True, True, False], - [ True, False, False, False, False, False, True, True, True, False]], - dtype=bool) + exp_descriptors = np.array([[ True, False, True, True, False, False, False, False, False, False], + [False, False, True, True, False, True, True, False, True, True], + [ True, False, False, False, True, False, True, True, True, False], + [ True, False, False, True, False, True, True, False, False, False], + [False, True, True, True, False, False, False, True, True, False], + [False, False, False, False, False, True, False, True, True, True], + [False, True, True, True, True, False, False, True, False, True], + [ True, True, True, False, True, True, True, True, False, False], + [ True, True, False, True, True, True, True, False, False, False], + [ True, False, False, False, False, True, False, False, True, True], + [ True, False, False, False, True, True, True, False, False, False], + [False, False, True, False, True, False, False, True, False, False], + [False, False, True, True, False, False, False, False, False, True], + [ True, True, False, False, False, True, True, True, True, True], + [ True, True, True, False, False, True, False, True, True, False], + [False, True, True, False, False, True, True, True, True, True], + [ True, True, True, False, False, False, False, True, True, True], + [False, False, False, False, True, False, False, True, True, False], + [False, True, False, False, True, False, False, False, True, True], + [ True, False, True, False, False, False, True, True, False, False]], dtype=bool) + keypoints1, scales1, orientations1, responses1 \ + = detector_extractor.detect(img) + descriptors1, mask1 \ + = detector_extractor.extract(img, keypoints1, scales1, orientations1) + assert_array_equal(exp_descriptors, descriptors1[100:120, 10:20]) - assert_array_equal(descriptors_120_129, descriptors[:, 120:130]) + keypoints2, descriptors2 = detector_extractor.detect_and_extract(img) + assert_array_equal(exp_descriptors, descriptors2[100:120, 10:20]) + + assert_array_equal(keypoints1[mask1], keypoints2) if __name__ == '__main__':