Skip to content

Commit 10fe3ed

Browse files
committed
add loma
1 parent c13273b commit 10fe3ed

5 files changed

Lines changed: 158 additions & 0 deletions

File tree

hloc/extract_features.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,28 @@
125125
"resize_max": 1024,
126126
},
127127
},
128+
"loma_aachen": {
129+
"output": "feats-loma-n4096-r1024",
130+
"model": {
131+
"name": "loma",
132+
"max_keypoints": 4096,
133+
},
134+
"preprocessing": {
135+
"grayscale": False,
136+
"resize_max": 1024,
137+
},
138+
},
139+
"loma_inloc": {
140+
"output": "feats-loma-n4096-r1600",
141+
"model": {
142+
"name": "loma",
143+
"max_keypoints": 4096,
144+
},
145+
"preprocessing": {
146+
"grayscale": False,
147+
"resize_max": 1600,
148+
},
149+
},
128150
# Global descriptors
129151
"dir": {
130152
"output": "global-feats-dir",

hloc/extractors/loma.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import torch
2+
import torch.nn.functional as F
3+
from loma.descriptor.dedode import DeDoDeDescriptor
4+
from loma.detector.dad import DaD
5+
from loma.device import device
6+
7+
from ..utils.base_model import BaseModel
8+
9+
10+
class LoMaExtractor(BaseModel):
11+
default_conf = {
12+
"max_keypoints": 2048,
13+
"compile": False,
14+
}
15+
required_inputs = ["image"]
16+
17+
def _init(self, conf):
18+
# DaD weights loaded by default
19+
self.detector = DaD(DaD.Cfg(compile=conf["compile"])).eval()
20+
21+
# Descriptor weights need to be manually loaded
22+
self.descriptor = DeDoDeDescriptor(
23+
DeDoDeDescriptor.Cfg(compile=conf["compile"], arch="dedode_g")
24+
).eval()
25+
weights = torch.hub.load_state_dict_from_url(
26+
"https://github.com/davnords/storage/releases/download/loma/loma_B.pt",
27+
map_location=device,
28+
)
29+
weights = {k: v for k, v in weights.items() if k.startswith("_descriptor.")}
30+
weights = {k[len("_descriptor.") :]: v for k, v in weights.items()}
31+
self.descriptor.load_state_dict(weights, strict=True)
32+
33+
def preprocess_image(self, image, H=784, W=784):
34+
image = F.interpolate(
35+
image,
36+
size=(H, W),
37+
mode="bilinear",
38+
align_corners=False,
39+
)[0]
40+
return image[None].to(device)
41+
42+
def detect_and_describe(self, batch: dict[str, torch.Tensor]):
43+
H, W = batch["image"].shape[2:]
44+
45+
detections = self.detector.detect(
46+
batch, num_keypoints=self.conf["max_keypoints"]
47+
)
48+
keypoints = detections["keypoints"]
49+
50+
description = self.descriptor.describe_keypoints(
51+
self.preprocess_image(batch["image"]),
52+
keypoints,
53+
)
54+
keypoints = self.detector.to_pixel_coords(keypoints, H, W)
55+
keypoints = keypoints - 0.5 # be consistent with hloc
56+
keypoints[..., 0] = keypoints[..., 0].clamp(0.5, W - 1.5)
57+
keypoints[..., 1] = keypoints[..., 1].clamp(0.5, H - 1.5)
58+
return {
59+
"keypoints": [keypoints[0]],
60+
"descriptors": [description["descriptions"].transpose(-1, -2)[0]],
61+
"scores": [detections["keypoint_probs"][0]],
62+
}
63+
64+
def _forward(self, data):
65+
return self.detect_and_describe(data)

hloc/match_features.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,10 @@
8585
"output": "matches-adalam",
8686
"model": {"name": "adalam"},
8787
},
88+
"loma": {
89+
"output": "matches-loma",
90+
"model": {"name": "loma"},
91+
},
8892
}
8993

9094

hloc/matchers/loma.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import torch
2+
from loma.geometry import to_normalized
3+
from loma.loma import LoMa, LoMaB, LoMaG, LoMaL, filter_matches
4+
5+
from ..utils.base_model import BaseModel
6+
7+
8+
class LoMaMatcher(BaseModel):
9+
default_conf = {
10+
"filter_threshold": 0.1,
11+
"arch": "LoMa-B",
12+
}
13+
required_inputs = [
14+
"image0",
15+
"keypoints0",
16+
"descriptors0",
17+
"image1",
18+
"keypoints1",
19+
"descriptors1",
20+
]
21+
22+
def _init(self, conf):
23+
if conf["arch"] == "LoMa-B":
24+
cfg = LoMaB()
25+
elif conf["arch"] == "LoMa-L":
26+
cfg = LoMaL()
27+
elif conf["arch"] == "LoMa-G":
28+
cfg = LoMaG()
29+
else:
30+
raise ValueError(f"Unknown architecture {conf['arch']}")
31+
self.net = LoMa(cfg)
32+
33+
def _forward(self, data):
34+
H_A, W_A = data["image0"].shape[2:]
35+
H_B, W_B = data["image1"].shape[2:]
36+
37+
data["keypoints0"] = to_normalized(data["keypoints0"], H=H_A, W=W_A)
38+
data["keypoints1"] = to_normalized(data["keypoints1"], H=H_B, W=W_B)
39+
data["descriptors0"] = data["descriptors0"].transpose(-1, -2)
40+
data["descriptors1"] = data["descriptors1"].transpose(-1, -2)
41+
output = self.net(
42+
data["keypoints0"],
43+
data["keypoints1"],
44+
data["descriptors0"],
45+
data["descriptors1"],
46+
)
47+
scores = output["scores"]
48+
49+
b = data["descriptors0"].shape[0]
50+
m0, m1, mscores0, mscores1 = filter_matches(
51+
scores, self.conf["filter_threshold"]
52+
)
53+
matches, mscores = [], []
54+
for k in range(b):
55+
valid = m0[k] > -1
56+
m_indices_0 = torch.where(valid)[0]
57+
m_indices_1 = m0[k][valid]
58+
matches.append(torch.stack([m_indices_0, m_indices_1], -1))
59+
mscores.append(mscores0[k][valid])
60+
61+
return {
62+
"matches0": m0,
63+
"matches1": m1,
64+
"matching_scores0": mscores0,
65+
"matching_scores1": mscores1,
66+
}

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,4 @@ pycolmap>=3.13.0
1111
kornia>=0.6.11
1212
gdown
1313
lightglue @ git+https://github.com/cvg/LightGlue
14+
lomatch @ git+https://github.com/davnords/LoMa

0 commit comments

Comments
 (0)