Skip to content

Commit a2d7282

Browse files
committed
Update
1 parent 97eab54 commit a2d7282

File tree

1 file changed

+194
-40
lines changed

1 file changed

+194
-40
lines changed

src/notes/checkerboard_detection.py

+194-40
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,36 @@
11
from math import pi
22
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
38

49
import cv2
510
import numpy as np
11+
from numpy.typing import NDArray
612
from scipy.signal import convolve2d
713
from scipy.stats import norm
814
from scipy.spatial import cKDTree
15+
from scipy.optimize import minimize
916
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):
1334
"""
1435
Normalize image to between 0 to 1
1536
"""
@@ -25,15 +46,15 @@ def normalize_image(image):
2546
# return diff
2647

2748

28-
def z_score_normalization(image):
49+
def z_score_normalization(image: Image):
2950
"""
3051
Z-score Normalization
3152
"""
3253
mean, std = np.mean(image), np.std(image)
3354
return (image - mean) / (std + 1e-8) # Avoid division by zero
3455

3556

36-
def gamma_correction(image, gamma=0.5):
57+
def gamma_correction(image: Image, gamma: float = 0.5):
3758
"""
3859
Gamma correction
3960
"""
@@ -52,13 +73,15 @@ def histogram_equalization(image):
5273
cdf_normalized).reshape(image.shape)
5374

5475

55-
def correlation_patch(angle_1, angle_2, radius):
76+
def correlation_patch(angle_1: float, angle_2: float, radius: float):
5677
"""
5778
Form correlation patch
5879
"""
5980
# 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
6285

6386
# Initialize template
6487
template = []
@@ -102,7 +125,10 @@ def correlation_patch(angle_1, angle_2, radius):
102125
return template
103126

104127

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):
106132
"""
107133
Non Maximum Suppression
108134
@@ -156,13 +182,14 @@ def non_maxima_suppression(image, n=3, tau=0.1, margin=2):
156182
return maxima
157183

158184

159-
def find_modes_mean_shift(hist, sigma):
185+
def find_modes_mean_shift(hist: VecN, sigma: float) -> Tuple[MatNx2, VecN]:
160186
"""
161187
Efficient mean-shift approximation by histogram smoothing.
162188
163189
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.
166193
167194
Returns:
168195
tuple: A tuple containing two numpy arrays:
@@ -214,7 +241,20 @@ def find_modes_mean_shift(hist, sigma):
214241
return modes, hist_smoothed
215242

216243

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+
"""
218258
# Initialize v1 and v2
219259
v1 = np.array([0, 0])
220260
v2 = np.array([0, 0])
@@ -238,7 +278,7 @@ def edge_orientations(img_angle, img_weight):
238278
angle_hist[bin_idx] += vec_weight[i]
239279

240280
# 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)
242282

243283
# If only one or no mode => return invalid corner
244284
if modes.shape[0] <= 1:
@@ -267,19 +307,40 @@ def edge_orientations(img_angle, img_weight):
267307
return v1, v2
268308

269309

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+
"""
271331
# Image dimensions
272-
height, width = img_du.shape
332+
assert len(img_shape) == 2
333+
height, width = img_shape
273334

274335
# Init orientations to invalid (corner is invalid iff orientation=0)
275-
N = len(corners)
276336
corners_inliers = []
277337
v1 = []
278338
v2 = []
279339

280340
# for all corners do
281-
for i, (cu, cv) in enumerate(corners):
341+
for i, (cu, cv, _) in enumerate(corners):
282342
# Estimate edge orientations
343+
cu, cv = int(cu), int(cv)
283344
rs = max(cv - r, 1)
284345
re = min(cv + r, height)
285346
cs = max(cu - r, 1)
@@ -301,7 +362,49 @@ def refine_corners(img_du, img_dv, img_angle, img_weight, corners, r=10):
301362
return corners, v1, v2
302363

303364

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):
305408
"""
306409
Extracts strong corner candidates from a corner response matrix using a
307410
grid-based local max-pooling approach.
@@ -361,7 +464,7 @@ def max_pooling(corr, step=40, thres=0.01):
361464
return np.array(out)
362465

363466

364-
def detect_corners(image, radiuses=[6, 8, 10]):
467+
def detect_corners(image: Image, radiuses: List[int] = [6, 8, 10]):
365468
"""
366469
Detect corners
367470
"""
@@ -375,6 +478,8 @@ def detect_corners(image, radiuses=[6, 8, 10]):
375478
for angle_1, angle_2 in template_props:
376479
for radius in radiuses:
377480
template = correlation_patch(angle_1, angle_2, radius)
481+
if template is None:
482+
continue
378483

379484
img_corners = [
380485
convolve2d(image, template[0], mode="same"),
@@ -399,21 +504,22 @@ def detect_corners(image, radiuses=[6, 8, 10]):
399504
corr = np.max([img_corners, corr], axis=0)
400505

401506
# 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)
403510

404511
# 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)
417523

418524
vis = cv2.cvtColor(image, cv2.COLOR_GRAY2BGR)
419525
for i in range(corners.shape[0]):
@@ -426,6 +532,8 @@ def detect_corners(image, radiuses=[6, 8, 10]):
426532
cv2.imshow("vis", vis)
427533
cv2.waitKey(0)
428534

535+
return corners, v1, v2
536+
429537

430538
def checkerboard_score(corners, size=(9, 6)):
431539
corners_reshaped = corners[:, :2].reshape(*size, 2)
@@ -449,6 +557,58 @@ def checkerboard_score(corners, size=(9, 6)):
449557
return maxm
450558

451559

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+
452612
# Load the image
453613
euroc_data = Path("/data/euroc")
454614
calib_dir = euroc_data / "cam_checkerboard" / "mav0" / "cam0" / "data"
@@ -458,11 +618,5 @@ def checkerboard_score(corners, size=(9, 6)):
458618
cb_size = (7, 6)
459619
winsize = 9
460620

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-
468621
detect_corners(image)
622+
# compute_edge_orientation(image)

0 commit comments

Comments
 (0)