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 +47 -0
- reid/architectures/__init__.py +23 -0
- reid/architectures/fastreid_sbs.py +141 -0
- reid/architectures/osnet.py +314 -0
- reid/architectures/registry.py +104 -0
- reid/catalog.py +222 -0
- reid/data/__init__.py +27 -0
- reid/data/benchmarks.py +163 -0
- reid/data/identity_crops.py +271 -0
- reid/data/splits.py +34 -0
- reid/eval/__init__.py +21 -0
- reid/eval/datasets.py +20 -0
- reid/eval/evaluator.py +148 -0
- reid/eval/metrics.py +142 -0
- reid/loaders.py +404 -0
- reid/model.py +322 -0
- reid/preprocessing.py +170 -0
- reid/py.typed +1 -0
- reid-0.1.0.dev0.dist-info/METADATA +173 -0
- reid-0.1.0.dev0.dist-info/RECORD +23 -0
- reid-0.1.0.dev0.dist-info/WHEEL +5 -0
- reid-0.1.0.dev0.dist-info/licenses/LICENSE +201 -0
- reid-0.1.0.dev0.dist-info/top_level.txt +1 -0
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()
|