315 lines
9.6 KiB
Python
315 lines
9.6 KiB
Python
|
|
"""
|
|||
|
|
GarbageDetectionAlgorithm 单元测试
|
|||
|
|
|
|||
|
|
覆盖场景:
|
|||
|
|
1. 无垃圾时保持 IDLE
|
|||
|
|
2. 持续检测到垃圾 → 确认 → 告警
|
|||
|
|
3. 冷却期内不重复触发
|
|||
|
|
4. 清理后发 resolve → 回到 IDLE
|
|||
|
|
5. 清理确认期内垃圾再次出现 → 回到 ALARMED
|
|||
|
|
6. reset() 正确清理状态
|
|||
|
|
"""
|
|||
|
|
import sys
|
|||
|
|
import os
|
|||
|
|
import logging
|
|||
|
|
|
|||
|
|
logging.basicConfig(level=logging.INFO, format='%(levelname)s - %(message)s')
|
|||
|
|
|
|||
|
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|||
|
|
|
|||
|
|
from datetime import datetime, timedelta
|
|||
|
|
from algorithms import GarbageDetectionAlgorithm
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ===== 工具函数 =====
|
|||
|
|
|
|||
|
|
def make_tracks(roi_id: str, classes: list, confidences: list = None):
|
|||
|
|
"""生成模拟检测结果"""
|
|||
|
|
if confidences is None:
|
|||
|
|
confidences = [0.85] * len(classes)
|
|||
|
|
tracks = []
|
|||
|
|
for i, cls in enumerate(classes):
|
|||
|
|
tracks.append({
|
|||
|
|
"track_id": f"{roi_id}_{i}",
|
|||
|
|
"class": cls,
|
|||
|
|
"confidence": confidences[i],
|
|||
|
|
"bbox": [100 + i * 50, 100, 200 + i * 50, 300],
|
|||
|
|
"matched_rois": [{"roi_id": roi_id}],
|
|||
|
|
})
|
|||
|
|
return tracks
|
|||
|
|
|
|||
|
|
|
|||
|
|
def simulate(algo, roi_id, camera_id, get_tracks_fn, count, interval=1.0, start_time=None):
|
|||
|
|
"""连续模拟帧,返回所有 alerts 和最后时间戳"""
|
|||
|
|
t = start_time or datetime(2026, 4, 17, 10, 0, 0)
|
|||
|
|
all_alerts = []
|
|||
|
|
for i in range(count):
|
|||
|
|
tracks = get_tracks_fn(i)
|
|||
|
|
alerts = algo.process(roi_id, camera_id, tracks, t)
|
|||
|
|
if alerts:
|
|||
|
|
all_alerts.extend(alerts)
|
|||
|
|
t += timedelta(seconds=interval)
|
|||
|
|
return all_alerts, t
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ===== 测试 1:无垃圾时保持 IDLE =====
|
|||
|
|
|
|||
|
|
def test_idle_when_no_garbage():
|
|||
|
|
print("\n" + "=" * 60)
|
|||
|
|
print("TEST 1: 无垃圾帧始终保持 IDLE")
|
|||
|
|
print("=" * 60)
|
|||
|
|
|
|||
|
|
algo = GarbageDetectionAlgorithm(confirm_garbage_sec=60)
|
|||
|
|
alerts, _ = simulate(
|
|||
|
|
algo, "roi_1", "cam_1",
|
|||
|
|
lambda i: make_tracks("roi_1", ["person"]), # 只有人,没有垃圾
|
|||
|
|
count=100,
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
assert algo.state == "IDLE", f"Expected IDLE, got {algo.state}"
|
|||
|
|
assert len(alerts) == 0, f"Expected no alerts, got {len(alerts)}"
|
|||
|
|
print(f" 状态: {algo.state},alerts: {len(alerts)} [OK]")
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ===== 测试 2:持续检测到垃圾 → 告警 =====
|
|||
|
|
|
|||
|
|
def test_garbage_triggers_alarm():
|
|||
|
|
print("\n" + "=" * 60)
|
|||
|
|
print("TEST 2: 持续 65 秒检测到垃圾 → 告警")
|
|||
|
|
print("=" * 60)
|
|||
|
|
|
|||
|
|
algo = GarbageDetectionAlgorithm(
|
|||
|
|
confirm_garbage_sec=60,
|
|||
|
|
cooldown_sec=1800,
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
alerts, _ = simulate(
|
|||
|
|
algo, "roi_1", "cam_1",
|
|||
|
|
lambda i: make_tracks("roi_1", ["garbage"]),
|
|||
|
|
count=65, # 65 秒,超过 60 秒确认期
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# 应该在第 60-61 秒触发 1 个告警
|
|||
|
|
assert algo.state == "ALARMED", f"Expected ALARMED, got {algo.state}"
|
|||
|
|
assert len(alerts) == 1, f"Expected 1 alert, got {len(alerts)}"
|
|||
|
|
alert = alerts[0]
|
|||
|
|
assert alert["alert_type"] == "garbage"
|
|||
|
|
assert alert["alarm_level"] == 2
|
|||
|
|
assert alert["garbage_count"] == 1
|
|||
|
|
assert "检测到垃圾" in alert["message"]
|
|||
|
|
print(f" 状态: {algo.state},告警: {alert['message']} [OK]")
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ===== 测试 3:冷却期内不重复触发 =====
|
|||
|
|
|
|||
|
|
def test_cooldown_prevents_duplicate():
|
|||
|
|
print("\n" + "=" * 60)
|
|||
|
|
print("TEST 3: 告警后冷却期内持续有垃圾,不重复触发")
|
|||
|
|
print("=" * 60)
|
|||
|
|
|
|||
|
|
algo = GarbageDetectionAlgorithm(
|
|||
|
|
confirm_garbage_sec=10, # 缩短便于测试
|
|||
|
|
confirm_clear_sec=10,
|
|||
|
|
cooldown_sec=300, # 5 分钟冷却
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# 持续 200 秒有垃圾(远超冷却时间但没超过 300 秒)
|
|||
|
|
alerts, _ = simulate(
|
|||
|
|
algo, "roi_1", "cam_1",
|
|||
|
|
lambda i: make_tracks("roi_1", ["garbage"]),
|
|||
|
|
count=200,
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
assert len(alerts) == 1, f"Expected 1 alert (cooldown), got {len(alerts)}"
|
|||
|
|
assert algo.state == "ALARMED"
|
|||
|
|
print(f" 告警次数: {len(alerts)}(冷却期内不重复)[OK]")
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ===== 测试 4:清理后发 resolve → IDLE =====
|
|||
|
|
|
|||
|
|
def test_resolve_after_cleaning():
|
|||
|
|
print("\n" + "=" * 60)
|
|||
|
|
print("TEST 4: 告警后清理 → 发 resolve → IDLE")
|
|||
|
|
print("=" * 60)
|
|||
|
|
|
|||
|
|
algo = GarbageDetectionAlgorithm(
|
|||
|
|
confirm_garbage_sec=10,
|
|||
|
|
confirm_clear_sec=10,
|
|||
|
|
cooldown_sec=300,
|
|||
|
|
)
|
|||
|
|
algo._last_alarm_id = "test_alarm_123" # 模拟 main.py 回填
|
|||
|
|
|
|||
|
|
t = datetime(2026, 4, 17, 10, 0, 0)
|
|||
|
|
all_alerts = []
|
|||
|
|
|
|||
|
|
# Phase 1: 15 秒有垃圾 → 触发告警
|
|||
|
|
for i in range(15):
|
|||
|
|
alerts = algo.process(
|
|||
|
|
"roi_1", "cam_1",
|
|||
|
|
make_tracks("roi_1", ["garbage"]),
|
|||
|
|
t + timedelta(seconds=i)
|
|||
|
|
)
|
|||
|
|
all_alerts.extend(alerts)
|
|||
|
|
assert algo.state == "ALARMED"
|
|||
|
|
|
|||
|
|
# Phase 2: 然后 30 秒无垃圾 → 发 resolve
|
|||
|
|
# 需要等滑动窗口(10s)清空 + confirm_clear_sec(10s) = 20+ 秒
|
|||
|
|
for i in range(15, 45):
|
|||
|
|
alerts = algo.process(
|
|||
|
|
"roi_1", "cam_1",
|
|||
|
|
make_tracks("roi_1", []), # 空
|
|||
|
|
t + timedelta(seconds=i)
|
|||
|
|
)
|
|||
|
|
all_alerts.extend(alerts)
|
|||
|
|
|
|||
|
|
assert algo.state == "IDLE", f"Expected IDLE, got {algo.state}"
|
|||
|
|
resolves = [a for a in all_alerts if a.get("alert_type") == "alarm_resolve"]
|
|||
|
|
assert len(resolves) == 1, f"Expected 1 resolve, got {len(resolves)}"
|
|||
|
|
resolve = resolves[0]
|
|||
|
|
assert resolve["resolve_alarm_id"] == "test_alarm_123"
|
|||
|
|
assert resolve["resolve_type"] == "garbage_removed"
|
|||
|
|
assert resolve["duration_ms"] > 0
|
|||
|
|
print(f" resolve: {resolve['resolve_type']}, 持续 {resolve['duration_ms']}ms [OK]")
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ===== 测试 5:清理期内垃圾再出现 → 回到 ALARMED =====
|
|||
|
|
|
|||
|
|
def test_garbage_reappears_during_clearing():
|
|||
|
|
print("\n" + "=" * 60)
|
|||
|
|
print("TEST 5: 清理确认期内垃圾再出现 → 回到 ALARMED")
|
|||
|
|
print("=" * 60)
|
|||
|
|
|
|||
|
|
algo = GarbageDetectionAlgorithm(
|
|||
|
|
confirm_garbage_sec=10,
|
|||
|
|
confirm_clear_sec=20, # 较长的清理确认期
|
|||
|
|
cooldown_sec=300,
|
|||
|
|
)
|
|||
|
|
algo._last_alarm_id = "test_alarm_456"
|
|||
|
|
|
|||
|
|
t = datetime(2026, 4, 17, 10, 0, 0)
|
|||
|
|
|
|||
|
|
# Phase 1: 15 秒有垃圾 → 告警 → ALARMED
|
|||
|
|
for i in range(15):
|
|||
|
|
algo.process("roi_1", "cam_1", make_tracks("roi_1", ["garbage"]),
|
|||
|
|
t + timedelta(seconds=i))
|
|||
|
|
assert algo.state == "ALARMED"
|
|||
|
|
|
|||
|
|
# Phase 2: 5 秒无垃圾 → CONFIRMING_CLEAR
|
|||
|
|
for i in range(15, 25):
|
|||
|
|
algo.process("roi_1", "cam_1", make_tracks("roi_1", []),
|
|||
|
|
t + timedelta(seconds=i))
|
|||
|
|
assert algo.state == "CONFIRMING_CLEAR", f"got {algo.state}"
|
|||
|
|
|
|||
|
|
# Phase 3: 垃圾又出现 5 秒 → 回到 ALARMED
|
|||
|
|
for i in range(25, 40):
|
|||
|
|
algo.process("roi_1", "cam_1", make_tracks("roi_1", ["garbage"]),
|
|||
|
|
t + timedelta(seconds=i))
|
|||
|
|
|
|||
|
|
assert algo.state == "ALARMED", f"Expected ALARMED, got {algo.state}"
|
|||
|
|
print(f" 状态恢复: CONFIRMING_CLEAR → ALARMED [OK]")
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ===== 测试 6:reset() 清理状态 =====
|
|||
|
|
|
|||
|
|
def test_reset_clears_state():
|
|||
|
|
print("\n" + "=" * 60)
|
|||
|
|
print("TEST 6: reset() 正确清理所有状态")
|
|||
|
|
print("=" * 60)
|
|||
|
|
|
|||
|
|
algo = GarbageDetectionAlgorithm(confirm_garbage_sec=5)
|
|||
|
|
algo._last_alarm_id = "test"
|
|||
|
|
|
|||
|
|
# 先让它进入某个状态
|
|||
|
|
t = datetime(2026, 4, 17, 10, 0, 0)
|
|||
|
|
for i in range(10):
|
|||
|
|
algo.process("roi_1", "cam_1", make_tracks("roi_1", ["garbage"]),
|
|||
|
|
t + timedelta(seconds=i))
|
|||
|
|
assert algo.state == "ALARMED"
|
|||
|
|
assert len(algo._detection_window) > 0
|
|||
|
|
assert len(algo.alert_cooldowns) > 0
|
|||
|
|
|
|||
|
|
# Reset
|
|||
|
|
algo.reset()
|
|||
|
|
|
|||
|
|
assert algo.state == "IDLE"
|
|||
|
|
assert algo.state_start_time is None
|
|||
|
|
assert algo._last_alarm_id is None
|
|||
|
|
assert algo._garbage_start_time is None
|
|||
|
|
assert len(algo._detection_window) == 0
|
|||
|
|
assert len(algo.alert_cooldowns) == 0
|
|||
|
|
print(" 所有状态已清空 [OK]")
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ===== 测试 7:多个垃圾目标计数 =====
|
|||
|
|
|
|||
|
|
def test_multiple_garbage_count():
|
|||
|
|
print("\n" + "=" * 60)
|
|||
|
|
print("TEST 7: ROI 内多个垃圾目标 → garbage_count 正确")
|
|||
|
|
print("=" * 60)
|
|||
|
|
|
|||
|
|
algo = GarbageDetectionAlgorithm(confirm_garbage_sec=5)
|
|||
|
|
|
|||
|
|
alerts, _ = simulate(
|
|||
|
|
algo, "roi_1", "cam_1",
|
|||
|
|
lambda i: make_tracks("roi_1", ["garbage", "garbage", "garbage"]),
|
|||
|
|
count=10,
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
assert len(alerts) == 1
|
|||
|
|
assert alerts[0]["garbage_count"] == 3
|
|||
|
|
print(f" garbage_count: {alerts[0]['garbage_count']} [OK]")
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ===== 测试 8:非 target_class 不计入 =====
|
|||
|
|
|
|||
|
|
def test_non_target_class_ignored():
|
|||
|
|
print("\n" + "=" * 60)
|
|||
|
|
print("TEST 8: person/car 类不计入(只看 garbage)")
|
|||
|
|
print("=" * 60)
|
|||
|
|
|
|||
|
|
algo = GarbageDetectionAlgorithm(confirm_garbage_sec=10)
|
|||
|
|
|
|||
|
|
alerts, _ = simulate(
|
|||
|
|
algo, "roi_1", "cam_1",
|
|||
|
|
lambda i: make_tracks("roi_1", ["person", "car"]), # 都不是 garbage
|
|||
|
|
count=30,
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
assert algo.state == "IDLE", f"Expected IDLE, got {algo.state}"
|
|||
|
|
assert len(alerts) == 0
|
|||
|
|
print(f" 状态: {algo.state},无告警 [OK]")
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ===== 运行所有测试 =====
|
|||
|
|
|
|||
|
|
if __name__ == "__main__":
|
|||
|
|
tests = [
|
|||
|
|
test_idle_when_no_garbage,
|
|||
|
|
test_garbage_triggers_alarm,
|
|||
|
|
test_cooldown_prevents_duplicate,
|
|||
|
|
test_resolve_after_cleaning,
|
|||
|
|
test_garbage_reappears_during_clearing,
|
|||
|
|
test_reset_clears_state,
|
|||
|
|
test_multiple_garbage_count,
|
|||
|
|
test_non_target_class_ignored,
|
|||
|
|
]
|
|||
|
|
passed = 0
|
|||
|
|
failed = 0
|
|||
|
|
for t in tests:
|
|||
|
|
try:
|
|||
|
|
t()
|
|||
|
|
passed += 1
|
|||
|
|
except AssertionError as e:
|
|||
|
|
print(f" FAIL: {e}")
|
|||
|
|
failed += 1
|
|||
|
|
except Exception as e:
|
|||
|
|
print(f" ERROR: {e}")
|
|||
|
|
import traceback
|
|||
|
|
traceback.print_exc()
|
|||
|
|
failed += 1
|
|||
|
|
|
|||
|
|
print("\n" + "=" * 60)
|
|||
|
|
print(f"结果: {passed} 通过, {failed} 失败")
|
|||
|
|
print("=" * 60)
|
|||
|
|
sys.exit(0 if failed == 0 else 1)
|