|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
"""Shared utility functions for OmniGlue.""" |
|
|
|
import cv2 |
|
import numpy as np |
|
|
|
from typing import Optional |
|
|
|
|
|
def visualize_matches( |
|
image0: np.ndarray, |
|
image1: np.ndarray, |
|
kp0: np.ndarray, |
|
kp1: np.ndarray, |
|
match_matrix: np.ndarray, |
|
match_labels: Optional[np.ndarray] = None, |
|
show_keypoints: bool = False, |
|
highlight_unmatched: bool = False, |
|
title: Optional[str] = None, |
|
line_width: int = 1, |
|
circle_radius: int = 4, |
|
circle_thickness: int = 2, |
|
rng: Optional['np.random.Generator'] = None, |
|
): |
|
"""Generates visualization of keypoints and matches for two images. |
|
|
|
Stacks image0 and image1 horizontally. In case the two images have different |
|
heights, scales image1 (and its keypoints) to match image0's height. Note |
|
that keypoints must be in (x, y) format, NOT (row, col). If match_matrix |
|
includes unmatched dustbins, the dustbins will be removed before visualizing |
|
matches. |
|
|
|
Args: |
|
image0: (H, W, 3) array containing image0 contents. |
|
image1: (H, W, 3) array containing image1 contents. |
|
kp0: (N, 2) array where each row represents (x, y) coordinates of keypoints |
|
in image0. |
|
kp1: (M, 2) array, where each row represents (x, y) coordinates of keypoints |
|
in image1. |
|
match_matrix: (N, M) binary array, where values are non-zero for keypoint |
|
indices making up a match. |
|
match_labels: (N, M) binary array, where values are non-zero for keypoint |
|
indices making up a ground-truth match. When None, matches from |
|
'match_matrix' are colored randomly. Otherwise, matches from |
|
'match_matrix' are colored according to accuracy (compared to labels). |
|
show_keypoints: if True, all image0 and image1 keypoints (including |
|
unmatched ones) are visualized. |
|
highlight_unmatched: if True, highlights unmatched keypoints in blue. |
|
title: if not None, adds title text to top left of visualization. |
|
line_width: width of correspondence line, in pixels. |
|
circle_radius: radius of keypoint circles, if visualized. |
|
circle_thickness: thickness of keypoint circles, if visualized. |
|
rng: np random number generator to generate the line colors. |
|
|
|
Returns: |
|
Numpy array of image0 and image1 side-by-side, with lines between matches |
|
according to match_matrix. If show_keypoints is True, keypoints from both |
|
images are also visualized. |
|
""" |
|
|
|
if rng is None: |
|
rng = np.random.default_rng() |
|
|
|
|
|
kp1 = np.copy(kp1) |
|
|
|
|
|
has_unmatched_dustbins = (match_matrix.shape[0] == kp0.shape[0] + 1) and ( |
|
match_matrix.shape[1] == kp1.shape[0] + 1 |
|
) |
|
|
|
|
|
height0 = image0.shape[0] |
|
height1 = image1.shape[0] |
|
if height0 != height1: |
|
scale_factor = height0 / height1 |
|
if scale_factor <= 1.0: |
|
interp_method = cv2.INTER_AREA |
|
else: |
|
interp_method = cv2.INTER_LINEAR |
|
new_dim1 = (int(image1.shape[1] * scale_factor), height0) |
|
image1 = cv2.resize(image1, new_dim1, interpolation=interp_method) |
|
kp1 *= scale_factor |
|
|
|
|
|
viz = cv2.hconcat([image0, image1]) |
|
w0 = image0.shape[1] |
|
matches = np.argwhere( |
|
match_matrix[:-1, :-1] if has_unmatched_dustbins else match_matrix |
|
) |
|
for match in matches: |
|
pt0 = (int(kp0[match[0], 0]), int(kp0[match[0], 1])) |
|
pt1 = (int(kp1[match[1], 0] + w0), int(kp1[match[1], 1])) |
|
if match_labels is None: |
|
color = tuple(rng.integers(0, 255, size=3).tolist()) |
|
else: |
|
if match_labels[match[0], match[1]]: |
|
color = (0, 255, 0) |
|
else: |
|
color = (255, 0, 0) |
|
cv2.line(viz, pt0, pt1, color, line_width) |
|
|
|
|
|
if show_keypoints: |
|
for i in range(np.shape(kp0)[0]): |
|
kp = kp0[i, :] |
|
if highlight_unmatched and has_unmatched_dustbins and match_matrix[i, -1]: |
|
cv2.circle( |
|
viz, |
|
tuple(kp.astype(np.int32).tolist()), |
|
circle_radius, |
|
(255, 0, 0), |
|
circle_thickness, |
|
) |
|
else: |
|
cv2.circle( |
|
viz, |
|
tuple(kp.astype(np.int32).tolist()), |
|
circle_radius, |
|
(0, 0, 255), |
|
circle_thickness, |
|
) |
|
for j in range(np.shape(kp1)[0]): |
|
kp = kp1[j, :] |
|
kp[0] += w0 |
|
if highlight_unmatched and has_unmatched_dustbins and match_matrix[-1, j]: |
|
cv2.circle( |
|
viz, |
|
tuple(kp.astype(np.int32).tolist()), |
|
circle_radius, |
|
(255, 0, 0), |
|
circle_thickness, |
|
) |
|
else: |
|
cv2.circle( |
|
viz, |
|
tuple(kp.astype(np.int32).tolist()), |
|
circle_radius, |
|
(0, 0, 255), |
|
circle_thickness, |
|
) |
|
if title is not None: |
|
viz = cv2.putText( |
|
viz, |
|
title, |
|
(5, 30), |
|
cv2.FONT_HERSHEY_SIMPLEX, |
|
1, |
|
(0, 0, 255), |
|
2, |
|
cv2.LINE_AA, |
|
) |
|
return viz |