Files
security-ai-edge/docs/code_review_report.md
16337 a9a5457583 优化:算法模块全面重构 — 提取基类、常量化阈值、性能优化
- 提取 BaseAlgorithm 基类,四个算法共享 ROI 检查、目标类过滤、告警 ID 管理
- 硬编码比率阈值提取为类常量(RATIO_ON_DUTY_CONFIRM 等)
- 滑动窗口添加 maxlen=1000 防内存溢出
- tracks 合并遍历 _scan_tracks() 减少重复遍历
- 比率/均值缓存,process() 入口计算一次
- 拥堵消散比例可配置(dissipation_ratio 参数)
- 入侵 CONFIRMING_CLEAR 逻辑拆分为独立方法
- 补齐 AlgorithmType 枚举(illegal_parking、vehicle_congestion)
- 修复 _leave_start_time None guard 防 TypeError
- 修复 AlgorithmManager 默认参数与构造函数不一致
- 热更新补充支持 illegal_parking 和 vehicle_congestion

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 14:10:27 +08:00

647 lines
27 KiB
Markdown
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.

# algorithms.py 代码审查报告
> 审查日期: 2026-04-02
> 审查文件: algorithms.py (1733行)
> 审查范围: LeavePostAlgorithm, IntrusionAlgorithm, IllegalParkingAlgorithm, VehicleCongestionAlgorithm, AlgorithmManager
---
## 1. 功能基线清单
### 1.1 LeavePostAlgorithm (离岗检测)
**状态定义 (7个):**
| 状态 | 常量 | 含义 |
|------|------|------|
| INIT | `STATE_INIT` | 初始化,等待检测到人 |
| CONFIRMING_ON_DUTY | `STATE_CONFIRMING_ON_DUTY` | 上岗确认中(需持续检测到人) |
| ON_DUTY | `STATE_ON_DUTY` | 已确认在岗 |
| CONFIRMING_OFF_DUTY | `STATE_CONFIRMING_OFF_DUTY` | 离岗确认中(持续未检测到人) |
| OFF_DUTY_COUNTDOWN | `STATE_OFF_DUTY_COUNTDOWN` | 离岗倒计时(确认离岗后等待告警) |
| ALARMED | `STATE_ALARMED` | 已告警(等待回岗) |
| NON_WORK_TIME | `STATE_NON_WORK_TIME` | 非工作时间 |
**状态转换矩阵:**
| 从 \ 到 | INIT | CONFIRMING_ON_DUTY | ON_DUTY | CONFIRMING_OFF_DUTY | OFF_DUTY_COUNTDOWN | ALARMED | NON_WORK_TIME |
|---------|------|--------------------|---------|--------------------|-------------------|---------|--------------|
| INIT | - | roi_has_person==True | - | - | - | - | not in_working_hours |
| CONFIRMING_ON_DUTY | detection_ratio==0 | - | elapsed>=confirm_on_duty_sec AND ratio>=0.6 | - | - | - | not in_working_hours |
| ON_DUTY | - | - | - | detection_ratio<0.2 | - | - | not in_working_hours |
| CONFIRMING_OFF_DUTY | - | - | detection_ratio>=0.5 | - | elapsed>=confirm_off_duty_sec AND ratio<0.2 | - | not in_working_hours |
| OFF_DUTY_COUNTDOWN | - | - | roi_has_person==True | - | - | elapsed>=leave_countdown_sec AND cooldown ok | not in_working_hours |
| ALARMED | - | roi_has_person==True | - | - | - | - | not in_working_hours |
| NON_WORK_TIME | in_working_hours | - | - | - | - | - | - |
**关键行为:**
- 使用滑动窗口10秒平滑检测结果计算 detection_ratio
- ALARMED -> CONFIRMING_ON_DUTY 复用上岗确认状态(不是独立的回岗确认状态)
- 进入 ON_DUTY 状态时,若存在 `_last_alarm_id`,自动发送 `alarm_resolve` 事件
- 进入 NON_WORK_TIME 时,若有未结束告警,发送 resolve_type="non_work_time" 的 resolve 事件
- 冷却期检查使用 `cooldown_key = f"{camera_id}_{roi_id}"`
- `_last_alarm_id` 由外部 main.py 通过 `set_last_alarm_id()` 回填
- `_leave_start_time` 在进入 OFF_DUTY_COUNTDOWN 时记录(值等于 state_start_time
**构造函数参数:**
- `confirm_on_duty_sec`: int = 10 (上岗确认窗口)
- `confirm_off_duty_sec`: int = 30 (离岗确认窗口)
- `confirm_return_sec`: int = 10 (回岗确认窗口 -- 注意: 代码中实际未使用此参数)
- `leave_countdown_sec`: int = 300 (离岗倒计时)
- `cooldown_sec`: int = 600 (告警冷却期)
- `working_hours`: Optional[List[Dict]] = None
- `target_class`: Optional[str] = "person"
- `alarm_level`: Optional[int] = None (默认2)
- `confirm_leave_sec`: Optional[int] = None (向后兼容旧参数名)
### 1.2 IntrusionAlgorithm (周界入侵)
**状态定义 (4个):**
| 状态 | 常量 | 含义 |
|------|------|------|
| IDLE | `STATE_IDLE` | 空闲,无入侵 |
| CONFIRMING_INTRUSION | `STATE_CONFIRMING_INTRUSION` | 入侵确认中 |
| ALARMED | `STATE_ALARMED` | 已告警(等待入侵消失) |
| CONFIRMING_CLEAR | `STATE_CONFIRMING_CLEAR` | 入侵消失确认中 |
**状态转换矩阵:**
| 从 \ 到 | IDLE | CONFIRMING_INTRUSION | ALARMED | CONFIRMING_CLEAR |
|---------|------|---------------------|---------|-----------------|
| IDLE | - | roi_has_person==True | - | - |
| CONFIRMING_INTRUSION | not roi_has_person OR cooldown内 OR state_start_time==None | - | elapsed>=confirm_intrusion_seconds AND cooldown ok | - |
| ALARMED | - | - | - | not roi_has_person |
| CONFIRMING_CLEAR | elapsed>=confirm_clear_seconds AND no person | - | person_elapsed>=confirm_intrusion_seconds (持续有人) OR state_start_time==None | - |
**关键行为:**
- 不使用滑动窗口,直接使用当前帧的 `roi_has_person` 判断
- CONFIRMING_CLEAR 有子状态追踪: `_person_detected_in_clear_time` 用于判断短暂有人 vs 持续有人
- 冷却期内入侵确认直接回到 IDLE不触发告警
- 包含防御性编程: state_start_time==None 时重置到 IDLE
- `_check_target_class` 允许 target_class 为 None匹配所有类别
**构造函数参数:**
- `cooldown_seconds`: int = 300
- `confirm_seconds`: int = 5 (向后兼容)
- `confirm_intrusion_seconds`: Optional[int] = None (默认使用 confirm_seconds)
- `confirm_clear_seconds`: Optional[int] = None (默认180)
- `target_class`: Optional[str] = None
- `alarm_level`: Optional[int] = None (默认1)
### 1.3 IllegalParkingAlgorithm (车辆违停)
**状态定义 (5个):**
| 状态 | 常量 | 含义 |
|------|------|------|
| IDLE | `STATE_IDLE` | 空闲 |
| CONFIRMING_VEHICLE | `STATE_CONFIRMING_VEHICLE` | 车辆确认中 |
| PARKED_COUNTDOWN | `STATE_PARKED_COUNTDOWN` | 违停倒计时 |
| ALARMED | `STATE_ALARMED` | 已告警 |
| CONFIRMING_CLEAR | `STATE_CONFIRMING_CLEAR` | 消失确认中 |
**状态转换矩阵:**
| 从 \ 到 | IDLE | CONFIRMING_VEHICLE | PARKED_COUNTDOWN | ALARMED | CONFIRMING_CLEAR |
|---------|------|-------------------|-----------------|---------|-----------------|
| IDLE | - | roi_has_vehicle | - | - | - |
| CONFIRMING_VEHICLE | ratio<0.3 OR state_start_time==None | - | elapsed>=confirm_vehicle_sec AND ratio>=0.6 | - | - |
| PARKED_COUNTDOWN | ratio<0.2 (车离开) OR cooldown内 OR state_start_time==None | - | - | elapsed>=parking_countdown_sec AND cooldown ok | - |
| ALARMED | - | - | - | - | ratio<0.15 |
| CONFIRMING_CLEAR | elapsed>=confirm_clear_sec AND ratio<0.2 OR state_start_time==None | - | - | ratio>=0.5 | - |
**关键行为:**
- 使用滑动窗口WINDOW_SIZE_SEC=10秒
- 支持多类车辆: target_classes 默认 ["car", "truck", "bus", "motorcycle"]
- 告警字段包含 `confidence``duration_minutes`
- CONFIRMING_CLEAR -> IDLE 时清除 alert_cooldowns新车违停可正常告警
- ALARMED 进入 CONFIRMING_CLEAR 的阈值(0.15)比其他算法更严格
**构造函数参数:**
- `confirm_vehicle_sec`: int = 15
- `parking_countdown_sec`: int = 300
- `confirm_clear_sec`: int = 120
- `cooldown_sec`: int = 1800
- `target_classes`: Optional[List[str]] = None (默认 ["car", "truck", "bus", "motorcycle"])
- `alarm_level`: Optional[int] = None (默认1)
### 1.4 VehicleCongestionAlgorithm (车辆拥堵)
**状态定义 (4个):**
| 状态 | 常量 | 含义 |
|------|------|------|
| NORMAL | `STATE_NORMAL` | 正常 |
| CONFIRMING_CONGESTION | `STATE_CONFIRMING_CONGESTION` | 拥堵确认中 |
| CONGESTED | `STATE_CONGESTED` | 拥堵中 |
| CONFIRMING_CLEAR | `STATE_CONFIRMING_CLEAR` | 消散确认中 |
**状态转换矩阵:**
| 从 \ 到 | NORMAL | CONFIRMING_CONGESTION | CONGESTED | CONFIRMING_CLEAR |
|---------|--------|----------------------|-----------|-----------------|
| NORMAL | - | avg_count >= count_threshold | - | - |
| CONFIRMING_CONGESTION | avg_count < count_threshold OR cooldown内 OR state_start_time==None | - | elapsed >= confirm_congestion_sec AND cooldown ok | - |
| CONGESTED | - | - | - | avg_count < count_threshold * 0.5 |
| CONFIRMING_CLEAR | elapsed >= confirm_clear_sec OR state_start_time==None | - | avg_count >= count_threshold | - |
**关键行为:**
- 使用滑动窗口WINDOW_SIZE_SEC=10秒存储车辆计数取平均值判断
- 消散需车辆数降到阈值的 **50%** 以下才开始确认(避免抖动)
- CONFIRMING_CLEAR -> NORMAL 时清除 alert_cooldowns
- 告警字段包含 `vehicle_count``confidence`
**构造函数参数:**
- `count_threshold`: int = 5
- `confirm_congestion_sec`: int = 60
- `confirm_clear_sec`: int = 180
- `cooldown_sec`: int = 1800
- `target_classes`: Optional[List[str]] = None (默认 ["car", "truck", "bus", "motorcycle"])
- `alarm_level`: Optional[int] = None (默认2)
### 1.5 AlgorithmManager
**数据结构:**
```
self.algorithms: Dict[str, Dict[str, Dict[str, Algorithm]]]
结构: { roi_id: { "{roi_id}_{bind_id}": { algo_type: algo_instance } } }
```
**公开方法:**
- `start_config_subscription()` - 启动 Redis 配置订阅
- `stop_config_subscription()` - 停止配置订阅
- `load_bind_from_redis(bind_id)` - 从 Redis 加载单个绑定配置
- `reload_bind_algorithm(bind_id)` - 重载单个绑定
- `reload_algorithm(roi_id)` - 重载单个 ROI 的所有算法
- `update_algorithm_params(roi_id, bind_id, bind_config)` - 仅更新参数,保留状态
- `reload_all_algorithms(preserve_state=True)` - 重载全部算法
- `register_algorithm(roi_id, bind_id, algorithm_type, params)` - 注册算法(带缓存)
- `process(roi_id, bind_id, camera_id, algorithm_type, tracks, current_time)` - 处理检测结果
- `update_roi_params(roi_id, bind_id, algorithm_type, params)` - 更新参数
- `reset_algorithm(roi_id, bind_id=None)` - 重置算法状态
- `reset_all()` - 重置所有算法
- `remove_roi(roi_id)` - 移除 ROI
- `remove_bind(roi_id, bind_id)` - 移除绑定
- `get_status(roi_id)` - 获取状态
---
## 2. 接口契约清单
### 2.1 process() 方法统一签名
所有四个算法的 `process()` 方法具有相同签名:
```python
def process(
self,
roi_id: str,
camera_id: str,
tracks: List[Dict],
current_time: Optional[datetime] = None,
) -> List[Dict]
```
**tracks 输入格式 (每个元素):**
```python
{
"track_id": str, # 跟踪ID
"class": str, # 检测类别 ("person", "car", "truck", ...)
"confidence": float, # 置信度
"bbox": List[float], # 边界框 [x1, y1, x2, y2]
"matched_rois": [ # 匹配的ROI列表
{"roi_id": str}
],
}
```
### 2.2 告警输出格式
#### LeavePostAlgorithm 告警:
```python
{
"track_id": str, # 等于 roi_id
"camera_id": str,
"bbox": List[float], # 可能为空 []
"alert_type": "leave_post",
"alarm_level": int, # 默认 2
"message": "人员离岗告警",
"first_frame_time": str, # 格式: '%Y-%m-%d %H:%M:%S'
}
```
注意: leave_post 告警使用 `track_id` 而非 `roi_id` 字段名(与其他算法不同)。
#### IntrusionAlgorithm 告警:
```python
{
"roi_id": str,
"camera_id": str,
"bbox": List[float],
"alert_type": "intrusion",
"alarm_level": int, # 默认 1
"message": "检测到周界入侵",
"first_frame_time": str, # 格式: '%Y-%m-%d %H:%M:%S'
}
```
#### IllegalParkingAlgorithm 告警:
```python
{
"roi_id": str,
"camera_id": str,
"bbox": List[float],
"alert_type": "illegal_parking",
"alarm_level": int, # 默认 1
"confidence": float,
"message": str, # 动态生成,包含停留分钟数
"first_frame_time": str, # 格式: '%Y-%m-%d %H:%M:%S',可能为 None
"duration_minutes": float,
}
```
#### VehicleCongestionAlgorithm 告警:
```python
{
"roi_id": str,
"camera_id": str,
"bbox": List[float],
"alert_type": "vehicle_congestion",
"alarm_level": int, # 默认 2
"confidence": float,
"message": str, # 动态生成,包含平均车辆数和持续秒数
"first_frame_time": str, # 格式: '%Y-%m-%d %H:%M:%S',可能为 None
"vehicle_count": int,
}
```
#### alarm_resolve 事件 (所有算法统一格式):
```python
{
"alert_type": "alarm_resolve",
"resolve_alarm_id": str,
"duration_ms": int,
"last_frame_time": str, # 格式: '%Y-%m-%d %H:%M:%S'
"resolve_type": str, # "person_returned" | "non_work_time" | "intrusion_cleared" | "vehicle_left" | "congestion_cleared"
}
```
### 2.3 AlgorithmManager.process() 签名
```python
def process(
self,
roi_id: str,
bind_id: str,
camera_id: str,
algorithm_type: str,
tracks: List[Dict],
current_time: Optional[datetime] = None,
) -> List[Dict]
```
### 2.4 AlgorithmManager.register_algorithm() 签名
```python
def register_algorithm(
self,
roi_id: str,
bind_id: str,
algorithm_type: str, # "leave_post" | "intrusion" | "illegal_parking" | "vehicle_congestion"
params: Optional[Dict[str, Any]] = None,
)
```
---
## 3. 已发现的潜在问题
### 3.1 Critical (必须修复)
**[C1] LeavePostAlgorithm: `confirm_return_sec` 参数声明但从未使用**
- 位置: 第53行声明但状态机中 ALARMED -> CONFIRMING_ON_DUTY -> ON_DUTY 的转换直接复用 `confirm_on_duty_sec`
- 影响: 使用者设置 `confirm_return_sec` 以为可以独立控制回岗确认时长,但实际无效
- 建议: 文档中声明此参数复用 `confirm_on_duty_sec`,或实现独立的回岗确认逻辑
**[C2] LeavePostAlgorithm: resolve 事件的 duration_ms 计算依赖 `_leave_start_time`,但该值可能为 None**
- 位置: 第325行 `duration_ms = int((current_time - self._leave_start_time).total_seconds() * 1000)`
- 场景: 如果算法在 OFF_DUTY_COUNTDOWN 之前(即 _leave_start_time 赋值之前)因某种异常跳到 ON_DUTY 且 _last_alarm_id 非空,会抛出 TypeError
- 风险: 低概率但会导致该帧整个 process 调用抛异常
**[C3] LeavePostAlgorithm: `_leave_start_time` 赋值时机问题**
- 位置: 第277行 `self._leave_start_time = self.state_start_time`
- `state_start_time` 此时是 CONFIRMING_OFF_DUTY 的开始时间(非 OFF_DUTY_COUNTDOWN 的开始时间,因为 state_start_time 在下一行第276行才被更新为 current_time
- 实际效果: `_leave_start_time` 记录的是**离岗确认开始时间**,不是**倒计时开始时间**
- 审查结论: 这是有意设计,离开时间应该从人离开被确认开始计算,但代码注释"记录离开时间"可能造成误解
### 3.2 Important (应该修复)
**[I1] LeavePostAlgorithm 告警字典使用 `track_id` 而非 `roi_id`**
- 位置: 第299行 `"track_id": roi_id`
- 其他三个算法统一使用 `"roi_id": roi_id`
- 影响: main.py 中的 `_handle_detections` 不直接使用此字段(它有自己的 roi_id所以不影响功能但接口不一致
**[I2] AlgorithmManager 缺乏线程安全**
- `process()` 方法未加锁第1637-1651行`register_algorithm()` 也未加锁
- `_update_lock` 仅在 `load_bind_from_redis``reload_all_algorithms` 中使用
- 风险: 如果 config_update_worker 线程调用 `reload_all_algorithms` 同时主线程调用 `process`,可能读到不一致的 `self.algorithms` 字典
- 缓解: Python GIL 在字典读操作上提供了一定程度的原子性保护,实际崩溃概率很低
**[I3] AlgorithmManager.default_params 中 illegal_parking 的 confirm_clear_sec 默认值(30) 与 IllegalParkingAlgorithm 构造函数默认值(120) 不一致**
- 位置: 第1233行 vs 第751行
- 影响: 通过 AlgorithmManager 创建的 illegal_parking 算法 confirm_clear_sec 为 30直接创建为 120
**[I4] AlgorithmManager.default_params 中 vehicle_congestion 的 count_threshold 默认值(3) 与 VehicleCongestionAlgorithm 构造函数默认值(5) 不一致**
- 位置: 第1237行 vs 第1004行
- 影响: 通过 AlgorithmManager 创建的算法阈值为 3直接创建为 5
**[I5] `update_algorithm_params` 仅支持 leave_post 和 intrusion**
- 位置: 第1461-1496行
- 缺少 illegal_parking 和 vehicle_congestion 的参数更新逻辑第1494行注释 "其他算法类型可以在此添加"
- 影响: `reload_all_algorithms(preserve_state=True)` 对 illegal_parking/vehicle_congestion 会回退到 `load_bind_from_redis`,会重置算法状态
**[I6] `load_bind_from_redis` 仅支持 leave_post 和 intrusion**
- 位置: 第1322-1403行
- 缺少 illegal_parking 和 vehicle_congestion 的 Redis 加载逻辑
- 影响: 从 Redis 热更新配置时,这两种算法无法被加载
**[I7] IntrusionAlgorithm: `_get_latest_bbox` 不检查 target_class**
- 位置: 第456-460行
- 与 LeavePostAlgorithm 不同第139-142行会检查 target_class
- 影响: 可能返回非目标类别的 bbox
**[I8] `_is_in_working_hours` 不支持跨午夜时间段**
- 位置: 第112行 `if start_minutes <= current_minutes < end_minutes`
- 如果 working_hours 配置为 `{"start": "22:00", "end": "06:00"}`,则无法正确判断
- 影响: 夜班场景可能不工作
### 3.3 Suggestions (建议改进)
**[S1] 滑动窗口的 window_size_sec 硬编码为 10 秒**
- LeavePostAlgorithm 第80行: `self.window_size_sec = 10`
- IllegalParkingAlgorithm 第744行: `WINDOW_SIZE_SEC = 10`
- VehicleCongestionAlgorithm 第1000行: `WINDOW_SIZE_SEC = 10`
- 建议: 提取为可配置参数
**[S2] LeavePostAlgorithm._update_detection_window 与 IllegalParkingAlgorithm._update_window 实现逻辑相同但代码重复**
- 可提取为基类方法或工具函数
**[S3] LeavePostAlgorithm.get_state() 使用 datetime.now() 而非参数传入的 current_time**
- 位置: 第367-372行
- 在测试场景中会导致状态信息不准确(不影响核心逻辑,仅影响监控展示)
**[S4] AlgorithmManager.get_status() 中 leave_post 分支访问 `alarm_sent` 属性**
- 位置: 第1724行
- LeavePostAlgorithm 实际上没有 `alarm_sent` 属性getattr 返回 False
- 这是旧版本残留代码
**[S5] AlgorithmManager.remove_roi() 中 bind_id 解析逻辑脆弱**
- 位置: 第1703行 `key.split("_")[-1]`
- 如果 bind_id 本身包含下划线,解析会出错
- key 格式为 `"{roi_id}_{bind_id}"`,应该用 `key[len(roi_id)+1:]` 提取 bind_id
**[S6] `_is_in_working_hours` 中的 bare `except:` (第99行, 第124行)**
- 应该至少 except Exception 或更具体的异常类型
---
## 4. 测试覆盖分析
### 4.1 test_leave_post_full_workflow.py 覆盖的场景
| 场景 | 状态路径 | 覆盖 |
|------|----------|------|
| 上岗确认成功 | INIT -> CONFIRMING_ON_DUTY -> ON_DUTY | YES |
| 离岗确认 | ON_DUTY -> CONFIRMING_OFF_DUTY -> OFF_DUTY_COUNTDOWN | YES |
| 倒计时触发告警 | OFF_DUTY_COUNTDOWN -> ALARMED | YES |
| 回岗 resolve | ALARMED -> CONFIRMING_ON_DUTY -> ON_DUTY (+ resolve) | YES |
| 告警字段验证 | 无 duration_minutes, 有 first_frame_time | YES |
| resolve 字段验证 | duration_ms, resolve_alarm_id, resolve_type | YES |
| set_last_alarm_id 回填 | - | YES |
### 4.2 test_vehicle_algorithms.py 覆盖的场景
**IllegalParkingAlgorithm:**
| 场景 | 覆盖 |
|------|------|
| 完整生命周期 IDLE->CONFIRMING->COUNTDOWN->ALARMED->CLEAR->IDLE | YES |
| 车辆短暂路过不触发 | YES |
| 多类车辆检测 (truck, bus) | YES |
| person 不触发违停 | YES |
| 冷却期内不重复告警 | YES |
| resolve 事件发送 | YES |
**VehicleCongestionAlgorithm:**
| 场景 | 覆盖 |
|------|------|
| 完整生命周期 NORMAL->CONFIRMING->CONGESTED->CLEAR->NORMAL | YES |
| 少于阈值不触发 | YES |
| 短暂拥堵不触发 | YES |
| resolve 事件发送 | YES |
**AlgorithmManager:**
| 场景 | 覆盖 |
|------|------|
| 注册 illegal_parking | YES |
| 注册 vehicle_congestion | YES |
| process 调用 | YES |
| get_status 调用 | YES |
| 重复注册走缓存 | YES |
| reset_algorithm | YES |
### 4.3 测试覆盖缺口
**LeavePostAlgorithm 未覆盖:**
- [ ] CONFIRMING_ON_DUTY -> INIT (人消失)
- [ ] CONFIRMING_OFF_DUTY -> ON_DUTY (人回来ratio>=0.5)
- [ ] OFF_DUTY_COUNTDOWN -> ON_DUTY (倒计时期间回来)
- [ ] 非工作时间自动 resolve
- [ ] NON_WORK_TIME -> INIT (工作时间恢复)
- [ ] 冷却期内不重复告警
- [ ] 空 tracks 输入
- [ ] working_hours 配置解析(字符串格式)
**IntrusionAlgorithm 完全未测试:**
- [ ] 完整生命周期 IDLE->CONFIRMING->ALARMED->CLEAR->IDLE
- [ ] 入侵确认中人消失
- [ ] CONFIRMING_CLEAR 中短暂有人 vs 持续有人
- [ ] 冷却期
- [ ] resolve 事件
- [ ] target_class=None 匹配所有类别
- [ ] state_start_time==None 的防御性代码分支
**IllegalParkingAlgorithm 未覆盖:**
- [ ] state_start_time==None 的防御性代码分支 (CONFIRMING_VEHICLE, PARKED_COUNTDOWN, CONFIRMING_CLEAR)
- [ ] CONFIRMING_CLEAR -> ALARMED (车辆又出现, ratio>=0.5)
- [ ] ALARMED 状态下 ratio 在 0.15-0.5 之间(维持 ALARMED
**VehicleCongestionAlgorithm 未覆盖:**
- [ ] state_start_time==None 的防御性代码分支
- [ ] CONFIRMING_CLEAR -> CONGESTED (又拥堵了)
- [ ] 消散阈值 0.5*count_threshold 的边界值
- [ ] 冷却期测试
**AlgorithmManager 未覆盖:**
- [ ] start_config_subscription / stop_config_subscription
- [ ] load_bind_from_redis
- [ ] reload_all_algorithms (含孤立实例清理)
- [ ] update_algorithm_params
- [ ] remove_roi / remove_bind
- [ ] 并发调用安全性
- [ ] register_algorithm 的 leave_post 和 intrusion 类型
---
## 5. 优化安全边界
### 5.1 不可修改区域 (功能合约)
以下代码是外部依赖的契约,修改会破坏 main.py 或其他模块:
1. **所有算法的 `process()` 方法签名** -- main.py 的 `_handle_detections` 直接调用
2. **告警字典的字段名和类型** -- main.py 的 `_handle_detections` 依赖 `alert_type`, `alarm_level`, `confidence`, `bbox`, `message`, `first_frame_time`, `duration_minutes`, `vehicle_count`
3. **`alarm_resolve` 事件格式** -- main.py 的 resolve 逻辑依赖 `resolve_alarm_id`, `duration_ms`, `last_frame_time`, `resolve_type`
4. **`set_last_alarm_id(alarm_id)` 方法** -- main.py 回填 alarm_id
5. **`reset()` 方法** -- AlgorithmManager 调用
6. **`get_state()` 方法** -- AlgorithmManager.get_status() 调用
7. **AlgorithmManager.process() 签名和返回值** -- main.py 直接调用
8. **AlgorithmManager.register_algorithm() 签名** -- main.py 直接调用
9. **AlgorithmManager.algorithms 的三层字典结构** -- main.py 直接访问内部实例来获取 `_leave_start_time` 等属性 (第905-911行)
### 5.2 可安全优化区域
以下代码修改不会影响外部行为:
1. **滑动窗口实现** -- `_update_detection_window`, `_update_window`, `_update_count_window` 的内部实现可以优化,只要 `_get_detection_ratio()`, `_get_window_ratio()`, `_get_avg_count()` 的语义不变
2. **`_check_detection_in_roi` / `_check_target_class` / `_check_target_classes`** -- 内部实现可优化,接口不变即可
3. **`_get_latest_bbox` / `_get_max_confidence`** -- 辅助方法,内部实现可优化
4. **`_is_in_working_hours` / `_parse_time_to_minutes`** -- 内部实现可优化(建议修复跨午夜问题)
5. **AlgorithmManager 的 Redis 相关方法** -- `load_bind_from_redis`, `reload_*`, `_config_update_worker` 可以修改,不影响算法核心逻辑
6. **日志输出** -- 所有 logger.* 调用可以调整
7. **`default_params` 字典** -- 可以修正默认值不一致的问题
### 5.3 高风险修改区域 (需要完整回归测试)
1. **状态转换条件 (ratio 阈值)** -- 任何 detection_ratio, window_ratio 的阈值变更都可能影响告警灵敏度
- LeavePostAlgorithm: 0.6 (上岗), 0.2 (离岗开始), 0.5 (离岗恢复), 0.2 (离岗确认)
- IllegalParkingAlgorithm: 0.3 (放弃确认), 0.6 (确认有车), 0.2 (车离开), 0.15 (开始消失确认), 0.5 (车又来), 0.2 (消失确认)
- VehicleCongestionAlgorithm: count_threshold (开始确认), 0.5*count_threshold (开始消散)
2. **时间比较逻辑** -- `elapsed >= xxx_sec` 的方向(大于等于 vs 大于)
3. **冷却期检查** -- `cooldown_key` 的构造方式和比较逻辑
4. **resolve 事件触发逻辑** -- LeavePostAlgorithm 的 "进入 ON_DUTY 且 _last_alarm_id 存在" 的检查
### 5.4 重复代码可提取区域
以下方法在多个算法中重复实现,可以提取为基类或 mixin
| 方法 | 出现在 | 可提取 |
|------|--------|--------|
| `_check_detection_in_roi` | 全部4个 | YES |
| `_check_target_class` | LeavePost, Intrusion | YES |
| `_check_target_classes` | IllegalParking, VehicleCongestion | YES |
| `_get_latest_bbox` | 全部4个 | YES (注意 Intrusion 不检查 target_class) |
| `_get_max_confidence` | IllegalParking, VehicleCongestion | YES |
| `set_last_alarm_id` | 全部4个 | YES |
| 滑动窗口逻辑 | LeavePost, IllegalParking, VehicleCongestion | YES |
---
## 6. config_models.py 与 algorithms.py 的一致性
### 6.1 AlgorithmType 枚举缺失
`config_models.py` 中的 `AlgorithmType` 枚举:
```python
LEAVE_POST = "leave_post"
INTRUSION = "intrusion"
CROWD_DETECTION = "crowd_detection" # 已在 algorithms.py 中注释掉
FACE_RECOGNITION = "face_recognition" # algorithms.py 中不存在
```
缺失:
- `ILLEGAL_PARKING = "illegal_parking"` -- algorithms.py 已实现但枚举未添加
- `VEHICLE_CONGESTION = "vehicle_congestion"` -- algorithms.py 已实现但枚举未添加
### 6.2 ROIInfo 默认值
`config_models.py``ROIInfo.confirm_leave_sec` 默认值为 **10**,而 `AlgorithmManager.default_params["leave_post"]["confirm_leave_sec"]`**30**`LeavePostAlgorithm.confirm_off_duty_sec` 默认为 **30**
---
## 7. main.py 集成要点
### 7.1 main.py 对 algorithms.py 的依赖
1. **直接访问算法内部属性** (第905-911行):
```python
for attr in ('_leave_start_time', '_parking_start_time', '_congestion_start_time', '_intrusion_start_time'):
val = getattr(algo, attr, None)
```
这是紧耦合,如果内部属性名变更会导致 first_frame_time 丢失。
2. **alarm_id 回填** (第943-945行):
```python
algo.set_last_alarm_id(alarm_info.alarm_id)
```
3. **两层去重机制**:
- ROI级别: `_active_alarms[f"{roi_id}_{alert_type}"]`
- 摄像头级别: `_camera_alert_cooldown[f"{camera_id}_{alert_type}"]` (30秒冷却)
4. **duration_ms 在 ext_data 中的计算** (第925行):
```python
"duration_ms": int(alert.get("duration_minutes", 0) * 60 * 1000) if alert.get("duration_minutes") else None,
```
仅 IllegalParkingAlgorithm 的告警包含 `duration_minutes`,其他算法的 ext_data.duration_ms 为 None。
---
## 附录: 状态机可视化
### LeavePostAlgorithm
```
+--> NON_WORK_TIME --+
| (any state) | (in_working_hours)
| v
INIT --+--> CONFIRMING_ON_DUTY ---> ON_DUTY ---> CONFIRMING_OFF_DUTY
^ | ^ |
+----------+ | v
(ratio==0) | OFF_DUTY_COUNTDOWN
| | |
| (roi_has_person) | v
| +---------------+ ALARMED
| | |
+-------+ (roi_has_person) |
ON_DUTY <-- (if _last_alarm_id, send resolve)
```
### IntrusionAlgorithm
```
IDLE ---> CONFIRMING_INTRUSION ---> ALARMED ---> CONFIRMING_CLEAR ---> IDLE
^ | | |
+------------+ +--------+
(person gone) (person back >= confirm_intrusion_sec)
-> ALARMED
```
### IllegalParkingAlgorithm
```
IDLE --> CONFIRMING_VEHICLE --> PARKED_COUNTDOWN --> ALARMED --> CONFIRMING_CLEAR --> IDLE
^ | | |
+----------+ | v
(ratio<0.3) (ratio<0.2) | ALARMED
^ | | (ratio>=0.5)
+--------------------------+ v
ALARMED
```
### VehicleCongestionAlgorithm
```
NORMAL --> CONFIRMING_CONGESTION --> CONGESTED --> CONFIRMING_CLEAR --> NORMAL
^ | |
+--------------+ v
(avg<threshold) CONGESTED
(avg>=threshold)
```