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 preview = camera.preview_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 "", "preview_stream_id": camera.preview_stream_id, "preview_stream_code": preview.code if preview else "", "preview_stream_app": preview.app if preview else "", "preview_stream_name": preview.name if preview else "", "preview_stream_nickname": preview.nickname if preview else "", "preview_stream_forward_state": preview.forward_state if preview else 0, "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", "preview_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.preview_stream = StreamModel.objects.filter( id=int(row.get("preview_stream_id") or 0) ).first() if camera.stream_id and camera.preview_stream_id == camera.stream_id: raise ValueError("摄像头 %d 的预览子码流不能与定位/标定流相同" % camera.slot) 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 _ensure_stream_online(stream, timeout=12.0): """标定抓帧前确保流在 ZLM 在线:未在线时触发拉流/GB28181 点播并轮询等待。""" from app.utils.GlobalUtils import g_zlm, GlobalUtils if not g_zlm.getMediaInfo(app=stream.app, name=stream.name): GlobalUtils.addStreamProxy(stream) deadline = time.monotonic() + timeout while time.monotonic() < deadline: if g_zlm.getMediaInfo(app=stream.app, name=stream.name): return True time.sleep(0.5) return False 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 地址") if not _ensure_stream_online(camera.stream): raise ValueError("视频流拉起失败,请确认摄像头在线") 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))