XiaoHuXX gberton commited on
Commit
b95bdf0
·
0 Parent(s):

Duplicate from gberton/MegaLoc

Browse files

Co-authored-by: Gabriele Berton <gberton@users.noreply.huggingface.co>

Files changed (5) hide show
  1. .gitattributes +35 -0
  2. README.md +51 -0
  3. config.json +16 -0
  4. megaloc_model.py +468 -0
  5. model.safetensors +3 -0
.gitattributes ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ *.7z filter=lfs diff=lfs merge=lfs -text
2
+ *.arrow filter=lfs diff=lfs merge=lfs -text
3
+ *.bin filter=lfs diff=lfs merge=lfs -text
4
+ *.bz2 filter=lfs diff=lfs merge=lfs -text
5
+ *.ckpt filter=lfs diff=lfs merge=lfs -text
6
+ *.ftz filter=lfs diff=lfs merge=lfs -text
7
+ *.gz filter=lfs diff=lfs merge=lfs -text
8
+ *.h5 filter=lfs diff=lfs merge=lfs -text
9
+ *.joblib filter=lfs diff=lfs merge=lfs -text
10
+ *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
+ *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
+ *.model filter=lfs diff=lfs merge=lfs -text
13
+ *.msgpack filter=lfs diff=lfs merge=lfs -text
14
+ *.npy filter=lfs diff=lfs merge=lfs -text
15
+ *.npz filter=lfs diff=lfs merge=lfs -text
16
+ *.onnx filter=lfs diff=lfs merge=lfs -text
17
+ *.ot filter=lfs diff=lfs merge=lfs -text
18
+ *.parquet filter=lfs diff=lfs merge=lfs -text
19
+ *.pb filter=lfs diff=lfs merge=lfs -text
20
+ *.pickle filter=lfs diff=lfs merge=lfs -text
21
+ *.pkl filter=lfs diff=lfs merge=lfs -text
22
+ *.pt filter=lfs diff=lfs merge=lfs -text
23
+ *.pth filter=lfs diff=lfs merge=lfs -text
24
+ *.rar filter=lfs diff=lfs merge=lfs -text
25
+ *.safetensors filter=lfs diff=lfs merge=lfs -text
26
+ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
+ *.tar.* filter=lfs diff=lfs merge=lfs -text
28
+ *.tar filter=lfs diff=lfs merge=lfs -text
29
+ *.tflite filter=lfs diff=lfs merge=lfs -text
30
+ *.tgz filter=lfs diff=lfs merge=lfs -text
31
+ *.wasm filter=lfs diff=lfs merge=lfs -text
32
+ *.xz filter=lfs diff=lfs merge=lfs -text
33
+ *.zip filter=lfs diff=lfs merge=lfs -text
34
+ *.zst filter=lfs diff=lfs merge=lfs -text
35
+ *tfevents* filter=lfs diff=lfs merge=lfs -text
README.md ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ pipeline_tag: visual-document-retrieval
3
+ library_name: pytorch
4
+ license: mit
5
+ tags:
6
+ - visual-place-recognition
7
+ - image-retrieval
8
+ - arxiv:2502.17237
9
+ ---
10
+
11
+ # MegaLoc
12
+
13
+ MegaLoc is an image retrieval model for visual place recognition (VPR) that achieves state-of-the-art on most VPR datasets, including indoor and outdoor environments.
14
+
15
+ **Paper:** [MegaLoc: One Retrieval to Place Them All](https://arxiv.org/abs/2502.17237) (CVPR 2025 Workshop)
16
+
17
+ **GitHub:** [gmberton/MegaLoc](https://github.com/gmberton/MegaLoc)
18
+
19
+ ## Usage
20
+
21
+ ```python
22
+ import torch
23
+ model = torch.hub.load("gmberton/MegaLoc", "get_trained_model")
24
+ model.eval()
25
+
26
+ # Extract descriptor from an image
27
+ image = torch.randn(1, 3, 322, 322) # [B, 3, H, W] - any size works
28
+ with torch.no_grad():
29
+ descriptor = model(image) # [B, 8448] L2-normalized descriptor
30
+ ```
31
+
32
+ For benchmarking on VPR datasets, see [VPR-methods-evaluation](https://github.com/gmberton/VPR-methods-evaluation).
33
+
34
+ ## Qualitative Examples
35
+
36
+ Top-1 retrieved images from the SF-XL test set (2.8M database images):
37
+
38
+ ![teaser](https://github.com/user-attachments/assets/a90b8d4c-ab53-4151-aacc-93493d583713)
39
+
40
+ ## Citation
41
+
42
+ ```bibtex
43
+ @InProceedings{Berton_2025_CVPR,
44
+ author = {Berton, Gabriele and Masone, Carlo},
45
+ title = {MegaLoc: One Retrieval to Place Them All},
46
+ booktitle = {Proceedings of the IEEE/CVF Conference on Computer Vision and Pattern Recognition (CVPR) Workshops},
47
+ month = {June},
48
+ year = {2025},
49
+ pages = {2861-2867}
50
+ }
51
+ ```
config.json ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "model_type": "megaloc",
3
+ "architectures": ["MegaLoc"],
4
+ "feat_dim": 8448,
5
+ "num_clusters": 64,
6
+ "cluster_dim": 256,
7
+ "token_dim": 256,
8
+ "mlp_dim": 512,
9
+ "backbone": {
10
+ "type": "dinov2",
11
+ "embed_dim": 768,
12
+ "patch_size": 14,
13
+ "depth": 12,
14
+ "num_heads": 12
15
+ }
16
+ }
megaloc_model.py ADDED
@@ -0,0 +1,468 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """MegaLoc: One Retrieval to Place Them All
2
+
3
+ This module implements the MegaLoc model for visual place recognition.
4
+ The model combines a Vision Transformer backbone with an optimal transport-based
5
+ feature aggregation module.
6
+
7
+ Paper: https://arxiv.org/abs/2502.17237
8
+ License: MIT
9
+ """
10
+
11
+ import math
12
+ from typing import Tuple
13
+
14
+ import torch
15
+ import torch.nn as nn
16
+ import torch.nn.functional as F
17
+ import torchvision.transforms.functional as tfm
18
+
19
+
20
+ # Code adapted from OpenGlue, MIT license
21
+ # https://github.com/ucuapps/OpenGlue/blob/main/models/superglue/optimal_transport.py
22
+ def log_otp_solver(log_a, log_b, M, num_iters: int = 20, reg: float = 1.0) -> torch.Tensor:
23
+ r"""Sinkhorn matrix scaling algorithm for Differentiable Optimal Transport problem.
24
+ This function solves the optimization problem and returns the OT matrix for the given parameters.
25
+ Args:
26
+ log_a : torch.Tensor
27
+ Source weights
28
+ log_b : torch.Tensor
29
+ Target weights
30
+ M : torch.Tensor
31
+ metric cost matrix
32
+ num_iters : int, default=100
33
+ The number of iterations.
34
+ reg : float, default=1.0
35
+ regularization value
36
+ """
37
+ M = M / reg # regularization
38
+
39
+ u, v = torch.zeros_like(log_a), torch.zeros_like(log_b)
40
+
41
+ for _ in range(num_iters):
42
+ u = log_a - torch.logsumexp(M + v.unsqueeze(1), dim=2).squeeze()
43
+ v = log_b - torch.logsumexp(M + u.unsqueeze(2), dim=1).squeeze()
44
+
45
+ return M + u.unsqueeze(2) + v.unsqueeze(1)
46
+
47
+
48
+ # Code adapted from OpenGlue, MIT license
49
+ # https://github.com/ucuapps/OpenGlue/blob/main/models/superglue/superglue.py
50
+ def get_matching_probs(S, dustbin_score=1.0, num_iters=3, reg=1.0):
51
+ """sinkhorn"""
52
+ batch_size, m, n = S.size()
53
+ # augment scores matrix
54
+ S_aug = torch.empty(batch_size, m + 1, n, dtype=S.dtype, device=S.device)
55
+ S_aug[:, :m, :n] = S
56
+ S_aug[:, m, :] = dustbin_score
57
+
58
+ # prepare normalized source and target log-weights
59
+ norm = -torch.tensor(math.log(n + m), device=S.device)
60
+ log_a, log_b = norm.expand(m + 1).contiguous(), norm.expand(n).contiguous()
61
+ log_a[-1] = log_a[-1] + math.log(n - m)
62
+ log_a, log_b = log_a.expand(batch_size, -1), log_b.expand(batch_size, -1)
63
+ log_P = log_otp_solver(log_a, log_b, S_aug, num_iters=num_iters, reg=reg)
64
+ return log_P - norm
65
+
66
+
67
+ class FeatureAggregator(nn.Module):
68
+ """Optimal transport-based aggregation of local features into global descriptor.
69
+
70
+ This module aggregates local patch features into a compact global representation
71
+ using differentiable optimal transport.
72
+
73
+ Args:
74
+ num_channels: Number of input feature channels (from backbone)
75
+ num_clusters: Number of cluster centers
76
+ cluster_dim: Dimensionality of cluster descriptors
77
+ token_dim: Dimensionality of global scene token
78
+ mlp_dim: Hidden dimension for MLPs
79
+ dropout: Dropout probability (0 to disable)
80
+ """
81
+
82
+ def __init__(
83
+ self,
84
+ num_channels=1536,
85
+ num_clusters=64,
86
+ cluster_dim=128,
87
+ token_dim=256,
88
+ mlp_dim=512,
89
+ dropout=0.3,
90
+ ) -> None:
91
+ super().__init__()
92
+
93
+ self.num_channels = num_channels
94
+ self.num_clusters = num_clusters
95
+ self.cluster_dim = cluster_dim
96
+ self.token_dim = token_dim
97
+ self.mlp_dim = mlp_dim
98
+
99
+ if dropout > 0:
100
+ dropout = nn.Dropout(dropout)
101
+ else:
102
+ dropout = nn.Identity()
103
+
104
+ # MLP for global scene token
105
+ self.token_features = nn.Sequential(
106
+ nn.Linear(self.num_channels, self.mlp_dim), nn.ReLU(), nn.Linear(self.mlp_dim, self.token_dim)
107
+ )
108
+ # MLP for local features
109
+ self.cluster_features = nn.Sequential(
110
+ nn.Conv2d(self.num_channels, self.mlp_dim, 1),
111
+ dropout,
112
+ nn.ReLU(),
113
+ nn.Conv2d(self.mlp_dim, self.cluster_dim, 1),
114
+ )
115
+ # MLP for score matrix
116
+ self.score = nn.Sequential(
117
+ nn.Conv2d(self.num_channels, self.mlp_dim, 1),
118
+ dropout,
119
+ nn.ReLU(),
120
+ nn.Conv2d(self.mlp_dim, self.num_clusters, 1),
121
+ )
122
+ # Dustbin parameter
123
+ self.dust_bin = nn.Parameter(torch.tensor(1.0))
124
+
125
+ def forward(self, x):
126
+ """
127
+ Args:
128
+ x: Tuple of (features, token)
129
+ features: [B, C, H, W] spatial feature map
130
+ token: [B, C] global CLS token
131
+
132
+ Returns:
133
+ Global descriptor [B, num_clusters * cluster_dim + token_dim]
134
+ """
135
+ x, t = x
136
+
137
+ f = self.cluster_features(x).flatten(2)
138
+ p = self.score(x).flatten(2)
139
+ t = self.token_features(t)
140
+
141
+ p = get_matching_probs(p, self.dust_bin, 3)
142
+ p = torch.exp(p)
143
+ p = p[:, :-1, :]
144
+
145
+ p = p.unsqueeze(1).repeat(1, self.cluster_dim, 1, 1)
146
+ f = f.unsqueeze(2).repeat(1, 1, self.num_clusters, 1)
147
+
148
+ f = torch.cat(
149
+ [
150
+ F.normalize(t, p=2, dim=-1),
151
+ F.normalize((f * p).sum(dim=-1), p=2, dim=1).flatten(1),
152
+ ],
153
+ dim=-1,
154
+ )
155
+
156
+ return F.normalize(f, p=2, dim=-1)
157
+
158
+
159
+ # ==============================================================================
160
+ # Vision Transformer Components
161
+ # ==============================================================================
162
+
163
+
164
+ class PatchEmbedding(nn.Module):
165
+ """Convert image patches to embeddings using a convolutional layer."""
166
+
167
+ def __init__(self, image_size: int = 518, patch_size: int = 14, in_channels: int = 3, embed_dim: int = 768):
168
+ super().__init__()
169
+ self.image_size = image_size
170
+ self.patch_size = patch_size
171
+ self.num_patches = (image_size // patch_size) ** 2
172
+ self.proj = nn.Conv2d(in_channels, embed_dim, kernel_size=patch_size, stride=patch_size)
173
+
174
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
175
+ x = self.proj(x)
176
+ x = x.flatten(2)
177
+ x = x.transpose(1, 2)
178
+ return x
179
+
180
+
181
+ class LayerScale(nn.Module):
182
+ """Learnable per-channel scaling as used in CaiT and DINOv2."""
183
+
184
+ def __init__(self, dim: int, init_value: float = 1e-5):
185
+ super().__init__()
186
+ self.gamma = nn.Parameter(init_value * torch.ones(dim))
187
+
188
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
189
+ return x * self.gamma
190
+
191
+
192
+ class MultiHeadAttention(nn.Module):
193
+ """Multi-head self-attention module."""
194
+
195
+ def __init__(
196
+ self, dim: int, num_heads: int = 12, qkv_bias: bool = True, attn_drop: float = 0.0, proj_drop: float = 0.0
197
+ ):
198
+ super().__init__()
199
+ self.num_heads = num_heads
200
+ self.head_dim = dim // num_heads
201
+ self.scale = self.head_dim**-0.5
202
+
203
+ self.qkv = nn.Linear(dim, dim * 3, bias=qkv_bias)
204
+ self.attn_drop = nn.Dropout(attn_drop)
205
+ self.proj = nn.Linear(dim, dim)
206
+ self.proj_drop = nn.Dropout(proj_drop)
207
+
208
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
209
+ B, N, C = x.shape
210
+
211
+ qkv = self.qkv(x).reshape(B, N, 3, self.num_heads, self.head_dim)
212
+ qkv = qkv.permute(2, 0, 3, 1, 4)
213
+ q, k, v = qkv[0], qkv[1], qkv[2]
214
+
215
+ attn = (q @ k.transpose(-2, -1)) * self.scale
216
+ attn = attn.softmax(dim=-1)
217
+ attn = self.attn_drop(attn)
218
+
219
+ x = (attn @ v).transpose(1, 2).reshape(B, N, C)
220
+ x = self.proj(x)
221
+ x = self.proj_drop(x)
222
+
223
+ return x
224
+
225
+
226
+ class MLP(nn.Module):
227
+ """MLP module with GELU activation."""
228
+
229
+ def __init__(self, in_features: int, hidden_features: int = None, out_features: int = None, drop: float = 0.0):
230
+ super().__init__()
231
+ out_features = out_features or in_features
232
+ hidden_features = hidden_features or in_features
233
+
234
+ self.fc1 = nn.Linear(in_features, hidden_features)
235
+ self.act = nn.GELU()
236
+ self.fc2 = nn.Linear(hidden_features, out_features)
237
+ self.drop = nn.Dropout(drop)
238
+
239
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
240
+ x = self.fc1(x)
241
+ x = self.act(x)
242
+ x = self.drop(x)
243
+ x = self.fc2(x)
244
+ x = self.drop(x)
245
+ return x
246
+
247
+
248
+ class TransformerBlock(nn.Module):
249
+ """Vision Transformer block with LayerScale."""
250
+
251
+ def __init__(
252
+ self,
253
+ dim: int,
254
+ num_heads: int,
255
+ mlp_ratio: float = 4.0,
256
+ qkv_bias: bool = True,
257
+ drop: float = 0.0,
258
+ attn_drop: float = 0.0,
259
+ init_values: float = 1e-5,
260
+ ):
261
+ super().__init__()
262
+ self.norm1 = nn.LayerNorm(dim, eps=1e-6)
263
+ self.attn = MultiHeadAttention(dim, num_heads=num_heads, qkv_bias=qkv_bias, attn_drop=attn_drop, proj_drop=drop)
264
+ self.ls1 = LayerScale(dim, init_value=init_values)
265
+
266
+ self.norm2 = nn.LayerNorm(dim, eps=1e-6)
267
+ self.mlp = MLP(in_features=dim, hidden_features=int(dim * mlp_ratio), drop=drop)
268
+ self.ls2 = LayerScale(dim, init_value=init_values)
269
+
270
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
271
+ x = x + self.ls1(self.attn(self.norm1(x)))
272
+ x = x + self.ls2(self.mlp(self.norm2(x)))
273
+ return x
274
+
275
+
276
+ class DINOv2(nn.Module):
277
+ """DINOv2 Vision Transformer backbone for feature extraction.
278
+
279
+ This implements a ViT-B/14 architecture compatible with DINOv2 weights.
280
+ """
281
+
282
+ def __init__(
283
+ self,
284
+ image_size: int = 518,
285
+ patch_size: int = 14,
286
+ in_channels: int = 3,
287
+ embed_dim: int = 768,
288
+ depth: int = 12,
289
+ num_heads: int = 12,
290
+ mlp_ratio: float = 4.0,
291
+ qkv_bias: bool = True,
292
+ ):
293
+ super().__init__()
294
+ self.patch_size = patch_size
295
+ self.embed_dim = embed_dim
296
+ self.num_channels = embed_dim
297
+
298
+ self.patch_embed = PatchEmbedding(
299
+ image_size=image_size, patch_size=patch_size, in_channels=in_channels, embed_dim=embed_dim
300
+ )
301
+
302
+ self.interpolate_offset = 0.1
303
+ self.interpolate_antialias = False
304
+
305
+ self.cls_token = nn.Parameter(torch.zeros(1, 1, embed_dim))
306
+ num_patches = (image_size // patch_size) ** 2
307
+ self.pos_embed = nn.Parameter(torch.zeros(1, num_patches + 1, embed_dim))
308
+
309
+ self.blocks = nn.ModuleList(
310
+ [
311
+ TransformerBlock(dim=embed_dim, num_heads=num_heads, mlp_ratio=mlp_ratio, qkv_bias=qkv_bias)
312
+ for _ in range(depth)
313
+ ]
314
+ )
315
+
316
+ self.norm = nn.LayerNorm(embed_dim, eps=1e-6)
317
+
318
+ def interpolate_pos_encoding(self, x: torch.Tensor, w: int, h: int) -> torch.Tensor:
319
+ """Interpolate positional encoding for different input sizes."""
320
+ previous_dtype = x.dtype
321
+ npatch = x.shape[1] - 1
322
+ N = self.pos_embed.shape[1] - 1
323
+
324
+ if npatch == N and w == h:
325
+ return self.pos_embed
326
+
327
+ pos_embed = self.pos_embed.float()
328
+ class_pos_embed = pos_embed[:, 0]
329
+ patch_pos_embed = pos_embed[:, 1:]
330
+
331
+ dim = x.shape[-1]
332
+ w0 = w // self.patch_size
333
+ h0 = h // self.patch_size
334
+ M = int(math.sqrt(N))
335
+
336
+ sx = float(w0 + self.interpolate_offset) / M
337
+ sy = float(h0 + self.interpolate_offset) / M
338
+
339
+ patch_pos_embed = F.interpolate(
340
+ patch_pos_embed.reshape(1, M, M, dim).permute(0, 3, 1, 2),
341
+ scale_factor=(sx, sy),
342
+ mode="bicubic",
343
+ antialias=self.interpolate_antialias,
344
+ )
345
+
346
+ assert (w0, h0) == patch_pos_embed.shape[-2:]
347
+ patch_pos_embed = patch_pos_embed.permute(0, 2, 3, 1).view(1, -1, dim)
348
+
349
+ return torch.cat((class_pos_embed.unsqueeze(0), patch_pos_embed), dim=1).to(previous_dtype)
350
+
351
+ def forward(self, images: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]:
352
+ """Extract features from images.
353
+
354
+ Args:
355
+ images: Input images [B, 3, H, W] where H, W are multiples of 14
356
+
357
+ Returns:
358
+ Tuple of (patch_features [B, 768, H//14, W//14], cls_token [B, 768])
359
+ """
360
+ B, _, H, W = images.shape
361
+
362
+ x = self.patch_embed(images)
363
+ cls_tokens = self.cls_token.expand(B, -1, -1)
364
+ x = torch.cat((cls_tokens, x), dim=1)
365
+ x = x + self.interpolate_pos_encoding(x, H, W)
366
+
367
+ for block in self.blocks:
368
+ x = block(x)
369
+
370
+ x = self.norm(x)
371
+
372
+ cls_token = x[:, 0]
373
+ patch_tokens = x[:, 1:]
374
+ patch_features = patch_tokens.reshape(B, H // self.patch_size, W // self.patch_size, self.embed_dim).permute(
375
+ 0, 3, 1, 2
376
+ )
377
+
378
+ return patch_features, cls_token
379
+
380
+
381
+ # ==============================================================================
382
+ # Main Model
383
+ # ==============================================================================
384
+
385
+
386
+ class L2Norm(nn.Module):
387
+ def __init__(self, dim=1):
388
+ super().__init__()
389
+ self.dim = dim
390
+
391
+ def forward(self, x):
392
+ return F.normalize(x, p=2.0, dim=self.dim)
393
+
394
+
395
+ class Aggregator(nn.Module):
396
+ def __init__(self, feat_dim, agg_config, salad_out_dim):
397
+ super().__init__()
398
+ self.agg = FeatureAggregator(**agg_config)
399
+ self.linear = nn.Linear(salad_out_dim, feat_dim)
400
+
401
+ def forward(self, x):
402
+ x = self.agg(x)
403
+ return self.linear(x)
404
+
405
+
406
+ class MegaLoc(nn.Module):
407
+ """MegaLoc: Unified visual place recognition model.
408
+
409
+ Combines a DINOv2 Vision Transformer backbone with optimal transport-based
410
+ feature aggregation to produce compact, discriminative image descriptors
411
+ for place recognition and image retrieval tasks.
412
+
413
+ Args:
414
+ feat_dim: Output descriptor dimensionality (default: 8448)
415
+ num_clusters: Number of cluster centers for aggregation (default: 64)
416
+ cluster_dim: Dimensionality of cluster descriptors (default: 256)
417
+ token_dim: Dimensionality of global scene token (default: 256)
418
+ mlp_dim: Hidden dimension for MLPs (default: 512)
419
+
420
+ Example:
421
+ >>> model = torch.hub.load("gmberton/MegaLoc", "get_trained_model")
422
+ >>> model.eval()
423
+ >>> descriptor = model(image) # [B, 8448]
424
+ """
425
+
426
+ def __init__(
427
+ self,
428
+ feat_dim: int = 8448,
429
+ num_clusters: int = 64,
430
+ cluster_dim: int = 256,
431
+ token_dim: int = 256,
432
+ mlp_dim: int = 512,
433
+ ):
434
+ super().__init__()
435
+
436
+ self.backbone = DINOv2()
437
+ self.salad_out_dim = num_clusters * cluster_dim + token_dim
438
+ self.aggregator = Aggregator(
439
+ feat_dim=feat_dim,
440
+ agg_config={
441
+ "num_channels": self.backbone.num_channels,
442
+ "num_clusters": num_clusters,
443
+ "cluster_dim": cluster_dim,
444
+ "token_dim": token_dim,
445
+ "mlp_dim": mlp_dim,
446
+ },
447
+ salad_out_dim=self.salad_out_dim,
448
+ )
449
+ self.feat_dim = feat_dim
450
+ self.l2norm = L2Norm()
451
+
452
+ def forward(self, images: torch.Tensor) -> torch.Tensor:
453
+ """Extract global descriptor from images.
454
+
455
+ Args:
456
+ images: Input images [B, 3, H, W]
457
+
458
+ Returns:
459
+ L2-normalized descriptors [B, feat_dim]
460
+ """
461
+ b, c, h, w = images.shape
462
+ if h % 14 != 0 or w % 14 != 0:
463
+ h = round(h / 14) * 14
464
+ w = round(w / 14) * 14
465
+ images = tfm.resize(images, [h, w], antialias=True)
466
+ features = self.aggregator(self.backbone(images))
467
+ features = self.l2norm(features)
468
+ return features
model.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:d4f9f2bcb60018f91eb6a8e061ed054fd55654e10c2569cf13841ea986ffb4f8
3
+ size 914577436