reid 0.1.0.dev0__py3-none-any.whl

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.
reid/__init__.py ADDED
@@ -0,0 +1,47 @@
1
+ # ------------------------------------------------------------------------
2
+ # re-ID
3
+ # Copyright (c) 2026 Roboflow. All Rights Reserved.
4
+ # Licensed under the Apache License, Version 2.0 [see LICENSE for details]
5
+ # ------------------------------------------------------------------------
6
+
7
+ """re-ID: appearance re-identification models and gallery evaluation.
8
+
9
+ The public surface is intentionally narrow. Advanced helpers live in
10
+ submodules: checkpoint loading in :mod:`reid.loaders`, the curated alias
11
+ catalog and ``ModelCard`` in :mod:`reid.catalog`, shared dataset types and
12
+ benchmark loaders in :mod:`reid.data`, and architecture dispatch in
13
+ :mod:`reid.architectures`.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ from reid.architectures import list_architectures
19
+ from reid.catalog import DEFAULT_MODEL, FASTREID_MOT17_SBS50, list_aliases
20
+ from reid.eval import (
21
+ ReIDEvaluator,
22
+ ReIDMetrics,
23
+ ReIDResult,
24
+ ReIDSplit,
25
+ compute_reid_metrics,
26
+ load_market1501,
27
+ load_msmt17,
28
+ )
29
+ from reid.model import ReIDModel
30
+ from reid.preprocessing import Interpolation, ReIDPreprocessing
31
+
32
+ __all__ = [
33
+ "DEFAULT_MODEL",
34
+ "FASTREID_MOT17_SBS50",
35
+ "Interpolation",
36
+ "ReIDEvaluator",
37
+ "ReIDMetrics",
38
+ "ReIDModel",
39
+ "ReIDPreprocessing",
40
+ "ReIDResult",
41
+ "ReIDSplit",
42
+ "compute_reid_metrics",
43
+ "list_aliases",
44
+ "list_architectures",
45
+ "load_market1501",
46
+ "load_msmt17",
47
+ ]
@@ -0,0 +1,23 @@
1
+ # ------------------------------------------------------------------------
2
+ # re-ID
3
+ # Copyright (c) 2026 Roboflow. All Rights Reserved.
4
+ # Licensed under the Apache License, Version 2.0 [see LICENSE for details]
5
+ # ------------------------------------------------------------------------
6
+
7
+ """ReID architecture public API."""
8
+
9
+ from __future__ import annotations
10
+
11
+ from reid.architectures.registry import (
12
+ build_architecture,
13
+ checkpoint_remap_for_architecture,
14
+ default_preprocessing_for_architecture,
15
+ list_architectures,
16
+ )
17
+
18
+ __all__ = [
19
+ "build_architecture",
20
+ "checkpoint_remap_for_architecture",
21
+ "default_preprocessing_for_architecture",
22
+ "list_architectures",
23
+ ]
@@ -0,0 +1,141 @@
1
+ # ------------------------------------------------------------------------
2
+ # re-ID
3
+ # Copyright (c) 2026 Roboflow. All Rights Reserved.
4
+ # Licensed under the Apache License, Version 2.0 [see LICENSE for details]
5
+ # ------------------------------------------------------------------------
6
+ #
7
+ # Adapted from JDAI-CV/fast-reid (Apache-2.0)
8
+ # Copyright 2019 JD.com Inc. JD AI
9
+ # Source: https://github.com/JDAI-CV/fast-reid
10
+ # - GeM: fastreid/layers/pooling.py (GeneralizedMeanPooling / GeneralizedMeanPoolingP)
11
+ # - SBS head layout: fastreid/modeling/heads/embedding_head.py
12
+ # - last_stride=1 ResNeSt semantics: fastreid/modeling/backbones/resnest.py
13
+ # Also used via BoT-SORT's FastReIDInterface (NirAharon/BoT-SORT, MIT).
14
+ # ------------------------------------------------------------------------
15
+
16
+ """FastReID Strong Baseline (SBS) inference stack for BoT-SORT checkpoints.
17
+
18
+ Uses ``timm`` ``resnest50d`` with a small FastReID ``last_stride=1`` patch on
19
+ ``layer4[0]``, then Generalized Mean Pooling and a BatchNorm neck — the same
20
+ inference path as FastReID ``EmbeddingHead`` in eval.
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ from typing import Any, cast
26
+
27
+ import timm
28
+ import torch
29
+ from torch import nn
30
+ from torch.nn import functional as F
31
+
32
+ from reid.preprocessing import ReIDPreprocessing
33
+
34
+ FASTREID_SBS_ARCHITECTURE = "fastreid_sbs_resnest50"
35
+ FASTREID_SBS_EMBED_DIM = 2048
36
+
37
+
38
+ def _patch_resnest50d_for_fastreid_last_stride(backbone: nn.Module) -> nn.Module:
39
+ """Align ``timm`` ``resnest50d`` (``output_stride=16``) with FastReID ResNeSt.
40
+
41
+ FastReID ``LAST_STRIDE=1`` keeps layer4 spatial stride at 1 via:
42
+ - downsample avg-pool kernel 1 (identity spatial size), and
43
+ - average-downsampling (AVD) after the Split-Attention conv with stride 1.
44
+
45
+ ``timm``'s dilated / stride-16 path instead uses ``AvgPool2dSame(2, stride=1)``
46
+ and leaves ``avd_last`` unset on ``layer4[0]``, which shifts embeddings even
47
+ when checkpoint keys load 100%.
48
+ """
49
+ backbone_any = cast(Any, backbone)
50
+ block = backbone_any.layer4[0]
51
+ block.downsample[0] = nn.AvgPool2d(kernel_size=1, stride=1, padding=0)
52
+ block.avd_last = nn.AvgPool2d(kernel_size=3, stride=1, padding=1)
53
+ return backbone
54
+
55
+
56
+ class GeneralizedMeanPooling(nn.Module):
57
+ """GeM pooling used by FastReID SBS (learnable exponent *p*).
58
+
59
+ ``p`` defaults to 3.0 only until checkpoint load; MOT17 SBS-S50 stores the
60
+ trained value under ``heads.pool_layer.p`` (≈1.72). See
61
+ :func:`remap_fastreid_sbs_state_dict`.
62
+ """
63
+
64
+ def __init__(self, p: float = 3.0, eps: float = 1e-6) -> None:
65
+ super().__init__()
66
+ self.p = nn.Parameter(torch.ones(1) * p)
67
+ self.eps = eps
68
+
69
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
70
+ x = x.clamp(min=self.eps).pow(self.p)
71
+ x = F.adaptive_avg_pool2d(x, 1)
72
+ return x.pow(1.0 / self.p)
73
+
74
+
75
+ class FastReIDSBSResNeSt50(nn.Module):
76
+ """ResNeSt50 SBS re-ID encoder matching BoT-SORT / FastReID MOT17 checkpoints.
77
+
78
+ Inference path: patched ``timm`` ResNeSt-50 → GeM (:attr:`pool`) → BNNeck
79
+ (:attr:`bottleneck`). Weights for all three stages load from a BoT-SORT
80
+ ``.pth`` via :func:`remap_fastreid_sbs_state_dict`.
81
+ """
82
+
83
+ @classmethod
84
+ def default_preprocessing(cls) -> ReIDPreprocessing:
85
+ """Default crop/tensor recipe for FastReID SBS (BoT-SORT FastReIDInterface).
86
+
87
+ Config (aligned with BoT-SORT / FastReID MOT17 SBS inference):
88
+ - input size: 384x128 (H, W)
89
+ - resize: stretch (cv2.resize, no letterbox)
90
+ - colour: BGR crops converted to RGB
91
+ - normalize: ImageNet mean/std
92
+ """
93
+ return ReIDPreprocessing(
94
+ input_size=(384, 128),
95
+ resize_mode="stretch",
96
+ to_rgb=True,
97
+ )
98
+
99
+ def __init__(self) -> None:
100
+ super().__init__()
101
+ backbone = timm.create_model(
102
+ "resnest50d",
103
+ pretrained=False,
104
+ num_classes=0,
105
+ global_pool="",
106
+ output_stride=16,
107
+ )
108
+ self.backbone = _patch_resnest50d_for_fastreid_last_stride(backbone)
109
+ self.pool = GeneralizedMeanPooling()
110
+ self.bottleneck = nn.BatchNorm1d(FASTREID_SBS_EMBED_DIM)
111
+
112
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
113
+ x = self.backbone(x)
114
+ x = self.pool(x).flatten(1)
115
+ return self.bottleneck(x)
116
+
117
+
118
+ def build_fastreid_sbs_resnest50(*, num_classes: int = 0, pretrained: bool = False) -> FastReIDSBSResNeSt50:
119
+ """Build the BoT-SORT FastReID SBS ResNeSt50 encoder (weights loaded separately)."""
120
+ del num_classes, pretrained
121
+ return FastReIDSBSResNeSt50()
122
+
123
+
124
+ def remap_fastreid_sbs_state_dict(state_dict: dict) -> dict:
125
+ """Map BoT-SORT / FastReID SBS checkpoint keys onto :class:`FastReIDSBSResNeSt50`.
126
+
127
+ Renames FastReID ``heads.*`` keys to ``pool.*`` / ``bottleneck.*`` and passes
128
+ ``backbone.*`` through unchanged. Skips ``heads.weight`` (classifier). GeM
129
+ ``heads.pool_layer.p`` becomes ``pool.p`` and replaces the 3.0 init default.
130
+ """
131
+ mapped: dict = {}
132
+ for key, value in state_dict.items():
133
+ key = key[7:] if key.startswith("module.") else key
134
+ if key.startswith("backbone."):
135
+ mapped[key] = value
136
+ elif key == "heads.pool_layer.p":
137
+ mapped["pool.p"] = value
138
+ elif key.startswith("heads.bottleneck.0."):
139
+ mapped["bottleneck." + key[len("heads.bottleneck.0.") :]] = value
140
+ # Skip heads.weight (identity classifier; unused at inference).
141
+ return mapped
@@ -0,0 +1,314 @@
1
+ # ------------------------------------------------------------------------
2
+ # re-ID
3
+ # Copyright (c) 2026 Roboflow. All Rights Reserved.
4
+ # Licensed under the Apache License, Version 2.0 [see LICENSE for details]
5
+ # ------------------------------------------------------------------------
6
+ #
7
+ # Adapted from KaiyangZhou/deep-person-reid
8
+ # Copyright (c) 2018-2021 Kaiyang Zhou
9
+ # Licensed under the MIT License
10
+ # Source: https://github.com/KaiyangZhou/deep-person-reid/blob/master/torchreid/models/osnet.py
11
+ # Paper: Zhou et al. Omni-Scale Feature Learning for Person Re-Identification. ICCV, 2019.
12
+ # ------------------------------------------------------------------------
13
+
14
+ from __future__ import annotations
15
+
16
+ import torch
17
+ from torch import nn
18
+ from torch.nn import functional as F
19
+
20
+ from reid.preprocessing import ReIDPreprocessing
21
+
22
+ # --------------------------------------------------------------------------- #
23
+ # Basic layers
24
+ # --------------------------------------------------------------------------- #
25
+
26
+
27
+ class _ConvBnRelu(nn.Module):
28
+ """Conv2d → BatchNorm2d → ReLU building block."""
29
+
30
+ def __init__(
31
+ self,
32
+ in_channels: int,
33
+ out_channels: int,
34
+ kernel_size: int,
35
+ stride: int = 1,
36
+ padding: int = 0,
37
+ groups: int = 1,
38
+ ) -> None:
39
+ super().__init__()
40
+ self.conv = nn.Conv2d(
41
+ in_channels,
42
+ out_channels,
43
+ kernel_size,
44
+ stride=stride,
45
+ padding=padding,
46
+ bias=False,
47
+ groups=groups,
48
+ )
49
+ self.bn = nn.BatchNorm2d(out_channels)
50
+ self.relu = nn.ReLU(inplace=True)
51
+
52
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
53
+ return self.relu(self.bn(self.conv(x)))
54
+
55
+
56
+ class _Conv1x1(_ConvBnRelu):
57
+ """1x1 conv + BN + ReLU."""
58
+
59
+ def __init__(self, in_channels: int, out_channels: int, stride: int = 1, groups: int = 1) -> None:
60
+ super().__init__(in_channels, out_channels, 1, stride=stride, padding=0, groups=groups)
61
+
62
+
63
+ class _Conv1x1Linear(nn.Module):
64
+ """1x1 conv + BN (no non-linearity)."""
65
+
66
+ def __init__(self, in_channels: int, out_channels: int, stride: int = 1) -> None:
67
+ super().__init__()
68
+ self.conv = nn.Conv2d(in_channels, out_channels, 1, stride=stride, padding=0, bias=False)
69
+ self.bn = nn.BatchNorm2d(out_channels)
70
+
71
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
72
+ return self.bn(self.conv(x))
73
+
74
+
75
+ class _LightConv3x3(nn.Module):
76
+ """Lightweight 3x3 conv: 1x1 linear + depthwise 3x3 + BN + ReLU."""
77
+
78
+ def __init__(self, in_channels: int, out_channels: int) -> None:
79
+ super().__init__()
80
+ self.conv1 = nn.Conv2d(in_channels, out_channels, 1, stride=1, padding=0, bias=False)
81
+ self.conv2 = nn.Conv2d(out_channels, out_channels, 3, stride=1, padding=1, bias=False, groups=out_channels)
82
+ self.bn = nn.BatchNorm2d(out_channels)
83
+ self.relu = nn.ReLU(inplace=True)
84
+
85
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
86
+ return self.relu(self.bn(self.conv2(self.conv1(x))))
87
+
88
+
89
+ # --------------------------------------------------------------------------- #
90
+ # Omni-scale building blocks
91
+ # --------------------------------------------------------------------------- #
92
+
93
+
94
+ class _ChannelGate(nn.Module):
95
+ """Channel-wise gating via squeeze-and-excitation."""
96
+
97
+ def __init__(self, in_channels: int, reduction: int = 16) -> None:
98
+ super().__init__()
99
+ self.global_avgpool = nn.AdaptiveAvgPool2d(1)
100
+ self.fc1 = nn.Conv2d(in_channels, in_channels // reduction, 1, bias=True)
101
+ self.relu = nn.ReLU(inplace=True)
102
+ self.fc2 = nn.Conv2d(in_channels // reduction, in_channels, 1, bias=True)
103
+ self.sigmoid = nn.Sigmoid()
104
+
105
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
106
+ g = self.sigmoid(self.fc2(self.relu(self.fc1(self.global_avgpool(x)))))
107
+ return x * g
108
+
109
+
110
+ class OSBlock(nn.Module):
111
+ """Omni-scale feature learning residual block.
112
+
113
+ Aggregates features from four parallel streams with different effective
114
+ receptive-field depths (1, 2, 3, 4 stacked lightweight 3x3 convs) gated
115
+ by a shared channel-wise gate before the residual addition.
116
+ """
117
+
118
+ def __init__(self, in_channels: int, out_channels: int, bottleneck_reduction: int = 4) -> None:
119
+ super().__init__()
120
+ mid = out_channels // bottleneck_reduction
121
+ self.conv1 = _Conv1x1(in_channels, mid)
122
+ self.conv2a = _LightConv3x3(mid, mid)
123
+ self.conv2b = nn.Sequential(_LightConv3x3(mid, mid), _LightConv3x3(mid, mid))
124
+ self.conv2c = nn.Sequential(_LightConv3x3(mid, mid), _LightConv3x3(mid, mid), _LightConv3x3(mid, mid))
125
+ self.conv2d = nn.Sequential(
126
+ _LightConv3x3(mid, mid),
127
+ _LightConv3x3(mid, mid),
128
+ _LightConv3x3(mid, mid),
129
+ _LightConv3x3(mid, mid),
130
+ )
131
+ self.gate = _ChannelGate(mid)
132
+ self.conv3 = _Conv1x1Linear(mid, out_channels)
133
+ self.downsample = _Conv1x1Linear(in_channels, out_channels) if in_channels != out_channels else None
134
+
135
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
136
+ identity = x
137
+ x1 = self.conv1(x)
138
+ x2 = (
139
+ self.gate(self.conv2a(x1))
140
+ + self.gate(self.conv2b(x1))
141
+ + self.gate(self.conv2c(x1))
142
+ + self.gate(self.conv2d(x1))
143
+ )
144
+ x3 = self.conv3(x2)
145
+ if self.downsample is not None:
146
+ identity = self.downsample(identity)
147
+ return F.relu(x3 + identity)
148
+
149
+
150
+ # --------------------------------------------------------------------------- #
151
+ # Full OSNet architecture
152
+ # --------------------------------------------------------------------------- #
153
+
154
+
155
+ class OSNet(nn.Module):
156
+ """Omni-Scale Network for instance re-identification.
157
+
158
+ Architecture from:
159
+ Zhou et al. *Omni-Scale Feature Learning for Person Re-Identification*. ICCV, 2019.
160
+ Zhou et al. *Learning Generalisable Omni-Scale Representations for Person Re-Identification*. TPAMI, 2021.
161
+
162
+ At inference (``model.eval()``), and whenever no classifier was built
163
+ (``num_classes <= 0``), ``forward()`` returns the ``feature_dim`` embedding.
164
+ Classifier logits are returned only in ``train()`` mode with a head.
165
+
166
+ Args:
167
+ num_classes: Number of identity classes for the classification head.
168
+ Use ``0`` (or less) to omit the head for inference-only models.
169
+ blocks: List of block classes for each stage.
170
+ layers: Number of blocks per stage.
171
+ channels: Channel widths ``[stem, stage1, stage2, stage3]``.
172
+ feature_dim: Dimensionality of the final embedding vector.
173
+ """
174
+
175
+ @classmethod
176
+ def default_preprocessing(cls) -> ReIDPreprocessing:
177
+ """Default crop/tensor recipe for OSNet (torchreid / ImageNet norms).
178
+
179
+ Config (matches common OSNet x1.0 zoo checkpoints):
180
+ - input size: 256x128 (H, W)
181
+ - resize: stretch (no letterbox)
182
+ - colour: BGR crops converted to RGB
183
+ - normalize: ImageNet mean/std
184
+ """
185
+ return ReIDPreprocessing(
186
+ input_size=(256, 128),
187
+ resize_mode="stretch",
188
+ to_rgb=True,
189
+ )
190
+
191
+ def __init__(
192
+ self,
193
+ num_classes: int,
194
+ blocks: list[type[nn.Module]],
195
+ layers: list[int],
196
+ channels: list[int],
197
+ feature_dim: int = 512,
198
+ ) -> None:
199
+ super().__init__()
200
+ if len(blocks) != len(layers) or len(layers) != len(channels) - 1:
201
+ raise ValueError("blocks, layers, and channels must have consistent lengths")
202
+
203
+ self.feature_dim = feature_dim
204
+ self.conv1 = _ConvBnRelu(3, channels[0], 7, stride=2, padding=3)
205
+ self.maxpool = nn.MaxPool2d(3, stride=2, padding=1)
206
+ self.conv2 = self._make_layer(blocks[0], layers[0], channels[0], channels[1], downsample=True)
207
+ self.conv3 = self._make_layer(blocks[1], layers[1], channels[1], channels[2], downsample=True)
208
+ self.conv4 = self._make_layer(blocks[2], layers[2], channels[2], channels[3], downsample=False)
209
+ self.conv5 = _Conv1x1(channels[3], channels[3])
210
+ self.global_avgpool = nn.AdaptiveAvgPool2d(1)
211
+ self.fc = nn.Sequential(
212
+ nn.Linear(channels[3], feature_dim),
213
+ nn.BatchNorm1d(feature_dim),
214
+ nn.ReLU(inplace=True),
215
+ )
216
+ # Inference builds use num_classes=0 and omit the head so checkpoints
217
+ # that drop classifier.* can load with required_match_fraction=1.0.
218
+ if num_classes > 0:
219
+ self.classifier = nn.Linear(feature_dim, num_classes)
220
+ self._init_params()
221
+
222
+ @staticmethod
223
+ def _make_layer(
224
+ block: type[nn.Module],
225
+ num_blocks: int,
226
+ in_channels: int,
227
+ out_channels: int,
228
+ downsample: bool,
229
+ ) -> nn.Sequential:
230
+ layers: list[nn.Module] = [block(in_channels, out_channels)]
231
+ for _ in range(1, num_blocks):
232
+ layers.append(block(out_channels, out_channels))
233
+ if downsample:
234
+ layers.append(nn.Sequential(_Conv1x1(out_channels, out_channels), nn.AvgPool2d(2, stride=2)))
235
+ return nn.Sequential(*layers)
236
+
237
+ def _init_params(self) -> None:
238
+ for m in self.modules():
239
+ if isinstance(m, nn.Conv2d):
240
+ nn.init.kaiming_normal_(m.weight, mode="fan_out", nonlinearity="relu")
241
+ if m.bias is not None:
242
+ nn.init.constant_(m.bias, 0)
243
+ elif isinstance(m, (nn.BatchNorm2d, nn.BatchNorm1d)):
244
+ nn.init.constant_(m.weight, 1)
245
+ nn.init.constant_(m.bias, 0)
246
+ elif isinstance(m, nn.Linear):
247
+ nn.init.normal_(m.weight, 0, 0.01)
248
+ if m.bias is not None:
249
+ nn.init.constant_(m.bias, 0)
250
+
251
+ def _featuremaps(self, x: torch.Tensor) -> torch.Tensor:
252
+ x = self.conv1(x)
253
+ x = self.maxpool(x)
254
+ x = self.conv2(x)
255
+ x = self.conv3(x)
256
+ x = self.conv4(x)
257
+ return self.conv5(x)
258
+
259
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
260
+ """Run a forward pass.
261
+
262
+ Returns the embedding at inference time (``model.eval()``) and when no
263
+ classifier was built. Returns classifier logits only in training mode
264
+ with ``num_classes > 0``.
265
+ """
266
+ x = self._featuremaps(x)
267
+ v = self.global_avgpool(x).view(x.size(0), -1)
268
+ v = self.fc(v)
269
+ if self.training and hasattr(self, "classifier"):
270
+ return self.classifier(v)
271
+ return v
272
+
273
+
274
+ # --------------------------------------------------------------------------- #
275
+ # Factory
276
+ # --------------------------------------------------------------------------- #
277
+
278
+ _CONFIGS: dict[str, dict] = {
279
+ "x1_0": {"channels": [64, 256, 384, 512]},
280
+ "x0_75": {"channels": [48, 192, 288, 384]},
281
+ "x0_5": {"channels": [32, 128, 192, 256]},
282
+ "x0_25": {"channels": [16, 64, 96, 128]},
283
+ }
284
+
285
+
286
+ def build_osnet(variant: str = "x1_0", num_classes: int = 0) -> OSNet:
287
+ """Instantiate an OSNet architecture without loading weights.
288
+
289
+ Args:
290
+ variant: Width multiplier variant. One of ``"x1_0"`` (default),
291
+ ``"x0_75"``, ``"x0_5"``, ``"x0_25"``.
292
+ num_classes: Identity classes for the classification head. Use ``0``
293
+ (default) for inference-only models with no classifier.
294
+
295
+ Returns:
296
+ An initialised ``OSNet`` in training mode.
297
+
298
+ Raises:
299
+ ValueError: If ``variant`` is not one of the supported values.
300
+
301
+ Examples:
302
+ >>> model = build_osnet("x1_0")
303
+ >>> model.feature_dim
304
+ 512
305
+ """
306
+ if variant not in _CONFIGS:
307
+ raise ValueError(f"Unknown OSNet variant '{variant}'. Choose from: {list(_CONFIGS)}")
308
+ cfg = _CONFIGS[variant]
309
+ return OSNet(
310
+ num_classes=num_classes,
311
+ blocks=[OSBlock, OSBlock, OSBlock],
312
+ layers=[2, 2, 2],
313
+ channels=cfg["channels"],
314
+ )
@@ -0,0 +1,104 @@
1
+ # ------------------------------------------------------------------------
2
+ # re-ID
3
+ # Copyright (c) 2026 Roboflow. All Rights Reserved.
4
+ # Licensed under the Apache License, Version 2.0 [see LICENSE for details]
5
+ # ------------------------------------------------------------------------
6
+
7
+ """Architecture construction, listing, remapping, and preprocessing defaults."""
8
+
9
+ from __future__ import annotations
10
+
11
+ from collections.abc import Callable
12
+
13
+ import timm
14
+ import torch.nn as nn
15
+
16
+ from reid.architectures.fastreid_sbs import (
17
+ FASTREID_SBS_ARCHITECTURE,
18
+ FastReIDSBSResNeSt50,
19
+ build_fastreid_sbs_resnest50,
20
+ remap_fastreid_sbs_state_dict,
21
+ )
22
+ from reid.architectures.osnet import OSNet, build_osnet
23
+ from reid.preprocessing import ReIDPreprocessing
24
+
25
+ _OSNET_VARIANTS: dict[str, str] = {
26
+ "osnet_x0_25": "x0_25",
27
+ "osnet_x0_5": "x0_5",
28
+ "osnet_x0_75": "x0_75",
29
+ "osnet_x1_0": "x1_0",
30
+ }
31
+
32
+ _DEFAULT_PREPROCESSING_BY_ARCH: dict[str, Callable[[], ReIDPreprocessing]] = {
33
+ **{name: OSNet.default_preprocessing for name in _OSNET_VARIANTS},
34
+ FASTREID_SBS_ARCHITECTURE: FastReIDSBSResNeSt50.default_preprocessing,
35
+ }
36
+
37
+ _TIMM_PREFIX = "timm:"
38
+
39
+
40
+ def build_architecture(
41
+ architecture: str | nn.Module,
42
+ *,
43
+ num_classes: int = 0,
44
+ pretrained: bool = False,
45
+ ) -> nn.Module:
46
+ """Build a backbone from ``osnet_*``, ``timm:<name>``, or a pre-built module.
47
+
48
+ ``pretrained=True`` only affects ``timm:<name>`` models (ImageNet weights).
49
+ OSNet ignores it because checkpoints are always loaded explicitly.
50
+
51
+ Args:
52
+ architecture: Registered name, ``timm:<name>``, or an ``nn.Module``.
53
+ num_classes: Classifier head size (``0`` for embedding-only).
54
+ pretrained: Load ImageNet weights for ``timm:`` models when ``True``.
55
+
56
+ Returns:
57
+ Backbone module ready for weight loading or inference.
58
+ """
59
+ if not isinstance(architecture, str):
60
+ return architecture
61
+
62
+ if architecture.startswith(_TIMM_PREFIX):
63
+ name = architecture[len(_TIMM_PREFIX) :]
64
+ return timm.create_model(name, pretrained=pretrained, num_classes=num_classes)
65
+
66
+ if architecture in _OSNET_VARIANTS:
67
+ return build_osnet(variant=_OSNET_VARIANTS[architecture], num_classes=num_classes)
68
+
69
+ if architecture == FASTREID_SBS_ARCHITECTURE:
70
+ return build_fastreid_sbs_resnest50(
71
+ num_classes=num_classes,
72
+ pretrained=pretrained,
73
+ )
74
+
75
+ raise ValueError(
76
+ f"Unknown architecture {architecture!r}. Choose a registered name "
77
+ f"({list_architectures()}), a timm model as 'timm:<name>' (e.g. "
78
+ f"'timm:resnet50'), or pass a torch.nn.Module instance."
79
+ )
80
+
81
+
82
+ def list_architectures() -> list[str]:
83
+ """Return registered architecture names (timm models use ``timm:<name>``)."""
84
+ return sorted([*_OSNET_VARIANTS, FASTREID_SBS_ARCHITECTURE])
85
+
86
+
87
+ def checkpoint_remap_for_architecture(
88
+ architecture: str,
89
+ ) -> Callable[[dict], dict] | None:
90
+ """Return the registered checkpoint key remapper, if any."""
91
+ if architecture == FASTREID_SBS_ARCHITECTURE:
92
+ return remap_fastreid_sbs_state_dict
93
+ return None
94
+
95
+
96
+ def default_preprocessing_for_architecture(
97
+ architecture: str | None,
98
+ ) -> ReIDPreprocessing:
99
+ """Return the preprocessing defaults owned by ``architecture``."""
100
+ if architecture is None:
101
+ return OSNet.default_preprocessing()
102
+ if architecture in _DEFAULT_PREPROCESSING_BY_ARCH:
103
+ return _DEFAULT_PREPROCESSING_BY_ARCH[architecture]()
104
+ return OSNet.default_preprocessing()