video_monitor/app/views/ControlView.py

666 lines
27 KiB
Python
Raw Normal View History

2026-08-30 22:22:11 +08:00
"""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
2026-09-04 18:16:14 +08:00
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))
2026-08-30 22:22:11 +08:00
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,
2026-09-04 18:16:14 +08:00
"alarm_repeat_sec": max(0.0, float(getattr(z, "alarm_repeat_sec", 30) or 0)),
2026-08-30 22:22:11 +08:00
"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,
2026-09-04 18:16:14 +08:00
alarm_repeat_sec=_parse_alarm_repeat(params),
2026-08-30 22:22:11 +08:00
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
2026-09-04 18:16:14 +08:00
if "alarm_repeat_sec" in params:
z.alarm_repeat_sec = _parse_alarm_repeat(params)
2026-08-30 22:22:11 +08:00
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})