video_monitor/workshop_monitor/views.py
2026-09-06 22:26:35 +08:00

390 lines
18 KiB
Python

import base64
import json
from pathlib import Path
import time
import uuid
from django.db import transaction
from django.http import JsonResponse
from django.shortcuts import render
from app.models import AlgorithmModel, StreamModel
from monitor_runtime.paths import RESOURCE_ROOT
from .calibration import CalibrationError, solve_planar_calibration
from .models import (
CalibrationObservation, CameraCalibration, GroundControlPoint,
WorkshopCamera, WorkshopSite,
)
from .runtime import get_runtime_manager
DEFAULT_STREAM_CODES = (
"34020000001320000001", "34020000001320000002",
"34020000001320000003", "34020000001320000004",
)
DEFAULT_CAMERA_POSES = (
(0.0, 0.0, 5.0, 45.0), (130.0, 0.0, 5.0, 135.0),
(130.0, 50.0, 5.0, 225.0), (0.0, 50.0, 5.0, 315.0),
)
def _reply(ok, data=None, msg="成功", status=200):
return JsonResponse({"code": 1000 if ok else 0, "msg": msg, "data": data or {}}, status=status)
def f_checkRequestSafe(request):
user = getattr(request, "user", None)
return (True, "成功") if user is not None and user.is_authenticated else (False, "未登录")
def f_parsePostParams(request):
if request.POST:
return {key: request.POST.get(key) for key in request.POST}
try:
return json.loads(request.body.decode("utf-8")) if request.body else {}
except Exception:
return {}
def _site():
site = WorkshopSite.objects.order_by("id").first()
if site is None:
detector = AlgorithmModel.objects.filter(is_default=1, state=1).first()
site = WorkshopSite.objects.create(detector=detector, target_labels=["person"])
existing_slots = set(site.cameras.values_list("slot", flat=True))
streams = {s.code: s for s in StreamModel.objects.filter(code__in=DEFAULT_STREAM_CODES)}
for slot, (code, pose) in enumerate(zip(DEFAULT_STREAM_CODES, DEFAULT_CAMERA_POSES), start=1):
if slot not in existing_slots:
stream = streams.get(code)
WorkshopCamera.objects.create(
site=site, slot=slot, stream=stream,
display_name=(stream.nickname if stream else "摄像头 %d" % slot),
install_x=pose[0], install_y=pose[1], install_z=pose[2], yaw_deg=pose[3],
)
return site
def _camera_dict(camera):
stream = camera.stream
calibration = camera.active_calibration
return {
"id": camera.id, "slot": camera.slot, "display_name": camera.display_name,
"enabled": camera.enabled, "stream_id": camera.stream_id,
"stream_code": stream.code if stream else "", "stream_app": stream.app if stream else "",
"stream_name": stream.name if stream else "", "stream_nickname": stream.nickname if stream else "",
"install_x": camera.install_x, "install_y": camera.install_y, "install_z": camera.install_z,
"yaw_deg": camera.yaw_deg, "pitch_deg": camera.pitch_deg,
"calibration": ({
"id": calibration.id, "status": calibration.status,
"fit_rmse_m": calibration.fit_rmse_m,
"validation_mean_m": calibration.validation_mean_m,
"validation_max_m": calibration.validation_max_m,
"frame_width": calibration.frame_width, "frame_height": calibration.frame_height,
"create_time": calibration.create_time,
} if calibration else None),
}
def _config_data(site):
streams = [{"id": s.id, "code": s.code, "nickname": s.nickname, "app": s.app, "name": s.name,
"pull_stream_ip": s.pull_stream_ip, "forward_state": s.forward_state}
for s in StreamModel.objects.order_by("id")]
detectors = [{"id": a.id, "name": a.name, "task_type": a.task_type,
"device": a.device, "engine": a.inference_engine}
for a in AlgorithmModel.objects.filter(state=1).order_by("id")]
points = [{"id": p.id, "name": p.name, "x": p.x, "y": p.y,
"description": p.description} for p in site.control_points.all()]
return {
"site": {"id": site.id, "name": site.name, "width_m": site.width_m,
"height_m": site.height_m, "detector_id": site.detector_id,
"reid_model_id": site.reid_model_id, "target_labels": site.target_labels or ["person"],
"analysis_fps": site.analysis_fps, "fusion_radius_m": site.fusion_radius_m,
"observation_window_sec": site.observation_window_sec,
"max_speed_mps": site.max_speed_mps, "lost_ttl_sec": site.lost_ttl_sec,
"trail_sec": site.trail_sec},
"cameras": [_camera_dict(c) for c in site.cameras.select_related("stream")],
"control_points": points, "streams": streams, "algorithms": detectors,
"runtime": get_runtime_manager().snapshot(),
}
def index(request):
return render(request, "workshop_monitor/index.html", {})
def open_config(request):
return _reply(True, _config_data(_site()))
def _number(params, name, minimum=None, maximum=None):
try:
value = float(params[name])
except Exception as exc:
raise ValueError("%s 数值无效" % name) from exc
if minimum is not None and value < minimum:
raise ValueError("%s 不能小于 %s" % (name, minimum))
if maximum is not None and value > maximum:
raise ValueError("%s 不能大于 %s" % (name, maximum))
return value
def open_save_config(request):
ok, msg = f_checkRequestSafe(request)
if request.method != "POST" or not ok:
return _reply(False, msg=msg if not ok else "仅支持 POST")
params = f_parsePostParams(request)
try:
with transaction.atomic():
site = _site()
site.name = str(params.get("name") or "主车间")[:100]
site.width_m = _number(params, "width_m", 1, 10000)
site.height_m = _number(params, "height_m", 1, 10000)
site.analysis_fps = _number(params, "analysis_fps", 0.1, 30)
site.fusion_radius_m = _number(params, "fusion_radius_m", 0.1, 20)
site.observation_window_sec = _number(params, "observation_window_sec", 0.1, 10)
site.max_speed_mps = _number(params, "max_speed_mps", 0.1, 50)
site.lost_ttl_sec = _number(params, "lost_ttl_sec", 1, 600)
site.trail_sec = _number(params, "trail_sec", 1, 600)
labels = params.get("target_labels") or ["person"]
if isinstance(labels, str):
labels = [x.strip() for x in labels.split(",") if x.strip()]
site.target_labels = labels or ["person"]
site.detector = AlgorithmModel.objects.filter(id=int(params.get("detector_id") or 0), state=1).first()
rid = int(params.get("reid_model_id") or 0)
site.reid_model = AlgorithmModel.objects.filter(id=rid, state=1, task_type="reid").first() if rid else None
if site.detector and site.detector.task_type == "reid":
raise ValueError("检测模型不能是 ReID 模型")
site.save()
for row in params.get("cameras") or []:
camera = site.cameras.get(id=int(row["id"]))
camera.stream = StreamModel.objects.filter(id=int(row.get("stream_id") or 0)).first()
camera.display_name = str(row.get("display_name") or "摄像头 %d" % camera.slot)[:100]
camera.enabled = bool(row.get("enabled", True))
camera.install_x = float(row.get("install_x", 0))
camera.install_y = float(row.get("install_y", 0))
camera.install_z = float(row.get("install_z", 0))
camera.yaw_deg = float(row.get("yaw_deg", 0)) % 360.0
camera.pitch_deg = max(-90.0, min(90.0, float(row.get("pitch_deg", 0))))
if not (0 <= camera.install_x <= site.width_m and 0 <= camera.install_y <= site.height_m):
raise ValueError("摄像头 %d 的安装 XY 超出厂房边界" % camera.slot)
camera.save()
return _reply(True, _config_data(site))
except Exception as exc:
return _reply(False, msg=str(exc))
def open_control_point_save(request):
ok, msg = f_checkRequestSafe(request)
if request.method != "POST" or not ok:
return _reply(False, msg=msg if not ok else "仅支持 POST")
params = f_parsePostParams(request)
try:
site = _site()
x, y = _number(params, "x", 0, site.width_m), _number(params, "y", 0, site.height_m)
pid = int(params.get("id") or 0)
point = site.control_points.filter(id=pid).first() if pid else GroundControlPoint(site=site)
if point is None:
raise ValueError("控制点不存在")
point.name = str(params.get("name") or "").strip()[:100]
if not point.name:
raise ValueError("控制点名称不能为空")
point.x, point.y = x, y
point.description = str(params.get("description") or "")[:300]
point.save()
return _reply(True, {"id": point.id})
except Exception as exc:
return _reply(False, msg=str(exc))
def open_control_point_delete(request):
ok, msg = f_checkRequestSafe(request)
if request.method != "POST" or not ok:
return _reply(False, msg=msg if not ok else "仅支持 POST")
try:
point = _site().control_points.get(id=int(f_parsePostParams(request).get("id") or 0))
point.delete()
return _reply(True)
except Exception as exc:
return _reply(False, msg=str(exc))
def open_capture(request):
ok, msg = f_checkRequestSafe(request)
if request.method != "POST" or not ok:
return _reply(False, msg=msg if not ok else "仅支持 POST")
cap = None
try:
import cv2
camera = _site().cameras.select_related("stream").get(id=int(f_parsePostParams(request).get("camera_id") or 0))
if not camera.stream:
raise ValueError("摄像头尚未绑定视频流")
from app.analysis.manager import AnalysisManager
url = AnalysisManager.build_rtsp_url(camera.stream)
if not url:
raise ValueError("无法生成摄像头 RTSP 地址")
cap = cv2.VideoCapture(url, cv2.CAP_FFMPEG)
frame = None
deadline = time.monotonic() + 8.0
while time.monotonic() < deadline:
ret, current = cap.read()
if ret and current is not None:
frame = current
break
if frame is None:
raise ValueError("抓帧失败,请确认 GB28181 视频流在线")
token = uuid.uuid4().hex
directory = Path(RESOURCE_ROOT) / "static" / "storage" / "workshop" / "calibration"
directory.mkdir(parents=True, exist_ok=True)
path = directory / (token + ".jpg")
if not cv2.imwrite(str(path), frame, [int(cv2.IMWRITE_JPEG_QUALITY), 92]):
raise ValueError("标定截图保存失败")
h, w = frame.shape[:2]
encoded = base64.b64encode(path.read_bytes()).decode("ascii")
return _reply(True, {"token": token, "width": w, "height": h,
"image": "data:image/jpeg;base64," + encoded})
except Exception as exc:
return _reply(False, msg=str(exc))
finally:
if cap is not None:
cap.release()
def open_calibrate(request):
ok, msg = f_checkRequestSafe(request)
if request.method != "POST" or not ok:
return _reply(False, msg=msg if not ok else "仅支持 POST")
params = f_parsePostParams(request)
try:
site = _site()
camera = site.cameras.get(id=int(params.get("camera_id") or 0))
observations = params.get("observations") or []
result = solve_planar_calibration(observations, params.get("frame_width"), params.get("frame_height"), 1.0)
token = str(params.get("snapshot_token") or "")
rel_path = "workshop/calibration/%s.jpg" % token if token else ""
with transaction.atomic():
calibration = CameraCalibration.objects.create(
camera=camera, snapshot_path=rel_path,
frame_width=int(params.get("frame_width")), frame_height=int(params.get("frame_height")),
homography=result["homography"], fit_rmse_m=result["fit_rmse_m"],
validation_mean_m=result["validation_mean_m"], validation_max_m=result["validation_max_m"],
status=CameraCalibration.STATUS_VALID if result["is_valid"] else CameraCalibration.STATUS_INVALID,
is_active=False, warnings=result["warnings"],
created_by=getattr(request.user, "id", 0) or 0,
)
point_map = {p.id: p for p in site.control_points.all()}
for row in result["observations"]:
pid = int(row.get("control_point_id") or 0)
CalibrationObservation.objects.create(
calibration=calibration, control_point=point_map.get(pid),
pixel_u=float(row["u"]), pixel_v=float(row["v"]),
world_x=float(row["x"]), world_y=float(row["y"]),
role=row.get("role", "fit"), error_m=float(row["error_m"]),
)
if result["is_valid"] and bool(params.get("activate", True)):
camera.calibrations.filter(is_active=True).update(is_active=False)
calibration.is_active = True
calibration.save(update_fields=("is_active", "last_update_time"))
return _reply(True, {"calibration_id": calibration.id, **result})
except (CalibrationError, ValueError, KeyError, TypeError) as exc:
return _reply(False, msg=str(exc))
def open_activate_calibration(request):
ok, msg = f_checkRequestSafe(request)
if request.method != "POST" or not ok:
return _reply(False, msg=msg if not ok else "仅支持 POST")
try:
calibration = CameraCalibration.objects.select_related("camera").get(
id=int(f_parsePostParams(request).get("calibration_id") or 0))
if calibration.status != CameraCalibration.STATUS_VALID or calibration.validation_mean_m is None or calibration.validation_mean_m > 1.0:
raise ValueError("只有验证平均误差不超过 1 米的标定才能激活")
with transaction.atomic():
calibration.camera.calibrations.filter(is_active=True).update(is_active=False)
calibration.is_active = True
calibration.save(update_fields=("is_active", "last_update_time"))
return _reply(True)
except Exception as exc:
return _reply(False, msg=str(exc))
def _algorithm_spec(algorithm, targets=None):
labels = algorithm.labels
if isinstance(labels, str):
try:
labels = json.loads(labels)
except Exception:
labels = []
model_path = Path(algorithm.model_file)
if not model_path.is_absolute():
model_path = Path(RESOURCE_ROOT) / "static" / "upload" / "weight" / model_path
if not model_path.is_file():
raise ValueError("模型文件不存在: %s" % algorithm.model_file)
return {
"id": algorithm.id, "name": algorithm.name, "model_file": str(model_path),
"labels": labels, "input_size": [algorithm.input_width, algorithm.input_height],
"conf_threshold": algorithm.conf_threshold, "iou_threshold": algorithm.iou_threshold,
"algorithm_type": algorithm.algorithm_type, "task_type": algorithm.task_type,
"inference_engine": algorithm.inference_engine, "device": algorithm.device,
"target_labels": targets or [],
}
def _runtime_config(site):
if not site.detector or site.detector.state != 1:
raise ValueError("请先选择可用的人员检测模型")
cameras = []
from app.analysis.manager import AnalysisManager
for camera in site.cameras.filter(enabled=True).select_related("stream"):
calibration = camera.active_calibration
if not camera.stream or not calibration:
continue
url = AnalysisManager.build_rtsp_url(camera.stream)
if not url:
continue
cameras.append({
"camera_id": camera.id, "stream_id": camera.stream_id, "slot": camera.slot,
"display_name": camera.display_name or str(camera), "rtsp_url": url,
"calibration": {"homography": calibration.homography,
"validation_mean_m": calibration.validation_mean_m},
})
if len(cameras) < 3:
raise ValueError("至少需要 3 台已绑定且完成合格标定的摄像头")
return {
"width_m": site.width_m, "height_m": site.height_m,
"analysis_fps": site.analysis_fps, "fusion_radius_m": site.fusion_radius_m,
"observation_window_sec": site.observation_window_sec,
"max_speed_mps": site.max_speed_mps, "lost_ttl_sec": site.lost_ttl_sec,
"trail_sec": site.trail_sec, "target_labels": site.target_labels or ["person"],
"detector": _algorithm_spec(site.detector, site.target_labels or ["person"]),
"reid_model": _algorithm_spec(site.reid_model) if site.reid_model else None,
"cameras": cameras,
}
def open_start(request):
ok, msg = f_checkRequestSafe(request)
if request.method != "POST" or not ok:
return _reply(False, msg=msg if not ok else "仅支持 POST")
try:
ok, info = get_runtime_manager().start(_runtime_config(_site()))
return _reply(ok, get_runtime_manager().snapshot(), info)
except Exception as exc:
return _reply(False, msg=str(exc))
def open_stop(request):
ok, msg = f_checkRequestSafe(request)
if request.method != "POST" or not ok:
return _reply(False, msg=msg if not ok else "仅支持 POST")
ok, info = get_runtime_manager().stop()
return _reply(ok, get_runtime_manager().snapshot(), info)
def open_state(request):
try:
since = request.GET.get("since")
return _reply(True, get_runtime_manager().snapshot(since))
except Exception as exc:
return _reply(False, msg=str(exc))