Skip to content

Commit e31d30b

Browse files
committed
initial commit
1 parent bf5273d commit e31d30b

35 files changed

+2587
-1
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,3 +158,5 @@ cython_debug/
158158
# and can be added to the global gitignore or merged into this file. For a more nuclear
159159
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
160160
#.idea/
161+
162+
.vscode*

DeDoDe/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from .model_zoo import dedode_detector_B, dedode_detector_L, dedode_descriptor_B

DeDoDe/benchmarks/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from .num_inliers import NumInliersBenchmark
2+
from .mega_pose_est import MegaDepthPoseEstimationBenchmark
3+
from .mega_pose_est_mnn import MegaDepthPoseMNNBenchmark

DeDoDe/benchmarks/mega_pose_est.py

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
import numpy as np
2+
import torch
3+
from DeDoDe.utils import *
4+
from PIL import Image
5+
from tqdm import tqdm
6+
import torch.nn.functional as F
7+
8+
class MegaDepthPoseEstimationBenchmark:
9+
def __init__(self, data_root="data/megadepth", scene_names = None) -> None:
10+
if scene_names is None:
11+
self.scene_names = [
12+
"0015_0.1_0.3.npz",
13+
"0015_0.3_0.5.npz",
14+
"0022_0.1_0.3.npz",
15+
"0022_0.3_0.5.npz",
16+
"0022_0.5_0.7.npz",
17+
]
18+
else:
19+
self.scene_names = scene_names
20+
self.scenes = [
21+
np.load(f"{data_root}/{scene}", allow_pickle=True)
22+
for scene in self.scene_names
23+
]
24+
self.data_root = data_root
25+
26+
def benchmark(self, keypoint_model, matching_model, model_name = None, resolution = None, scale_intrinsics = True, calibrated = True):
27+
H,W = matching_model.get_output_resolution()
28+
with torch.no_grad():
29+
data_root = self.data_root
30+
tot_e_t, tot_e_R, tot_e_pose = [], [], []
31+
thresholds = [5, 10, 20]
32+
for scene_ind in range(len(self.scenes)):
33+
import os
34+
scene_name = os.path.splitext(self.scene_names[scene_ind])[0]
35+
scene = self.scenes[scene_ind]
36+
pairs = scene["pair_infos"]
37+
intrinsics = scene["intrinsics"]
38+
poses = scene["poses"]
39+
im_paths = scene["image_paths"]
40+
pair_inds = range(len(pairs))
41+
for pairind in tqdm(pair_inds):
42+
idx1, idx2 = pairs[pairind][0]
43+
K1 = intrinsics[idx1].copy()
44+
T1 = poses[idx1].copy()
45+
R1, t1 = T1[:3, :3], T1[:3, 3]
46+
K2 = intrinsics[idx2].copy()
47+
T2 = poses[idx2].copy()
48+
R2, t2 = T2[:3, :3], T2[:3, 3]
49+
R, t = compute_relative_pose(R1, t1, R2, t2)
50+
T1_to_2 = np.concatenate((R,t[:,None]), axis=-1)
51+
im_A_path = f"{data_root}/{im_paths[idx1]}"
52+
im_B_path = f"{data_root}/{im_paths[idx2]}"
53+
54+
keypoints_A = keypoint_model.detect_from_path(im_A_path, num_keypoints = 20_000)["keypoints"][0]
55+
keypoints_B = keypoint_model.detect_from_path(im_B_path, num_keypoints = 20_000)["keypoints"][0]
56+
warp, certainty = matching_model.match(im_A_path, im_B_path)
57+
matches = matching_model.match_keypoints(keypoints_A, keypoints_B, warp, certainty, return_tuple = False)
58+
im_A = Image.open(im_A_path)
59+
w1, h1 = im_A.size
60+
im_B = Image.open(im_B_path)
61+
w2, h2 = im_B.size
62+
if scale_intrinsics:
63+
scale1 = 1200 / max(w1, h1)
64+
scale2 = 1200 / max(w2, h2)
65+
w1, h1 = scale1 * w1, scale1 * h1
66+
w2, h2 = scale2 * w2, scale2 * h2
67+
K1, K2 = K1.copy(), K2.copy()
68+
K1[:2] = K1[:2] * scale1
69+
K2[:2] = K2[:2] * scale2
70+
kpts1, kpts2 = matching_model.to_pixel_coordinates(matches, h1, w1, h2, w2)
71+
for _ in range(1):
72+
shuffling = np.random.permutation(np.arange(len(kpts1)))
73+
kpts1 = kpts1[shuffling]
74+
kpts2 = kpts2[shuffling]
75+
try:
76+
threshold = 0.5
77+
if calibrated:
78+
norm_threshold = threshold / (np.mean(np.abs(K1[:2, :2])) + np.mean(np.abs(K2[:2, :2])))
79+
R_est, t_est, mask = estimate_pose(
80+
kpts1.cpu().numpy(),
81+
kpts2.cpu().numpy(),
82+
K1,
83+
K2,
84+
norm_threshold,
85+
conf=0.99999,
86+
)
87+
T1_to_2_est = np.concatenate((R_est, t_est), axis=-1) #
88+
e_t, e_R = compute_pose_error(T1_to_2_est, R, t)
89+
e_pose = max(e_t, e_R)
90+
except Exception as e:
91+
print(repr(e))
92+
e_t, e_R = 90, 90
93+
e_pose = max(e_t, e_R)
94+
tot_e_t.append(e_t)
95+
tot_e_R.append(e_R)
96+
tot_e_pose.append(e_pose)
97+
tot_e_pose = np.array(tot_e_pose)
98+
auc = pose_auc(tot_e_pose, thresholds)
99+
acc_5 = (tot_e_pose < 5).mean()
100+
acc_10 = (tot_e_pose < 10).mean()
101+
acc_15 = (tot_e_pose < 15).mean()
102+
acc_20 = (tot_e_pose < 20).mean()
103+
map_5 = acc_5
104+
map_10 = np.mean([acc_5, acc_10])
105+
map_20 = np.mean([acc_5, acc_10, acc_15, acc_20])
106+
print(f"{model_name} auc: {auc}")
107+
return {
108+
"auc_5": auc[0],
109+
"auc_10": auc[1],
110+
"auc_20": auc[2],
111+
"map_5": map_5,
112+
"map_10": map_10,
113+
"map_20": map_20,
114+
}
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
import numpy as np
2+
import torch
3+
from DeDoDe.utils import *
4+
from PIL import Image
5+
from tqdm import tqdm
6+
import torch.nn.functional as F
7+
8+
class MegaDepthPoseMNNBenchmark:
9+
def __init__(self, data_root="data/megadepth", scene_names = None) -> None:
10+
if scene_names is None:
11+
self.scene_names = [
12+
"0015_0.1_0.3.npz",
13+
"0015_0.3_0.5.npz",
14+
"0022_0.1_0.3.npz",
15+
"0022_0.3_0.5.npz",
16+
"0022_0.5_0.7.npz",
17+
]
18+
else:
19+
self.scene_names = scene_names
20+
self.scenes = [
21+
np.load(f"{data_root}/{scene}", allow_pickle=True)
22+
for scene in self.scene_names
23+
]
24+
self.data_root = data_root
25+
26+
def benchmark(self, detector_model, descriptor_model, matcher_model, model_name = None, resolution = None, scale_intrinsics = True, calibrated = True):
27+
with torch.no_grad():
28+
data_root = self.data_root
29+
tot_e_t, tot_e_R, tot_e_pose = [], [], []
30+
thresholds = [5, 10, 20]
31+
for scene_ind in range(len(self.scenes)):
32+
import os
33+
scene_name = os.path.splitext(self.scene_names[scene_ind])[0]
34+
scene = self.scenes[scene_ind]
35+
pairs = scene["pair_infos"]
36+
intrinsics = scene["intrinsics"]
37+
poses = scene["poses"]
38+
im_paths = scene["image_paths"]
39+
pair_inds = range(len(pairs))
40+
for pairind in tqdm(pair_inds):
41+
idx1, idx2 = pairs[pairind][0]
42+
K1 = intrinsics[idx1].copy()
43+
T1 = poses[idx1].copy()
44+
R1, t1 = T1[:3, :3], T1[:3, 3]
45+
K2 = intrinsics[idx2].copy()
46+
T2 = poses[idx2].copy()
47+
R2, t2 = T2[:3, :3], T2[:3, 3]
48+
R, t = compute_relative_pose(R1, t1, R2, t2)
49+
T1_to_2 = np.concatenate((R,t[:,None]), axis=-1)
50+
im_A_path = f"{data_root}/{im_paths[idx1]}"
51+
im_B_path = f"{data_root}/{im_paths[idx2]}"
52+
detections_A = detector_model.detect_from_path(im_A_path)
53+
keypoints_A, P_A = detections_A["keypoints"], detections_A["confidence"]
54+
detections_B = detector_model.detect_from_path(im_B_path)
55+
keypoints_B, P_B = detections_B["keypoints"], detections_B["confidence"]
56+
description_A = descriptor_model.describe_keypoints_from_path(im_A_path, keypoints_A)["descriptions"]
57+
description_B = descriptor_model.describe_keypoints_from_path(im_B_path, keypoints_B)["descriptions"]
58+
matches_A, matches_B, batch_ids = matcher_model.match(keypoints_A, description_A,
59+
keypoints_B, description_B,
60+
P_A = P_A, P_B = P_B,
61+
normalize = True, inv_temp=20, threshold = 0.01)
62+
63+
im_A = Image.open(im_A_path)
64+
w1, h1 = im_A.size
65+
im_B = Image.open(im_B_path)
66+
w2, h2 = im_B.size
67+
if scale_intrinsics:
68+
scale1 = 1200 / max(w1, h1)
69+
scale2 = 1200 / max(w2, h2)
70+
w1, h1 = scale1 * w1, scale1 * h1
71+
w2, h2 = scale2 * w2, scale2 * h2
72+
K1, K2 = K1.copy(), K2.copy()
73+
K1[:2] = K1[:2] * scale1
74+
K2[:2] = K2[:2] * scale2
75+
kpts1, kpts2 = matcher_model.to_pixel_coords(matches_A, matches_B, h1, w1, h2, w2)
76+
for _ in range(1):
77+
shuffling = np.random.permutation(np.arange(len(kpts1)))
78+
kpts1 = kpts1[shuffling]
79+
kpts2 = kpts2[shuffling]
80+
try:
81+
threshold = 0.5
82+
if calibrated:
83+
norm_threshold = threshold / (np.mean(np.abs(K1[:2, :2])) + np.mean(np.abs(K2[:2, :2])))
84+
R_est, t_est, mask = estimate_pose(
85+
kpts1.cpu().numpy(),
86+
kpts2.cpu().numpy(),
87+
K1,
88+
K2,
89+
norm_threshold,
90+
conf=0.99999,
91+
)
92+
T1_to_2_est = np.concatenate((R_est, t_est), axis=-1) #
93+
e_t, e_R = compute_pose_error(T1_to_2_est, R, t)
94+
e_pose = max(e_t, e_R)
95+
except Exception as e:
96+
print(repr(e))
97+
e_t, e_R = 90, 90
98+
e_pose = max(e_t, e_R)
99+
tot_e_t.append(e_t)
100+
tot_e_R.append(e_R)
101+
tot_e_pose.append(e_pose)
102+
tot_e_pose = np.array(tot_e_pose)
103+
auc = pose_auc(tot_e_pose, thresholds)
104+
acc_5 = (tot_e_pose < 5).mean()
105+
acc_10 = (tot_e_pose < 10).mean()
106+
acc_15 = (tot_e_pose < 15).mean()
107+
acc_20 = (tot_e_pose < 20).mean()
108+
map_5 = acc_5
109+
map_10 = np.mean([acc_5, acc_10])
110+
map_20 = np.mean([acc_5, acc_10, acc_15, acc_20])
111+
print(f"{model_name} auc: {auc}")
112+
return {
113+
"auc_5": auc[0],
114+
"auc_10": auc[1],
115+
"auc_20": auc[2],
116+
"map_5": map_5,
117+
"map_10": map_10,
118+
"map_20": map_20,
119+
}

DeDoDe/benchmarks/num_inliers.py

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import torch
2+
import torch.nn as nn
3+
from DeDoDe.utils import *
4+
import DeDoDe
5+
6+
class NumInliersBenchmark(nn.Module):
7+
8+
def __init__(self, dataset, num_samples = 1000, batch_size = 8, num_keypoints = 10_000, device = "cuda") -> None:
9+
super().__init__()
10+
sampler = torch.utils.data.WeightedRandomSampler(
11+
torch.ones(len(dataset)), replacement=False, num_samples=num_samples
12+
)
13+
dataloader = torch.utils.data.DataLoader(
14+
dataset, batch_size=batch_size, num_workers=batch_size, sampler=sampler
15+
)
16+
self.dataloader = dataloader
17+
self.tracked_metrics = {}
18+
self.batch_size = batch_size
19+
self.N = len(dataloader)
20+
self.num_keypoints = num_keypoints
21+
22+
def compute_batch_metrics(self, outputs, batch, device = "cuda"):
23+
kpts_A, kpts_B = outputs["keypoints_A"], outputs["keypoints_B"]
24+
B, K, H, W = batch["im_A"].shape
25+
gt_warp_A_to_B, valid_mask_A_to_B = get_gt_warp(
26+
batch["im_A_depth"],
27+
batch["im_B_depth"],
28+
batch["T_1to2"],
29+
batch["K1"],
30+
batch["K2"],
31+
H=H,
32+
W=W,
33+
)
34+
kpts_A_to_B = F.grid_sample(gt_warp_A_to_B[...,2:].float().permute(0,3,1,2), kpts_A[...,None,:],
35+
align_corners=False, mode = 'bilinear')[...,0].mT
36+
legit_A_to_B = F.grid_sample(valid_mask_A_to_B.reshape(B,1,H,W), kpts_A[...,None,:],
37+
align_corners=False, mode = 'bilinear')[...,0,:,0]
38+
dists = (torch.cdist(kpts_A_to_B, kpts_B).min(dim=-1).values[legit_A_to_B > 0.]).float()
39+
if legit_A_to_B.sum() == 0:
40+
return
41+
percent_inliers_at_1 = (dists < 0.02).float().mean()
42+
percent_inliers_at_05 = (dists < 0.01).float().mean()
43+
percent_inliers_at_025 = (dists < 0.005).float().mean()
44+
percent_inliers_at_01 = (dists < 0.002).float().mean()
45+
percent_inliers_at_005 = (dists < 0.001).float().mean()
46+
47+
inlier_bins = torch.linspace(0, 0.002, steps = 100, device = device)[None]
48+
inlier_counts = (dists[...,None] < inlier_bins).float().mean(dim=0)
49+
self.tracked_metrics["inlier_counts"] = self.tracked_metrics.get("inlier_counts", 0) + 1/self.N * inlier_counts
50+
self.tracked_metrics["percent_inliers_at_1"] = self.tracked_metrics.get("percent_inliers_at_1", 0) + 1/self.N * percent_inliers_at_1
51+
self.tracked_metrics["percent_inliers_at_05"] = self.tracked_metrics.get("percent_inliers_at_05", 0) + 1/self.N * percent_inliers_at_05
52+
self.tracked_metrics["percent_inliers_at_025"] = self.tracked_metrics.get("percent_inliers_at_025", 0) + 1/self.N * percent_inliers_at_025
53+
self.tracked_metrics["percent_inliers_at_01"] = self.tracked_metrics.get("percent_inliers_at_01", 0) + 1/self.N * percent_inliers_at_01
54+
self.tracked_metrics["percent_inliers_at_005"] = self.tracked_metrics.get("percent_inliers_at_005", 0) + 1/self.N * percent_inliers_at_005
55+
56+
def benchmark(self, detector):
57+
self.tracked_metrics = {}
58+
from tqdm import tqdm
59+
print("Evaluating percent inliers...")
60+
for idx, batch in tqdm(enumerate(self.dataloader), mininterval = 10.):
61+
batch = to_cuda(batch)
62+
outputs = detector.detect(batch, num_keypoints = self.num_keypoints)
63+
keypoints_A, keypoints_B = outputs["keypoints"][:self.batch_size], outputs["keypoints"][self.batch_size:]
64+
if isinstance(outputs["keypoints"], (tuple, list)):
65+
keypoints_A, keypoints_B = torch.stack(keypoints_A), torch.stack(keypoints_B)
66+
outputs = {"keypoints_A": keypoints_A, "keypoints_B": keypoints_B}
67+
self.compute_batch_metrics(outputs, batch)
68+
import matplotlib.pyplot as plt
69+
plt.plot(torch.linspace(0, 0.002, steps = 100), self.tracked_metrics["inlier_counts"].cpu())
70+
import numpy as np
71+
x = np.linspace(0,0.002, 100)
72+
sigma = 0.52 * 2 / 512
73+
F = 1 - np.exp(-x**2 / (2*sigma**2))
74+
plt.plot(x, F)
75+
plt.savefig("vis/inlier_counts")
76+
[print(name, metric.item() * self.N / (idx+1)) for name, metric in self.tracked_metrics.items() if "percent" in name]

DeDoDe/checkpoint.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import os
2+
import torch
3+
from torch.nn.parallel.data_parallel import DataParallel
4+
from torch.nn.parallel.distributed import DistributedDataParallel
5+
import gc
6+
7+
import DeDoDe
8+
9+
class CheckPoint:
10+
def __init__(self, dir=None, name="tmp"):
11+
self.name = name
12+
self.dir = dir
13+
os.makedirs(self.dir, exist_ok=True)
14+
15+
def save(
16+
self,
17+
model,
18+
optimizer,
19+
lr_scheduler,
20+
n,
21+
):
22+
if DeDoDe.RANK == 0:
23+
assert model is not None
24+
if isinstance(model, (DataParallel, DistributedDataParallel)):
25+
model = model.module
26+
states = {
27+
"model": model.state_dict(),
28+
"n": n,
29+
"optimizer": optimizer.state_dict(),
30+
"lr_scheduler": lr_scheduler.state_dict(),
31+
}
32+
torch.save(states, self.dir + self.name + f"_latest.pth")
33+
print(f"Saved states {list(states.keys())}, at step {n}")
34+
35+
def load(
36+
self,
37+
model,
38+
optimizer,
39+
lr_scheduler,
40+
n,
41+
):
42+
if os.path.exists(self.dir + self.name + f"_latest.pth") and DeDoDe.RANK == 0:
43+
states = torch.load(self.dir + self.name + f"_latest.pth")
44+
if "model" in states:
45+
model.load_state_dict(states["model"])
46+
if "n" in states:
47+
n = states["n"] if states["n"] else n
48+
if "optimizer" in states:
49+
try:
50+
optimizer.load_state_dict(states["optimizer"])
51+
except Exception as e:
52+
print(f"Failed to load states for optimizer, with error {e}")
53+
if "lr_scheduler" in states:
54+
lr_scheduler.load_state_dict(states["lr_scheduler"])
55+
print(f"Loaded states {list(states.keys())}, at step {n}")
56+
del states
57+
gc.collect()
58+
torch.cuda.empty_cache()
59+
return model, optimizer, lr_scheduler, n

DeDoDe/datasets/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)