788 lines
29 KiB
Python
788 lines
29 KiB
Python
|
|
"""算法离线测试:异步任务 + 进度 + 渲染输出(临时文件存 storage/temp)"""
|
|||
|
|
import os
|
|||
|
|
import re
|
|||
|
|
import time
|
|||
|
|
import uuid
|
|||
|
|
import shutil
|
|||
|
|
import threading
|
|||
|
|
import logging
|
|||
|
|
import subprocess
|
|||
|
|
|
|||
|
|
logger = logging.getLogger("services.algorithm_test")
|
|||
|
|
|
|||
|
|
try:
|
|||
|
|
import cv2
|
|||
|
|
_CV2 = True
|
|||
|
|
except Exception:
|
|||
|
|
cv2 = None
|
|||
|
|
_CV2 = False
|
|||
|
|
|
|||
|
|
try:
|
|||
|
|
import numpy as np
|
|||
|
|
_NP = True
|
|||
|
|
except Exception:
|
|||
|
|
np = None
|
|||
|
|
_NP = False
|
|||
|
|
|
|||
|
|
_TASKS = {}
|
|||
|
|
_TASK_LOCK = threading.Lock()
|
|||
|
|
_MAX_TASKS = 200
|
|||
|
|
_MAX_FILE_BYTES = 100 * 1024 * 1024
|
|||
|
|
_MAX_VIDEO_FRAMES = 600
|
|||
|
|
_MAX_VIDEO_SECONDS = 60.0
|
|||
|
|
|
|||
|
|
_IMAGE_EXT = {".jpg", ".jpeg", ".png", ".bmp", ".webp", ".tif", ".tiff"}
|
|||
|
|
_VIDEO_EXT = {".mp4", ".avi", ".mov", ".mkv", ".webm", ".m4v"}
|
|||
|
|
|
|||
|
|
|
|||
|
|
def temp_root():
|
|||
|
|
"""算法测试临时根目录:{storageTempDir}/algorithm_test"""
|
|||
|
|
try:
|
|||
|
|
from app.utils.GlobalUtils import g_config
|
|||
|
|
root = os.path.join(g_config.storageTempDir, "algorithm_test")
|
|||
|
|
except Exception:
|
|||
|
|
from django.conf import settings
|
|||
|
|
root = os.path.join(str(settings.BASE_DIR), "static", "storage", "temp", "algorithm_test")
|
|||
|
|
try:
|
|||
|
|
os.makedirs(root, exist_ok=True)
|
|||
|
|
except Exception:
|
|||
|
|
pass
|
|||
|
|
return root
|
|||
|
|
|
|||
|
|
|
|||
|
|
def upload_dir():
|
|||
|
|
d = os.path.join(temp_root(), "_uploads")
|
|||
|
|
os.makedirs(d, exist_ok=True)
|
|||
|
|
return d
|
|||
|
|
|
|||
|
|
|
|||
|
|
def task_dir(task_id):
|
|||
|
|
if not re.fullmatch(r"[a-f0-9]{32}", task_id or ""):
|
|||
|
|
raise ValueError("invalid task_id")
|
|||
|
|
d = os.path.join(temp_root(), task_id)
|
|||
|
|
os.makedirs(d, exist_ok=True)
|
|||
|
|
return d
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _cleanup_tasks():
|
|||
|
|
now = time.time()
|
|||
|
|
with _TASK_LOCK:
|
|||
|
|
stale = [k for k, v in _TASKS.items() if now - v.get("created_at", now) > 3600]
|
|||
|
|
for k in stale:
|
|||
|
|
_TASKS.pop(k, None)
|
|||
|
|
if len(_TASKS) > _MAX_TASKS:
|
|||
|
|
keys = sorted(_TASKS.keys(), key=lambda k: _TASKS[k].get("created_at", 0))
|
|||
|
|
for k in keys[: len(_TASKS) - _MAX_TASKS]:
|
|||
|
|
_TASKS.pop(k, None)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _task_update(task_id, **kwargs):
|
|||
|
|
with _TASK_LOCK:
|
|||
|
|
t = _TASKS.get(task_id)
|
|||
|
|
if t:
|
|||
|
|
t.update(kwargs)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def get_task(task_id):
|
|||
|
|
with _TASK_LOCK:
|
|||
|
|
t = _TASKS.get(task_id)
|
|||
|
|
return dict(t) if t else None
|
|||
|
|
|
|||
|
|
|
|||
|
|
def output_url_for_task(task_id):
|
|||
|
|
return "/smallmodel/openTestOutput?task_id=%s" % task_id
|
|||
|
|
|
|||
|
|
|
|||
|
|
def resolve_output_file(task_id):
|
|||
|
|
if not re.fullmatch(r"[a-f0-9]{32}", task_id or ""):
|
|||
|
|
return None, None
|
|||
|
|
base = os.path.join(temp_root(), task_id)
|
|||
|
|
for name, ctype in (("output.mp4", "video/mp4"), ("output.jpg", "image/jpeg"), ("output.png", "image/png")):
|
|||
|
|
fp = os.path.join(base, name)
|
|||
|
|
if os.path.isfile(fp) and os.path.getsize(fp) > 0:
|
|||
|
|
return fp, ctype
|
|||
|
|
return None, None
|
|||
|
|
|
|||
|
|
|
|||
|
|
def clear_temp_files():
|
|||
|
|
"""清理算法测试产生的全部临时文件,并清空内存任务表。"""
|
|||
|
|
roots = [temp_root()]
|
|||
|
|
try:
|
|||
|
|
from django.conf import settings
|
|||
|
|
legacy = os.path.join(str(settings.BASE_DIR), "static", "storage", "test")
|
|||
|
|
if os.path.isdir(legacy):
|
|||
|
|
roots.append(legacy)
|
|||
|
|
except Exception:
|
|||
|
|
pass
|
|||
|
|
removed = 0
|
|||
|
|
bytes_freed = 0
|
|||
|
|
for root in roots:
|
|||
|
|
if not os.path.isdir(root):
|
|||
|
|
continue
|
|||
|
|
for name in os.listdir(root):
|
|||
|
|
fp = os.path.join(root, name)
|
|||
|
|
try:
|
|||
|
|
if os.path.isfile(fp):
|
|||
|
|
bytes_freed += os.path.getsize(fp)
|
|||
|
|
os.remove(fp)
|
|||
|
|
removed += 1
|
|||
|
|
elif os.path.isdir(fp):
|
|||
|
|
for dirpath, _, filenames in os.walk(fp):
|
|||
|
|
for fn in filenames:
|
|||
|
|
try:
|
|||
|
|
fpath = os.path.join(dirpath, fn)
|
|||
|
|
bytes_freed += os.path.getsize(fpath)
|
|||
|
|
removed += 1
|
|||
|
|
except Exception:
|
|||
|
|
pass
|
|||
|
|
shutil.rmtree(fp, ignore_errors=True)
|
|||
|
|
except Exception as e:
|
|||
|
|
logger.warning("clear temp remove %s err: %s", fp, e)
|
|||
|
|
with _TASK_LOCK:
|
|||
|
|
_TASKS.clear()
|
|||
|
|
return {"files_removed": removed, "bytes_freed": bytes_freed}
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _ffmpeg_path():
|
|||
|
|
try:
|
|||
|
|
from app.utils.GlobalUtils import g_config
|
|||
|
|
return getattr(g_config, "ffmpeg", None) or "ffmpeg"
|
|||
|
|
except Exception:
|
|||
|
|
return "ffmpeg"
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _encode_video_h264(raw_path, final_path):
|
|||
|
|
"""将 OpenCV 输出的视频转码为浏览器可播的 H.264 MP4。"""
|
|||
|
|
if not os.path.isfile(raw_path) or os.path.getsize(raw_path) <= 0:
|
|||
|
|
return False
|
|||
|
|
tmp = final_path + ".part.mp4"
|
|||
|
|
cmd = [
|
|||
|
|
_ffmpeg_path(), "-y", "-loglevel", "error",
|
|||
|
|
"-i", raw_path,
|
|||
|
|
"-c:v", "libx264", "-preset", "fast", "-crf", "23",
|
|||
|
|
"-pix_fmt", "yuv420p", "-movflags", "+faststart",
|
|||
|
|
tmp,
|
|||
|
|
]
|
|||
|
|
try:
|
|||
|
|
r = subprocess.run(cmd, capture_output=True, timeout=600)
|
|||
|
|
if r.returncode == 0 and os.path.isfile(tmp) and os.path.getsize(tmp) > 0:
|
|||
|
|
if os.path.isfile(final_path):
|
|||
|
|
os.remove(final_path)
|
|||
|
|
os.replace(tmp, final_path)
|
|||
|
|
if os.path.abspath(raw_path) != os.path.abspath(final_path) and os.path.isfile(raw_path):
|
|||
|
|
os.remove(raw_path)
|
|||
|
|
return True
|
|||
|
|
if r.stderr:
|
|||
|
|
logger.warning("ffmpeg encode stderr: %s", r.stderr.decode("utf-8", errors="ignore")[:500])
|
|||
|
|
except Exception as e:
|
|||
|
|
logger.warning("ffmpeg encode failed: %s", e)
|
|||
|
|
if os.path.isfile(tmp):
|
|||
|
|
try:
|
|||
|
|
os.remove(tmp)
|
|||
|
|
except Exception:
|
|||
|
|
pass
|
|||
|
|
if os.path.isfile(raw_path) and not os.path.isfile(final_path):
|
|||
|
|
try:
|
|||
|
|
shutil.copy2(raw_path, final_path)
|
|||
|
|
return os.path.getsize(final_path) > 0
|
|||
|
|
except Exception:
|
|||
|
|
pass
|
|||
|
|
return os.path.isfile(final_path) and os.path.getsize(final_path) > 0
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _open_video_writer(out_path, fps, w, h):
|
|||
|
|
for codec in ("avc1", "mp4v", "XVID"):
|
|||
|
|
fourcc = cv2.VideoWriter_fourcc(*codec)
|
|||
|
|
writer = cv2.VideoWriter(out_path, fourcc, fps, (w, h))
|
|||
|
|
if writer.isOpened():
|
|||
|
|
return writer
|
|||
|
|
writer.release()
|
|||
|
|
return None
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _color_for_label(label, idx=0):
|
|||
|
|
palette = [
|
|||
|
|
(22, 159, 133), (59, 130, 246), (234, 88, 12), (168, 85, 247),
|
|||
|
|
(220, 38, 38), (14, 165, 233), (132, 204, 22), (236, 72, 153),
|
|||
|
|
]
|
|||
|
|
if label:
|
|||
|
|
h = sum(ord(c) for c in str(label)) % len(palette)
|
|||
|
|
return palette[h]
|
|||
|
|
return palette[idx % len(palette)]
|
|||
|
|
|
|||
|
|
|
|||
|
|
def draw_detections(frame_bgr, detections, task_type="detect"):
|
|||
|
|
if not _CV2 or frame_bgr is None:
|
|||
|
|
return frame_bgr
|
|||
|
|
img = frame_bgr.copy()
|
|||
|
|
h, w = img.shape[:2]
|
|||
|
|
for i, det in enumerate(detections or []):
|
|||
|
|
color = _color_for_label(det.get("label"), i)
|
|||
|
|
task = (det.get("task") or task_type or "detect").lower()
|
|||
|
|
if task == "classify":
|
|||
|
|
continue
|
|||
|
|
poly = det.get("mask_polygon")
|
|||
|
|
if poly and len(poly) >= 3:
|
|||
|
|
pts = np.array([(int(p[0]), int(p[1])) for p in poly], dtype=np.int32)
|
|||
|
|
overlay = img.copy()
|
|||
|
|
cv2.fillPoly(overlay, [pts], color)
|
|||
|
|
cv2.addWeighted(overlay, 0.35, img, 0.65, 0, img)
|
|||
|
|
cv2.polylines(img, [pts], True, color, 2)
|
|||
|
|
box = det.get("box") or [0, 0, 0, 0]
|
|||
|
|
x1, y1, x2, y2 = [int(v) for v in box[:4]]
|
|||
|
|
if x2 > x1 and y2 > y1:
|
|||
|
|
cv2.rectangle(img, (x1, y1), (x2, y2), color, 2)
|
|||
|
|
label = str(det.get("label") or "")
|
|||
|
|
score = det.get("score")
|
|||
|
|
txt = label + (" %.2f" % score if isinstance(score, (int, float)) else "")
|
|||
|
|
if txt.strip():
|
|||
|
|
(tw, th), _ = cv2.getTextSize(txt, cv2.FONT_HERSHEY_SIMPLEX, 0.55, 1)
|
|||
|
|
ty = max(0, y1 - 6)
|
|||
|
|
cv2.rectangle(img, (x1, max(0, ty - th - 4)), (x1 + tw + 8, ty + 2), color, -1)
|
|||
|
|
cv2.putText(img, txt, (x1 + 4, ty - 2), cv2.FONT_HERSHEY_SIMPLEX, 0.55, (255, 255, 255), 1, cv2.LINE_AA)
|
|||
|
|
kpts = det.get("keypoints")
|
|||
|
|
if kpts:
|
|||
|
|
for kp in kpts:
|
|||
|
|
if len(kp) >= 3 and kp[2] > 0.3:
|
|||
|
|
cv2.circle(img, (int(kp[0]), int(kp[1])), 3, color, -1)
|
|||
|
|
if task_type == "classify" and detections:
|
|||
|
|
y = 28
|
|||
|
|
for i, det in enumerate(detections[:8]):
|
|||
|
|
txt = "%s %.2f" % (det.get("label", ""), float(det.get("score") or 0))
|
|||
|
|
cv2.putText(img, txt, (12, y), cv2.FONT_HERSHEY_SIMPLEX, 0.65, (255, 255, 255), 2, cv2.LINE_AA)
|
|||
|
|
cv2.putText(img, txt, (12, y), cv2.FONT_HERSHEY_SIMPLEX, 0.65, _color_for_label(det.get("label"), i), 1, cv2.LINE_AA)
|
|||
|
|
y += 26
|
|||
|
|
cv2.rectangle(img, (w - 130, 6), (w - 6, 28), (22, 159, 133), -1)
|
|||
|
|
cv2.putText(img, "ALGO TEST", (w - 122, 22), cv2.FONT_HERSHEY_SIMPLEX, 0.45, (255, 255, 255), 1, cv2.LINE_AA)
|
|||
|
|
return img
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _summarize_detections(all_dets):
|
|||
|
|
summary = {}
|
|||
|
|
for det in all_dets:
|
|||
|
|
lb = str(det.get("label") or "unknown")
|
|||
|
|
sc = float(det.get("score") or 0)
|
|||
|
|
if lb not in summary:
|
|||
|
|
summary[lb] = {"label": lb, "count": 0, "max_score": sc, "score_sum": 0.0}
|
|||
|
|
summary[lb]["count"] += 1
|
|||
|
|
summary[lb]["max_score"] = max(summary[lb]["max_score"], sc)
|
|||
|
|
summary[lb]["score_sum"] += sc
|
|||
|
|
out = []
|
|||
|
|
for lb, s in sorted(summary.items(), key=lambda x: -x[1]["count"]):
|
|||
|
|
out.append({
|
|||
|
|
"label": lb,
|
|||
|
|
"count": s["count"],
|
|||
|
|
"max_score": round(s["max_score"], 4),
|
|||
|
|
"avg_score": round(s["score_sum"] / max(1, s["count"]), 4),
|
|||
|
|
})
|
|||
|
|
return out
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _resolve_model_abs(model_file):
|
|||
|
|
"""与推理池一致:仅 uploadDir/weight。"""
|
|||
|
|
if not model_file:
|
|||
|
|
return ""
|
|||
|
|
try:
|
|||
|
|
from app.analysis.worker_pool import resolve_model_path
|
|||
|
|
p = resolve_model_path(model_file)
|
|||
|
|
if p and os.path.isfile(p):
|
|||
|
|
return p
|
|||
|
|
except Exception as e:
|
|||
|
|
logger.warning("_resolve_model_abs: %s", e)
|
|||
|
|
return ""
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _build_engine(algo, abs_model):
|
|||
|
|
import json
|
|||
|
|
from app.analysis.engines.factory import EngineFactory
|
|||
|
|
labels = algo.labels or "[]"
|
|||
|
|
try:
|
|||
|
|
labels_list = json.loads(labels) if isinstance(labels, str) else (labels or [])
|
|||
|
|
except Exception:
|
|||
|
|
labels_list = []
|
|||
|
|
return EngineFactory.create(
|
|||
|
|
algo.inference_engine,
|
|||
|
|
model_file=abs_model,
|
|||
|
|
labels=labels_list,
|
|||
|
|
input_size=(algo.input_width or 640, algo.input_height or 640),
|
|||
|
|
conf_threshold=float(algo.conf_threshold or 0.4),
|
|||
|
|
iou_threshold=float(algo.iou_threshold or 0.5),
|
|||
|
|
algorithm_type=algo.algorithm_type or "yolo8",
|
|||
|
|
task_type=algo.task_type or "detect",
|
|||
|
|
device=algo.device or "cpu",
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
|
|||
|
|
_PERSON_LABELS = frozenset({"person", "Person", "行人", "0"})
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _filter_person_detections(dets):
|
|||
|
|
out = []
|
|||
|
|
for d in dets or []:
|
|||
|
|
lb = str(d.get("label") or "")
|
|||
|
|
if lb in _PERSON_LABELS or lb.lower() == "person":
|
|||
|
|
out.append(d)
|
|||
|
|
return out
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _iou_box(a, b):
|
|||
|
|
ax1, ay1, ax2, ay2 = a
|
|||
|
|
bx1, by1, bx2, by2 = b
|
|||
|
|
ix1, iy1 = max(ax1, bx1), max(ay1, by1)
|
|||
|
|
ix2, iy2 = min(ax2, bx2), min(ay2, by2)
|
|||
|
|
iw, ih = max(0, ix2 - ix1), max(0, iy2 - iy1)
|
|||
|
|
inter = iw * ih
|
|||
|
|
if inter <= 0:
|
|||
|
|
return 0.0
|
|||
|
|
area_a = max(0, ax2 - ax1) * max(0, ay2 - ay1)
|
|||
|
|
area_b = max(0, bx2 - bx1) * max(0, by2 - by1)
|
|||
|
|
union = area_a + area_b - inter
|
|||
|
|
return float(inter / union) if union > 0 else 0.0
|
|||
|
|
|
|||
|
|
|
|||
|
|
class _ReidTrack(object):
|
|||
|
|
__slots__ = ("track_id", "box", "label", "score", "embedding", "embedding_hist", "missed", "hits", "_hist_max")
|
|||
|
|
|
|||
|
|
def __init__(self, track_id, box, label, score, embedding, hist_max=5):
|
|||
|
|
self.track_id = track_id
|
|||
|
|
self.box = box
|
|||
|
|
self.label = label
|
|||
|
|
self.score = score
|
|||
|
|
self.embedding = embedding
|
|||
|
|
self.embedding_hist = [embedding.copy()]
|
|||
|
|
self.missed = 0
|
|||
|
|
self.hits = 1
|
|||
|
|
self._hist_max = hist_max
|
|||
|
|
|
|||
|
|
def update(self, box, score, emb):
|
|||
|
|
self.box = box
|
|||
|
|
self.score = score
|
|||
|
|
self.embedding = emb
|
|||
|
|
self.embedding_hist.append(emb.copy())
|
|||
|
|
if len(self.embedding_hist) > self._hist_max:
|
|||
|
|
self.embedding_hist.pop(0)
|
|||
|
|
self.missed = 0
|
|||
|
|
self.hits += 1
|
|||
|
|
|
|||
|
|
def mean_embedding(self):
|
|||
|
|
if not self.embedding_hist:
|
|||
|
|
return self.embedding
|
|||
|
|
return np.mean(np.stack(self.embedding_hist, axis=0), axis=0)
|
|||
|
|
|
|||
|
|
|
|||
|
|
class _SimpleReIDTracker(object):
|
|||
|
|
def __init__(self, iou_thr=0.3, emb_thr=0.5, max_missed=8):
|
|||
|
|
self.iou_thr = iou_thr
|
|||
|
|
self.emb_thr = emb_thr
|
|||
|
|
self.max_missed = max_missed
|
|||
|
|
self.tracks = {}
|
|||
|
|
self._next_id = 1
|
|||
|
|
|
|||
|
|
def update(self, detections, embeddings):
|
|||
|
|
assigned = set()
|
|||
|
|
active_ids = []
|
|||
|
|
for det, emb in zip(detections, embeddings):
|
|||
|
|
best_id, best_score = None, self.emb_thr
|
|||
|
|
for tid, tr in self.tracks.items():
|
|||
|
|
if tr.label != det.get("label"):
|
|||
|
|
continue
|
|||
|
|
iou = _iou_box(tr.box, det.get("box") or [0, 0, 0, 0])
|
|||
|
|
if iou < self.iou_thr:
|
|||
|
|
continue
|
|||
|
|
sim = float(np.dot(tr.mean_embedding(), emb))
|
|||
|
|
if sim > best_score:
|
|||
|
|
best_score = sim
|
|||
|
|
best_id = tid
|
|||
|
|
if best_id is not None:
|
|||
|
|
self.tracks[best_id].update(det.get("box"), det.get("score"), emb)
|
|||
|
|
assigned.add(best_id)
|
|||
|
|
active_ids.append(best_id)
|
|||
|
|
else:
|
|||
|
|
tid = self._next_id
|
|||
|
|
self._next_id += 1
|
|||
|
|
tr = _ReidTrack(tid, det.get("box"), det.get("label"), det.get("score"), emb)
|
|||
|
|
self.tracks[tid] = tr
|
|||
|
|
assigned.add(tid)
|
|||
|
|
active_ids.append(tid)
|
|||
|
|
for tid, tr in list(self.tracks.items()):
|
|||
|
|
if tid in assigned:
|
|||
|
|
continue
|
|||
|
|
tr.missed += 1
|
|||
|
|
if tr.missed >= self.max_missed:
|
|||
|
|
del self.tracks[tid]
|
|||
|
|
return active_ids
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _draw_reid_frame(frame_bgr, tracker, active_ids):
|
|||
|
|
img = frame_bgr.copy()
|
|||
|
|
for tid in active_ids:
|
|||
|
|
tr = tracker.tracks.get(tid)
|
|||
|
|
if not tr:
|
|||
|
|
continue
|
|||
|
|
x1, y1, x2, y2 = [int(v) for v in tr.box]
|
|||
|
|
color = (0, 200, 80) if tr.hits >= 3 else (0, 180, 255)
|
|||
|
|
cv2.rectangle(img, (x1, y1), (x2, y2), color, 2)
|
|||
|
|
txt = "id=%d %s %.2f" % (tid, tr.label, float(tr.score or 0))
|
|||
|
|
cv2.putText(img, txt, (x1, max(20, y1 - 8)), cv2.FONT_HERSHEY_SIMPLEX, 0.55, color, 2, cv2.LINE_AA)
|
|||
|
|
h, w = img.shape[:2]
|
|||
|
|
cv2.rectangle(img, (w - 130, 6), (w - 6, 28), (22, 159, 133), -1)
|
|||
|
|
cv2.putText(img, "REID TEST", (w - 122, 22), cv2.FONT_HERSHEY_SIMPLEX, 0.45, (255, 255, 255), 1, cv2.LINE_AA)
|
|||
|
|
return img
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _process_reid_frame(detector_engine, reid_engine, tracker, frame_bgr, sim_samples):
|
|||
|
|
dets = detector_engine.detect(frame_bgr)
|
|||
|
|
persons = _filter_person_detections(dets)
|
|||
|
|
boxes = [p.get("box") for p in persons if p.get("box")]
|
|||
|
|
valid_idx, embeddings = reid_engine.extract_embeddings(frame_bgr, boxes)
|
|||
|
|
matched_dets = [persons[i] for i in valid_idx]
|
|||
|
|
emb_rows = [embeddings[j] for j in range(len(valid_idx))]
|
|||
|
|
active_ids = tracker.update(matched_dets, emb_rows) if matched_dets else []
|
|||
|
|
for tid in active_ids:
|
|||
|
|
tr = tracker.tracks.get(tid)
|
|||
|
|
if tr and tr.hits >= 2:
|
|||
|
|
sim_samples.append(float(np.dot(tr.embedding, tr.mean_embedding())))
|
|||
|
|
vis = _draw_reid_frame(frame_bgr, tracker, active_ids)
|
|||
|
|
return vis, len(persons), len(valid_idx), active_ids
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _run_reid_test(task_id, algo, detector_algo, input_path, media_type, out_dir, t0):
|
|||
|
|
abs_reid = _resolve_model_abs(algo.model_file)
|
|||
|
|
abs_det = _resolve_model_abs(detector_algo.model_file)
|
|||
|
|
if not abs_reid or not abs_det:
|
|||
|
|
raise RuntimeError("model file not found")
|
|||
|
|
reid_engine = _build_engine(algo, abs_reid)
|
|||
|
|
det_engine = _build_engine(detector_algo, abs_det)
|
|||
|
|
if not reid_engine.load() or not det_engine.load():
|
|||
|
|
raise RuntimeError("engine load failed")
|
|||
|
|
tracker = _SimpleReIDTracker()
|
|||
|
|
sim_samples = []
|
|||
|
|
infer_ms = 0.0
|
|||
|
|
emb_total = 0
|
|||
|
|
person_total = 0
|
|||
|
|
track_peak = 0
|
|||
|
|
all_dets = []
|
|||
|
|
|
|||
|
|
if media_type == "image":
|
|||
|
|
_task_update(task_id, progress=15, message="processing image (detect+reid)")
|
|||
|
|
frame = cv2.imread(input_path)
|
|||
|
|
if frame is None:
|
|||
|
|
raise RuntimeError("cannot read image")
|
|||
|
|
h, w = frame.shape[:2]
|
|||
|
|
t_inf = time.time()
|
|||
|
|
vis, pc, ec, active_ids = _process_reid_frame(det_engine, reid_engine, tracker, frame, sim_samples)
|
|||
|
|
infer_ms = (time.time() - t_inf) * 1000
|
|||
|
|
person_total += pc
|
|||
|
|
emb_total += ec
|
|||
|
|
track_peak = max(track_peak, len(active_ids))
|
|||
|
|
for tid in active_ids:
|
|||
|
|
tr = tracker.tracks.get(tid)
|
|||
|
|
if tr:
|
|||
|
|
all_dets.append({
|
|||
|
|
"label": tr.label,
|
|||
|
|
"score": tr.score,
|
|||
|
|
"box": tr.box,
|
|||
|
|
"track_id": tr.track_id,
|
|||
|
|
"task": "reid",
|
|||
|
|
})
|
|||
|
|
out_path = os.path.join(out_dir, "output.jpg")
|
|||
|
|
if not cv2.imwrite(out_path, vis, [int(cv2.IMWRITE_JPEG_QUALITY), 92]):
|
|||
|
|
raise RuntimeError("failed to write output image")
|
|||
|
|
report = {
|
|||
|
|
"media_type": "image",
|
|||
|
|
"input_size": [w, h],
|
|||
|
|
"frame_count": 1,
|
|||
|
|
"processed_frames": 1,
|
|||
|
|
"inference_ms_total": round(infer_ms, 2),
|
|||
|
|
"inference_ms_avg": round(infer_ms, 2),
|
|||
|
|
"detection_count": person_total,
|
|||
|
|
"embedding_count": emb_total,
|
|||
|
|
"track_count": track_peak,
|
|||
|
|
"reid_sim_mean": round(float(np.mean(sim_samples)), 4) if sim_samples else None,
|
|||
|
|
"detections_summary": _summarize_detections(all_dets),
|
|||
|
|
"detections": all_dets[:100],
|
|||
|
|
"engine": algo.inference_engine,
|
|||
|
|
"device": algo.device,
|
|||
|
|
"task_type": "reid",
|
|||
|
|
"detector_id": detector_algo.id,
|
|||
|
|
"detector_name": detector_algo.name,
|
|||
|
|
"elapsed_ms": round((time.time() - t0) * 1000, 2),
|
|||
|
|
}
|
|||
|
|
_task_update(
|
|||
|
|
task_id, status="done", progress=100, message="done",
|
|||
|
|
report=report, output_url=output_url_for_task(task_id), output_type="image",
|
|||
|
|
)
|
|||
|
|
return
|
|||
|
|
|
|||
|
|
cap = cv2.VideoCapture(input_path)
|
|||
|
|
if not cap.isOpened():
|
|||
|
|
raise RuntimeError("cannot open video")
|
|||
|
|
fps = float(cap.get(cv2.CAP_PROP_FPS) or 25.0)
|
|||
|
|
if fps <= 0 or fps > 120:
|
|||
|
|
fps = 25.0
|
|||
|
|
total = int(cap.get(cv2.CAP_PROP_FRAME_COUNT) or 0)
|
|||
|
|
w = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH) or 0)
|
|||
|
|
h = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT) or 0)
|
|||
|
|
if w <= 0 or h <= 0:
|
|||
|
|
raise RuntimeError("invalid video dimensions")
|
|||
|
|
if total <= 0:
|
|||
|
|
total = int(fps * 10)
|
|||
|
|
max_frames = min(total, _MAX_VIDEO_FRAMES, max(1, int(fps * _MAX_VIDEO_SECONDS)))
|
|||
|
|
raw_path = os.path.join(out_dir, "output_raw.mp4")
|
|||
|
|
final_path = os.path.join(out_dir, "output.mp4")
|
|||
|
|
writer = _open_video_writer(raw_path, fps, w, h)
|
|||
|
|
if writer is None:
|
|||
|
|
raise RuntimeError("cannot create video writer")
|
|||
|
|
idx = 0
|
|||
|
|
processed = 0
|
|||
|
|
while idx < max_frames:
|
|||
|
|
ret, frame = cap.read()
|
|||
|
|
if not ret or frame is None:
|
|||
|
|
break
|
|||
|
|
t_inf = time.time()
|
|||
|
|
vis, pc, ec, active_ids = _process_reid_frame(det_engine, reid_engine, tracker, frame, sim_samples)
|
|||
|
|
infer_ms += (time.time() - t_inf) * 1000
|
|||
|
|
person_total += pc
|
|||
|
|
emb_total += ec
|
|||
|
|
track_peak = max(track_peak, len(active_ids))
|
|||
|
|
for tid in active_ids:
|
|||
|
|
tr = tracker.tracks.get(tid)
|
|||
|
|
if tr:
|
|||
|
|
all_dets.append({
|
|||
|
|
"label": tr.label,
|
|||
|
|
"score": tr.score,
|
|||
|
|
"box": tr.box,
|
|||
|
|
"track_id": tr.track_id,
|
|||
|
|
"task": "reid",
|
|||
|
|
})
|
|||
|
|
writer.write(vis)
|
|||
|
|
processed += 1
|
|||
|
|
idx += 1
|
|||
|
|
pct = 15 + int(70 * idx / max(1, max_frames))
|
|||
|
|
_task_update(task_id, progress=min(85, pct), message="reid frame %d / %d" % (idx, max_frames))
|
|||
|
|
cap.release()
|
|||
|
|
writer.release()
|
|||
|
|
if processed <= 0:
|
|||
|
|
raise RuntimeError("no video frames processed")
|
|||
|
|
if not os.path.isfile(raw_path) or os.path.getsize(raw_path) <= 0:
|
|||
|
|
raise RuntimeError("raw video missing")
|
|||
|
|
_task_update(task_id, progress=88, message="encoding video (H.264)")
|
|||
|
|
if not _encode_video_h264(raw_path, final_path):
|
|||
|
|
raise RuntimeError("video encode failed")
|
|||
|
|
report = {
|
|||
|
|
"media_type": "video",
|
|||
|
|
"input_size": [w, h],
|
|||
|
|
"frame_count": total,
|
|||
|
|
"processed_frames": processed,
|
|||
|
|
"fps": round(fps, 2),
|
|||
|
|
"inference_ms_total": round(infer_ms, 2),
|
|||
|
|
"inference_ms_avg": round(infer_ms / max(1, processed), 2),
|
|||
|
|
"detection_count": person_total,
|
|||
|
|
"embedding_count": emb_total,
|
|||
|
|
"track_count": track_peak,
|
|||
|
|
"reid_sim_mean": round(float(np.mean(sim_samples)), 4) if sim_samples else None,
|
|||
|
|
"detections_summary": _summarize_detections(all_dets[:200]),
|
|||
|
|
"detections": all_dets[:50],
|
|||
|
|
"engine": algo.inference_engine,
|
|||
|
|
"device": algo.device,
|
|||
|
|
"task_type": "reid",
|
|||
|
|
"detector_id": detector_algo.id,
|
|||
|
|
"detector_name": detector_algo.name,
|
|||
|
|
"elapsed_ms": round((time.time() - t0) * 1000, 2),
|
|||
|
|
"output_video": True,
|
|||
|
|
}
|
|||
|
|
_task_update(
|
|||
|
|
task_id, status="done", progress=100, message="done",
|
|||
|
|
report=report, output_url=output_url_for_task(task_id), output_type="video",
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _run_test(task_id, algo, input_path, media_type, out_dir, detector_algo=None):
|
|||
|
|
t0 = time.time()
|
|||
|
|
try:
|
|||
|
|
_task_update(task_id, status="running", progress=5, message="loading model")
|
|||
|
|
task_type = (algo.task_type or "detect").lower()
|
|||
|
|
if task_type == "reid":
|
|||
|
|
if not detector_algo:
|
|||
|
|
raise RuntimeError("ReID test requires detector model")
|
|||
|
|
_run_reid_test(task_id, algo, detector_algo, input_path, media_type, out_dir, t0)
|
|||
|
|
return
|
|||
|
|
|
|||
|
|
abs_model = _resolve_model_abs(algo.model_file)
|
|||
|
|
if not abs_model:
|
|||
|
|
raise RuntimeError("model file not found: %s" % (algo.model_file or ""))
|
|||
|
|
engine = _build_engine(algo, abs_model)
|
|||
|
|
if not engine.load():
|
|||
|
|
raise RuntimeError("engine load failed")
|
|||
|
|
|
|||
|
|
task_type = (algo.task_type or "detect").lower()
|
|||
|
|
all_dets = []
|
|||
|
|
infer_ms = 0.0
|
|||
|
|
processed = 0
|
|||
|
|
|
|||
|
|
if media_type == "image":
|
|||
|
|
_task_update(task_id, progress=15, message="processing image")
|
|||
|
|
frame = cv2.imread(input_path)
|
|||
|
|
if frame is None:
|
|||
|
|
raise RuntimeError("cannot read image")
|
|||
|
|
h, w = frame.shape[:2]
|
|||
|
|
t_inf = time.time()
|
|||
|
|
dets = engine.detect(frame)
|
|||
|
|
infer_ms = (time.time() - t_inf) * 1000
|
|||
|
|
all_dets.extend([dict(d) for d in dets])
|
|||
|
|
out_img = draw_detections(frame, dets, task_type)
|
|||
|
|
out_path = os.path.join(out_dir, "output.jpg")
|
|||
|
|
if not cv2.imwrite(out_path, out_img, [int(cv2.IMWRITE_JPEG_QUALITY), 92]):
|
|||
|
|
raise RuntimeError("failed to write output image")
|
|||
|
|
if not os.path.isfile(out_path) or os.path.getsize(out_path) <= 0:
|
|||
|
|
raise RuntimeError("output image file missing")
|
|||
|
|
processed = 1
|
|||
|
|
report = {
|
|||
|
|
"media_type": "image",
|
|||
|
|
"input_size": [w, h],
|
|||
|
|
"frame_count": 1,
|
|||
|
|
"processed_frames": 1,
|
|||
|
|
"inference_ms_total": round(infer_ms, 2),
|
|||
|
|
"inference_ms_avg": round(infer_ms, 2),
|
|||
|
|
"detection_count": len(all_dets),
|
|||
|
|
"detections_summary": _summarize_detections(all_dets),
|
|||
|
|
"detections": all_dets[:100],
|
|||
|
|
"engine": algo.inference_engine,
|
|||
|
|
"device": algo.device,
|
|||
|
|
"task_type": task_type,
|
|||
|
|
"elapsed_ms": round((time.time() - t0) * 1000, 2),
|
|||
|
|
}
|
|||
|
|
_task_update(
|
|||
|
|
task_id, status="done", progress=100, message="done",
|
|||
|
|
report=report, output_url=output_url_for_task(task_id), output_type="image",
|
|||
|
|
)
|
|||
|
|
return
|
|||
|
|
|
|||
|
|
# video:逐帧推理渲染 → 合成视频 → ffmpeg 转 H.264
|
|||
|
|
cap = cv2.VideoCapture(input_path)
|
|||
|
|
if not cap.isOpened():
|
|||
|
|
raise RuntimeError("cannot open video")
|
|||
|
|
fps = float(cap.get(cv2.CAP_PROP_FPS) or 25.0)
|
|||
|
|
if fps <= 0 or fps > 120:
|
|||
|
|
fps = 25.0
|
|||
|
|
total = int(cap.get(cv2.CAP_PROP_FRAME_COUNT) or 0)
|
|||
|
|
w = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH) or 0)
|
|||
|
|
h = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT) or 0)
|
|||
|
|
if w <= 0 or h <= 0:
|
|||
|
|
raise RuntimeError("invalid video dimensions")
|
|||
|
|
if total <= 0:
|
|||
|
|
total = int(fps * 10)
|
|||
|
|
max_frames = min(total, _MAX_VIDEO_FRAMES, max(1, int(fps * _MAX_VIDEO_SECONDS)))
|
|||
|
|
|
|||
|
|
raw_path = os.path.join(out_dir, "output_raw.mp4")
|
|||
|
|
final_path = os.path.join(out_dir, "output.mp4")
|
|||
|
|
writer = _open_video_writer(raw_path, fps, w, h)
|
|||
|
|
if writer is None:
|
|||
|
|
raise RuntimeError("cannot create video writer")
|
|||
|
|
|
|||
|
|
idx = 0
|
|||
|
|
while idx < max_frames:
|
|||
|
|
ret, frame = cap.read()
|
|||
|
|
if not ret or frame is None:
|
|||
|
|
break
|
|||
|
|
t_inf = time.time()
|
|||
|
|
dets = engine.detect(frame)
|
|||
|
|
infer_ms += (time.time() - t_inf) * 1000
|
|||
|
|
all_dets.extend([dict(d) for d in dets])
|
|||
|
|
vis = draw_detections(frame, dets, task_type)
|
|||
|
|
writer.write(vis)
|
|||
|
|
processed += 1
|
|||
|
|
idx += 1
|
|||
|
|
pct = 15 + int(70 * idx / max(1, max_frames))
|
|||
|
|
_task_update(task_id, progress=min(85, pct), message="rendering frame %d / %d" % (idx, max_frames))
|
|||
|
|
|
|||
|
|
cap.release()
|
|||
|
|
writer.release()
|
|||
|
|
if processed <= 0:
|
|||
|
|
raise RuntimeError("no video frames processed")
|
|||
|
|
if not os.path.isfile(raw_path) or os.path.getsize(raw_path) <= 0:
|
|||
|
|
raise RuntimeError("raw video missing")
|
|||
|
|
|
|||
|
|
_task_update(task_id, progress=88, message="encoding video (H.264)")
|
|||
|
|
if not _encode_video_h264(raw_path, final_path):
|
|||
|
|
raise RuntimeError("video encode failed")
|
|||
|
|
|
|||
|
|
_task_update(task_id, progress=95, message="saving result")
|
|||
|
|
report = {
|
|||
|
|
"media_type": "video",
|
|||
|
|
"input_size": [w, h],
|
|||
|
|
"frame_count": total,
|
|||
|
|
"processed_frames": processed,
|
|||
|
|
"fps": round(fps, 2),
|
|||
|
|
"inference_ms_total": round(infer_ms, 2),
|
|||
|
|
"inference_ms_avg": round(infer_ms / max(1, processed), 2),
|
|||
|
|
"detection_count": len(all_dets),
|
|||
|
|
"detections_summary": _summarize_detections(all_dets),
|
|||
|
|
"detections": all_dets[:50],
|
|||
|
|
"engine": algo.inference_engine,
|
|||
|
|
"device": algo.device,
|
|||
|
|
"task_type": task_type,
|
|||
|
|
"elapsed_ms": round((time.time() - t0) * 1000, 2),
|
|||
|
|
"output_video": True,
|
|||
|
|
}
|
|||
|
|
_task_update(
|
|||
|
|
task_id, status="done", progress=100, message="done",
|
|||
|
|
report=report, output_url=output_url_for_task(task_id), output_type="video",
|
|||
|
|
)
|
|||
|
|
except Exception as e:
|
|||
|
|
logger.exception("algorithm test task %s failed", task_id)
|
|||
|
|
_task_update(task_id, status="error", progress=100, message=str(e), error=str(e))
|
|||
|
|
|
|||
|
|
|
|||
|
|
def start_test(algo, uploaded_path, original_name, detector_algo=None):
|
|||
|
|
if not _CV2 or not _NP:
|
|||
|
|
raise RuntimeError("opencv/numpy not available")
|
|||
|
|
task_type = (algo.task_type or "detect").lower()
|
|||
|
|
if task_type == "reid" and not detector_algo:
|
|||
|
|
raise RuntimeError("ReID test requires detector model")
|
|||
|
|
ext = os.path.splitext(original_name or uploaded_path)[1].lower()
|
|||
|
|
if ext in _IMAGE_EXT:
|
|||
|
|
media_type = "image"
|
|||
|
|
elif ext in _VIDEO_EXT:
|
|||
|
|
media_type = "video"
|
|||
|
|
else:
|
|||
|
|
raise RuntimeError("unsupported media type: %s" % ext)
|
|||
|
|
size = os.path.getsize(uploaded_path)
|
|||
|
|
if size > _MAX_FILE_BYTES:
|
|||
|
|
raise RuntimeError("file too large (max %d MB)" % (_MAX_FILE_BYTES // (1024 * 1024)))
|
|||
|
|
|
|||
|
|
_cleanup_tasks()
|
|||
|
|
task_id = uuid.uuid4().hex
|
|||
|
|
out_dir = task_dir(task_id)
|
|||
|
|
input_path = os.path.join(out_dir, "input" + ext)
|
|||
|
|
try:
|
|||
|
|
shutil.move(uploaded_path, input_path)
|
|||
|
|
except Exception:
|
|||
|
|
shutil.copy2(uploaded_path, input_path)
|
|||
|
|
try:
|
|||
|
|
os.remove(uploaded_path)
|
|||
|
|
except Exception:
|
|||
|
|
pass
|
|||
|
|
|
|||
|
|
with _TASK_LOCK:
|
|||
|
|
_TASKS[task_id] = {
|
|||
|
|
"id": task_id,
|
|||
|
|
"status": "pending",
|
|||
|
|
"progress": 0,
|
|||
|
|
"message": "queued",
|
|||
|
|
"report": None,
|
|||
|
|
"output_url": "",
|
|||
|
|
"output_type": "",
|
|||
|
|
"error": "",
|
|||
|
|
"algorithm_id": algo.id,
|
|||
|
|
"created_at": time.time(),
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
th = threading.Thread(
|
|||
|
|
target=_run_test,
|
|||
|
|
args=(task_id, algo, input_path, media_type, out_dir, detector_algo),
|
|||
|
|
daemon=True,
|
|||
|
|
)
|
|||
|
|
th.start()
|
|||
|
|
return task_id
|