video_monitor/app/views/AlgorithmView.py
2026-08-30 22:23:12 +08:00

565 lines
23 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 作者:北小菜
"""业务算法管理 — 小模型/大模型 + 后处理业务逻辑"""
import json
from app.views.ViewsBase import *
from django.shortcuts import render
from app.models import BizAlgorithmModel, AlgorithmModel, LLMModel, ZoneModel
def _parse_labels(raw):
if isinstance(raw, list):
return [str(x).strip() for x in raw if str(x).strip()]
if isinstance(raw, str):
try:
arr = json.loads(raw)
if isinstance(arr, list):
return [str(x).strip() for x in arr if str(x).strip()]
except Exception:
pass
return [s.strip() for s in raw.split(",") if s.strip()]
return []
def _resolve_model_abs_path(model_file):
"""返回模型文件的绝对路径(不存在则返回空串)"""
if not model_file:
return ""
try:
from app.analysis.worker_pool import resolve_model_path
p = resolve_model_path(model_file)
import os as _os
if p and _os.path.exists(p):
return p
except Exception:
pass
return ""
def _check_model_file_exists(model_file):
"""检查模型文件是否存在"""
return bool(_resolve_model_abs_path(model_file))
def _biz_to_dict(b, detail=False):
labels = _parse_labels(b.target_labels or '[]')
d = {
"id": b.id,
"name": b.name,
"flow_type": b.flow_type,
"small_model_id": b.small_model_id,
"small_model_name": b.small_model.name if b.small_model_id and b.small_model else "",
"detector_model_id": b.detector_model_id,
"detector_model_name": b.detector_model.name if b.detector_model_id and b.detector_model else "",
"target_labels": labels,
"llm_id": b.llm_id,
"llm_name": ((b.llm.name or b.llm.model_name) if b.llm_id and b.llm else ""),
"llm_prompt": b.llm_prompt or "",
"llm_validate": b.llm_validate or "",
"post_process": b.post_process or BizAlgorithmModel.POST_AREA,
"ref_angle": float(getattr(b, "ref_angle", 90.0) or 90.0),
"angle_tolerance": float(getattr(b, "angle_tolerance", 45.0) or 45.0),
"forward_count_threshold": int(getattr(b, "forward_count_threshold", 0) or 0),
"reverse_count_threshold": int(getattr(b, "reverse_count_threshold", 0) or 0),
"state": b.state,
"create_time": str(b.create_time),
"zone_count": b.zones.count(),
}
# 小模型文件状态
small = b.small_model if (b.small_model_id and b.small_model) else None
if small:
d["small_model_file"] = small.model_file or ""
d["small_model_engine"] = small.inference_engine or ""
d["small_model_file_exists"] = _check_model_file_exists(small.model_file or "")
d["small_model_file_path"] = _resolve_model_abs_path(small.model_file or "")
else:
d["small_model_file"] = ""
d["small_model_engine"] = ""
d["small_model_file_exists"] = False
d["small_model_file_path"] = ""
detector = b.detector_model if (b.detector_model_id and b.detector_model) else None
if detector:
d["detector_model_file"] = detector.model_file or ""
d["detector_model_engine"] = detector.inference_engine or ""
d["detector_model_file_exists"] = _check_model_file_exists(detector.model_file or "")
else:
d["detector_model_file"] = ""
d["detector_model_engine"] = ""
d["detector_model_file_exists"] = False
flow_names = {
BizAlgorithmModel.FLOW_SMALL: "小模型+后处理",
BizAlgorithmModel.FLOW_LLM: "大模型+后处理",
BizAlgorithmModel.FLOW_BOTH: "小模型+大模型+后处理",
BizAlgorithmModel.FLOW_DETECT_REID: "检测+ReID+后处理",
}
d["flow_type_name"] = flow_names.get(b.flow_type, str(b.flow_type))
post_names = {
BizAlgorithmModel.POST_AREA: "区域入侵",
BizAlgorithmModel.POST_LINE_CROSS: "越线检测",
BizAlgorithmModel.POST_LINE_COUNT: "越线计数",
BizAlgorithmModel.POST_DIRECTION: "方向入侵",
BizAlgorithmModel.POST_DENSITY: "密度报警",
BizAlgorithmModel.POST_DWELL: "滞留报警",
}
d["post_process_name"] = post_names.get(d["post_process"], d["post_process"])
return d
def _validate_biz_fields(params, biz_id=0):
name = (params.get("name") or "").strip()
if not name:
raise ValueError("算法名称不能为空")
try:
flow_type = int(params.get("flow_type", 1))
except Exception:
flow_type = 1
if flow_type not in (1, 2, 3, 4):
raise ValueError("无效的流程类型")
small_model_id = None
detector_model_id = None
llm_id = None
target_labels = []
llm_prompt = (params.get("llm_prompt") or "").strip()
llm_validate = (params.get("llm_validate") or "").strip()
post_process = (params.get("post_process") or BizAlgorithmModel.POST_AREA).strip()
if flow_type in (BizAlgorithmModel.FLOW_SMALL, BizAlgorithmModel.FLOW_BOTH):
try:
small_model_id = int(params.get("small_model_id", 0))
except Exception:
small_model_id = 0
if small_model_id <= 0:
raise ValueError("请选择小模型")
sm = AlgorithmModel.objects.filter(id=small_model_id, state=1).first()
if not sm:
raise ValueError("小模型不存在或已禁用")
if (getattr(sm, "task_type", "") or "detect").lower() == "reid":
raise ValueError("ReID 模型请使用「检测+ReID+后处理」流程,并同时选择 YOLO 检测小模型")
target_labels = _parse_labels(params.get("target_labels"))
if not target_labels:
raise ValueError("请至少选择一个检测目标")
if flow_type == BizAlgorithmModel.FLOW_DETECT_REID:
try:
detector_model_id = int(params.get("detector_model_id", 0))
except Exception:
detector_model_id = 0
try:
small_model_id = int(params.get("small_model_id", 0))
except Exception:
small_model_id = 0
if detector_model_id <= 0:
raise ValueError("请选择检测小模型 (YOLO)")
if small_model_id <= 0:
raise ValueError("请选择 ReID 小模型 (OSNet)")
if detector_model_id == small_model_id:
raise ValueError("检测小模型与 ReID 小模型不能相同")
det = AlgorithmModel.objects.filter(id=detector_model_id, state=1).first()
if not det:
raise ValueError("检测小模型不存在或已禁用")
if (getattr(det, "task_type", "") or "detect").lower() != "detect":
raise ValueError("检测小模型必须是 YOLO 检测模型 (task_type=detect)")
reid = AlgorithmModel.objects.filter(id=small_model_id, state=1).first()
if not reid:
raise ValueError("ReID 小模型不存在或已禁用")
if (getattr(reid, "task_type", "") or "").lower() != "reid":
raise ValueError("ReID 小模型必须是 OSNet ReID 模型 (task_type=reid)")
target_labels = _parse_labels(params.get("target_labels"))
if not target_labels:
raise ValueError("请至少选择一个检测目标")
det_labels = _parse_labels(det.labels or "[]")
invalid = [lb for lb in target_labels if lb not in det_labels]
if invalid:
raise ValueError("检测目标不在检测小模型标签列表中:%s" % "".join(invalid))
if flow_type in (BizAlgorithmModel.FLOW_LLM, BizAlgorithmModel.FLOW_BOTH):
try:
llm_id = int(params.get("llm_id", 0))
except Exception:
llm_id = 0
if llm_id <= 0:
raise ValueError("请选择大模型")
if not LLMModel.objects.filter(id=llm_id, state=1).exists():
raise ValueError("大模型不存在或已禁用")
if not llm_prompt:
raise ValueError("请输入大模型提示词")
if not llm_validate:
raise ValueError("请输入提示词校验值")
valid_posts = (
BizAlgorithmModel.POST_AREA,
BizAlgorithmModel.POST_LINE_CROSS,
BizAlgorithmModel.POST_LINE_COUNT,
BizAlgorithmModel.POST_DIRECTION,
BizAlgorithmModel.POST_DENSITY,
BizAlgorithmModel.POST_DWELL,
)
if post_process not in valid_posts:
raise ValueError("无效的后处理逻辑")
try:
forward_count_threshold = int(params.get("forward_count_threshold", 0) or 0)
except Exception:
forward_count_threshold = 0
try:
reverse_count_threshold = int(params.get("reverse_count_threshold", 0) or 0)
except Exception:
reverse_count_threshold = 0
forward_count_threshold = max(0, forward_count_threshold)
reverse_count_threshold = max(0, reverse_count_threshold)
if post_process == BizAlgorithmModel.POST_LINE_COUNT:
if forward_count_threshold <= 0 and reverse_count_threshold <= 0:
raise ValueError("越线计数至少设置一个方向的报警阈值(大于 0")
# DIRECTION 后处理参数
try:
ref_angle = float(params.get("ref_angle", 90.0))
except Exception:
ref_angle = 90.0
try:
angle_tolerance = float(params.get("angle_tolerance", 45.0))
except Exception:
angle_tolerance = 45.0
return {
"name": name,
"flow_type": flow_type,
"small_model_id": small_model_id,
"detector_model_id": detector_model_id,
"target_labels": json.dumps(target_labels, ensure_ascii=False),
"llm_id": llm_id,
"llm_prompt": llm_prompt,
"llm_validate": llm_validate,
"post_process": post_process,
"ref_angle": ref_angle,
"angle_tolerance": angle_tolerance,
"forward_count_threshold": forward_count_threshold,
"reverse_count_threshold": reverse_count_threshold,
"state": int(params.get("state", 1)),
}
def algorithm_index(request):
return render(request, 'app/algorithm/index.html', {})
def algorithm_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:
qs = BizAlgorithmModel.objects.select_related('small_model', 'detector_model', 'llm').order_by('-id')
state = request.GET.get('state', '').strip()
if state != '':
qs = qs.filter(state=int(state))
flow = request.GET.get('flow_type', '').strip()
if flow != '':
qs = qs.filter(flow_type=int(flow))
data = [_biz_to_dict(b) for b 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 algorithm_openCheckModels(request):
"""检查所有小模型的模型文件是否存在,返回就绪列表与缺失列表。
用于前端进入页面时全局告警提示,便于排查具体哪个模型成功/失败。
"""
ret = False
msg = LANG_VIEWS_T(request, "msg_unknown_error")
data = {"missing": [], "ok_list": [], "total": 0, "ok_count": 0}
if request.method == 'GET':
__check_ret, __check_msg = f_checkRequestSafe(request)
if __check_ret:
try:
qs = AlgorithmModel.objects.filter(state=1).order_by('id')
total = 0
ok = 0
missing = []
ok_list = []
for a in qs:
total += 1
mf = a.model_file or ""
exists = _check_model_file_exists(mf)
item = {
"id": a.id,
"name": a.name,
"model_file": mf,
"engine": a.inference_engine or "",
}
if exists:
ok += 1
item["hint"] = "模型文件就绪"
ok_list.append(item)
else:
item["hint"] = "模型文件未配置" if not mf else "模型文件不存在"
missing.append(item)
data = {"missing": missing, "ok_list": ok_list, "total": total, "ok_count": ok}
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 algorithm_openOptions(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:
small_models = []
for a in AlgorithmModel.objects.filter(state=1).order_by('-is_default', 'name'):
labels = _parse_labels(a.labels or '[]')
small_models.append({
"id": a.id,
"name": a.name,
"labels": labels,
"algorithm_type": a.algorithm_type,
"task_type": a.task_type,
"model_file": a.model_file or "",
"model_file_exists": _check_model_file_exists(a.model_file or ""),
"engine": a.inference_engine or "",
})
llms = [{"id": x.id, "name": x.name, "model_name": x.model_name}
for x in LLMModel.objects.filter(state=1).order_by('sort', 'id')]
data = {
"small_models": small_models,
"llms": llms,
"post_processes": [
{"value": BizAlgorithmModel.POST_AREA, "label": "区域入侵"},
{"value": BizAlgorithmModel.POST_LINE_CROSS, "label": "越线检测"},
{"value": BizAlgorithmModel.POST_LINE_COUNT, "label": "越线计数"},
{"value": BizAlgorithmModel.POST_DIRECTION, "label": "方向入侵"},
{"value": BizAlgorithmModel.POST_DENSITY, "label": "密度报警"},
{"value": BizAlgorithmModel.POST_DWELL, "label": "滞留报警"},
],
"flow_types": [
{"value": 1, "label": "小模型 + 后处理"},
{"value": 2, "label": "大模型 + 后处理"},
{"value": 3, "label": "小模型 + 大模型 + 后处理"},
{"value": 4, "label": "检测小模型 + ReID小模型 + 后处理"},
],
}
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 algorithm_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:
fields = _validate_biz_fields(params)
BizAlgorithmModel.objects.create(**fields)
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 algorithm_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:
bid = int(params.get("id", 0))
b = BizAlgorithmModel.objects.get(id=bid)
fields = _validate_biz_fields(params, biz_id=bid)
for k, v in fields.items():
setattr(b, k, v)
b.save()
_reload_affected_pipelines(b)
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 algorithm_openDel(request):
ret = False
msg = LANG_VIEWS_T(request, "msg_unknown_error")
data = {"referenced_zones": []}
if request.method == 'POST':
__check_ret, __check_msg = f_checkRequestSafe(request)
if __check_ret:
params = f_parsePostParams(request)
try:
bid = int(params.get("id", 0))
b = BizAlgorithmModel.objects.get(id=bid)
ref_zones = list(
b.zones.select_related('stream').order_by('id')
)
if ref_zones:
# 列出引用该算法的布控名称,便于用户排查并先解除绑定
names = []
for z in ref_zones:
sname = z.stream.nickname if z.stream else ("#%s" % z.stream_id)
names.append("%s/%s" % (sname, z.name))
data["referenced_zones"] = names
raise ValueError("该算法已被 %d 个布控引用,请先解除绑定后再删除(%s"
% (len(ref_zones), "".join(names)))
b.delete()
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 _reload_affected_pipelines(biz):
try:
from app.analysis.manager import AnalysisManager
mgr = AnalysisManager()
stream_ids = set()
for z in biz.zones.select_related('stream').all():
if z.stream_id:
stream_ids.add(z.stream_id)
for sid in stream_ids:
if mgr.is_running(sid):
mgr.reload_zones(sid)
except Exception:
pass
def algorithm_openAssignContext(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:
params = f_parseGetParams(request)
try:
bid = int(params.get("biz_algorithm_id", 0) or params.get("id", 0))
biz = BizAlgorithmModel.objects.get(id=bid)
zones = []
for z in ZoneModel.objects.select_related('stream').order_by('stream_id', 'id'):
selected = z.algorithms.filter(id=bid).exists()
zones.append({
"id": z.id,
"stream_id": z.stream_id,
"stream_name": z.stream.nickname if z.stream else "",
"zone_name": z.name,
"state": z.state,
"selected": selected,
})
data = {
"biz_algorithm": _biz_to_dict(biz),
"zones": zones,
}
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 algorithm_openAssignZones(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:
bid = int(params.get("biz_algorithm_id", 0) or params.get("id", 0))
biz = BizAlgorithmModel.objects.get(id=bid)
zone_ids = params.get("zone_ids") or []
if isinstance(zone_ids, str):
try:
zone_ids = json.loads(zone_ids)
except Exception:
zone_ids = [s for s in zone_ids.split(",") if s.strip()]
zone_ids = {int(x) for x in zone_ids if str(x).strip()}
affected_streams = set()
blocked_zones = []
to_remove = [] # 先收集待解绑区域,校验通过后统一执行
# 取消未选中的绑定
for z in ZoneModel.objects.filter(algorithms=biz).prefetch_related('algorithms'):
if z.id not in zone_ids:
if z.algorithms.count() <= 1:
blocked_zones.append(z.name or ("#%s" % z.id))
continue
to_remove.append(z)
affected_streams.add(z.stream_id)
if blocked_zones:
raise ValueError(
LANG_VIEWS_T(request, "zone_algo_required")
+ " (" + "".join(blocked_zones) + ")"
)
# 校验通过,统一执行解绑
for z in to_remove:
z.algorithms.remove(biz)
# 添加新绑定
if zone_ids:
for z in ZoneModel.objects.filter(id__in=zone_ids):
if not z.algorithms.filter(id=bid).exists():
z.algorithms.add(biz)
affected_streams.add(z.stream_id)
try:
from app.analysis.manager import AnalysisManager
mgr = AnalysisManager()
for sid in affected_streams:
if sid and mgr.is_running(sid):
mgr.reload_zones(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})