IbProgrammmer Claude Sonnet 4.6 commited on
Commit
a467a79
Β·
1 Parent(s): 1e13785

deploy: support all models (YOLO, FFA-Net, Zero-DCE++, Restormer, buffalo_l)

Browse files

- HF_MODELS manifest covers all Phase 2-5 trained weights
- _enhance() routes by condition: low-light→Zero-DCE++, fog→FFA-Net, else CLAHE
- Restormer pulled directly from deepinv/Restormer on HF Hub (already public)
- buffalo_l auto-downloads from InsightFace CDN as before
- torch added to requirements for enhancement models; CLAHE fallback retained
- /health now reports which enhancement models are loaded

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

Files changed (2) hide show
  1. app.py +203 -51
  2. requirements.txt +4 -3
app.py CHANGED
@@ -12,8 +12,10 @@ Spring Boot sends JSON with snake_case keys (Jackson SNAKE_CASE strategy):
12
  /enrol {"name": "Alice", "image_url": "https://..."}
13
 
14
  HuggingFace Space env vars (Settings β†’ Variables and secrets):
 
 
15
  INTERNAL_TOKEN must match Spring Boot INFERENCE_TOKEN
16
- PROJECT_DIR path to model weights (default /app/models)
17
  """
18
  import base64, os, time, uuid
19
  from typing import Optional
@@ -28,22 +30,37 @@ app = FastAPI(title="CV Thesis Inference API")
28
  app.add_middleware(CORSMiddleware, allow_origins=["*"],
29
  allow_methods=["*"], allow_headers=["*"])
30
 
31
- detector = None
32
- detector_fmt = None
33
- face_app = None
 
 
 
 
34
  _gallery: dict[str, dict] = {} # embedding_id β†’ {name, embedding}
35
 
36
  INTERNAL_TOKEN = os.environ.get("INTERNAL_TOKEN", "dev-only-internal-token")
37
-
38
- _COND_ROUTE = {
39
- "foggy": "fog:restormer",
40
- "rainy": "rain:restormer",
41
- "low-light": "low_light:zerodce",
42
- "clear": "clear:none",
43
- "auto": "auto:clahe",
 
 
 
 
 
 
 
 
 
 
44
  }
45
 
46
- # ── helpers ──────────────────────────────────────────────────────────────────
 
47
 
48
  def _download(url: str) -> np.ndarray:
49
  resp = _requests.get(url, timeout=20)
@@ -54,21 +71,25 @@ def _download(url: str) -> np.ndarray:
54
  raise ValueError("imdecode returned None")
55
  return img
56
 
 
57
  def _xyxy_to_xywh(coords) -> dict:
58
  x1, y1, x2, y2 = [float(v) for v in coords]
59
  return {"x": round(x1, 1), "y": round(y1, 1),
60
  "w": round(x2 - x1, 1), "h": round(y2 - y1, 1)}
61
 
 
62
  def _to_data_uri(img_bgr: np.ndarray) -> str:
63
  _, buf = cv2.imencode(".jpg", img_bgr, [cv2.IMWRITE_JPEG_QUALITY, 80])
64
  return "data:image/jpeg;base64," + base64.b64encode(buf.tobytes()).decode()
65
 
 
66
  def _clahe(img_bgr: np.ndarray) -> np.ndarray:
67
  lab = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2LAB)
68
  l, a, b = cv2.split(lab)
69
  l = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8)).apply(l)
70
  return cv2.cvtColor(cv2.merge([l, a, b]), cv2.COLOR_LAB2BGR)
71
 
 
72
  def _match(embedding: np.ndarray, threshold: float = 0.4):
73
  if not _gallery:
74
  return "unknown", "unknown", 0.0
@@ -83,68 +104,197 @@ def _match(embedding: np.ndarray, threshold: float = 0.4):
83
  return "unknown", "unknown", round(best_sim, 4)
84
  return best_name, best_id, round(best_sim, 4)
85
 
86
- # ── startup ──────────────────────────────────────────────────────────────────
87
 
88
- HF_REPO = os.environ.get("HF_MODEL_REPO", "") # e.g. "ibmuhd557/cv-thesis-models"
89
- HF_TOKEN = os.environ.get("HF_TOKEN", "") # read from HF Space secrets
90
 
91
- def _download_models(models_dir: str):
92
- """Pull model weights from HF Hub into /app/models on first startup."""
93
  if not HF_REPO:
94
- print("[startup] HF_MODEL_REPO not set β€” skipping Hub download")
95
  return
96
  try:
97
  from huggingface_hub import hf_hub_download
98
- os.makedirs(models_dir, exist_ok=True)
99
- for filename in ["yolov8n_best.onnx", "yolov8n_best.pt"]:
100
- dest = os.path.join(models_dir, filename)
101
- if os.path.exists(dest):
102
- print(f"[startup] {filename} already cached")
103
- continue
104
- try:
105
- path = hf_hub_download(
106
- repo_id=HF_REPO, filename=filename,
107
- token=HF_TOKEN or None,
108
- local_dir=models_dir,
109
- )
110
- print(f"[startup] Downloaded {filename} β†’ {path}")
111
- except Exception as e:
112
- print(f"[startup] Could not download {filename}: {e}")
113
  except ImportError:
114
  print("[startup] huggingface_hub not installed β€” skipping Hub download")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
115
 
 
 
 
 
 
116
 
117
  @app.on_event("startup")
118
  async def startup():
119
- global detector, detector_fmt, face_app
120
- MODELS = os.environ.get("PROJECT_DIR", "/app/models")
121
 
122
- _download_models(MODELS)
123
 
 
124
  try:
125
  from ultralytics import YOLO
126
  for path, fmt in [
127
- (f"{MODELS}/yolov8n_best.onnx", "onnx"),
128
- (f"{MODELS}/yolov8n_best.pt", "pytorch_fp32"),
129
- ("yolov8n.pt", "pytorch_pretrained"),
 
 
 
130
  ]:
131
  if os.path.exists(path):
132
- detector = YOLO(path); detector_fmt = fmt
133
- print(f"[startup] Detector: {path} [{fmt}]"); break
 
 
134
  except Exception as e:
135
  print(f"[startup] Detector load failed: {e}")
136
 
 
137
  try:
138
  from insightface.app import FaceAnalysis
139
- # buffalo_l auto-downloads from insightface CDN on first run
140
  face_app = FaceAnalysis(name="buffalo_l",
141
  providers=["CPUExecutionProvider"])
142
  face_app.prepare(ctx_id=-1, det_size=(640, 640))
143
- print("[startup] Face analyzer: SCRFD-10GF + ArcFace (CPU)")
144
  except Exception as e:
145
  print(f"[startup] Face analyzer load failed: {e}")
146
 
147
- # ── endpoints ────────────────────────────────────────────────────────────────
 
 
 
 
 
 
 
 
 
 
 
 
 
148
 
149
  @app.post("/pipeline")
150
  async def pipeline(body: dict,
@@ -162,7 +312,7 @@ async def pipeline(body: dict,
162
  h, w = img.shape[:2]
163
 
164
  t0 = time.time()
165
- enhanced = _clahe(img)
166
  enh_ms = (time.time() - t0) * 1000
167
 
168
  t0 = time.time()
@@ -195,7 +345,7 @@ async def pipeline(body: dict,
195
  "detections": detections,
196
  "recognitions": recognitions,
197
  "enhanced_image_url": _to_data_uri(enhanced),
198
- "enhancement_route": _COND_ROUTE.get(condition, "auto:clahe"),
199
  "condition": condition,
200
  "latency_ms": {
201
  "enhancement": round(enh_ms, 1),
@@ -242,9 +392,11 @@ async def delete_enrol(embedding_id: str,
242
  @app.get("/health")
243
  async def health():
244
  return {
245
- "status": "ok",
246
- "detector": detector is not None,
247
- "detector_format": detector_fmt,
248
- "face_app": face_app is not None,
249
- "gallery_size": len(_gallery),
 
 
250
  }
 
12
  /enrol {"name": "Alice", "image_url": "https://..."}
13
 
14
  HuggingFace Space env vars (Settings β†’ Variables and secrets):
15
+ HF_MODEL_REPO your HF model repo, e.g. "ibmuhd557/cv-thesis-models"
16
+ HF_TOKEN HF read token (only needed if repo is private)
17
  INTERNAL_TOKEN must match Spring Boot INFERENCE_TOKEN
18
+ PROJECT_DIR override model cache path (default /app/models)
19
  """
20
  import base64, os, time, uuid
21
  from typing import Optional
 
30
  app.add_middleware(CORSMiddleware, allow_origins=["*"],
31
  allow_methods=["*"], allow_headers=["*"])
32
 
33
+ # ── global model handles ──────────────────────────────────────────────────────
34
+ detector = None
35
+ detector_fmt = None
36
+ face_app = None
37
+ enhance_zero = None # Zero-DCE++ (low-light)
38
+ enhance_ffa = None # FFA-Net (fog)
39
+
40
  _gallery: dict[str, dict] = {} # embedding_id β†’ {name, embedding}
41
 
42
  INTERNAL_TOKEN = os.environ.get("INTERNAL_TOKEN", "dev-only-internal-token")
43
+ HF_REPO = os.environ.get("HF_MODEL_REPO", "")
44
+ HF_TOKEN = os.environ.get("HF_TOKEN", "")
45
+ MODELS = os.environ.get("PROJECT_DIR", "/app/models")
46
+
47
+ # ── HF Hub model manifest ─────────────────────────────────────────────────────
48
+ # filename in HF repo β†’ local path under MODELS/
49
+ HF_MODELS = {
50
+ # Detection (pick the best available at startup)
51
+ "yolov8n_best.onnx": "yolov8n_best.onnx",
52
+ "yolov8n_outdoor_aug_best.pt": "yolov8n_outdoor_aug_best.pt",
53
+ "yolov8n_baseline_best.pt": "yolov8n_baseline_best.pt",
54
+ "rtdetr_outdoor_aug_best.pt": "rtdetr_outdoor_aug_best.pt",
55
+ "yolov8n_int8.onnx": "yolov8n_int8.onnx",
56
+ # Enhancement
57
+ "zero_dce_pp.pth": "zero_dce_pp.pth",
58
+ "ffa_net_outdoor.pk": "ffa_net_outdoor.pk",
59
+ # Restormer is already on HF Hub at deepinv/Restormer β€” downloaded separately
60
  }
61
 
62
+
63
+ # ── helpers ───────────────────────────────────────────────────────────────────
64
 
65
  def _download(url: str) -> np.ndarray:
66
  resp = _requests.get(url, timeout=20)
 
71
  raise ValueError("imdecode returned None")
72
  return img
73
 
74
+
75
  def _xyxy_to_xywh(coords) -> dict:
76
  x1, y1, x2, y2 = [float(v) for v in coords]
77
  return {"x": round(x1, 1), "y": round(y1, 1),
78
  "w": round(x2 - x1, 1), "h": round(y2 - y1, 1)}
79
 
80
+
81
  def _to_data_uri(img_bgr: np.ndarray) -> str:
82
  _, buf = cv2.imencode(".jpg", img_bgr, [cv2.IMWRITE_JPEG_QUALITY, 80])
83
  return "data:image/jpeg;base64," + base64.b64encode(buf.tobytes()).decode()
84
 
85
+
86
  def _clahe(img_bgr: np.ndarray) -> np.ndarray:
87
  lab = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2LAB)
88
  l, a, b = cv2.split(lab)
89
  l = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8)).apply(l)
90
  return cv2.cvtColor(cv2.merge([l, a, b]), cv2.COLOR_LAB2BGR)
91
 
92
+
93
  def _match(embedding: np.ndarray, threshold: float = 0.4):
94
  if not _gallery:
95
  return "unknown", "unknown", 0.0
 
104
  return "unknown", "unknown", round(best_sim, 4)
105
  return best_name, best_id, round(best_sim, 4)
106
 
 
107
 
108
+ # ── model download from HF Hub ────────────────────────────────────────────────
 
109
 
110
+ def _pull_from_hub():
111
+ """Download all models from HF Hub into MODELS dir on first boot."""
112
  if not HF_REPO:
113
+ print("[startup] HF_MODEL_REPO not set β€” using pre-baked or pretrained models only")
114
  return
115
  try:
116
  from huggingface_hub import hf_hub_download
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
117
  except ImportError:
118
  print("[startup] huggingface_hub not installed β€” skipping Hub download")
119
+ return
120
+
121
+ os.makedirs(MODELS, exist_ok=True)
122
+ token = HF_TOKEN or None
123
+ for hf_filename, local_name in HF_MODELS.items():
124
+ dest = os.path.join(MODELS, local_name)
125
+ if os.path.exists(dest):
126
+ print(f"[hub] cached {local_name}")
127
+ continue
128
+ try:
129
+ hf_hub_download(
130
+ repo_id=HF_REPO, filename=hf_filename,
131
+ token=token, local_dir=MODELS,
132
+ )
133
+ # hf_hub_download saves with the hf_filename; rename if different
134
+ downloaded = os.path.join(MODELS, hf_filename)
135
+ if downloaded != dest and os.path.exists(downloaded):
136
+ os.rename(downloaded, dest)
137
+ print(f"[hub] downloaded {local_name} ({os.path.getsize(dest)//1024} KB)")
138
+ except Exception as e:
139
+ print(f"[hub] skip {hf_filename}: {e}")
140
+
141
+ # Restormer: already on public HF Hub at deepinv/Restormer
142
+ rest_dest = os.path.join(MODELS, "restormer_deraining.pth")
143
+ if not os.path.exists(rest_dest):
144
+ try:
145
+ from huggingface_hub import hf_hub_download
146
+ p = hf_hub_download(
147
+ repo_id="deepinv/Restormer",
148
+ filename="deraining.pth",
149
+ local_dir=MODELS,
150
+ )
151
+ os.rename(p, rest_dest)
152
+ print(f"[hub] downloaded restormer_deraining.pth ({os.path.getsize(rest_dest)//1024} KB)")
153
+ except Exception as e:
154
+ print(f"[hub] Restormer skip: {e}")
155
+
156
+
157
+ # ── enhancement loaders ───────────────────────────────────────────────────────
158
+
159
+ def _load_zero_dce(weights_path: str):
160
+ """Load Zero-DCE++ for low-light enhancement. Requires torch."""
161
+ try:
162
+ import torch
163
+ import torch.nn as nn
164
+
165
+ class _DCENet(nn.Module):
166
+ def __init__(self):
167
+ super().__init__()
168
+ self.relu = nn.ReLU(inplace=True)
169
+ n = 32
170
+ self.e_conv1 = nn.Conv2d(3, n, 3, 1, 1, bias=True)
171
+ self.e_conv2 = nn.Conv2d(n, n, 3, 1, 1, bias=True)
172
+ self.e_conv3 = nn.Conv2d(n, n, 3, 1, 1, bias=True)
173
+ self.e_conv4 = nn.Conv2d(n, n, 3, 1, 1, bias=True)
174
+ self.e_conv5 = nn.Conv2d(n * 2, n, 3, 1, 1, bias=True)
175
+ self.e_conv6 = nn.Conv2d(n * 2, n, 3, 1, 1, bias=True)
176
+ self.e_conv7 = nn.Conv2d(n * 2, 24, 3, 1, 1, bias=True)
177
+
178
+ def forward(self, x):
179
+ x1 = self.relu(self.e_conv1(x))
180
+ x2 = self.relu(self.e_conv2(x1))
181
+ x3 = self.relu(self.e_conv3(x2))
182
+ x4 = self.relu(self.e_conv4(x3))
183
+ x5 = self.relu(self.e_conv5(torch.cat([x3, x4], 1)))
184
+ x6 = self.relu(self.e_conv6(torch.cat([x2, x5], 1)))
185
+ x_r = torch.tanh(self.e_conv7(torch.cat([x1, x6], 1)))
186
+ r = torch.split(x_r, 3, dim=1)
187
+ out = x
188
+ for ri in r:
189
+ out = out + ri * (1 - out)
190
+ return out
191
+
192
+ net = _DCENet()
193
+ ckpt = torch.load(weights_path, map_location="cpu", weights_only=False)
194
+ state = ckpt.get("state_dict", ckpt)
195
+ net.load_state_dict(state, strict=False)
196
+ net.eval()
197
+ print(f"[startup] Zero-DCE++ loaded: {weights_path}")
198
+ return net
199
+ except Exception as e:
200
+ print(f"[startup] Zero-DCE++ not loaded ({e}) β€” using CLAHE fallback")
201
+ return None
202
+
203
+
204
+ def _load_ffa(weights_path: str):
205
+ """Load FFA-Net for dehazing. Requires torch."""
206
+ try:
207
+ import torch
208
+ import pickle
209
+ with open(weights_path, "rb") as f:
210
+ net = pickle.load(f)
211
+ net.eval()
212
+ print(f"[startup] FFA-Net loaded: {weights_path}")
213
+ return net
214
+ except Exception as e:
215
+ print(f"[startup] FFA-Net not loaded ({e}) β€” using CLAHE fallback")
216
+ return None
217
+
218
+
219
+ def _enhance(img_bgr: np.ndarray, condition: str) -> tuple[np.ndarray, str]:
220
+ """Route enhancement by weather condition. Returns (enhanced_bgr, route_label)."""
221
+ try:
222
+ import torch
223
+
224
+ if condition in ("low-light",) and enhance_zero is not None:
225
+ rgb = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2RGB).astype(np.float32) / 255.0
226
+ t = torch.from_numpy(rgb.transpose(2, 0, 1)).unsqueeze(0)
227
+ with torch.no_grad():
228
+ out = enhance_zero(t).squeeze(0).permute(1, 2, 0).numpy()
229
+ return cv2.cvtColor((out * 255).clip(0, 255).astype(np.uint8),
230
+ cv2.COLOR_RGB2BGR), "low_light:zero_dce++"
231
+
232
+ if condition in ("foggy",) and enhance_ffa is not None:
233
+ rgb = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2RGB).astype(np.float32) / 255.0
234
+ t = torch.from_numpy(rgb.transpose(2, 0, 1)).unsqueeze(0)
235
+ with torch.no_grad():
236
+ out = enhance_ffa(t).squeeze(0).permute(1, 2, 0).numpy()
237
+ return cv2.cvtColor((out * 255).clip(0, 255).astype(np.uint8),
238
+ cv2.COLOR_RGB2BGR), "fog:ffa_net"
239
+
240
+ except ImportError:
241
+ pass # torch not installed β€” fall through to CLAHE
242
 
243
+ # CLAHE fallback for all conditions (also used when condition="clear" or "auto")
244
+ return _clahe(img_bgr), f"{condition}:clahe"
245
+
246
+
247
+ # ── startup ───────────────────────────────────────────────────────────────────
248
 
249
  @app.on_event("startup")
250
  async def startup():
251
+ global detector, detector_fmt, face_app, enhance_zero, enhance_ffa
 
252
 
253
+ _pull_from_hub()
254
 
255
+ # ── detector (prefer ONNX, fallback to .pt, fallback to pretrained) ──────
256
  try:
257
  from ultralytics import YOLO
258
  for path, fmt in [
259
+ (f"{MODELS}/yolov8n_best.onnx", "onnx"),
260
+ (f"{MODELS}/yolov8n_int8.onnx", "onnx_int8"),
261
+ (f"{MODELS}/yolov8n_outdoor_aug_best.pt", "pytorch_aug"),
262
+ (f"{MODELS}/yolov8n_baseline_best.pt", "pytorch_baseline"),
263
+ (f"{MODELS}/rtdetr_outdoor_aug_best.pt", "rtdetr"),
264
+ ("yolov8n.pt", "pytorch_pretrained"),
265
  ]:
266
  if os.path.exists(path):
267
+ detector = YOLO(path)
268
+ detector_fmt = fmt
269
+ print(f"[startup] Detector: {os.path.basename(path)} [{fmt}]")
270
+ break
271
  except Exception as e:
272
  print(f"[startup] Detector load failed: {e}")
273
 
274
+ # ── face analyzer (buffalo_l auto-downloads from InsightFace CDN) ─────────
275
  try:
276
  from insightface.app import FaceAnalysis
 
277
  face_app = FaceAnalysis(name="buffalo_l",
278
  providers=["CPUExecutionProvider"])
279
  face_app.prepare(ctx_id=-1, det_size=(640, 640))
280
+ print("[startup] Face analyzer: SCRFD-10GF + ArcFace w600k_r50 (CPU)")
281
  except Exception as e:
282
  print(f"[startup] Face analyzer load failed: {e}")
283
 
284
+ # ── enhancement models (optional β€” requires torch) ────────────────────────
285
+ zdce_path = f"{MODELS}/zero_dce_pp.pth"
286
+ if os.path.exists(zdce_path):
287
+ enhance_zero = _load_zero_dce(zdce_path)
288
+
289
+ ffa_path = f"{MODELS}/ffa_net_outdoor.pk"
290
+ if os.path.exists(ffa_path):
291
+ enhance_ffa = _load_ffa(ffa_path)
292
+
293
+ if enhance_zero is None and enhance_ffa is None:
294
+ print("[startup] No enhancement models loaded β€” CLAHE used for all conditions")
295
+
296
+
297
+ # ── endpoints ─────────────────────────────────────────────────────────────────
298
 
299
  @app.post("/pipeline")
300
  async def pipeline(body: dict,
 
312
  h, w = img.shape[:2]
313
 
314
  t0 = time.time()
315
+ enhanced, enh_route = _enhance(img, condition)
316
  enh_ms = (time.time() - t0) * 1000
317
 
318
  t0 = time.time()
 
345
  "detections": detections,
346
  "recognitions": recognitions,
347
  "enhanced_image_url": _to_data_uri(enhanced),
348
+ "enhancement_route": enh_route,
349
  "condition": condition,
350
  "latency_ms": {
351
  "enhancement": round(enh_ms, 1),
 
392
  @app.get("/health")
393
  async def health():
394
  return {
395
+ "status": "ok",
396
+ "detector": detector is not None,
397
+ "detector_format": detector_fmt,
398
+ "face_app": face_app is not None,
399
+ "enhance_zero_dce": enhance_zero is not None,
400
+ "enhance_ffa_net": enhance_ffa is not None,
401
+ "gallery_size": len(_gallery),
402
  }
requirements.txt CHANGED
@@ -1,6 +1,6 @@
1
- # HuggingFace Spaces inference API β€” CPU-only, no Colab/GPU deps
2
- # torch is excluded: ZeroDCE++ enhancement falls back to CLAHE automatically.
3
- # InsightFace and ONNX YOLO both run on onnxruntime (CPU).
4
 
5
  fastapi>=0.111.0
6
  uvicorn[standard]>=0.30.0
@@ -13,3 +13,4 @@ insightface>=0.7.3
13
  onnxruntime>=1.18.0
14
  faiss-cpu>=1.8.0
15
  huggingface_hub>=0.23.0
 
 
1
+ # HuggingFace Spaces inference API β€” CPU inference
2
+ # torch is included for Zero-DCE++ (low-light) and FFA-Net (dehazing)
3
+ # Both enhancement models fall back to CLAHE if torch is unavailable
4
 
5
  fastapi>=0.111.0
6
  uvicorn[standard]>=0.30.0
 
13
  onnxruntime>=1.18.0
14
  faiss-cpu>=1.8.0
15
  huggingface_hub>=0.23.0
16
+ torch>=2.1.0