blank4hd commited on
Commit
1d7cec6
·
verified ·
1 Parent(s): d2186fc

Upload Re-ID model checkpoints and model card

Browse files
README.md CHANGED
@@ -1,3 +1,166 @@
1
- ---
2
- license: mit
3
- ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ license: mit
3
+ library_name: pytorch
4
+ tags:
5
+ - person-reidentification
6
+ - person-reid
7
+ - computer-vision
8
+ - pytorch
9
+ - resnet
10
+ - bnneck
11
+ - triplet-loss
12
+ - multi-camera-tracking
13
+ datasets:
14
+ - Market-1501
15
+ metrics:
16
+ - mAP
17
+ - Rank-1
18
+ ---
19
+
20
+ # MCTrack Re-ID Model
21
+
22
+ Person re-identification model trained on Market-1501 as part of the
23
+ **MSML 640 (Computer Vision) Final Project** at the University of Maryland.
24
+ This model is the appearance backbone used in our multi-camera object tracking
25
+ system.
26
+
27
+ ## Model Variants
28
+
29
+ This repo contains two checkpoints:
30
+
31
+ - **best_60ep.pth** (primary) - trained for 60 epochs.
32
+ Used as the deployed Re-ID model in our final cross-camera demos due to
33
+ qualitatively cleaner cluster outputs.
34
+ - **best_120ep.pth** (ablation) - same recipe, trained for 120 epochs.
35
+ Slightly higher Re-ID accuracy but only marginal improvement on downstream
36
+ tasks (see "Performance" below).
37
+
38
+ Both checkpoints contain only the model state_dict. No optimizer or scheduler
39
+ state.
40
+
41
+ ## Architecture
42
+
43
+ - **Backbone:** ResNet-50 (ImageNet-initialized; final stride changed from
44
+ 2 to 1 to retain higher-resolution features)
45
+ - **Pooling:** Global average pooling
46
+ - **Neck:** BNNeck (Batch Normalization neck) - separates triplet-loss
47
+ features from classification features
48
+ - **Embedding dimension:** 256
49
+ - **Total parameters:** ~25M
50
+
51
+ ## Training Recipe
52
+
53
+ | Setting | Value |
54
+ | ---------------- | ---------------------------------------------------------------- |
55
+ | Dataset | Market-1501 (12,936 train images, 751 train IDs) |
56
+ | Identity sampler | P=16 IDs x K=4 instances per batch |
57
+ | Batch size | 64 |
58
+ | Optimizer | Adam, lr=3.5e-4, weight_decay=5e-4 |
59
+ | LR schedule | Linear warmup (10 epochs), step decay (x0.1 at epochs 40 and 70) |
60
+ | Loss | Combined CE (label smoothing 0.1) + triplet (soft margin) |
61
+ | Image size | 256x128 |
62
+ | Augmentation | Random horizontal flip, random erasing |
63
+
64
+ ## Performance
65
+
66
+ ### Standalone Re-ID (Market-1501)
67
+
68
+ | Variant | mAP | Rank-1 | Rank-5 | Rank-10 |
69
+ | --------- | ----- | ------ | ------ | ------- |
70
+ | 60-epoch | 73.73 | 89.32 | 96.04 | 97.74 |
71
+ | 120-epoch | 75.15 | 90.66 | 96.79 | 98.04 |
72
+
73
+ ### Downstream - Single-camera tracking (MOT17, with DeepSORT)
74
+
75
+ | Variant | HOTA | MOTA | IDF1 | IDSW |
76
+ | --------- | ---- | ---- | ---- | ---- |
77
+ | 60-epoch | 41.1 | 36.5 | 48.6 | 260 |
78
+ | 120-epoch | 41.0 | 36.6 | 48.8 | 246 |
79
+
80
+ ### Downstream - Cross-camera tracking (Wildtrack, with ground-plane filter)
81
+
82
+ | Variant | IDF1 | IDP | IDR |
83
+ | --------- | ----- | ----- | ----- |
84
+ | 60-epoch | 16.99 | 23.80 | 13.21 |
85
+ | 120-epoch | 18.72 | 26.41 | 14.50 |
86
+
87
+ ## Usage
88
+
89
+ Download and load with `huggingface_hub`:
90
+
91
+ ```python
92
+ from huggingface_hub import hf_hub_download
93
+ import torch
94
+
95
+ ckpt_path = hf_hub_download(
96
+ repo_id="blank4hd/mctrack-reid",
97
+ filename="best_60ep.pth",
98
+ )
99
+
100
+ state = torch.load(ckpt_path, map_location="cpu", weights_only=False)
101
+ model_state = state["state_dict"]
102
+ ```
103
+
104
+ To use this with the model architecture, you need the `ReIDModel` class from
105
+ the project repository. A minimal standalone loader (`load_reid.py`) is
106
+ provided alongside this model card with the architecture definition inlined,
107
+ so the model can be used without cloning the full project.
108
+
109
+ ## Intended Use
110
+
111
+ This model produces 256-dimensional appearance embeddings for cropped person
112
+ images. Two crops of the same person are expected to produce embeddings with
113
+ high cosine similarity; crops of different people produce embeddings with low
114
+ similarity.
115
+
116
+ **Suitable for:**
117
+
118
+ - Person re-identification in research / academic settings
119
+ - Appearance feature extraction in tracking pipelines (e.g., DeepSORT)
120
+ - Educational demonstration of metric learning
121
+
122
+ **Not suitable for:**
123
+
124
+ - Surveillance applications without explicit consent
125
+ - Identification of individuals across populations (high false-positive rate
126
+ in cross-domain settings)
127
+ - Any use where reliability is safety-critical
128
+
129
+ ## Limitations
130
+
131
+ - **Domain gap.** Trained on Market-1501 (Tsinghua University campus, ~5
132
+ surveillance cameras). Performance degrades on outdoor pedestrian-square
133
+ scenes (e.g., Wildtrack), where IDF1 drops to ~17-19% in cross-camera
134
+ matching.
135
+ - **Person crops only.** Expects the input to be a tightly-cropped person
136
+ image. Whole scenes or non-person inputs produce meaningless embeddings.
137
+ - **Resolution sensitive.** Trained at 256x128. Significantly different input
138
+ resolutions will degrade quality.
139
+ - **No fairness audit.** Not evaluated for performance disparities across
140
+ demographic groups.
141
+
142
+ ## Training Details (compute and time)
143
+
144
+ - **Hardware:** Apple M4 Pro (MPS backend)
145
+ - **Per-epoch time:** ~46 seconds
146
+ - **Total training time:** 60-epoch ~46 min; 120-epoch ~92 min
147
+ - **Memory usage:** ~3 GB unified memory at batch size 64
148
+
149
+ ## Citation
150
+
151
+ This work was completed for the MSML 640 final project, Spring 2026.
152
+
153
+ ```
154
+ Group 9 - MSML 640 Final Project
155
+ Multi-Camera Object Tracking with Person Re-Identification
156
+ ```
157
+
158
+ ## Acknowledgments
159
+
160
+ - Architecture inspired by Luo et al. ("Bag of Tricks and a Strong Baseline
161
+ for Deep Person Re-Identification", CVPR Workshop 2019)
162
+ - BNNeck design from the same paper
163
+ - Triplet loss formulation from Hermans et al. ("In Defense of the Triplet
164
+ Loss for Person Re-Identification", arXiv 2017)
165
+ - Market-1501 dataset from Zheng et al. ("Scalable Person Re-Identification:
166
+ A Benchmark", ICCV 2015)
__pycache__/load_reid.cpython-311.pyc ADDED
Binary file (8.35 kB). View file
 
best_120ep.pth ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:007fbe03dd4cb3c2b8b44d9b33a35f73b28b460a17c397018705c71113a3ac23
3
+ size 102632278
best_60ep.pth ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:c56364ec2b8f8477be7675b58ff7583964c431d5f1109cd76aa5c96faecd9c3f
3
+ size 102631947
load_reid.py ADDED
@@ -0,0 +1,114 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pathlib import Path
2
+
3
+ import torch
4
+ import torch.nn as nn
5
+ import torch.nn.functional as F
6
+ from PIL import Image
7
+ from torchvision import models, transforms
8
+
9
+
10
+ class ReIDModel(nn.Module):
11
+ """Minimal Re-ID model matching the trained checkpoint architecture.
12
+
13
+ ResNet-50 backbone (final stride 1) -> global average pooling -> BNNeck.
14
+ Returns 256-dim L2-normalized embeddings during inference.
15
+ """
16
+
17
+ def __init__(self, num_classes: int = 751, embedding_dim: int = 256):
18
+ super().__init__()
19
+ backbone = models.resnet50(weights=None)
20
+ backbone.layer4[0].conv2.stride = (1, 1)
21
+ backbone.layer4[0].downsample[0].stride = (1, 1)
22
+ self.backbone = nn.Sequential(*list(backbone.children())[:-2])
23
+
24
+ self.global_pool = nn.AdaptiveAvgPool2d(1)
25
+ self.feature_dim = 2048
26
+ self.bnneck = nn.BatchNorm1d(self.feature_dim)
27
+ self.bnneck.bias.requires_grad_(False)
28
+ nn.init.constant_(self.bnneck.weight, 1.0)
29
+ nn.init.constant_(self.bnneck.bias, 0.0)
30
+
31
+ self.embedding_layer = nn.Linear(self.feature_dim, embedding_dim, bias=False)
32
+ nn.init.kaiming_normal_(self.embedding_layer.weight, mode="fan_out")
33
+
34
+ self.num_classes = num_classes
35
+ if num_classes > 0:
36
+ self.classifier = nn.Linear(self.feature_dim, num_classes, bias=False)
37
+ nn.init.normal_(self.classifier.weight, std=0.001)
38
+ else:
39
+ self.classifier = None
40
+
41
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
42
+ """Return an L2-normalized embedding of shape (N, embedding_dim)."""
43
+ feat_map = self.backbone(x)
44
+ pooled = self.global_pool(feat_map).flatten(1)
45
+ bn_features = self.bnneck(pooled)
46
+ embedding = self.embedding_layer(bn_features)
47
+ embedding = F.normalize(embedding, p=2, dim=1)
48
+ return embedding
49
+
50
+
51
+ def load_model(checkpoint_path: str | Path, device: str = "cpu") -> ReIDModel:
52
+ """Load the trained Re-ID model from a stripped checkpoint."""
53
+ model = ReIDModel(num_classes=751, embedding_dim=256)
54
+ state = torch.load(str(checkpoint_path), map_location=device, weights_only=False)
55
+ sd = state["state_dict"] if "state_dict" in state else state
56
+ missing_keys, unexpected_keys = model.load_state_dict(sd, strict=False)
57
+ if missing_keys:
58
+ print(f"Warning: missing keys: {len(missing_keys)} keys")
59
+ if unexpected_keys:
60
+ print(f"Warning: unexpected keys: {len(unexpected_keys)} keys")
61
+ model.eval()
62
+ model.to(device)
63
+ return model
64
+
65
+
66
+ _PREPROCESS = transforms.Compose(
67
+ [
68
+ transforms.Resize((256, 128)),
69
+ transforms.ToTensor(),
70
+ transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),
71
+ ]
72
+ )
73
+
74
+
75
+ def preprocess_image(image: Image.Image) -> torch.Tensor:
76
+ """Preprocess a PIL image to model input format."""
77
+ if image.mode != "RGB":
78
+ image = image.convert("RGB")
79
+ return _PREPROCESS(image).unsqueeze(0)
80
+
81
+
82
+ def cosine_similarity(emb_a: torch.Tensor, emb_b: torch.Tensor) -> float:
83
+ """Cosine similarity between two L2-normalized embeddings, in [-1, 1]."""
84
+ return float(F.cosine_similarity(emb_a, emb_b).item())
85
+
86
+
87
+ if __name__ == "__main__":
88
+ import sys
89
+
90
+ from huggingface_hub import hf_hub_download
91
+
92
+ print("Downloading model from Hugging Face...")
93
+ ckpt = hf_hub_download(repo_id="blank4hd/mctrack-reid", filename="best_60ep.pth")
94
+ model = load_model(ckpt)
95
+ print(f"Model loaded. Embedding dim: 256, parameter count: {sum(p.numel() for p in model.parameters()) / 1e6:.1f}M")
96
+
97
+ if len(sys.argv) >= 3:
98
+ img_a = Image.open(sys.argv[1])
99
+ img_b = Image.open(sys.argv[2])
100
+ x_a = preprocess_image(img_a)
101
+ x_b = preprocess_image(img_b)
102
+ with torch.no_grad():
103
+ emb_a = model(x_a)
104
+ emb_b = model(x_b)
105
+ sim = cosine_similarity(emb_a, emb_b)
106
+ print(f"Cosine similarity: {sim:.4f}")
107
+ if sim > 0.7:
108
+ print("Likely SAME person")
109
+ elif sim > 0.4:
110
+ print("Possibly same person (uncertain)")
111
+ else:
112
+ print("Likely DIFFERENT people")
113
+ else:
114
+ print("Usage: python load_reid.py <image_a.jpg> <image_b.jpg>")