1
1
from math import pi
2
2
from pathlib import Path
3
+ from typing import TypeVar
4
+ from typing import Annotated
5
+ from typing import Literal
6
+ from typing import List
7
+ from typing import Tuple
3
8
4
9
import cv2
5
10
import numpy as np
11
+ from numpy .typing import NDArray
6
12
from scipy .signal import convolve2d
7
13
from scipy .stats import norm
8
14
from scipy .spatial import cKDTree
15
+ from scipy .optimize import minimize
9
16
import scipy .ndimage as ndi
10
-
11
-
12
- def normalize_image (image ):
17
+ import matplotlib .pylab as plt
18
+
19
+ DType = TypeVar ("DType" , bound = np .generic )
20
+ Vec2 = Annotated [NDArray [DType ], Literal [2 ]]
21
+ Vec3 = Annotated [NDArray [DType ], Literal [3 ]]
22
+ Vec4 = Annotated [NDArray [DType ], Literal [4 ]]
23
+ VecN = Annotated [NDArray [DType ], Literal ["N" ]]
24
+ Mat2x2 = Annotated [NDArray [DType ], Literal [2 , 2 ]]
25
+ Mat3x3 = Annotated [NDArray [DType ], Literal [3 , 3 ]]
26
+ Mat4x4 = Annotated [NDArray [DType ], Literal [4 , 4 ]]
27
+ MatN = Annotated [NDArray [DType ], Literal ["N" , "N" ]]
28
+ MatNx2 = Annotated [NDArray [DType ], Literal ["N" , "2" ]]
29
+ MatNx3 = Annotated [NDArray [DType ], Literal ["N" , "3" ]]
30
+ Image = Annotated [NDArray [DType ], Literal ["N" , "N" ]]
31
+
32
+
33
+ def normalize_image (image : Image ):
13
34
"""
14
35
Normalize image to between 0 to 1
15
36
"""
@@ -25,15 +46,15 @@ def normalize_image(image):
25
46
# return diff
26
47
27
48
28
- def z_score_normalization (image ):
49
+ def z_score_normalization (image : Image ):
29
50
"""
30
51
Z-score Normalization
31
52
"""
32
53
mean , std = np .mean (image ), np .std (image )
33
54
return (image - mean ) / (std + 1e-8 ) # Avoid division by zero
34
55
35
56
36
- def gamma_correction (image , gamma = 0.5 ):
57
+ def gamma_correction (image : Image , gamma : float = 0.5 ):
37
58
"""
38
59
Gamma correction
39
60
"""
@@ -52,13 +73,15 @@ def histogram_equalization(image):
52
73
cdf_normalized ).reshape (image .shape )
53
74
54
75
55
- def correlation_patch (angle_1 , angle_2 , radius ):
76
+ def correlation_patch (angle_1 : float , angle_2 : float , radius : float ):
56
77
"""
57
78
Form correlation patch
58
79
"""
59
80
# Width and height
60
- width = radius * 2 + 1
61
- height = radius * 2 + 1
81
+ width = int (radius * 2 + 1 )
82
+ height = int (radius * 2 + 1 )
83
+ if width == 0 or height == 0 :
84
+ return None
62
85
63
86
# Initialize template
64
87
template = []
@@ -102,7 +125,10 @@ def correlation_patch(angle_1, angle_2, radius):
102
125
return template
103
126
104
127
105
- def non_maxima_suppression (image , n = 3 , tau = 0.1 , margin = 2 ):
128
+ def non_maxima_suppression (image : Image ,
129
+ n : int = 3 ,
130
+ tau : float = 0.1 ,
131
+ margin : int = 2 ):
106
132
"""
107
133
Non Maximum Suppression
108
134
@@ -156,13 +182,14 @@ def non_maxima_suppression(image, n=3, tau=0.1, margin=2):
156
182
return maxima
157
183
158
184
159
- def find_modes_mean_shift (hist , sigma ) :
185
+ def find_modes_mean_shift (hist : VecN , sigma : float ) -> Tuple [ MatNx2 , VecN ] :
160
186
"""
161
187
Efficient mean-shift approximation by histogram smoothing.
162
188
163
189
Args:
164
- hist (numpy.ndarray): 1D histogram.
165
- sigma (float): Standard deviation of Gaussian kernel.
190
+
191
+ hist: 1D histogram.
192
+ sigma: Standard deviation of Gaussian kernel.
166
193
167
194
Returns:
168
195
tuple: A tuple containing two numpy arrays:
@@ -214,7 +241,20 @@ def find_modes_mean_shift(hist, sigma):
214
241
return modes , hist_smoothed
215
242
216
243
217
- def edge_orientations (img_angle , img_weight ):
244
+ def edge_orientations (img_angle : Image , img_weight : Image ) -> Tuple [Vec2 , Vec2 ]:
245
+ """
246
+ Calculate Edge Orientations
247
+
248
+ Args:
249
+
250
+ img_angle: Image angles
251
+ img_weight: Image weight
252
+
253
+ Returns:
254
+
255
+ Refined edge orientation vectors v1, v2
256
+
257
+ """
218
258
# Initialize v1 and v2
219
259
v1 = np .array ([0 , 0 ])
220
260
v2 = np .array ([0 , 0 ])
@@ -238,7 +278,7 @@ def edge_orientations(img_angle, img_weight):
238
278
angle_hist [bin_idx ] += vec_weight [i ]
239
279
240
280
# Find modes of smoothed histogram
241
- modes , angle_hist_smoothed = find_modes_mean_shift (angle_hist , 1 )
281
+ modes , _ = find_modes_mean_shift (angle_hist , 1 )
242
282
243
283
# If only one or no mode => return invalid corner
244
284
if modes .shape [0 ] <= 1 :
@@ -267,19 +307,40 @@ def edge_orientations(img_angle, img_weight):
267
307
return v1 , v2
268
308
269
309
270
- def refine_corners (img_du , img_dv , img_angle , img_weight , corners , r = 10 ):
310
+ def refine_corners (img_shape : Tuple [int , ...],
311
+ img_angle : MatN ,
312
+ img_weight : MatN ,
313
+ corners ,
314
+ r = 10 ):
315
+ """
316
+ Refine detected corners
317
+
318
+ Args:
319
+
320
+ img_shape: Image shape (rows, cols)
321
+ img_angle: Image angles [degrees]
322
+ img_weight: Image weight
323
+ corners: List of corners to refine
324
+ r: Patch radius size [pixels]
325
+
326
+ Returns
327
+
328
+ corners, v1, v2
329
+
330
+ """
271
331
# Image dimensions
272
- height , width = img_du .shape
332
+ assert len (img_shape ) == 2
333
+ height , width = img_shape
273
334
274
335
# Init orientations to invalid (corner is invalid iff orientation=0)
275
- N = len (corners )
276
336
corners_inliers = []
277
337
v1 = []
278
338
v2 = []
279
339
280
340
# for all corners do
281
- for i , (cu , cv ) in enumerate (corners ):
341
+ for i , (cu , cv , _ ) in enumerate (corners ):
282
342
# Estimate edge orientations
343
+ cu , cv = int (cu ), int (cv )
283
344
rs = max (cv - r , 1 )
284
345
re = min (cv + r , height )
285
346
cs = max (cu - r , 1 )
@@ -301,7 +362,49 @@ def refine_corners(img_du, img_dv, img_angle, img_weight, corners, r=10):
301
362
return corners , v1 , v2
302
363
303
364
304
- def max_pooling (corr , step = 40 , thres = 0.01 ):
365
+ def compute_edge_orientation (image : Image ):
366
+ # Compute Sobel gradients
367
+ Gx = cv2 .Sobel (image , cv2 .CV_64F , 1 , 0 , ksize = 3 )
368
+ Gy = cv2 .Sobel (image , cv2 .CV_64F , 0 , 1 , ksize = 3 )
369
+
370
+ # Compute edge magnitude and orientation
371
+ magnitude = np .sqrt (Gx ** 2 + Gy ** 2 )
372
+ orientation = np .arctan2 (Gy , Gx ) # Orientation in radians
373
+
374
+ # Normalize orientation to 0-180 degrees
375
+ orientation_degrees = np .degrees (orientation )
376
+ orientation_degrees = (orientation_degrees + 180 ) % 180
377
+
378
+ # Visualizing edge orientations
379
+ _ , ax = plt .subplots (1 , 2 , figsize = (12 , 6 ))
380
+
381
+ ax [0 ].imshow (image , cmap = 'gray' )
382
+ ax [0 ].set_title ("Original Image" )
383
+ ax [0 ].axis ("off" )
384
+
385
+ ax [1 ].imshow (magnitude , cmap = 'gray' )
386
+ ax [1 ].set_title ("Edge Magnitude" )
387
+ ax [1 ].axis ("off" )
388
+
389
+ plt .show ()
390
+
391
+ # Quiver plot for edge orientation visualization
392
+ step = 10 # Downsampling for visualization
393
+ y , x = np .mgrid [0 :image .shape [0 ]:step , 0 :image .shape [1 ]:step ]
394
+ U = Gx [::step , ::step ]
395
+ V = Gy [::step , ::step ]
396
+
397
+ plt .figure (figsize = (6 , 6 ))
398
+ plt .imshow (image , cmap = 'gray' , alpha = 0.5 )
399
+ plt .quiver (x , y , U , - V , color = 'red' , angles = 'xy' , scale_units = 'xy' , scale = 20 )
400
+ plt .title ("Edge Orientation" )
401
+ plt .axis ("off" )
402
+ plt .show ()
403
+
404
+ return orientation_degrees
405
+
406
+
407
+ def max_pooling (corr : Image , step : int = 40 , thres : float = 0.01 ):
305
408
"""
306
409
Extracts strong corner candidates from a corner response matrix using a
307
410
grid-based local max-pooling approach.
@@ -361,7 +464,7 @@ def max_pooling(corr, step=40, thres=0.01):
361
464
return np .array (out )
362
465
363
466
364
- def detect_corners (image , radiuses = [6 , 8 , 10 ]):
467
+ def detect_corners (image : Image , radiuses : List [ int ] = [6 , 8 , 10 ]):
365
468
"""
366
469
Detect corners
367
470
"""
@@ -375,6 +478,8 @@ def detect_corners(image, radiuses=[6, 8, 10]):
375
478
for angle_1 , angle_2 in template_props :
376
479
for radius in radiuses :
377
480
template = correlation_patch (angle_1 , angle_2 , radius )
481
+ if template is None :
482
+ continue
378
483
379
484
img_corners = [
380
485
convolve2d (image , template [0 ], mode = "same" ),
@@ -399,21 +504,22 @@ def detect_corners(image, radiuses=[6, 8, 10]):
399
504
corr = np .max ([img_corners , corr ], axis = 0 )
400
505
401
506
# Max pooling
402
- corners = max_pooling (corr , 10 , np .max (corr ) * 0.2 )
507
+ step = 40
508
+ threshold = float (np .max (corr ) * 0.2 )
509
+ corners = max_pooling (corr , step , threshold )
403
510
404
511
# Refine corners
405
- # du = np.array([
406
- # [-1, 0, 1],
407
- # [-1, 0, 1],
408
- # [-1, 0, 1],
409
- # ])
410
- # dv = du.T
411
- # img_du = convolve2d(image, du, mode='same')
412
- # img_dv = convolve2d(image, dv, mode='same')
413
- # img_angle = np.arctan2(img_dv, img_du)
414
- # img_weight = np.sqrt(img_du**2 + img_dv**2)
415
- # corners, v1, v2 = refine_corners(img_du, img_dv, img_angle, img_weight,
416
- # corners)
512
+ du = np .array ([
513
+ [- 1 , 0 , 1 ],
514
+ [- 1 , 0 , 1 ],
515
+ [- 1 , 0 , 1 ],
516
+ ])
517
+ dv = du .T
518
+ img_du = convolve2d (image , du , mode = 'same' )
519
+ img_dv = convolve2d (image , dv , mode = 'same' )
520
+ img_angle = np .arctan2 (img_dv , img_du )
521
+ img_weight = np .sqrt (img_du ** 2 + img_dv ** 2 )
522
+ corners , v1 , v2 = refine_corners (image .shape , img_angle , img_weight , corners )
417
523
418
524
vis = cv2 .cvtColor (image , cv2 .COLOR_GRAY2BGR )
419
525
for i in range (corners .shape [0 ]):
@@ -426,6 +532,8 @@ def detect_corners(image, radiuses=[6, 8, 10]):
426
532
cv2 .imshow ("vis" , vis )
427
533
cv2 .waitKey (0 )
428
534
535
+ return corners , v1 , v2
536
+
429
537
430
538
def checkerboard_score (corners , size = (9 , 6 )):
431
539
corners_reshaped = corners [:, :2 ].reshape (* size , 2 )
@@ -449,6 +557,58 @@ def checkerboard_score(corners, size=(9, 6)):
449
557
return maxm
450
558
451
559
560
+ def generate_synthetic_corner (image_shape : Tuple [int , int ] = (10 , 10 )):
561
+ """
562
+ Generate a synthetic image with a corner feature.
563
+
564
+ Args:
565
+
566
+ image_shape: (rows, cols)
567
+
568
+ """
569
+ img = np .zeros (image_shape , dtype = np .float32 )
570
+ img [5 + 3 :, :5 - 3 ] = 255 # Simulating an L-shaped corner
571
+ img [:5 + 3 , 5 - 3 :] = 255
572
+ return img
573
+
574
+
575
+ def subpixel_refine (image : Image ):
576
+ """
577
+ Sub-pixel Refinement.
578
+ """
579
+ dx = cv2 .Sobel (image , cv2 .CV_32F , 1 , 0 , ksize = 3 )
580
+ dy = cv2 .Sobel (image , cv2 .CV_32F , 0 , 1 , ksize = 3 )
581
+
582
+ matsum = np .zeros ((2 , 2 ))
583
+ pointsum = np .zeros (2 )
584
+ for i in range (dx .shape [0 ]):
585
+ for j in range (dx .shape [1 ]):
586
+ vec = [dy [i , j ], dx [i , j ]]
587
+ pos = (i , j )
588
+ mat = np .outer (vec , vec )
589
+ pointsum += mat @ pos
590
+ matsum += mat
591
+
592
+ try :
593
+ minv = np .linalg .inv (matsum )
594
+ except np .linalg .LinAlgError :
595
+ return None
596
+
597
+ newp = minv .dot (pointsum )
598
+
599
+ return newp
600
+
601
+
602
+ # Example usage
603
+ # image = generate_synthetic_corner()
604
+ # initial_corner = (6, 6) # Approximate detection
605
+ # refined_corner = subpixel_refine(image)
606
+
607
+ # plt.imshow(image, cmap='gray', vmin=0, vmax=255)
608
+ # plt.xticks(range(image.shape[0]))
609
+ # plt.yticks(range(image.shape[1]))
610
+ # plt.show()
611
+
452
612
# Load the image
453
613
euroc_data = Path ("/data/euroc" )
454
614
calib_dir = euroc_data / "cam_checkerboard" / "mav0" / "cam0" / "data"
@@ -458,11 +618,5 @@ def checkerboard_score(corners, size=(9, 6)):
458
618
cb_size = (7 , 6 )
459
619
winsize = 9
460
620
461
- # vis = cv2.imread(str(calib_image))
462
- # image = image.astype(np.float32)
463
- # image = normalize_image(image)
464
- # image = z_score_normalization(image)
465
- # image = gamma_correction(image)
466
- # image = histogram_equalization(image)
467
-
468
621
detect_corners (image )
622
+ # compute_edge_orientation(image)
0 commit comments