-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcsrnet.py
More file actions
168 lines (132 loc) · 5.23 KB
/
Copy pathcsrnet.py
File metadata and controls
168 lines (132 loc) · 5.23 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
"""
CSRNet crowd counting model (CVPR 2018).
VGG-16 frontend + dilated conv backend.
Weights from rootstrap-org/crowd-counting (HuggingFace).
"""
import os
import time
import cv2
import numpy as np
import torch
import torch.nn as nn
import torch.nn.functional as F
# ── Model Architecture ──────────────────────────────────────
def _make_vgg_layers():
cfg = [64, 64, 'M', 128, 128, 'M', 256, 256, 256, 'M', 512, 512, 512]
layers = []
in_channels = 3
for v in cfg:
if v == 'M':
layers.append(nn.MaxPool2d(kernel_size=2, stride=2))
else:
layers.append(nn.Conv2d(in_channels, v, kernel_size=3, padding=1))
layers.append(nn.ReLU(inplace=True))
in_channels = v
return nn.Sequential(*layers)
class CSRNet(nn.Module):
def __init__(self):
super().__init__()
self.frontend = _make_vgg_layers()
self.backend = nn.Sequential(
nn.Conv2d(512, 512, 3, padding=2, dilation=2),
nn.ReLU(inplace=True),
nn.Conv2d(512, 512, 3, padding=2, dilation=2),
nn.ReLU(inplace=True),
nn.Conv2d(512, 512, 3, padding=2, dilation=2),
nn.ReLU(inplace=True),
nn.Conv2d(512, 256, 3, padding=2, dilation=2),
nn.ReLU(inplace=True),
nn.Conv2d(256, 128, 3, padding=2, dilation=2),
nn.ReLU(inplace=True),
nn.Conv2d(128, 64, 3, padding=2, dilation=2),
nn.ReLU(inplace=True),
)
self.output_layer = nn.Conv2d(64, 1, 1)
def forward(self, x):
x = self.frontend(x)
x = self.backend(x)
x = self.output_layer(x)
return x
# ── Global model singleton ──────────────────────────────────
MODEL_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "models")
WEIGHTS_PATH = os.path.join(MODEL_DIR, "csrnet_weights.pth")
_csrnet_model = None
_csrnet_available = False
_inited = False
def _init_csrnet():
global _inited
if _inited:
return
_inited = True
global _csrnet_model, _csrnet_available
if not os.path.exists(WEIGHTS_PATH):
print(" [CSRNet] weights not found, run download first")
return
try:
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
_csrnet_model = CSRNet().to(device)
ckpt = torch.load(WEIGHTS_PATH, map_location=device, weights_only=True)
_csrnet_model.load_state_dict(ckpt)
_csrnet_model.eval()
_csrnet_available = True
print(f" [CSRNet] loaded on {device} ({sum(p.numel() for p in _csrnet_model.parameters())/1e6:.1f}M params)")
except Exception as e:
print(f" [CSRNet] init failed: {e}")
_csrnet_available = False
def get_csrnet():
global _csrnet_model
if _csrnet_model is None:
_init_csrnet()
return _csrnet_model if _csrnet_available else None
def is_csrnet_available() -> bool:
return _csrnet_available
# ── Inference helpers ───────────────────────────────────────
TARGET_MAX_DIM = 1024
def _resize_dims(h: int, w: int, max_dim: int = TARGET_MAX_DIM):
scale = min(max_dim / max(h, w), 1.0)
new_h = int(h * scale)
new_w = int(w * scale)
new_h = (new_h // 8) * 8 # CSRNet downsamples by 8x
new_w = (new_w // 8) * 8
return new_h, new_w
def count_csrnet(image: np.ndarray) -> dict:
"""Run CSRNet inference on BGR image. Returns count + density + heatmap."""
t0 = time.time()
original_h, original_w = image.shape[:2]
model = get_csrnet()
if model is None:
return None
device = next(model.parameters()).device
# Preprocess: BGR → RGB, resize, normalize
img_rgb = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
new_h, new_w = _resize_dims(original_h, original_w)
img_resized = cv2.resize(img_rgb, (new_w, new_h))
# Convert to tensor: [0,255] → [0,1]
tensor = torch.from_numpy(img_resized).float().permute(2, 0, 1) / 255.0
tensor = tensor.unsqueeze(0).to(device) # [1, 3, H, W]
with torch.no_grad():
density = model(tensor)
density = density.squeeze().cpu().numpy() # [H/8, W/8]
count = float(density.sum())
count_rounded = int(round(count))
# Resize density to original size
density_resized = cv2.resize(density, (original_w, original_h))
# Clip negative values, create heatmap
d_clipped = np.maximum(density_resized, 0)
d_min = d_clipped.min()
d_max = d_clipped.max()
if d_max > d_min:
d_norm = ((d_clipped - d_min) / (d_max - d_min) * 255).astype(np.uint8)
else:
d_norm = np.zeros_like(d_clipped, dtype=np.uint8)
heatmap = cv2.applyColorMap(d_norm, cv2.COLORMAP_JET)
overlay = cv2.addWeighted(image, 0.6, heatmap, 0.4, 0)
_, heatmap_buf = cv2.imencode(".jpg", overlay, [cv2.IMWRITE_JPEG_QUALITY, 85])
elapsed = (time.time() - t0) * 1000
return {
"count": count,
"count_rounded": count_rounded,
"density_map": density_resized,
"heatmap_bytes": heatmap_buf.tobytes(),
"processing_time_ms": round(elapsed, 1),
}