Spaces:
Running
Running
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>
- app.py +203 -51
- 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
|
| 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 |
-
|
| 32 |
-
|
| 33 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 34 |
_gallery: dict[str, dict] = {} # embedding_id β {name, embedding}
|
| 35 |
|
| 36 |
INTERNAL_TOKEN = os.environ.get("INTERNAL_TOKEN", "dev-only-internal-token")
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 44 |
}
|
| 45 |
|
| 46 |
-
|
|
|
|
| 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 |
-
|
| 89 |
-
HF_TOKEN = os.environ.get("HF_TOKEN", "") # read from HF Space secrets
|
| 90 |
|
| 91 |
-
def
|
| 92 |
-
"""
|
| 93 |
if not HF_REPO:
|
| 94 |
-
print("[startup] HF_MODEL_REPO not set β
|
| 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 |
-
|
| 123 |
|
|
|
|
| 124 |
try:
|
| 125 |
from ultralytics import YOLO
|
| 126 |
for path, fmt in [
|
| 127 |
-
(f"{MODELS}/yolov8n_best.onnx",
|
| 128 |
-
(f"{MODELS}/
|
| 129 |
-
("
|
|
|
|
|
|
|
|
|
|
| 130 |
]:
|
| 131 |
if os.path.exists(path):
|
| 132 |
-
detector = YOLO(path)
|
| 133 |
-
|
|
|
|
|
|
|
| 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 |
-
# ββ
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 =
|
| 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":
|
| 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":
|
| 246 |
-
"detector":
|
| 247 |
-
"detector_format":
|
| 248 |
-
"face_app":
|
| 249 |
-
"
|
|
|
|
|
|
|
| 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
|
| 2 |
-
# torch is
|
| 3 |
-
#
|
| 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
|