0
# Feature Detection
1
2
OpenCV-compatible feature detectors and descriptors with cross-version compatibility. These utilities provide unified interfaces for creating feature detectors, extractors, and matchers that work across different OpenCV versions.
3
4
## Capabilities
5
6
### Factory Functions
7
8
Factory functions that provide unified interfaces for creating feature detectors, extractors, and matchers with cross-version compatibility.
9
10
```python { .api }
11
def FeatureDetector_create(detector, *args, **kwargs):
12
"""
13
Create feature detector with cross-version compatibility.
14
15
Args:
16
detector (str): Type of detector to create
17
*args: Positional arguments for detector
18
**kwargs: Keyword arguments for detector
19
20
Returns:
21
Detector object with detect() method
22
23
Supported detectors:
24
BRISK, DENSE, FAST, GFTT, HARRIS, MSER, ORB, SIFT, SURF, STAR
25
"""
26
27
def DescriptorExtractor_create(extractor, *args, **kwargs):
28
"""
29
Create descriptor extractor with cross-version compatibility.
30
31
Args:
32
extractor (str): Type of extractor to create
33
*args: Positional arguments for extractor
34
**kwargs: Keyword arguments for extractor
35
36
Returns:
37
Extractor object with compute() method
38
39
Supported extractors:
40
SIFT, ROOTSIFT, SURF, BRIEF, ORB, BRISK, FREAK
41
"""
42
43
def DescriptorMatcher_create(matcher):
44
"""
45
Create descriptor matcher with cross-version compatibility.
46
47
Args:
48
matcher (str): Type of matcher to create
49
50
Returns:
51
Matcher object
52
53
Supported matchers:
54
BruteForce, BruteForce-SL2, BruteForce-L1, BruteForce-Hamming, FlannBased
55
"""
56
```
57
58
**Usage Example:**
59
```python
60
import cv2
61
from imutils.feature import FeatureDetector_create, DescriptorExtractor_create, DescriptorMatcher_create
62
63
# Load images
64
image1 = cv2.imread("image1.jpg", cv2.IMREAD_GRAYSCALE)
65
image2 = cv2.imread("image2.jpg", cv2.IMREAD_GRAYSCALE)
66
67
# Create detector and extractor
68
detector = FeatureDetector_create("SIFT")
69
extractor = DescriptorExtractor_create("SIFT")
70
71
# Detect keypoints
72
kp1 = detector.detect(image1)
73
kp2 = detector.detect(image2)
74
75
# Compute descriptors
76
kp1, desc1 = extractor.compute(image1, kp1)
77
kp2, desc2 = extractor.compute(image2, kp2)
78
79
# Create matcher and find matches
80
matcher = DescriptorMatcher_create("BruteForce")
81
matches = matcher.match(desc1, desc2)
82
83
# Draw matches
84
output = cv2.drawMatches(image1, kp1, image2, kp2, matches, None)
85
cv2.imshow("Matches", output)
86
cv2.waitKey(0)
87
cv2.destroyAllWindows()
88
```
89
90
### Custom Feature Detectors
91
92
Custom implementations of feature detectors that maintain consistent APIs across OpenCV versions.
93
94
#### Dense Feature Detector
95
96
```python { .api }
97
class DENSE:
98
def __init__(self, step=6, radius=0.5):
99
"""
100
Dense keypoint detector that samples keypoints on a grid.
101
102
Args:
103
step (int): Step size for grid sampling (default: 6)
104
radius (float): Keypoint radius (default: 0.5)
105
"""
106
107
def detect(self, img):
108
"""
109
Detect dense keypoints on a grid.
110
111
Args:
112
img (np.ndarray): Input image
113
114
Returns:
115
list: List of cv2.KeyPoint objects
116
"""
117
118
def setInt(self, var, val):
119
"""
120
Set integer parameters.
121
122
Args:
123
var (str): Parameter name ("initXyStep")
124
val (int): Parameter value
125
"""
126
```
127
128
#### Good Features To Track Detector
129
130
```python { .api }
131
class GFTT:
132
def __init__(self, maxCorners=0, qualityLevel=0.01, minDistance=1, mask=None,
133
blockSize=3, useHarrisDetector=False, k=0.04):
134
"""
135
Good Features To Track detector.
136
137
Args:
138
maxCorners (int): Maximum number of corners (0 = no limit) (default: 0)
139
qualityLevel (float): Quality level parameter (default: 0.01)
140
minDistance (int): Minimum distance between corners (default: 1)
141
mask (np.ndarray, optional): Mask image
142
blockSize (int): Size of averaging block (default: 3)
143
useHarrisDetector (bool): Use Harris detector (default: False)
144
k (float): Harris detector free parameter (default: 0.04)
145
"""
146
147
def detect(self, img):
148
"""
149
Detect good features to track.
150
151
Args:
152
img (np.ndarray): Input image
153
154
Returns:
155
list: List of cv2.KeyPoint objects
156
"""
157
```
158
159
#### Harris Corner Detector
160
161
```python { .api }
162
class HARRIS:
163
def __init__(self, blockSize=2, apertureSize=3, k=0.1, T=0.02):
164
"""
165
Harris corner detector.
166
167
Args:
168
blockSize (int): Size of neighborhood (default: 2)
169
apertureSize (int): Aperture parameter for Sobel operator (default: 3)
170
k (float): Harris detector free parameter (default: 0.1)
171
T (float): Threshold for corner detection (default: 0.02)
172
"""
173
174
def detect(self, img):
175
"""
176
Detect Harris corners.
177
178
Args:
179
img (np.ndarray): Input image
180
181
Returns:
182
list: List of cv2.KeyPoint objects with 3-pixel radius
183
"""
184
```
185
186
**Usage Example:**
187
```python
188
import cv2
189
from imutils.feature import DENSE, GFTT, HARRIS
190
191
image = cv2.imread("example.jpg", cv2.IMREAD_GRAYSCALE)
192
193
# Dense detector
194
dense = DENSE(step=10, radius=3)
195
dense_kp = dense.detect(image)
196
197
# GFTT detector
198
gftt = GFTT(maxCorners=100, qualityLevel=0.01, minDistance=10)
199
gftt_kp = gftt.detect(image)
200
201
# Harris detector
202
harris = HARRIS(blockSize=2, k=0.04, T=0.01)
203
harris_kp = harris.detect(image)
204
205
# Visualize keypoints
206
dense_img = cv2.drawKeypoints(image, dense_kp, None, color=(0, 255, 0))
207
gftt_img = cv2.drawKeypoints(image, gftt_kp, None, color=(255, 0, 0))
208
harris_img = cv2.drawKeypoints(image, harris_kp, None, color=(0, 0, 255))
209
210
cv2.imshow("Dense", dense_img)
211
cv2.imshow("GFTT", gftt_img)
212
cv2.imshow("Harris", harris_img)
213
cv2.waitKey(0)
214
cv2.destroyAllWindows()
215
```
216
217
### Enhanced Descriptors
218
219
#### RootSIFT Descriptor
220
221
Enhanced SIFT descriptor with L1 normalization and square root operation for improved matching performance.
222
223
```python { .api }
224
class RootSIFT:
225
def __init__(self):
226
"""
227
RootSIFT descriptor extractor.
228
229
Automatically initializes SIFT extractor based on OpenCV version.
230
Applies L1 normalization followed by square root operation.
231
"""
232
233
def compute(self, image, kps, eps=1e-7):
234
"""
235
Compute RootSIFT descriptors.
236
237
Args:
238
image (np.ndarray): Input image
239
kps (list): List of keypoints
240
eps (float): Small epsilon value for numerical stability (default: 1e-7)
241
242
Returns:
243
tuple: (keypoints, descriptors) where descriptors are RootSIFT-normalized
244
245
Note:
246
RootSIFT normalization: L1 normalize -> square root -> L2 normalize
247
This provides better matching performance than standard SIFT.
248
"""
249
```
250
251
**Usage Example:**
252
```python
253
import cv2
254
from imutils.feature import RootSIFT, FeatureDetector_create
255
256
# Load images
257
image1 = cv2.imread("image1.jpg", cv2.IMREAD_GRAYSCALE)
258
image2 = cv2.imread("image2.jpg", cv2.IMREAD_GRAYSCALE)
259
260
# Create detector and RootSIFT extractor
261
detector = FeatureDetector_create("SIFT")
262
rootsift = RootSIFT()
263
264
# Detect keypoints
265
kp1 = detector.detect(image1)
266
kp2 = detector.detect(image2)
267
268
# Compute RootSIFT descriptors
269
kp1, desc1 = rootsift.compute(image1, kp1)
270
kp2, desc2 = rootsift.compute(image2, kp2)
271
272
# Match descriptors
273
bf = cv2.BFMatcher()
274
matches = bf.knnMatch(desc1, desc2, k=2)
275
276
# Apply ratio test
277
good_matches = []
278
for m, n in matches:
279
if m.distance < 0.75 * n.distance:
280
good_matches.append([m])
281
282
# Draw matches
283
output = cv2.drawMatchesKnn(image1, kp1, image2, kp2, good_matches, None, flags=2)
284
cv2.imshow("RootSIFT Matches", output)
285
cv2.waitKey(0)
286
cv2.destroyAllWindows()
287
```
288
289
### Helper Utilities
290
291
```python { .api }
292
def corners_to_keypoints(corners):
293
"""
294
Convert corners from cv2.goodFeaturesToTrack to cv2.KeyPoint objects.
295
296
Args:
297
corners (np.ndarray): Corners array from cv2.goodFeaturesToTrack
298
299
Returns:
300
list: List of cv2.KeyPoint objects
301
"""
302
```
303
304
**Usage Example:**
305
```python
306
import cv2
307
from imutils.feature import corners_to_keypoints
308
309
image = cv2.imread("example.jpg", cv2.IMREAD_GRAYSCALE)
310
311
# Use OpenCV's goodFeaturesToTrack
312
corners = cv2.goodFeaturesToTrack(image, maxCorners=100, qualityLevel=0.01, minDistance=10)
313
314
# Convert to KeyPoint objects
315
keypoints = corners_to_keypoints(corners)
316
317
# Draw keypoints
318
output = cv2.drawKeypoints(image, keypoints, None, color=(0, 255, 0))
319
cv2.imshow("Keypoints from Corners", output)
320
cv2.waitKey(0)
321
cv2.destroyAllWindows()
322
```
323
324
### Complete Feature Matching Pipeline
325
326
Here's a comprehensive example demonstrating feature detection, description, and matching:
327
328
```python
329
import cv2
330
import numpy as np
331
from imutils.feature import (FeatureDetector_create, DescriptorExtractor_create,
332
DescriptorMatcher_create, RootSIFT)
333
334
def match_features(image1_path, image2_path, detector_type="SIFT", extractor_type="ROOTSIFT"):
335
# Load images
336
img1 = cv2.imread(image1_path, cv2.IMREAD_GRAYSCALE)
337
img2 = cv2.imread(image2_path, cv2.IMREAD_GRAYSCALE)
338
339
# Create detector
340
detector = FeatureDetector_create(detector_type)
341
342
# Create extractor
343
if extractor_type == "ROOTSIFT":
344
extractor = RootSIFT()
345
else:
346
extractor = DescriptorExtractor_create(extractor_type)
347
348
# Detect keypoints
349
kp1 = detector.detect(img1)
350
kp2 = detector.detect(img2)
351
352
print(f"Image 1: {len(kp1)} keypoints")
353
print(f"Image 2: {len(kp2)} keypoints")
354
355
# Compute descriptors
356
if extractor_type == "ROOTSIFT":
357
kp1, desc1 = extractor.compute(img1, kp1)
358
kp2, desc2 = extractor.compute(img2, kp2)
359
else:
360
desc1 = extractor.compute(img1, kp1)[1]
361
desc2 = extractor.compute(img2, kp2)[1]
362
363
# Skip if no descriptors found
364
if desc1 is None or desc2 is None:
365
print("No descriptors found")
366
return
367
368
# Create matcher
369
if extractor_type in ["ORB", "BRISK"]:
370
matcher = DescriptorMatcher_create("BruteForce-Hamming")
371
else:
372
matcher = DescriptorMatcher_create("BruteForce")
373
374
# Match descriptors
375
matches = matcher.knnMatch(desc1, desc2, k=2)
376
377
# Apply ratio test (Lowe's ratio test)
378
good_matches = []
379
for match_pair in matches:
380
if len(match_pair) == 2:
381
m, n = match_pair
382
if m.distance < 0.75 * n.distance:
383
good_matches.append(m)
384
385
print(f"Good matches: {len(good_matches)}")
386
387
# Draw matches
388
output = cv2.drawMatches(img1, kp1, img2, kp2, good_matches, None,
389
flags=cv2.DrawMatchesFlags_NOT_DRAW_SINGLE_POINTS)
390
391
# Resize for display if too large
392
if output.shape[1] > 1200:
393
scale = 1200 / output.shape[1]
394
new_width = int(output.shape[1] * scale)
395
new_height = int(output.shape[0] * scale)
396
output = cv2.resize(output, (new_width, new_height))
397
398
cv2.imshow(f"{detector_type}-{extractor_type} Matches", output)
399
cv2.waitKey(0)
400
cv2.destroyAllWindows()
401
402
return good_matches
403
404
def compare_detectors(image1_path, image2_path):
405
"""Compare different detector/extractor combinations."""
406
combinations = [
407
("SIFT", "SIFT"),
408
("SIFT", "ROOTSIFT"),
409
("ORB", "ORB"),
410
("GFTT", "SIFT"),
411
("HARRIS", "SIFT")
412
]
413
414
results = {}
415
416
for detector, extractor in combinations:
417
try:
418
print(f"\nTesting {detector}-{extractor}...")
419
matches = match_features(image1_path, image2_path, detector, extractor)
420
results[f"{detector}-{extractor}"] = len(matches) if matches else 0
421
except Exception as e:
422
print(f"Error with {detector}-{extractor}: {e}")
423
results[f"{detector}-{extractor}"] = 0
424
425
# Print comparison results
426
print("\nComparison Results:")
427
print("-" * 30)
428
for combo, count in sorted(results.items(), key=lambda x: x[1], reverse=True):
429
print(f"{combo:15}: {count:3} matches")
430
431
return results
432
433
# Usage
434
if __name__ == "__main__":
435
# Match features between two images
436
matches = match_features("image1.jpg", "image2.jpg", "SIFT", "ROOTSIFT")
437
438
# Compare different detector/extractor combinations
439
comparison_results = compare_detectors("image1.jpg", "image2.jpg")
440
```
441
442
### Advanced Feature Matching with Homography
443
444
Example of robust feature matching with RANSAC homography estimation:
445
446
```python
447
import cv2
448
import numpy as np
449
from imutils.feature import RootSIFT, FeatureDetector_create
450
451
def robust_feature_matching(image1_path, image2_path, min_matches=10):
452
# Load images
453
img1 = cv2.imread(image1_path, cv2.IMREAD_GRAYSCALE)
454
img2 = cv2.imread(image2_path, cv2.IMREAD_GRAYSCALE)
455
456
# Initialize detector and RootSIFT
457
detector = FeatureDetector_create("SIFT")
458
rootsift = RootSIFT()
459
460
# Detect and compute
461
kp1 = detector.detect(img1)
462
kp2 = detector.detect(img2)
463
kp1, desc1 = rootsift.compute(img1, kp1)
464
kp2, desc2 = rootsift.compute(img2, kp2)
465
466
# Match descriptors
467
bf = cv2.BFMatcher()
468
matches = bf.knnMatch(desc1, desc2, k=2)
469
470
# Apply ratio test
471
good_matches = []
472
for m, n in matches:
473
if m.distance < 0.75 * n.distance:
474
good_matches.append(m)
475
476
if len(good_matches) < min_matches:
477
print(f"Not enough matches found: {len(good_matches)}/{min_matches}")
478
return None
479
480
# Extract matched points
481
src_pts = np.float32([kp1[m.queryIdx].pt for m in good_matches]).reshape(-1, 1, 2)
482
dst_pts = np.float32([kp2[m.trainIdx].pt for m in good_matches]).reshape(-1, 1, 2)
483
484
# Find homography with RANSAC
485
homography, mask = cv2.findHomography(src_pts, dst_pts,
486
cv2.RANSAC, 5.0)
487
488
# Count inliers
489
inliers = mask.ravel().tolist()
490
inlier_matches = [good_matches[i] for i in range(len(good_matches)) if inliers[i]]
491
492
print(f"Total matches: {len(good_matches)}")
493
print(f"Inlier matches: {len(inlier_matches)}")
494
495
# Draw matches
496
draw_params = dict(matchColor=(0, 255, 0),
497
singlePointColor=None,
498
matchesMask=inliers,
499
flags=2)
500
501
output = cv2.drawMatches(img1, kp1, img2, kp2, good_matches, None, **draw_params)
502
503
# Draw bounding box of detected object in second image
504
if homography is not None:
505
h, w = img1.shape
506
corners = np.float32([[0, 0], [w, 0], [w, h], [0, h]]).reshape(-1, 1, 2)
507
transformed_corners = cv2.perspectiveTransform(corners, homography)
508
509
# Draw the bounding box
510
img2_color = cv2.cvtColor(img2, cv2.COLOR_GRAY2BGR)
511
cv2.polylines(img2_color, [np.int32(transformed_corners)], True, (0, 255, 0), 3)
512
513
cv2.imshow("Detected Object", img2_color)
514
515
cv2.imshow("Robust Feature Matching", output)
516
cv2.waitKey(0)
517
cv2.destroyAllWindows()
518
519
return homography
520
521
# Usage
522
homography = robust_feature_matching("template.jpg", "scene.jpg", min_matches=15)
523
```