390 lines
18 KiB
Python
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))
|