video_monitor/app/views/AlgorithmView.py

565 lines
23 KiB
Python
Raw Normal View History

2026-08-30 22:22:11 +08:00
# 作者:北小菜
"""业务算法管理 — 小模型/大模型 + 后处理业务逻辑"""
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})