fix(aiot): 修复离岗检测启动立即报警的三个Bug

Bug#1(严重): 无人帧不调用算法
- _batch_process_rois 中 len(boxes)>0 才调用 _handle_detections
- 导致离岗检测永远收不到"人走了"的信号
- 修复: 无论检测结果是否为空都调用算法
- 同时移除 _handle_detections 中 tracks 为空的 early return

Bug#2(高): WAITING 一帧就跳 ON_DUTY
- 检测到人第一帧就立即从 WAITING 跳到 ON_DUTY
- confirm_on_duty_sec 参数完全未被使用
- 修复: 新增 CONFIRMING 状态,需连续 10s 检测到人才确认上岗

Bug#3(中): confirm_leave_sec 默认值过短
- 默认 10 秒,用户预期 30 秒
- 修复: 所有默认值统一改为 30s

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-11 10:01:20 +08:00
parent 181623428a
commit b6fba4639d
3 changed files with 29 additions and 14 deletions

View File

@@ -18,6 +18,7 @@ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
class LeavePostAlgorithm:
STATE_WAITING = "WAITING"
STATE_CONFIRMING = "CONFIRMING"
STATE_ON_DUTY = "ON_DUTY"
STATE_LEAVING = "LEAVING"
STATE_OFF_DUTY = "OFF_DUTY"
@@ -26,7 +27,7 @@ class LeavePostAlgorithm:
def __init__(
self,
confirm_on_duty_sec: int = 10,
confirm_leave_sec: int = 10,
confirm_leave_sec: int = 30,
cooldown_sec: int = 600,
working_hours: Optional[List[Dict]] = None,
target_class: Optional[str] = "person",
@@ -140,13 +141,28 @@ class LeavePostAlgorithm:
if self.state == self.STATE_WAITING:
if roi_has_person:
self.state = self.STATE_ON_DUTY
# 检测到人,进入上岗确认阶段
self.state = self.STATE_CONFIRMING
self.state_start_time = current_time
self.detection_history.clear()
self.detection_history.append((current_time, True))
else:
pass
elif self.state == self.STATE_CONFIRMING:
self.detection_history.append((current_time, roi_has_person))
if not roi_has_person:
# 人消失,回到等待状态
self.state = self.STATE_WAITING
self.state_start_time = None
self.detection_history.clear()
else:
elapsed = (current_time - self.state_start_time).total_seconds()
if elapsed >= self.confirm_on_duty_sec:
# 持续在岗达到确认时长,正式确认上岗
self.state = self.STATE_ON_DUTY
self.state_start_time = current_time
elif self.state == self.STATE_ON_DUTY:
self.detection_history.append((current_time, roi_has_person))
if not roi_has_person:
@@ -395,7 +411,7 @@ class AlgorithmManager:
self.default_params = {
"leave_post": {
"confirm_on_duty_sec": 10,
"confirm_leave_sec": 10,
"confirm_leave_sec": 30,
"cooldown_sec": 600,
"target_class": "person",
},
@@ -520,7 +536,7 @@ class AlgorithmManager:
if algo_code == "leave_post":
algo_params = {
"confirm_on_duty_sec": params.get("confirm_on_duty_sec", 10),
"confirm_leave_sec": params.get("confirm_leave_sec", 10),
"confirm_leave_sec": params.get("confirm_leave_sec", 30),
"cooldown_sec": params.get("cooldown_sec", 600),
"working_hours": params.get("working_hours", []),
"target_class": params.get("target_class", bind_config.get("target_class", "person")),
@@ -638,7 +654,7 @@ class AlgorithmManager:
roi_working_hours = algo_params.get("working_hours") or self.working_hours
self.algorithms[roi_id][key]["leave_post"] = LeavePostAlgorithm(
confirm_on_duty_sec=algo_params.get("confirm_on_duty_sec", 10),
confirm_leave_sec=algo_params.get("confirm_leave_sec", 10),
confirm_leave_sec=algo_params.get("confirm_leave_sec", 30),
cooldown_sec=algo_params.get("cooldown_sec", 600),
working_hours=roi_working_hours,
target_class=algo_params.get("target_class", "person"),

View File

@@ -265,7 +265,7 @@ class SQLiteManager:
'target_class': 'person',
'param_schema': json.dumps({
"confirm_on_duty_sec": {"type": "int", "default": 10, "min": 1},
"confirm_leave_sec": {"type": "int", "default": 10, "min": 1},
"confirm_leave_sec": {"type": "int", "default": 30, "min": 1},
"cooldown_sec": {"type": "int", "default": 600, "min": 0},
"working_hours": {"type": "list", "default": []},
}),

View File

@@ -404,7 +404,7 @@ class EdgeInferenceService:
for idx, (camera_id, roi, bind, frame, _, scale_info) in enumerate(roi_items):
boxes, scores, class_ids = batch_results[idx]
if len(boxes) > 0:
# 无论是否检测到目标都要调用算法(离岗检测需要"无人"信号)
self._handle_detections(
camera_id, roi, bind, frame,
boxes, scores, class_ids,
@@ -489,8 +489,7 @@ class EdgeInferenceService:
tracks = self._build_tracks(roi, boxes, scores, class_ids, scale_info)
if not tracks:
return
# 离岗检测需要"无人"信号,不能因为 tracks 为空就跳过算法
# 诊断日志tracks 内容(非告警诊断日志,使用 DEBUG 级别)
self._logger.debug(f"[{camera_id}] tracks: {[t.get('class') for t in tracks]}, target_class={bind.target_class}")