"""Monitor · 布控管理 Web 层 页面: - /control/index 布控管理(按摄像头绘制多边形区域) API: - /control/openIndex/openAdd/openEdit/openDel/openPageData/openRecentAlarms """ from app.views.ViewsBase import * from django.shortcuts import render from app.models import StreamModel, ZoneModel, AlarmModel, BizAlgorithmModel import json as _json import time as _time LINE_POSTS = (BizAlgorithmModel.POST_LINE_CROSS, BizAlgorithmModel.POST_LINE_COUNT) REGION_POSTS = ( BizAlgorithmModel.POST_AREA, BizAlgorithmModel.POST_DWELL, BizAlgorithmModel.POST_DENSITY, BizAlgorithmModel.POST_DIRECTION, ) def _parse_zone_algo_ids(raw): if isinstance(raw, list): return [int(x) for x in raw if str(x).strip()] if isinstance(raw, str): try: arr = _json.loads(raw) if isinstance(arr, list): return [int(x) for x in arr if str(x).strip()] except Exception: pass return [int(s) for s in raw.split(",") if s.strip()] return [] def _validate_zone_geometry(params, algo_ids=None): """校验布控绘制与所选业务算法是否匹配。""" is_required = int(params.get("is_required", 1) or 0) coords_raw = params.get("coordinates", "[]") try: coords = _json.loads(coords_raw) if isinstance(coords_raw, str) else (coords_raw or []) except Exception: coords = [] if not isinstance(coords, list): coords = [] line_a = (params.get("line_a") or "").strip() line_b = (params.get("line_b") or "").strip() ids = algo_ids if algo_ids is not None else _parse_zone_algo_ids(params.get("algorithm_ids")) posts = set() if ids: posts = set( BizAlgorithmModel.objects.filter(id__in=ids, state=1).values_list("post_process", flat=True) ) needs_line = bool(posts & set(LINE_POSTS)) needs_region = bool(is_required and (not posts or posts & set(REGION_POSTS) or needs_line)) if needs_line and (not line_a or not line_b): raise ValueError("所选越线类算法需绘制方向线 A→B") if needs_region and len(coords) < 3: raise ValueError("必需区域已开启,请绘制布控区域(至少 3 个点)") if not is_required and posts & set(REGION_POSTS): raise ValueError("区域类后处理(区域入侵/滞留/密度/方向)需开启「必需区域」并绘制区域") return True CONTROL_ALARM_EVENT_TYPES = ('entered_zone', 'loiter') # ---------- 状态接口内存缓存 ---------- # 布控页/控制面板高频轮询 openStatus,原始实现每次都遍历所有运行实例 + # 查 DB,布控执行时叠加 SQLite 锁会导致页面卡顿。 # 这里加一个带 TTL 的进程内缓存,多个请求共享同一份快照。 _STATUS_CACHE = {"lite": {"t": 0, "data": None}, "full": {"t": 0, "data": None}} _STATUS_CACHE_TTL = 2.0 # 秒:缓存有效期,2s 内重复请求直接返回缓存 def _get_cached_status(lite=False): key = "lite" if lite else "full" entry = _STATUS_CACHE.get(key) now = _time.time() if entry and entry["data"] is not None and (now - entry["t"]) < _STATUS_CACHE_TTL: return entry["data"] return None def _set_cached_status(data, lite=False): key = "lite" if lite else "full" _STATUS_CACHE[key] = {"t": _time.time(), "data": data} def _parse_control_detect_rate(params): try: interval = float(params.get("detect_interval_sec", 1) or 1) except (TypeError, ValueError): interval = 1.0 try: frames = int(params.get("detect_frames", 1) or 1) except (TypeError, ValueError): frames = 1 interval = max(0.1, min(86400.0, interval)) frames = max(1, min(999, frames)) return interval, frames def _parse_alarm_repeat(params, default=30.0): try: value = float(params.get("alarm_repeat_sec", default)) except (TypeError, ValueError): value = default return max(0.0, min(86400.0, value)) def _control_to_dict(z): algos = [] try: algos = [{"id": a.id, "name": a.name} for a in z.algorithms.all().order_by('id')] except Exception: pass stream = getattr(z, "stream", None) interval = max(0.1, float(getattr(z, "detect_interval_sec", 1) or 1)) frames = max(1, int(getattr(z, "detect_frames", 1) or 1)) return { "id": z.id, "stream_id": z.stream_id, "stream_name": (stream.nickname if stream else ""), "name": z.name, "coordinates": z.coordinates, "is_required": z.is_required, "loiter_threshold": z.loiter_threshold, "detect_interval_sec": interval, "detect_frames": frames, "alarm_repeat_sec": max(0.0, float(getattr(z, "alarm_repeat_sec", 30) or 0)), "color": z.color, "line_a": getattr(z, "line_a", "") or "", "line_b": getattr(z, "line_b", "") or "", "density_threshold": int(getattr(z, "density_threshold", 0) or 0), "algorithms": algos, "algorithm_ids": [a["id"] for a in algos], "state": int(getattr(z, "state", 1) or 0), "create_time": str(z.create_time), } def _control_queryset(stream_id=None): qs = ZoneModel.objects.select_related("stream").prefetch_related("algorithms").order_by("-id") if stream_id: qs = qs.filter(stream_id=int(stream_id)) return qs def build_analysis_status_data(lite=False): """汇总分析运行状态;lite 模式跳过引擎探测等重操作,供布控页轮询。""" # 命中缓存则直接返回,避免每次都遍历运行实例 + 查 DB cached = _get_cached_status(lite=lite) if cached is not None: return cached from app.analysis.manager import AnalysisManager m = AnalysisManager() shared_inference = m._use_shared_inference() and m._use_multiprocess() multi_process = m._use_multiprocess() running = [] for sid in m.list_running(): info = m.get_pipeline_info(sid) if info: running.append(info) # 收集运行中 pipeline 的 stream_name 映射,供引擎实例详情使用 stream_name_map = {} for r in running: sid = r.get("stream_id") if sid: stream_name_map[sid] = r.get("stream_name") or r.get("algorithm_name") or ("#%s" % sid) algo_usage = {} for r in running: sid = r.get("stream_id") sname = stream_name_map.get(sid, "") for d in (r.get("detectors") or []): aid = d.get("algorithm_id") if aid is None: continue u = algo_usage.setdefault( aid, { "algorithm_id": aid, "algorithm_name": d.get("algorithm_name", ""), "engine": d.get("engine", ""), "stream_count": 0, "stream_names": [], }, ) u["stream_count"] += 1 if sname and sname not in u["stream_names"]: u["stream_names"].append(sname) # 补充小模型详情 + 反查业务算法 if algo_usage: try: from app.models import AlgorithmModel, BizAlgorithmModel sm_qs = AlgorithmModel.objects.filter(id__in=list(algo_usage.keys())) sm_map = {sm.id: sm for sm in sm_qs} # 反查业务算法(通过 small_model 外键) biz_qs = BizAlgorithmModel.objects.filter( small_model_id__in=list(algo_usage.keys()), state=1 ).select_related("small_model") biz_by_sm = {} for ba in biz_qs: biz_by_sm.setdefault(ba.small_model_id, []).append({ "id": ba.id, "name": ba.name, "flow_type": ba.flow_type, }) for aid, u in algo_usage.items(): sm = sm_map.get(aid) if sm: u["model_file"] = sm.model_file u["task_type"] = sm.task_type u["device"] = sm.device u["input_size"] = [sm.input_width, sm.input_height] u["conf_threshold"] = sm.conf_threshold u["iou_threshold"] = sm.iou_threshold u["algorithm_type"] = sm.algorithm_type u["small_model_state"] = sm.state u["biz_algorithms"] = biz_by_sm.get(aid, []) u["instance_enabled"] = m.is_algo_instance_enabled(aid) except Exception as e: import logging logging.getLogger("app").warning("build_analysis_status_data 补充详情失败: %s" % str(e)) local_instances = m._worker_pool.instance_info() inference_workers_alive = 0 inference_workers_config = 0 inference_degraded = False inference_timeout_count = 0 if shared_inference: try: from app.analysis.inference_pool import get_inference_pool pool = get_inference_pool() inference_workers_alive = pool.instance_count() inference_workers_config = pool.num_workers pool_st = pool.status() inference_degraded = bool(pool_st.get("inference_degraded")) inference_timeout_count = int(pool_st.get("timeout_count") or 0) except Exception: inference_workers_alive = 0 inference_degraded = False inference_timeout_count = 0 else: try: from app.utils.GlobalUtils import g_config inference_workers_config = int(getattr(g_config, "analysisInferenceWorkers", 2)) except Exception: inference_workers_config = 2 if shared_inference: engine_total = len(algo_usage) if algo_usage else inference_workers_alive else: engine_total = len(local_instances) # 所有业务算法的实例化开关状态 algo_instance_states = [] try: from app.models import BizAlgorithmModel disabled = m.get_disabled_algos() for ba in BizAlgorithmModel.objects.filter(state=1).order_by("id"): sm = ba.small_model algo_instance_states.append({ "id": ba.id, "name": ba.name, "flow_type": ba.flow_type, "small_model_id": sm.id if sm else None, "small_model_name": sm.name if sm else "", "engine": sm.inference_engine if sm else "", "instance_enabled": (sm.id not in disabled) if sm else True, }) except Exception: pass data = { "running_streams": running, "running_count": len(running), "total_streams": StreamModel.objects.count(), "engine_instances": list(algo_usage.values()), "engine_instance_total": engine_total, "engine_instances_local": len(local_instances), "inference_shared": shared_inference, "inference_workers_alive": inference_workers_alive, "inference_degraded": inference_degraded if shared_inference else False, "inference_timeout_count": inference_timeout_count if shared_inference else 0, "analysis_fps_avg": ( round(sum(r.get("analysis_fps", 0) for r in running) / len(running), 1) if running else 0.0 ), "inference_config": { "shared": bool(shared_inference), "multi_process": bool(multi_process), "workers": inference_workers_config, "workers_alive": inference_workers_alive, }, "algo_instance_states": algo_instance_states, } if not lite: from app.analysis.engines.factory import EngineFactory from app.analysis.motion import MotionDetector data["engines"] = EngineFactory.list_engines() data["motion_available"] = MotionDetector.is_available() _set_cached_status(data, lite=lite) return data def invalidate_analysis_status_cache(): """供启停分析、热更新后主动失效缓存,确保下次请求拿到最新状态。""" _STATUS_CACHE["lite"]["data"] = None _STATUS_CACHE["full"]["data"] = None def control_index(request): return render(request, 'app/control/index.html', {}) def control_openIndex(request): ret = False msg = LANG_VIEWS_T(request, "msg_unknown_error") data = [] if request.method == 'GET': __check_ret, __check_msg = f_checkRequestSafe(request) if __check_ret: stream_id = request.GET.get('stream_id') qs = _control_queryset(stream_id) if stream_id else _control_queryset() data = [_control_to_dict(z) for z in qs] ret = True msg = LANG_VIEWS_T(request, "msg_success") else: msg = __check_msg else: msg = LANG_VIEWS_T(request, "msg_method_not_supported") return f_responseJson({"code": 1000 if ret else 0, "msg": msg, "data": data}) def control_openPageData(request): """布控页一次性加载:区域列表 + 摄像头下拉 + 分析概览""" ret = False msg = LANG_VIEWS_T(request, "msg_unknown_error") data = {} if request.method == 'GET': __check_ret, __check_msg = f_checkRequestSafe(request) if __check_ret: try: stream_id = request.GET.get('stream_id') zones = [_control_to_dict(z) for z in _control_queryset(stream_id)] streams = list( StreamModel.objects.order_by("-id").values("id", "app", "name", "code", "nickname") ) analysis = build_analysis_status_data(lite=True) alarm_qs = AlarmModel.objects.filter(event_type__in=CONTROL_ALARM_EVENT_TYPES) if stream_id: alarm_qs = alarm_qs.filter(stream_id=int(stream_id)) data = { "zones": zones, "streams": streams, "analysis": analysis, "recent_alarm_count": alarm_qs.count(), } ret = True msg = LANG_VIEWS_T(request, "msg_success") except Exception as e: msg = str(e) else: msg = __check_msg else: msg = LANG_VIEWS_T(request, "msg_method_not_supported") return f_responseJson({"code": 1000 if ret else 0, "msg": msg, "data": data}) def control_openAdd(request): ret = False msg = LANG_VIEWS_T(request, "msg_unknown_error") if request.method == 'POST': __check_ret, __check_msg = f_checkRequestSafe(request) if __check_ret: params = f_parsePostParams(request) try: stream_id = int(params.get("stream_id", 0)) if stream_id <= 0: app = (params.get("stream_app") or params.get("app") or "").strip() name = (params.get("stream_name") or params.get("name") or "").strip() if app and name: stream = StreamModel.objects.get(app=app, name=name) stream_id = stream.id else: raise ValueError(LANG_VIEWS_T(request, "zone_form_incomplete")) else: stream = StreamModel.objects.get(id=stream_id) algo_ids_req = params.get("algorithm_ids") or [] if isinstance(algo_ids_req, str): try: import json as _json2 algo_ids_req = _json2.loads(algo_ids_req) except Exception: algo_ids_req = [s for s in algo_ids_req.split(",") if s] if not algo_ids_req: raise ValueError(LANG_VIEWS_T(request, "zone_algo_required")) algo_ids_int = [int(x) for x in algo_ids_req] _validate_zone_geometry(params, algo_ids_int) detect_interval_sec, detect_frames = _parse_control_detect_rate(params) zone = ZoneModel( stream=stream, name=params.get("name", "").strip(), coordinates=params.get("coordinates", "[]"), is_required=int(params.get("is_required", 1)), loiter_threshold=int(params.get("loiter_threshold", 0)), detect_interval_sec=detect_interval_sec, detect_frames=detect_frames, alarm_repeat_sec=_parse_alarm_repeat(params), color=params.get("color", "#169F85"), line_a=params.get("line_a", ""), line_b=params.get("line_b", ""), density_threshold=int(params.get("density_threshold", 0) or 0), state=0, ) zone.save() algo_ids = params.get("algorithm_ids") or [] if isinstance(algo_ids, str): try: import json as _json algo_ids = _json.loads(algo_ids) except Exception: algo_ids = [s for s in algo_ids.split(",") if s] if algo_ids: from app.models import BizAlgorithmModel qs = BizAlgorithmModel.objects.filter(id__in=[int(x) for x in algo_ids], state=1) zone.algorithms.set(qs) try: from app.analysis.manager import AnalysisManager m = AnalysisManager() if stream_id in m.list_running(): m.reload_zones(stream_id) except Exception: pass ret = True msg = LANG_VIEWS_T(request, "msg_success") except Exception as e: msg = str(e) else: msg = __check_msg else: msg = LANG_VIEWS_T(request, "msg_method_not_supported") return f_responseJson({"code": 1000 if ret else 0, "msg": msg}) def control_openEdit(request): ret = False msg = LANG_VIEWS_T(request, "msg_unknown_error") if request.method == 'POST': __check_ret, __check_msg = f_checkRequestSafe(request) if __check_ret: params = f_parsePostParams(request) try: zid = int(params.get("id", 0)) z = ZoneModel.objects.get(id=zid) if "name" in params: z.name = params["name"].strip() if "coordinates" in params: z.coordinates = params["coordinates"] if "is_required" in params: z.is_required = int(params["is_required"]) if "loiter_threshold" in params: z.loiter_threshold = int(params["loiter_threshold"]) if "detect_interval_sec" in params or "detect_frames" in params: interval, frames = _parse_control_detect_rate(params) z.detect_interval_sec = interval z.detect_frames = frames if "alarm_repeat_sec" in params: z.alarm_repeat_sec = _parse_alarm_repeat(params) if "color" in params: z.color = params["color"] if "line_a" in params: z.line_a = params.get("line_a", "") if "line_b" in params: z.line_b = params.get("line_b", "") if "density_threshold" in params: z.density_threshold = int(params.get("density_threshold", 0) or 0) z.save() if "algorithm_ids" in params: algo_ids = params.get("algorithm_ids") or [] if isinstance(algo_ids, str): try: import json as _json algo_ids = _json.loads(algo_ids) except Exception: algo_ids = [s for s in algo_ids.split(",") if s] if not algo_ids: raise ValueError(LANG_VIEWS_T(request, "zone_algo_required")) algo_ids_int = [int(x) for x in algo_ids] _validate_zone_geometry(params, algo_ids_int) from app.models import BizAlgorithmModel qs = BizAlgorithmModel.objects.filter(id__in=algo_ids_int, state=1) z.algorithms.set(qs) elif any(k in params for k in ("coordinates", "line_a", "line_b", "is_required")): algo_ids_int = list(z.algorithms.filter(state=1).values_list("id", flat=True)) _validate_zone_geometry(params, algo_ids_int) try: from app.analysis.manager import AnalysisManager m = AnalysisManager() if z.stream_id in m.list_running(): m.reload_zones(z.stream_id) except Exception: pass ret = True msg = LANG_VIEWS_T(request, "msg_success") except Exception as e: msg = str(e) else: msg = __check_msg else: msg = LANG_VIEWS_T(request, "msg_method_not_supported") return f_responseJson({"code": 1000 if ret else 0, "msg": msg}) def control_openDel(request): ret = False msg = LANG_VIEWS_T(request, "msg_unknown_error") if request.method == 'POST': __check_ret, __check_msg = f_checkRequestSafe(request) if __check_ret: params = f_parsePostParams(request) try: zid = int(params.get("id", 0)) z = ZoneModel.objects.get(id=zid) sid = z.stream_id z.delete() try: from app.analysis.manager import AnalysisManager from app.models import ZoneModel as _ZM m = AnalysisManager() if m.is_running(sid): if _ZM.objects.filter(stream_id=sid, state=1).exists(): m.reload_zones(sid) else: m.stop(sid) except Exception: pass ret = True msg = LANG_VIEWS_T(request, "msg_success") except Exception as e: msg = str(e) else: msg = __check_msg else: msg = LANG_VIEWS_T(request, "msg_method_not_supported") return f_responseJson({"code": 1000 if ret else 0, "msg": msg}) def control_openToggleZone(request): """单条布控手动启停:仅影响该布控,同摄像头其它布控互不干扰。""" ret = False msg = LANG_VIEWS_T(request, "msg_unknown_error") data = {} if request.method == 'POST': __check_ret, __check_msg = f_checkRequestSafe(request) if __check_ret: params = f_parsePostParams(request) try: zid = int(params.get("id", 0)) if zid <= 0: raise ValueError(LANG_VIEWS_T(request, "msg_invalid_parameter")) z = ZoneModel.objects.get(id=zid) if "enabled" in params or "state" in params: enabled = int(params.get("enabled", params.get("state", 0))) else: enabled = 0 if int(z.state or 0) == 1 else 1 from app.analysis.manager import AnalysisManager from app.models import StreamModel m = AnalysisManager() sid = int(z.stream_id) stream = StreamModel.objects.get(id=sid) def _restart_stream_analysis(): """重启分析流水线,确保布控列表与运行时状态一致(避免热更新失败导致无报警)。""" if m.is_running(sid): m.stop(sid) ok, info = m.start(stream) if not ok: raise ValueError(info or LANG_VIEWS_T(request, "msg_unknown_error")) return ok, info if enabled: z.state = 1 z.save() if not m.is_running(sid): ok, info = m.start(stream) if not ok: z.state = 0 z.save() raise ValueError(info or LANG_VIEWS_T(request, "msg_unknown_error")) else: _restart_stream_analysis() else: z.state = 0 z.save() if m.is_running(sid): if ZoneModel.objects.filter(stream_id=sid, state=1).exists(): _restart_stream_analysis() else: m.stop(sid) invalidate_analysis_status_cache() ret = True data = {"id": z.id, "state": z.state, "stream_id": z.stream_id} msg = LANG_VIEWS_T( request, "zone_started" if z.state == 1 else "zone_stopped", ) except Exception as e: msg = str(e) else: msg = __check_msg else: msg = LANG_VIEWS_T(request, "msg_method_not_supported") return f_responseJson({"code": 1000 if ret else 0, "msg": msg, "data": data}) def control_openRecentAlarms(request): """返回近期报警事件,供布控页展示""" from app.views.AnalysisView import _timeline_alarm_fields ret = False msg = LANG_VIEWS_T(request, "msg_unknown_error") data = [] if request.method == 'GET': __check_ret, __check_msg = f_checkRequestSafe(request) if __check_ret: try: import json as _json stream_id = request.GET.get('stream_id') limit = int(request.GET.get('limit', 12)) base_qs = AlarmModel.objects.all() if stream_id: base_qs = base_qs.filter(stream_id=int(stream_id)) zone_qs = base_qs.filter(event_type__in=CONTROL_ALARM_EVENT_TYPES).order_by('-timestamp')[:limit] items = list(zone_qs) out = [] for t in items[:limit]: try: meta = _json.loads(t.metadata) if t.metadata else {} except Exception: meta = {} out.append({ "id": t.id, "stream_id": t.stream_id, "stream_name": (t.stream.nickname if t.stream else ""), "event_type": t.event_type, "label": meta.get("label", ""), "zone_id": meta.get("zone_id"), "box": meta.get("box"), "duration": meta.get("duration"), "snapshot_path": meta.get("snapshot_path", ""), "timestamp": str(t.timestamp), **_timeline_alarm_fields(t, meta), }) data = out ret = True msg = LANG_VIEWS_T(request, "msg_success") except Exception as e: msg = str(e) else: msg = __check_msg else: msg = LANG_VIEWS_T(request, "msg_method_not_supported") return f_responseJson({"code": 1000 if ret else 0, "msg": msg, "data": data})