|
import os |
|
import random |
|
import numpy as np |
|
import torch |
|
from itertools import combinations |
|
import cv2 |
|
import gradio as gr |
|
from hloc import matchers, extractors |
|
from hloc.utils.base_model import dynamic_load |
|
from hloc import match_dense, match_features, extract_features |
|
from hloc.utils.viz import add_text, plot_keypoints |
|
from .viz import draw_matches, fig2im, plot_images, plot_color_line_matches |
|
|
|
device = "cuda" if torch.cuda.is_available() else "cpu" |
|
|
|
DEFAULT_SETTING_THRESHOLD = 0.1 |
|
DEFAULT_SETTING_MAX_FEATURES = 2000 |
|
DEFAULT_DEFAULT_KEYPOINT_THRESHOLD = 0.01 |
|
DEFAULT_ENABLE_RANSAC = True |
|
DEFAULT_RANSAC_METHOD = "USAC_MAGSAC" |
|
DEFAULT_RANSAC_REPROJ_THRESHOLD = 8 |
|
DEFAULT_RANSAC_CONFIDENCE = 0.999 |
|
DEFAULT_RANSAC_MAX_ITER = 10000 |
|
DEFAULT_MIN_NUM_MATCHES = 4 |
|
DEFAULT_MATCHING_THRESHOLD = 0.2 |
|
DEFAULT_SETTING_GEOMETRY = "Homography" |
|
|
|
|
|
def get_model(match_conf): |
|
Model = dynamic_load(matchers, match_conf["model"]["name"]) |
|
model = Model(match_conf["model"]).eval().to(device) |
|
return model |
|
|
|
|
|
def get_feature_model(conf): |
|
Model = dynamic_load(extractors, conf["model"]["name"]) |
|
model = Model(conf["model"]).eval().to(device) |
|
return model |
|
|
|
|
|
def gen_examples(): |
|
random.seed(1) |
|
example_matchers = [ |
|
"disk+lightglue", |
|
"loftr", |
|
"disk", |
|
"d2net", |
|
"topicfm", |
|
"superpoint+superglue", |
|
"disk+dualsoftmax", |
|
"lanet", |
|
] |
|
|
|
def gen_images_pairs(path: str, count: int = 5): |
|
imgs_list = [ |
|
os.path.join(path, file) |
|
for file in os.listdir(path) |
|
if file.lower().endswith((".jpg", ".jpeg", ".png")) |
|
] |
|
pairs = list(combinations(imgs_list, 2)) |
|
selected = random.sample(range(len(pairs)), count) |
|
return [pairs[i] for i in selected] |
|
|
|
|
|
path = "datasets/sacre_coeur/mapping" |
|
pairs = gen_images_pairs(path, len(example_matchers)) |
|
match_setting_threshold = DEFAULT_SETTING_THRESHOLD |
|
match_setting_max_features = DEFAULT_SETTING_MAX_FEATURES |
|
detect_keypoints_threshold = DEFAULT_DEFAULT_KEYPOINT_THRESHOLD |
|
ransac_method = DEFAULT_RANSAC_METHOD |
|
ransac_reproj_threshold = DEFAULT_RANSAC_REPROJ_THRESHOLD |
|
ransac_confidence = DEFAULT_RANSAC_CONFIDENCE |
|
ransac_max_iter = DEFAULT_RANSAC_MAX_ITER |
|
input_lists = [] |
|
for pair, mt in zip(pairs, example_matchers): |
|
input_lists.append( |
|
[ |
|
pair[0], |
|
pair[1], |
|
match_setting_threshold, |
|
match_setting_max_features, |
|
detect_keypoints_threshold, |
|
mt, |
|
|
|
ransac_method, |
|
ransac_reproj_threshold, |
|
ransac_confidence, |
|
ransac_max_iter, |
|
] |
|
) |
|
return input_lists |
|
|
|
|
|
def filter_matches( |
|
pred, |
|
ransac_method=DEFAULT_RANSAC_METHOD, |
|
ransac_reproj_threshold=DEFAULT_RANSAC_REPROJ_THRESHOLD, |
|
ransac_confidence=DEFAULT_RANSAC_CONFIDENCE, |
|
ransac_max_iter=DEFAULT_RANSAC_MAX_ITER, |
|
): |
|
mkpts0 = None |
|
mkpts1 = None |
|
feature_type = None |
|
if "keypoints0_orig" in pred.keys() and "keypoints1_orig" in pred.keys(): |
|
mkpts0 = pred["keypoints0_orig"] |
|
mkpts1 = pred["keypoints1_orig"] |
|
feature_type = "KEYPOINT" |
|
elif ( |
|
"line_keypoints0_orig" in pred.keys() |
|
and "line_keypoints1_orig" in pred.keys() |
|
): |
|
mkpts0 = pred["line_keypoints0_orig"] |
|
mkpts1 = pred["line_keypoints1_orig"] |
|
feature_type = "LINE" |
|
else: |
|
return pred |
|
if mkpts0 is None or mkpts0 is None: |
|
return pred |
|
if ransac_method not in ransac_zoo.keys(): |
|
ransac_method = DEFAULT_RANSAC_METHOD |
|
|
|
if len(mkpts0) < DEFAULT_MIN_NUM_MATCHES: |
|
return pred |
|
H, mask = cv2.findHomography( |
|
mkpts0, |
|
mkpts1, |
|
method=ransac_zoo[ransac_method], |
|
ransacReprojThreshold=ransac_reproj_threshold, |
|
confidence=ransac_confidence, |
|
maxIters=ransac_max_iter, |
|
) |
|
mask = np.array(mask.ravel().astype("bool"), dtype="bool") |
|
if H is not None: |
|
if feature_type == "KEYPOINT": |
|
pred["keypoints0_orig"] = mkpts0[mask] |
|
pred["keypoints1_orig"] = mkpts1[mask] |
|
pred["mconf"] = pred["mconf"][mask] |
|
elif feature_type == "LINE": |
|
pred["line_keypoints0_orig"] = mkpts0[mask] |
|
pred["line_keypoints1_orig"] = mkpts1[mask] |
|
return pred |
|
|
|
|
|
def compute_geom( |
|
pred, |
|
ransac_method=DEFAULT_RANSAC_METHOD, |
|
ransac_reproj_threshold=DEFAULT_RANSAC_REPROJ_THRESHOLD, |
|
ransac_confidence=DEFAULT_RANSAC_CONFIDENCE, |
|
ransac_max_iter=DEFAULT_RANSAC_MAX_ITER, |
|
) -> dict: |
|
mkpts0 = None |
|
mkpts1 = None |
|
|
|
if "keypoints0_orig" in pred.keys() and "keypoints1_orig" in pred.keys(): |
|
mkpts0 = pred["keypoints0_orig"] |
|
mkpts1 = pred["keypoints1_orig"] |
|
|
|
if ( |
|
"line_keypoints0_orig" in pred.keys() |
|
and "line_keypoints1_orig" in pred.keys() |
|
): |
|
mkpts0 = pred["line_keypoints0_orig"] |
|
mkpts1 = pred["line_keypoints1_orig"] |
|
|
|
if mkpts0 is not None and mkpts1 is not None: |
|
if len(mkpts0) < 2 * DEFAULT_MIN_NUM_MATCHES: |
|
return {} |
|
h1, w1, _ = pred["image0_orig"].shape |
|
geo_info = {} |
|
F, inliers = cv2.findFundamentalMat( |
|
mkpts0, |
|
mkpts1, |
|
method=ransac_zoo[ransac_method], |
|
ransacReprojThreshold=ransac_reproj_threshold, |
|
confidence=ransac_confidence, |
|
maxIters=ransac_max_iter, |
|
) |
|
if F is not None: |
|
geo_info["Fundamental"] = F.tolist() |
|
H, _ = cv2.findHomography( |
|
mkpts1, |
|
mkpts0, |
|
method=ransac_zoo[ransac_method], |
|
ransacReprojThreshold=ransac_reproj_threshold, |
|
confidence=ransac_confidence, |
|
maxIters=ransac_max_iter, |
|
) |
|
if H is not None: |
|
geo_info["Homography"] = H.tolist() |
|
_, H1, H2 = cv2.stereoRectifyUncalibrated( |
|
mkpts0.reshape(-1, 2), |
|
mkpts1.reshape(-1, 2), |
|
F, |
|
imgSize=(w1, h1), |
|
) |
|
geo_info["H1"] = H1.tolist() |
|
geo_info["H2"] = H2.tolist() |
|
return geo_info |
|
else: |
|
return {} |
|
|
|
|
|
def wrap_images(img0, img1, geo_info, geom_type): |
|
h1, w1, _ = img0.shape |
|
h2, w2, _ = img1.shape |
|
result_matrix = None |
|
if geo_info is not None and len(geo_info) != 0: |
|
rectified_image0 = img0 |
|
rectified_image1 = None |
|
H = np.array(geo_info["Homography"]) |
|
F = np.array(geo_info["Fundamental"]) |
|
title = [] |
|
if geom_type == "Homography": |
|
rectified_image1 = cv2.warpPerspective( |
|
img1, H, (img0.shape[1], img0.shape[0]) |
|
) |
|
result_matrix = H |
|
title = ["Image 0", "Image 1 - warped"] |
|
elif geom_type == "Fundamental": |
|
H1, H2 = np.array(geo_info["H1"]), np.array(geo_info["H2"]) |
|
rectified_image0 = cv2.warpPerspective(img0, H1, (w1, h1)) |
|
rectified_image1 = cv2.warpPerspective(img1, H2, (w2, h2)) |
|
result_matrix = F |
|
title = ["Image 0 - warped", "Image 1 - warped"] |
|
else: |
|
print("Error: Unknown geometry type") |
|
fig = plot_images( |
|
[rectified_image0.squeeze(), rectified_image1.squeeze()], |
|
title, |
|
dpi=300, |
|
) |
|
dictionary = { |
|
"row1": result_matrix[0].tolist(), |
|
"row2": result_matrix[1].tolist(), |
|
"row3": result_matrix[2].tolist(), |
|
} |
|
return fig2im(fig), dictionary |
|
else: |
|
return None, None |
|
|
|
|
|
def change_estimate_geom(input_image0, input_image1, matches_info, choice): |
|
if ( |
|
matches_info is None |
|
or len(matches_info) < 1 |
|
or "geom_info" not in matches_info.keys() |
|
): |
|
return None, None |
|
geom_info = matches_info["geom_info"] |
|
wrapped_images = None |
|
if choice != "No": |
|
wrapped_images, _ = wrap_images( |
|
input_image0, input_image1, geom_info, choice |
|
) |
|
return wrapped_images, matches_info |
|
else: |
|
return None, None |
|
|
|
|
|
def display_matches(pred: dict, titles=[], dpi=300): |
|
img0 = pred["image0_orig"] |
|
img1 = pred["image1_orig"] |
|
|
|
num_inliers = 0 |
|
if "keypoints0_orig" in pred.keys() and "keypoints1_orig" in pred.keys(): |
|
mkpts0 = pred["keypoints0_orig"] |
|
mkpts1 = pred["keypoints1_orig"] |
|
num_inliers = len(mkpts0) |
|
if "mconf" in pred.keys(): |
|
mconf = pred["mconf"] |
|
else: |
|
mconf = np.ones(len(mkpts0)) |
|
fig_mkpts = draw_matches( |
|
mkpts0, |
|
mkpts1, |
|
img0, |
|
img1, |
|
mconf, |
|
dpi=dpi, |
|
titles=titles, |
|
) |
|
fig = fig_mkpts |
|
if "line0_orig" in pred.keys() and "line1_orig" in pred.keys(): |
|
|
|
mtlines0 = pred["line0_orig"] |
|
mtlines1 = pred["line1_orig"] |
|
num_inliers = len(mtlines0) |
|
fig_lines = plot_images( |
|
[img0.squeeze(), img1.squeeze()], |
|
["Image 0 - matched lines", "Image 1 - matched lines"], |
|
dpi=300, |
|
) |
|
fig_lines = plot_color_line_matches([mtlines0, mtlines1], lw=2) |
|
fig_lines = fig2im(fig_lines) |
|
|
|
|
|
mkpts0 = pred["line_keypoints0_orig"] |
|
mkpts1 = pred["line_keypoints1_orig"] |
|
|
|
if mkpts0 is not None and mkpts1 is not None: |
|
num_inliers = len(mkpts0) |
|
if "mconf" in pred.keys(): |
|
mconf = pred["mconf"] |
|
else: |
|
mconf = np.ones(len(mkpts0)) |
|
fig_mkpts = draw_matches(mkpts0, mkpts1, img0, img1, mconf, dpi=300) |
|
fig_lines = cv2.resize( |
|
fig_lines, (fig_mkpts.shape[1], fig_mkpts.shape[0]) |
|
) |
|
fig = np.concatenate([fig_mkpts, fig_lines], axis=0) |
|
else: |
|
fig = fig_lines |
|
return fig, num_inliers |
|
|
|
|
|
def run_matching( |
|
image0, |
|
image1, |
|
match_threshold, |
|
extract_max_keypoints, |
|
keypoint_threshold, |
|
key, |
|
ransac_method=DEFAULT_RANSAC_METHOD, |
|
ransac_reproj_threshold=DEFAULT_RANSAC_REPROJ_THRESHOLD, |
|
ransac_confidence=DEFAULT_RANSAC_CONFIDENCE, |
|
ransac_max_iter=DEFAULT_RANSAC_MAX_ITER, |
|
choice_estimate_geom=DEFAULT_SETTING_GEOMETRY, |
|
): |
|
|
|
if image0 is None or image1 is None: |
|
raise gr.Error("Error: No images found! Please upload two images.") |
|
|
|
output_keypoints = None |
|
output_matches_raw = None |
|
output_matches_ransac = None |
|
|
|
model = matcher_zoo[key] |
|
match_conf = model["config"] |
|
|
|
match_conf["model"]["match_threshold"] = match_threshold |
|
match_conf["model"]["max_keypoints"] = extract_max_keypoints |
|
|
|
matcher = get_model(match_conf) |
|
if model["dense"]: |
|
pred = match_dense.match_images( |
|
matcher, image0, image1, match_conf["preprocessing"], device=device |
|
) |
|
del matcher |
|
extract_conf = None |
|
else: |
|
extract_conf = model["config_feature"] |
|
|
|
extract_conf["model"]["max_keypoints"] = extract_max_keypoints |
|
extract_conf["model"]["keypoint_threshold"] = keypoint_threshold |
|
extractor = get_feature_model(extract_conf) |
|
pred0 = extract_features.extract( |
|
extractor, image0, extract_conf["preprocessing"] |
|
) |
|
pred1 = extract_features.extract( |
|
extractor, image1, extract_conf["preprocessing"] |
|
) |
|
pred = match_features.match_images(matcher, pred0, pred1) |
|
del extractor |
|
|
|
|
|
titles = [ |
|
"Image 0 - Keypoints", |
|
"Image 1 - Keypoints", |
|
] |
|
output_keypoints = plot_images([image0, image1], titles=titles, dpi=300) |
|
if "keypoints0" in pred.keys() and "keypoints1" in pred.keys(): |
|
plot_keypoints([pred["keypoints0"], pred["keypoints1"]]) |
|
text = ( |
|
f"# keypoints0: {len(pred['keypoints0'])} \n" |
|
+ f"# keypoints1: {len(pred['keypoints1'])}" |
|
) |
|
add_text(0, text, fs=15) |
|
output_keypoints = fig2im(output_keypoints) |
|
|
|
|
|
titles = [ |
|
"Image 0 - Raw matched keypoints", |
|
"Image 1 - Raw matched keypoints", |
|
] |
|
|
|
output_matches_raw, num_matches_raw = display_matches(pred, titles=titles) |
|
|
|
|
|
filter_matches( |
|
pred, |
|
ransac_method=ransac_method, |
|
ransac_reproj_threshold=ransac_reproj_threshold, |
|
ransac_confidence=ransac_confidence, |
|
ransac_max_iter=ransac_max_iter, |
|
) |
|
|
|
|
|
titles = [ |
|
"Image 0 - Ransac matched keypoints", |
|
"Image 1 - Ransac matched keypoints", |
|
] |
|
output_matches_ransac, num_matches_ransac = display_matches( |
|
pred, titles=titles |
|
) |
|
|
|
|
|
geom_info = compute_geom(pred) |
|
output_wrapped, _ = change_estimate_geom( |
|
pred["image0_orig"], |
|
pred["image1_orig"], |
|
{"geom_info": geom_info}, |
|
choice_estimate_geom, |
|
) |
|
|
|
del pred |
|
|
|
return ( |
|
output_keypoints, |
|
output_matches_raw, |
|
output_matches_ransac, |
|
{ |
|
"number raw matches": num_matches_raw, |
|
"number ransac matches": num_matches_ransac, |
|
}, |
|
{ |
|
"match_conf": match_conf, |
|
"extractor_conf": extract_conf, |
|
}, |
|
{ |
|
"geom_info": geom_info, |
|
}, |
|
output_wrapped, |
|
) |
|
|
|
|
|
|
|
|
|
ransac_zoo = { |
|
"RANSAC": cv2.RANSAC, |
|
"USAC_MAGSAC": cv2.USAC_MAGSAC, |
|
"USAC_DEFAULT": cv2.USAC_DEFAULT, |
|
"USAC_FM_8PTS": cv2.USAC_FM_8PTS, |
|
"USAC_PROSAC": cv2.USAC_PROSAC, |
|
"USAC_FAST": cv2.USAC_FAST, |
|
"USAC_ACCURATE": cv2.USAC_ACCURATE, |
|
"USAC_PARALLEL": cv2.USAC_PARALLEL, |
|
} |
|
|
|
|
|
matcher_zoo = { |
|
"gluestick": {"config": match_dense.confs["gluestick"], "dense": True}, |
|
"sold2": {"config": match_dense.confs["sold2"], "dense": True}, |
|
|
|
|
|
|
|
|
|
"loftr": {"config": match_dense.confs["loftr"], "dense": True}, |
|
"topicfm": {"config": match_dense.confs["topicfm"], "dense": True}, |
|
"aspanformer": {"config": match_dense.confs["aspanformer"], "dense": True}, |
|
"dedode": { |
|
"config": match_features.confs["Dual-Softmax"], |
|
"config_feature": extract_features.confs["dedode"], |
|
"dense": False, |
|
}, |
|
"superpoint+superglue": { |
|
"config": match_features.confs["superglue"], |
|
"config_feature": extract_features.confs["superpoint_max"], |
|
"dense": False, |
|
}, |
|
"superpoint+lightglue": { |
|
"config": match_features.confs["superpoint-lightglue"], |
|
"config_feature": extract_features.confs["superpoint_max"], |
|
"dense": False, |
|
}, |
|
"disk": { |
|
"config": match_features.confs["NN-mutual"], |
|
"config_feature": extract_features.confs["disk"], |
|
"dense": False, |
|
}, |
|
"disk+dualsoftmax": { |
|
"config": match_features.confs["Dual-Softmax"], |
|
"config_feature": extract_features.confs["disk"], |
|
"dense": False, |
|
}, |
|
"superpoint+dualsoftmax": { |
|
"config": match_features.confs["Dual-Softmax"], |
|
"config_feature": extract_features.confs["superpoint_max"], |
|
"dense": False, |
|
}, |
|
"disk+lightglue": { |
|
"config": match_features.confs["disk-lightglue"], |
|
"config_feature": extract_features.confs["disk"], |
|
"dense": False, |
|
}, |
|
"superpoint+mnn": { |
|
"config": match_features.confs["NN-mutual"], |
|
"config_feature": extract_features.confs["superpoint_max"], |
|
"dense": False, |
|
}, |
|
"sift+sgmnet": { |
|
"config": match_features.confs["sgmnet"], |
|
"config_feature": extract_features.confs["sift"], |
|
"dense": False, |
|
}, |
|
"sosnet": { |
|
"config": match_features.confs["NN-mutual"], |
|
"config_feature": extract_features.confs["sosnet"], |
|
"dense": False, |
|
}, |
|
"hardnet": { |
|
"config": match_features.confs["NN-mutual"], |
|
"config_feature": extract_features.confs["hardnet"], |
|
"dense": False, |
|
}, |
|
"d2net": { |
|
"config": match_features.confs["NN-mutual"], |
|
"config_feature": extract_features.confs["d2net-ss"], |
|
"dense": False, |
|
}, |
|
"rord": { |
|
"config": match_features.confs["NN-mutual"], |
|
"config_feature": extract_features.confs["rord"], |
|
"dense": False, |
|
}, |
|
|
|
|
|
|
|
|
|
|
|
"alike": { |
|
"config": match_features.confs["NN-mutual"], |
|
"config_feature": extract_features.confs["alike"], |
|
"dense": False, |
|
}, |
|
"lanet": { |
|
"config": match_features.confs["NN-mutual"], |
|
"config_feature": extract_features.confs["lanet"], |
|
"dense": False, |
|
}, |
|
"r2d2": { |
|
"config": match_features.confs["NN-mutual"], |
|
"config_feature": extract_features.confs["r2d2"], |
|
"dense": False, |
|
}, |
|
"darkfeat": { |
|
"config": match_features.confs["NN-mutual"], |
|
"config_feature": extract_features.confs["darkfeat"], |
|
"dense": False, |
|
}, |
|
"sift": { |
|
"config": match_features.confs["NN-mutual"], |
|
"config_feature": extract_features.confs["sift"], |
|
"dense": False, |
|
}, |
|
|
|
|
|
} |
|
|