romav2 2.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
romav2-2.0.0/PKG-INFO ADDED
@@ -0,0 +1,161 @@
1
+ Metadata-Version: 2.3
2
+ Name: romav2
3
+ Version: 2.0.0
4
+ Summary: RoMa v2: Harder Better Faster Denser Feature Matching
5
+ Author: Johan Edstedt
6
+ Author-email: Johan Edstedt <johan.edstedt@liu.se>
7
+ Requires-Dist: dataclasses>=0.8
8
+ Requires-Dist: einops>=0.8.1
9
+ Requires-Dist: pillow>=12.0.0
10
+ Requires-Dist: rich>=14.2.0
11
+ Requires-Dist: torch
12
+ Requires-Dist: torchvision>=0.23.0
13
+ Requires-Dist: tqdm>=4.67.1
14
+ Requires-Dist: slurm-util>=0.2.7 ; extra == 'dev'
15
+ Requires-Dist: ruff>=0.14.5 ; extra == 'dev'
16
+ Requires-Dist: kornia>=0.8.2 ; extra == 'eval'
17
+ Requires-Dist: matplotlib>=3.10.7 ; extra == 'eval'
18
+ Requires-Dist: opencv-python>=4.12.0.88 ; extra == 'eval'
19
+ Requires-Dist: wandb>=0.23.0 ; extra == 'eval'
20
+ Requires-Dist: wxbs-benchmark>=0.0.4 ; extra == 'eval'
21
+ Requires-Dist: fused-local-corr ; sys_platform == 'linux' and extra == 'fused-local-corr'
22
+ Requires-Python: >=3.10
23
+ Provides-Extra: dev
24
+ Provides-Extra: eval
25
+ Provides-Extra: fused-local-corr
26
+ Description-Content-Type: text/markdown
27
+
28
+ <p align="center">
29
+ <h1 align="center"> <ins>RoMa v2</ins> 馃: Harder Better Faster Denser Feature Matching
30
+ <h2 align="center">
31
+ <a href="https://scholar.google.com/citations?user=Ul-vMR0AAAAJ">Johan Edstedt</a>
32
+
33
+ <a href="https://scholar.google.com/citations?user=-vJPE04AAAAJ">David Nordstr枚m</a>
34
+
35
+ <a href="https://scholar.google.com/citations?user=mvY4rdIAAAAJ">Yushan Zhang</a>
36
+
37
+ <a href="https://scholar.google.com/citations?user=FUE3Wd0AAAAJ">Georg B枚kman</a>
38
+
39
+ <a href="https://scholar.google.com/citations?user=dsEPAvUAAAAJ">Jonathan Astermark</a>
40
+
41
+ <a href="https://scholar.google.com/citations?user=vHeD0TYAAAAJ">Viktor Larsson</a>
42
+
43
+ <a href="https://scholar.google.com/citations?user=9j-6i_oAAAAJ&hl">Anders Heyden</a>
44
+
45
+ <a href="https://scholar.google.com/citations?user=P_w6UgMAAAAJ&hl">Fredrik Kahl</a>
46
+
47
+ <a href="https://scholar.google.com/citations?user=6WRQpCQAAAAJ">M氓rten Wadenb盲ck</a>
48
+
49
+ <a href="https://scholar.google.com/citations?user=lkWfR08AAAAJ">Michael Felsberg</a>
50
+ </p>
51
+ <h2 align="center"><p>
52
+ <a href="https://arxiv.org/abs/2511.15706" align="center">Paper</a> |
53
+ <a href="TBD" align="center">Project Page</a>
54
+ </p></h2>
55
+ <div align="center"></div>
56
+ </p>
57
+ <br/>
58
+ <p align="center">
59
+ <img src="assets/qualitative.png" alt="example" width=80%>
60
+ </p>
61
+
62
+ ## How to Use
63
+ ```python
64
+ from romav2 import RoMaV2
65
+
66
+ # load pretrained model
67
+ model = RoMaV2()
68
+ # Match densely for any image-like pair of inputs
69
+ preds = model.match(img_A_path, img_B_path)
70
+
71
+ # you can also run the forward method directly as
72
+ # preds = model(img_A, img_B)
73
+
74
+ # Sample 5000 matches for estimation
75
+ matches, overlaps, precision_AB, precision_BA = model.sample(preds, 5000)
76
+
77
+ # Convert to pixel coordinates (RoMaV2 produces matches in [-1,1]x[-1,1])
78
+ kptsA, kptsB = model.to_pixel_coordinates(matches, H_A, W_A, H_B, W_B)
79
+
80
+ # Find a fundamental matrix (or anything else of interest)
81
+ F, mask = cv2.findFundamentalMat(
82
+ kptsA.cpu().numpy(), kptsB.cpu().numpy(), ransacReprojThreshold=0.2, method=cv2.USAC_MAGSAC, confidence=0.999999, maxIters=10000
83
+ )
84
+ ```
85
+ We additionally provide two demos in the [demos folder](demo), which might help in understanding.
86
+
87
+
88
+ ## Setup/Install
89
+ In your python environment (tested on Linux python 3.12), run:
90
+ ```bash
91
+ uv pip install -e .
92
+ ```
93
+ or
94
+ ```bash
95
+ uv sync
96
+ ```
97
+
98
+ ## Benchmarks
99
+ If you do not already have MegaDepth and ScanNet, you can the following to download them:
100
+ ```bash
101
+ source scripts/eval_prep.sh
102
+ ```
103
+ ### Mega-1500
104
+ ```bash
105
+ uv run tests/test_mega1500.py
106
+ ```
107
+ ### ScanNet-1500
108
+ ```bash
109
+ uv run tests/test_scannet1500.py
110
+ ```
111
+ ### Expected Results
112
+ Experiments on ScanNet-1500 and MegaDepth-1500 are provided in the [tests folder](tests).
113
+ Running these gave me `ScanNet-1500: [34.0, 56.5, 73.9]`, and `Mega-1500: [62.8, 76,8, 86.5]`, which are similar to the results of the paper.
114
+
115
+
116
+ ## Fused local correlation kernel
117
+ Include the `--extra fused-local-corr` flag as:
118
+ ```bash
119
+ uv sync --extra fused-local-corr
120
+ ```
121
+ or
122
+ ```bash
123
+ uv pip install romav2[fused-local-corr]
124
+ ```
125
+ or
126
+ ```bash
127
+ uv add romav2[fused-local-corr]
128
+ ```
129
+
130
+ ## Settings
131
+ By twiddling with some different settings you may reach better results on your task of interest.
132
+ Some important ones, which we enable setting to some reasonable defaults through `model.apply_setting`, are:
133
+
134
+ `model.H_lr, model.W_lr`: height and width for the image pair.
135
+
136
+ `model.H_hr, model.W_hr`: height and width for a high resolution version of the image pair (used for upsampling as in RoMa)
137
+
138
+ `model.bidirectional`: Useful for getting more diverse matches, and for estimating the covariance matrix in both directions.
139
+
140
+ `model.threshold`: Value between [0,1]. Used to set overlap prediction above it to 1. Useful for Mega1500.
141
+
142
+ `model.balanced_sampling`: Diverse sampling, same as RoMa. Typically helps to get better RANSAC estimates.
143
+
144
+ ## License
145
+ All our code except DINOv3 is MIT license.
146
+ DINOv3 has a custom license, see [DINOv3](https://github.com/facebookresearch/dinov3/tree/main?tab=License-1-ov-file#readme).
147
+
148
+ ## Acknowledgement
149
+ Our codebase builds mainly on the code in [RoMa](https://github.com/Parskatt/RoMa).
150
+ We were additionally inspired by [UFM](https://github.com/UniFlowMatch/UFM) and [MapAnything](https://github.com/facebookresearch/map-anything), particularly for the datasets used to train the models.
151
+
152
+ ## BibTeX
153
+ If you find our models useful, please consider citing our paper!
154
+ ```
155
+ @article{edstedt2025romav2,
156
+ title={{RoMa v2: Harder Better Faster Denser Feature Matching}},
157
+ author={Johan Edstedt, David Nordstr枚m, Yushan Zhang, Georg B枚kman, Jonathan Astermark, Viktor Larsson, Anders Heyden, Fredrik Kahl, M氓rten Wadenb盲ck, Michael Felsberg},
158
+ journal={arXiv preprint arXiv:2511.15706},
159
+ year={2025}
160
+ }
161
+ ```
romav2-2.0.0/README.md ADDED
@@ -0,0 +1,134 @@
1
+ <p align="center">
2
+ <h1 align="center"> <ins>RoMa v2</ins> 馃: Harder Better Faster Denser Feature Matching
3
+ <h2 align="center">
4
+ <a href="https://scholar.google.com/citations?user=Ul-vMR0AAAAJ">Johan Edstedt</a>
5
+
6
+ <a href="https://scholar.google.com/citations?user=-vJPE04AAAAJ">David Nordstr枚m</a>
7
+
8
+ <a href="https://scholar.google.com/citations?user=mvY4rdIAAAAJ">Yushan Zhang</a>
9
+
10
+ <a href="https://scholar.google.com/citations?user=FUE3Wd0AAAAJ">Georg B枚kman</a>
11
+
12
+ <a href="https://scholar.google.com/citations?user=dsEPAvUAAAAJ">Jonathan Astermark</a>
13
+
14
+ <a href="https://scholar.google.com/citations?user=vHeD0TYAAAAJ">Viktor Larsson</a>
15
+
16
+ <a href="https://scholar.google.com/citations?user=9j-6i_oAAAAJ&hl">Anders Heyden</a>
17
+
18
+ <a href="https://scholar.google.com/citations?user=P_w6UgMAAAAJ&hl">Fredrik Kahl</a>
19
+
20
+ <a href="https://scholar.google.com/citations?user=6WRQpCQAAAAJ">M氓rten Wadenb盲ck</a>
21
+
22
+ <a href="https://scholar.google.com/citations?user=lkWfR08AAAAJ">Michael Felsberg</a>
23
+ </p>
24
+ <h2 align="center"><p>
25
+ <a href="https://arxiv.org/abs/2511.15706" align="center">Paper</a> |
26
+ <a href="TBD" align="center">Project Page</a>
27
+ </p></h2>
28
+ <div align="center"></div>
29
+ </p>
30
+ <br/>
31
+ <p align="center">
32
+ <img src="assets/qualitative.png" alt="example" width=80%>
33
+ </p>
34
+
35
+ ## How to Use
36
+ ```python
37
+ from romav2 import RoMaV2
38
+
39
+ # load pretrained model
40
+ model = RoMaV2()
41
+ # Match densely for any image-like pair of inputs
42
+ preds = model.match(img_A_path, img_B_path)
43
+
44
+ # you can also run the forward method directly as
45
+ # preds = model(img_A, img_B)
46
+
47
+ # Sample 5000 matches for estimation
48
+ matches, overlaps, precision_AB, precision_BA = model.sample(preds, 5000)
49
+
50
+ # Convert to pixel coordinates (RoMaV2 produces matches in [-1,1]x[-1,1])
51
+ kptsA, kptsB = model.to_pixel_coordinates(matches, H_A, W_A, H_B, W_B)
52
+
53
+ # Find a fundamental matrix (or anything else of interest)
54
+ F, mask = cv2.findFundamentalMat(
55
+ kptsA.cpu().numpy(), kptsB.cpu().numpy(), ransacReprojThreshold=0.2, method=cv2.USAC_MAGSAC, confidence=0.999999, maxIters=10000
56
+ )
57
+ ```
58
+ We additionally provide two demos in the [demos folder](demo), which might help in understanding.
59
+
60
+
61
+ ## Setup/Install
62
+ In your python environment (tested on Linux python 3.12), run:
63
+ ```bash
64
+ uv pip install -e .
65
+ ```
66
+ or
67
+ ```bash
68
+ uv sync
69
+ ```
70
+
71
+ ## Benchmarks
72
+ If you do not already have MegaDepth and ScanNet, you can the following to download them:
73
+ ```bash
74
+ source scripts/eval_prep.sh
75
+ ```
76
+ ### Mega-1500
77
+ ```bash
78
+ uv run tests/test_mega1500.py
79
+ ```
80
+ ### ScanNet-1500
81
+ ```bash
82
+ uv run tests/test_scannet1500.py
83
+ ```
84
+ ### Expected Results
85
+ Experiments on ScanNet-1500 and MegaDepth-1500 are provided in the [tests folder](tests).
86
+ Running these gave me `ScanNet-1500: [34.0, 56.5, 73.9]`, and `Mega-1500: [62.8, 76,8, 86.5]`, which are similar to the results of the paper.
87
+
88
+
89
+ ## Fused local correlation kernel
90
+ Include the `--extra fused-local-corr` flag as:
91
+ ```bash
92
+ uv sync --extra fused-local-corr
93
+ ```
94
+ or
95
+ ```bash
96
+ uv pip install romav2[fused-local-corr]
97
+ ```
98
+ or
99
+ ```bash
100
+ uv add romav2[fused-local-corr]
101
+ ```
102
+
103
+ ## Settings
104
+ By twiddling with some different settings you may reach better results on your task of interest.
105
+ Some important ones, which we enable setting to some reasonable defaults through `model.apply_setting`, are:
106
+
107
+ `model.H_lr, model.W_lr`: height and width for the image pair.
108
+
109
+ `model.H_hr, model.W_hr`: height and width for a high resolution version of the image pair (used for upsampling as in RoMa)
110
+
111
+ `model.bidirectional`: Useful for getting more diverse matches, and for estimating the covariance matrix in both directions.
112
+
113
+ `model.threshold`: Value between [0,1]. Used to set overlap prediction above it to 1. Useful for Mega1500.
114
+
115
+ `model.balanced_sampling`: Diverse sampling, same as RoMa. Typically helps to get better RANSAC estimates.
116
+
117
+ ## License
118
+ All our code except DINOv3 is MIT license.
119
+ DINOv3 has a custom license, see [DINOv3](https://github.com/facebookresearch/dinov3/tree/main?tab=License-1-ov-file#readme).
120
+
121
+ ## Acknowledgement
122
+ Our codebase builds mainly on the code in [RoMa](https://github.com/Parskatt/RoMa).
123
+ We were additionally inspired by [UFM](https://github.com/UniFlowMatch/UFM) and [MapAnything](https://github.com/facebookresearch/map-anything), particularly for the datasets used to train the models.
124
+
125
+ ## BibTeX
126
+ If you find our models useful, please consider citing our paper!
127
+ ```
128
+ @article{edstedt2025romav2,
129
+ title={{RoMa v2: Harder Better Faster Denser Feature Matching}},
130
+ author={Johan Edstedt, David Nordstr枚m, Yushan Zhang, Georg B枚kman, Jonathan Astermark, Viktor Larsson, Anders Heyden, Fredrik Kahl, M氓rten Wadenb盲ck, Michael Felsberg},
131
+ journal={arXiv preprint arXiv:2511.15706},
132
+ year={2025}
133
+ }
134
+ ```
@@ -0,0 +1,38 @@
1
+ [project]
2
+ name = "romav2"
3
+ version = "2.0.0"
4
+ description = "RoMa v2: Harder Better Faster Denser Feature Matching"
5
+ readme = "README.md"
6
+ authors = [
7
+ { name = "Johan Edstedt", email = "johan.edstedt@liu.se" }
8
+ ]
9
+ requires-python = ">=3.10"
10
+ dependencies = [
11
+ "dataclasses>=0.8",
12
+ "einops>=0.8.1",
13
+ "pillow>=12.0.0",
14
+ "rich>=14.2.0",
15
+ "torch",
16
+ "torchvision>=0.23.0",
17
+ "tqdm>=4.67.1",
18
+ ]
19
+
20
+ [build-system]
21
+ requires = ["uv_build>=0.8.15,<0.9.0"]
22
+ build-backend = "uv_build"
23
+
24
+ [project.optional-dependencies]
25
+ fused-local-corr = [
26
+ "fused-local-corr ; sys_platform == 'linux'",
27
+ ]
28
+ eval = [
29
+ "kornia>=0.8.2",
30
+ "matplotlib>=3.10.7",
31
+ "opencv-python>=4.12.0.88",
32
+ "wandb>=0.23.0",
33
+ "wxbs-benchmark>=0.0.4",
34
+ ]
35
+ dev = [
36
+ "slurm-util>=0.2.7",
37
+ "ruff>=0.14.5",
38
+ ]
@@ -0,0 +1,8 @@
1
+ import logging as _logging
2
+ from .logging import configure_logger as configure_logger
3
+ from .logging import logger as _logger
4
+
5
+ if not any(not isinstance(h, _logging.NullHandler) for h in _logger.handlers):
6
+ configure_logger()
7
+
8
+ from .romav2 import RoMaV2 as RoMaV2
@@ -0,0 +1,4 @@
1
+ from .scannet1500 import ScanNet1500 as ScanNet1500
2
+ from .mega1500 import Mega1500 as Mega1500
3
+ from .wxbs import WxBSBenchmark as WxBSBenchmark
4
+ from .satast import SatAst as SatAst
@@ -0,0 +1,115 @@
1
+ import numpy as np
2
+ import logging
3
+ from romav2.geometry import (
4
+ compute_pose_error,
5
+ pose_auc,
6
+ estimate_pose_cv2_ransac,
7
+ compute_relative_pose,
8
+ )
9
+ from PIL import Image
10
+ from tqdm import tqdm
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ class Mega1500:
16
+ def __init__(self, data_root="data/megadepth") -> None:
17
+ self.scene_names = [
18
+ "0015_0.1_0.3.npz",
19
+ "0015_0.3_0.5.npz",
20
+ "0022_0.1_0.3.npz",
21
+ "0022_0.3_0.5.npz",
22
+ "0022_0.5_0.7.npz",
23
+ ]
24
+ self.scenes = [
25
+ np.load(f"{data_root}/{scene}", allow_pickle=True)
26
+ for scene in self.scene_names
27
+ ]
28
+ self.data_root = data_root
29
+
30
+ def benchmark(self, model, model_name=None):
31
+ data_root = self.data_root
32
+ tot_e_t, tot_e_R, tot_e_pose = [], [], []
33
+ thresholds = [5, 10, 20]
34
+ for scene_ind in range(len(self.scenes)):
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 (pbar := tqdm(pair_inds, desc="Mega1500 eval")):
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
+ im_A_path = f"{data_root}/{im_paths[idx1]}"
51
+ im_B_path = f"{data_root}/{im_paths[idx2]}"
52
+ preds = model.match(im_A_path, im_B_path)
53
+ im_A = Image.open(im_A_path)
54
+ w1, h1 = im_A.size
55
+ im_B = Image.open(im_B_path)
56
+ w2, h2 = im_B.size
57
+ if True: # Note: we keep this true as it was used in DKM/RoMa papers. There is very little difference compared to setting to False.
58
+ scale1 = 1200 / max(w1, h1)
59
+ scale2 = 1200 / max(w2, h2)
60
+ w1, h1 = scale1 * w1, scale1 * h1
61
+ w2, h2 = scale2 * w2, scale2 * h2
62
+ K1, K2 = K1.copy(), K2.copy()
63
+ K1[:2] = K1[:2] * scale1
64
+ K2[:2] = K2[:2] * scale2
65
+ for _ in range(5):
66
+ matches, _, _, _ = model.sample(preds, 5_000)
67
+ kpts1, kpts2 = model.to_pixel_coordinates(matches, h1, w1, h2, w2)
68
+ kpts1, kpts2 = kpts1.cpu().numpy(), kpts2.cpu().numpy()
69
+ shuffling = np.random.permutation(np.arange(len(kpts1)))
70
+ kpts1 = kpts1[shuffling]
71
+ kpts2 = kpts2[shuffling]
72
+ try:
73
+ threshold = 0.5
74
+ norm_threshold = threshold / (
75
+ np.mean(np.abs(K1[:2, :2])) + np.mean(np.abs(K2[:2, :2]))
76
+ )
77
+ R_est, t_est, _ = estimate_pose_cv2_ransac(
78
+ kpts1,
79
+ kpts2,
80
+ K1,
81
+ K2,
82
+ norm_threshold,
83
+ conf=0.99999,
84
+ )
85
+ e_t, e_R = compute_pose_error(R_est, t_est[:, 0], R, t)
86
+ e_pose = max(e_t, e_R)
87
+ except Exception as e:
88
+ logger.debug(f"Pose estimation error: {e}")
89
+ e_t, e_R = 90, 90
90
+ e_pose = max(e_t, e_R)
91
+ tot_e_t.append(e_t)
92
+ tot_e_R.append(e_R)
93
+ tot_e_pose.append(e_pose)
94
+ pbar.set_postfix(
95
+ auc=f"{[f'{a.item():.3f}' for a in pose_auc(tot_e_pose, thresholds)]}"
96
+ )
97
+
98
+ tot_e_pose = np.array(tot_e_pose)
99
+ auc = pose_auc(tot_e_pose, thresholds)
100
+ acc_5 = (tot_e_pose < 5).mean()
101
+ acc_10 = (tot_e_pose < 10).mean()
102
+ acc_15 = (tot_e_pose < 15).mean()
103
+ acc_20 = (tot_e_pose < 20).mean()
104
+ map_5 = acc_5
105
+ map_10 = np.mean([acc_5, acc_10])
106
+ map_20 = np.mean([acc_5, acc_10, acc_15, acc_20])
107
+ logger.info("%s auc: %s", model_name, auc)
108
+ return {
109
+ "auc_5": auc[0],
110
+ "auc_10": auc[1],
111
+ "auc_20": auc[2],
112
+ "map_5": map_5,
113
+ "map_10": map_10,
114
+ "map_20": map_20,
115
+ }