Upload Re-ID model checkpoints and model card
Browse files- README.md +166 -3
- __pycache__/load_reid.cpython-311.pyc +0 -0
- best_120ep.pth +3 -0
- best_60ep.pth +3 -0
- load_reid.py +114 -0
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>")
|