test(v1): lock reconnect and disconnect-no-alarm behavior; close T-203
Adds a mid-stream drop recovery test and a pipeline test proving a disconnected frame yields no events and interrupts a pending suspect (stream loss never alarms). Live preview and fall detection on the real Hikvision stream were confirmed on Windows. 74 tests pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+1
-1
@@ -27,7 +27,7 @@
|
||||
| --- | --- | --- | --- | --- |
|
||||
| T-201 | 实现 PyQt 顶部双 Tab 的监控与设置界面 | T-106 | 实时监控 Tab 显示视频、骨架、ID、NORMAL/SUSPECT/CONFIRMED 和连接状态;设置 Tab 只编辑非敏感草稿并明确下次启动生效;UI 不直接执行事件计算。 | DONE |
|
||||
| T-202 | 实现本地报警、弹窗、截图和 JSONL 日志 | T-201 | CONFIRMED 产生一次声音、一次弹窗、一张带标注截图和一条事件日志。 | DONE |
|
||||
| T-203 | 接入海康 RTSP 与断流恢复 | T-202 | 现场有效流可预览;断流状态明确、可重连且不报警。 | DOING |
|
||||
| T-203 | 接入海康 RTSP 与断流恢复 | T-202 | 现场有效流可预览;断流状态明确、可重连且不报警。 | DONE |
|
||||
| T-204 | 建立正反例录像回归与现场验收记录 | T-203 | 摔倒、行走、坐下、弯腰、捡物和持续倒地都有预期结果;报警延迟记录在 1–3 秒。 | BLOCKED |
|
||||
| T-205 | 固化客户演示脚本与 V1 发布包 | T-204 | 可按文档完成演示脚本,所有证据可复查,适用边界显示给操作员。 | BLOCKED |
|
||||
| T-206 | 结构化摄像头配置、连接测试与设置页预览 | T-201 | 配置支持 host/端口/通道/账号/密码 结构化来源并拼出 RTSP(凭证百分号编码);公开示例保持无凭证、`config_version` 不含凭证;设置页可掩码输入、点击测试连接,连通后抓拍一帧显示在预览区。 | DONE |
|
||||
|
||||
@@ -5,13 +5,13 @@
|
||||
## 当前快照
|
||||
|
||||
- 日期:2026-07-21
|
||||
- 阶段:V1 事件管线、PyQt 双 Tab 界面、本地报警工件、结构化摄像头配置/连接测试、抓流低延迟参数与每帧判定诊断叠加已建立;T-202、T-206、T-207、T-208、T-209 已验收,T-203 进行中(真实海康流已连通,GUI 现场预览待 Windows 冒烟)。判定灵敏度默认已放宽(低位俯视:持续水平即可疑,膝踝可缺,水平阈值 45°),确认窗为主防误报闸;真实误报率待 T-204 验证。
|
||||
- 阶段:V1 事件管线、PyQt 双 Tab 界面、本地报警工件、结构化摄像头配置/连接测试、抓流低延迟参数与每帧判定诊断叠加已建立;T-202、T-203、T-206、T-207、T-208、T-209、T-210 已验收(T-203 真实海康流现场跑通实时预览与摔倒确认,断流/重连由确定性测试锁定);下一可领取为 T-204(需现场正反例录像)。判定灵敏度默认已放宽(低位俯视:持续水平即可疑,膝踝可缺,水平阈值 45°),确认窗为主防误报闸;真实误报率待 T-204 验证。
|
||||
- 已验证环境:Windows PowerShell;Python 3.8.10;Ultralytics 8.3.205;PyQt5 可导入。注意:当前 WSL/Linux 环境未安装 PyQt5,且其 ultralytics/torch 版本与该基线不兼容(模型前向不可用),故 GUI 可视化与真实模型端到端渲染只在 Windows 验证;本环境用 Qt-free 的 `view_model.py` 单元测试与假适配器管线冒烟覆盖视图逻辑。
|
||||
- 旧生产基线:`demo/main.py`、`demo/fall_detection_gui.py`、`demo/detect_fall.py`、`demo/best.pt`。
|
||||
- V1 代码:已建立安全配置、显式 Replay/Stream 视频源、Pose、轻量跟踪、质量/几何证据、倒地领域规则、按 ID 四态事件机及 `v1/pipeline.py` 事件管线;运行事件带非敏感 `config_version`,缺帧/低质量/断流会中断证据确认。新增 `v1/view_model.py`(Qt-free 监控视图状态与设置草稿三份隔离)、`v1/gui.py` 薄 PyQt5 双 Tab 外壳与 `v1/app.py` 装配(`FrameWorker` 只发出已判定的 `FrameAnalysis`,窗口只渲染)。新增 `v1/alerts.py`:按 `event_id` 去重,对首次 CONFIRMED 保存带标注截图、追加 JSONL 事件行,并经可注入 `AlertSink`(Windows 侧 `QtAlertSink` 提供声音与一次性弹窗)触发一次声音/弹窗。新增 `v1/camera.py`(RTSP URL 构建 + 有界连接测试抓帧)与 `config.py` 的结构化来源(host/端口/通道/账号/密码,凭证百分号编码)、`write_local_camera_source`;设置页新增摄像头连接分组、测试连接与预览,以及传输协议/连接超时/低延迟三项抓流调优(由 `config` 存值、`app` 开流前写入 `OPENCV_FFMPEG_CAPTURE_OPTIONS`)。真实海康流已用 `VideoSource(STREAM)` 连通(1920×1080)。
|
||||
- V2 代码:`v2/` 目录存在但尚无实现。
|
||||
- 非代码设计工件:docs/ui/silver-pose-ui-ux-spec.md、docs/ui/2026-07-20-html-prototype-plan.md、docs/ui/silver-pose-v1-prototype.html 与 docs/ui/silver-pose-v2-prototype.html 已建立。v2 HTML 是符合正式浅色 Windows 规范的当前视觉参考:浅灰蓝底、白色卡片,红色只表示确认摔倒、其弹窗和事件证据;文件名中的 v2 只表示原型设计修订,不能理解为 Go V2 实现已开始。v1 HTML 保留为历史深色对照。两者均使用顶部双 Tab、设置草稿与状态交互,且画面、事件和时间都是模拟数据,不连接真实摄像头、模型或网络,也不改变 Phase 1 任务顺序。
|
||||
- 测试:`python -m compileall -q v1 demo` 已通过(含 `gui.py`、`app.py`、`alerts.py`、`camera.py` 语法);`python -m pytest v1/tests -v` 当前有 72 项配置、视频源、Pose、跟踪、证据、领域规则、状态机、管线、视图模型、报警工件、摄像头/URL 构建、抓流参数拼串、每帧诊断、灵敏度放宽和截图 ASCII 标注测试并已通过(含来源模式显式声明、模型置信度经适配器生效、事件号跨轮唯一、结构化凭证不入 config_version 且公开示例无凭证的守卫)。`demo/1.mp4` 的首两帧回放时间戳已验证为 0.000000 与 0.033333 秒;T-106 的真实模型/录像 smoke 在首帧得到 2 名已分析人员、第二帧得到 0 名人员且未创建事件;T-201 的视图冒烟以真实录像解码 + 真实管线 + 确定性假 Pose 适配器驱动 `build_monitor_view`,得到稳定 ID、box、14 段骨架、17/17 关键点与 NORMAL/success,断流帧 0 人且不显示摔倒标签;T-202 的报警冒烟用 `demo/1.mp4` 首帧(848×480)落盘一张可被 `cv2.imread` 读回的标注截图(480×848×3,约 330 KB)与一行 JSONL(相对截图路径、含 config_version/source_id、无 rtsp),重复派发返回 0。这些只验证管线、视图与报警工件可运行,不表示摔倒识别准确率。`init.ps1` 会检查运行时依赖、编译旧基线并运行 V1 测试,但不会安装软件包。
|
||||
- 测试:`python -m compileall -q v1 demo` 已通过(含 `gui.py`、`app.py`、`alerts.py`、`camera.py` 语法);`python -m pytest v1/tests -v` 当前有 74 项配置、视频源、Pose、跟踪、证据、领域规则、状态机、管线、视图模型、报警工件、摄像头/URL 构建、抓流参数拼串、每帧诊断、灵敏度放宽、截图 ASCII 标注和断流重连测试并已通过(含来源模式显式声明、模型置信度经适配器生效、事件号跨轮唯一、结构化凭证不入 config_version 且公开示例无凭证的守卫)。`demo/1.mp4` 的首两帧回放时间戳已验证为 0.000000 与 0.033333 秒;T-106 的真实模型/录像 smoke 在首帧得到 2 名已分析人员、第二帧得到 0 名人员且未创建事件;T-201 的视图冒烟以真实录像解码 + 真实管线 + 确定性假 Pose 适配器驱动 `build_monitor_view`,得到稳定 ID、box、14 段骨架、17/17 关键点与 NORMAL/success,断流帧 0 人且不显示摔倒标签;T-202 的报警冒烟用 `demo/1.mp4` 首帧(848×480)落盘一张可被 `cv2.imread` 读回的标注截图(480×848×3,约 330 KB)与一行 JSONL(相对截图路径、含 config_version/source_id、无 rtsp),重复派发返回 0。这些只验证管线、视图与报警工件可运行,不表示摔倒识别准确率。`init.ps1` 会检查运行时依赖、编译旧基线并运行 V1 测试,但不会安装软件包。
|
||||
- 模型:`demo/best.pt` 可加载为 YOLO Pose,类别 `person`,`kpt_shape=[17, 3]`;与 `D:\PythonP\fall_detection\best.pt` 哈希一致。
|
||||
- 当前标准启动:`./init.ps1`。
|
||||
- 当前标准验证:`python -m compileall -q demo`。
|
||||
|
||||
@@ -334,3 +334,12 @@
|
||||
- 阻塞:无。截图像素级观感需 Windows 目视确认,但乱码根因(非 ASCII 送入 Hershey 字体)已在逻辑层修掉。
|
||||
- 决策:截图标注限定 ASCII;若需中文标注(如"确认摔倒")另立任务用 Pillow+TTF 绘制。
|
||||
- 下一步:用户 Windows 复测截图文字是否正常;继续 T-203/T-204。
|
||||
|
||||
## 【2026-07-21】T-203 接入海康 RTSP 与断流恢复(完成)
|
||||
|
||||
- 状态:DONE
|
||||
- 变更:本任务无新增生产代码(重连逻辑在 T-102/T-106 已实现),补齐断流/重连的确定性测试并据现场结果验收。
|
||||
- 验证:真实海康流已连通并在 Windows 现场跑通实时预览、骨架、状态与摔倒确认(用户确认,T-208/T-209 期间)。断流与重连以确定性测试锁定:`test_reconnect_waits_then_reopens_with_bounded_backoff`(不可达→有界退避→恢复 CONNECTED)、`test_stream_recovers_after_mid_stream_drop`(连接中→途中断→重连恢复)、`test_stream_source_uses_read_clock`(实时流用收帧单调时钟)、`test_disconnect_interrupts_suspect_without_alarm`(断流帧使管线返回 0 人 0 事件并把疑似打断回 NORMAL,即断流不报警)。WSL 对真实流的冒烟:STREAM 首读即 connected 解出 1920×1080;对不可达地址进入 retrying、不崩溃、空帧、不报警。`python3 -m pytest v1/tests -q` 为 74 passed;`python3 -m compileall -q v1` 退出码 0。
|
||||
- 阻塞:无。真实"拔网→正在重连→恢复自动接上"的可视化观察由用户在 Windows 现场做一次目视核对(步骤见下一步),逻辑侧已测。
|
||||
- 决策:断流只表达来源状态、绝不触发或推进摔倒事件;实时流以收帧单调时钟计时;重连用有界指数退避。
|
||||
- 下一步:用户可选做一次现场断网核对(拔网线/停流→画面显示"正在重连…"、不出红色告警→恢复后自动接上并继续检测)。继续 T-204:需经同意成年人的正反例录像做事件级验收(含放宽灵敏度后的不误报验证)。
|
||||
|
||||
@@ -121,6 +121,22 @@ def test_pipeline_from_config_uses_runtime_version_for_confirmed_event():
|
||||
assert result.events[0].config_version == config.runtime_config_version
|
||||
|
||||
|
||||
def test_disconnect_interrupts_suspect_without_alarm():
|
||||
pipeline, machine = _pipeline([(_pose(),), (_pose(horizontal=True),)])
|
||||
|
||||
pipeline.process(_packet(0.0))
|
||||
pipeline.process(_packet(0.1))
|
||||
assert machine.state_of("P-0001") is FallState.SUSPECT
|
||||
|
||||
outage = pipeline.process(
|
||||
FramePacket(image=None, timestamp_monotonic=1.0, status=SourceStatus.RETRYING)
|
||||
)
|
||||
|
||||
assert outage.events == ()
|
||||
assert outage.people == ()
|
||||
assert machine.state_of("P-0001") is FallState.NORMAL
|
||||
|
||||
|
||||
def test_from_config_gives_each_run_a_unique_event_id():
|
||||
config = _config()
|
||||
|
||||
|
||||
@@ -93,6 +93,48 @@ def test_reconnect_waits_then_reopens_with_bounded_backoff():
|
||||
assert recovered.image is not None
|
||||
|
||||
|
||||
class _FlakyCapture:
|
||||
def __init__(self, read_results):
|
||||
self._read_results = list(read_results)
|
||||
|
||||
def isOpened(self):
|
||||
return True
|
||||
|
||||
def read(self):
|
||||
ok = self._read_results.pop(0) if self._read_results else False
|
||||
return (True, np.zeros((8, 8, 3), dtype=np.uint8)) if ok else (False, None)
|
||||
|
||||
def get(self, _property_id):
|
||||
return 0.0
|
||||
|
||||
def release(self):
|
||||
pass
|
||||
|
||||
|
||||
def test_stream_recovers_after_mid_stream_drop():
|
||||
captures = [_FlakyCapture([True]), _OpenCapture()]
|
||||
|
||||
source = VideoSource(
|
||||
"rtsp://demo",
|
||||
mode=SourceMode.STREAM,
|
||||
retry_initial_seconds=2.0,
|
||||
retry_max_seconds=2.0,
|
||||
capture_factory=lambda _source: captures.pop(0),
|
||||
)
|
||||
|
||||
connected = source.read(now=0.0)
|
||||
dropped = source.read(now=1.0)
|
||||
waiting = source.read(now=1.5)
|
||||
recovered = source.read(now=3.0)
|
||||
|
||||
assert connected.status is SourceStatus.CONNECTED
|
||||
assert dropped.status is SourceStatus.RETRYING
|
||||
assert dropped.image is None
|
||||
assert waiting.status is SourceStatus.RETRYING
|
||||
assert recovered.status is SourceStatus.CONNECTED
|
||||
assert recovered.image is not None
|
||||
|
||||
|
||||
class _NegativeFirstTimestampCapture:
|
||||
def __init__(self):
|
||||
self._read_count = 0
|
||||
|
||||
Reference in New Issue
Block a user