From b35082a64391b8d48c4eb0c877722eac3a60be44 Mon Sep 17 00:00:00 2001 From: ila Date: Tue, 21 Jul 2026 23:38:53 +0800 Subject: [PATCH] fix(v1): use ASCII screenshot annotation to avoid garbled text cv2.putText only supports ASCII (Hershey fonts); the middle-dot in the label rendered as garbage on saved screenshots. screenshot_label builds an ASCII-only 'ID STATE' string for the annotation; the Qt live-view label is unchanged. 72 tests pass. Co-Authored-By: Claude Opus 4.8 --- docs/06-tasks.md | 2 +- docs/current-state.md | 2 +- progress.md | 9 +++++++++ v1/alerts.py | 14 +++++++++++++- v1/tests/test_alerts.py | 22 +++++++++++++++++++++- 5 files changed, 45 insertions(+), 4 deletions(-) diff --git a/docs/06-tasks.md b/docs/06-tasks.md index d2021d5..bf34806 100644 --- a/docs/06-tasks.md +++ b/docs/06-tasks.md @@ -34,7 +34,7 @@ | T-207 | 抓流低延迟参数进配置与设置页 | T-206 | 配置支持 transport(tcp/udp)/连接超时/低延迟,程序拼 `OPENCV_FFMPEG_CAPTURE_OPTIONS` 并在开流前注入;设置页用下拉/数字/勾选而非原始字符串;启动可不依赖 run 脚本设环境变量。 | DONE | | T-208 | 每帧摔倒判定诊断叠加 | T-201 | 管线暴露每人策略证据;实时画面角落叠加每人 `accepted/horizontal/角度/rapid/candidate/state`(拒绝时显示原因),便于定位真实摔倒未确认卡在哪一步;诊断字符串构建 Qt-free 可测。 | DONE | | T-209 | 放宽俯视场景摔倒判定灵敏度(可配置) | T-208 | 判定灵敏度参数化:`require_rapid_drop`(默认关)、`require_lower_body`(默认关)、`horizontal_angle_threshold_degrees`(默认放宽);默认下持续水平即可进入疑似,仅靠确认窗防误报;参数进 config 与 config_version,逻辑可测。 | DONE | -| T-210 | 修复截图标注乱码(cv2 ASCII 文字) | T-202 | 截图标注文字改为纯 ASCII(`cv2.putText` 只支持 ASCII),去掉非 ASCII 分隔符导致的乱码;实时画面标签不变;ASCII 标签逻辑可测。 | DOING | +| T-210 | 修复截图标注乱码(cv2 ASCII 文字) | T-202 | 截图标注文字改为纯 ASCII(`cv2.putText` 只支持 ASCII),去掉非 ASCII 分隔符导致的乱码;实时画面标签不变;ASCII 标签逻辑可测。 | DONE | ## Phase 3 · Go V2 迁移门槛与实现 diff --git a/docs/current-state.md b/docs/current-state.md index 1ec25ff..9b7e3a4 100644 --- a/docs/current-state.md +++ b/docs/current-state.md @@ -11,7 +11,7 @@ - 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` 当前有 70 项配置、视频源、Pose、跟踪、证据、领域规则、状态机、管线、视图模型、报警工件、摄像头/URL 构建、抓流参数拼串、每帧诊断和灵敏度放宽测试并已通过(含来源模式显式声明、模型置信度经适配器生效、事件号跨轮唯一、结构化凭证不入 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` 当前有 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 测试,但不会安装软件包。 - 模型:`demo/best.pt` 可加载为 YOLO Pose,类别 `person`,`kpt_shape=[17, 3]`;与 `D:\PythonP\fall_detection\best.pt` 哈希一致。 - 当前标准启动:`./init.ps1`。 - 当前标准验证:`python -m compileall -q demo`。 diff --git a/progress.md b/progress.md index f01945f..c83309b 100644 --- a/progress.md +++ b/progress.md @@ -325,3 +325,12 @@ - 阻塞:无(纯标注文字,本环境可测;截图像素级外观需 Windows 目视,但 ASCII 逻辑本环境可测)。 - 决策:根因是 `cv2.putText` 用 Hershey 字体只支持 ASCII,而标签里的 `·`(U+00B7) 非 ASCII → 乱码。修法:截图专用标签只用 track_id + 状态英文值拼成纯 ASCII;实时画面 Qt 标签保留 `·`(QPainter 能正常渲染)。中文截图标注需 Pillow+TTF,暂不做。 - 下一步:加 `screenshot_label` 纯 ASCII 生成 + 测试,`annotate_frame` 改用它。 + +## 【2026-07-21】T-210 修复截图标注乱码(cv2 ASCII 文字)(完成) + +- 状态:DONE +- 变更:`alerts.py` 新增 `screenshot_label(track_id, state_value)`——用 `track_id + 状态英文值` 拼串并 `encode("ascii","ignore")` 剥离非 ASCII;`annotate_frame` 的 `cv2.putText` 改用它,去掉了原标签里 `·`(U+00B7) 导致的乱码。实时画面(Qt `QPainter`)标签保持不变,仍显示 `P-0001 · CONFIRMED`。 +- 验证:新增 test_alerts 两项(`screenshot_label` 为纯 ASCII 且等于 `P-0001 CONFIRMED`、不含 `·`);`python3 -m pytest v1/tests -q` 为 72 passed;`python3 -m compileall -q v1` 退出码 0。 +- 阻塞:无。截图像素级观感需 Windows 目视确认,但乱码根因(非 ASCII 送入 Hershey 字体)已在逻辑层修掉。 +- 决策:截图标注限定 ASCII;若需中文标注(如"确认摔倒")另立任务用 Pillow+TTF 绘制。 +- 下一步:用户 Windows 复测截图文字是否正常;继续 T-203/T-204。 diff --git a/v1/alerts.py b/v1/alerts.py index 69f8711..6fbe2d5 100644 --- a/v1/alerts.py +++ b/v1/alerts.py @@ -46,6 +46,18 @@ class AlertSink: pass +def screenshot_label(track_id: str, state_value: str) -> str: + """ASCII-only label for ``cv2.putText``. + + OpenCV's Hershey fonts render only ASCII; non-ASCII characters (such as the + middle dot used in the live-view label) come out garbled, so the screenshot + label is built from the id and state and stripped to ASCII. + """ + + text = "{0} {1}".format(track_id, state_value) + return text.encode("ascii", "ignore").decode("ascii") + + def annotate_frame(image: np.ndarray, view: MonitorViewState) -> np.ndarray: """Return a copy of ``image`` with boxes, skeleton, labels and a state border.""" @@ -67,7 +79,7 @@ def annotate_frame(image: np.ndarray, view: MonitorViewState) -> np.ndarray: cv2.circle(canvas, (int(round(point.x)), int(round(point.y))), 3, color, -1) cv2.putText( canvas, - overlay.label, + screenshot_label(overlay.track_id, overlay.state.value), (left, max(0, top - 6)), cv2.FONT_HERSHEY_SIMPLEX, 0.5, diff --git a/v1/tests/test_alerts.py b/v1/tests/test_alerts.py index af679d4..abb38d9 100644 --- a/v1/tests/test_alerts.py +++ b/v1/tests/test_alerts.py @@ -4,7 +4,13 @@ from datetime import datetime, timezone import numpy as np import pytest -from v1.alerts import AlertDispatcher, AlertSink, EventArtifactWriter, annotate_frame +from v1.alerts import ( + AlertDispatcher, + AlertSink, + EventArtifactWriter, + annotate_frame, + screenshot_label, +) from v1.fall_state import FallEvent, FallState from v1.view_model import MonitorViewState, PersonOverlay, Point, StatusColor @@ -115,6 +121,20 @@ def test_distinct_events_produce_two_log_lines(tmp_path): assert len(lines) == 2 +def test_screenshot_label_is_ascii_only(): + label = screenshot_label("P-0001", "CONFIRMED") + + assert label == "P-0001 CONFIRMED" + assert all(ord(ch) < 128 for ch in label) + + +def test_screenshot_label_strips_non_ascii_separator(): + # The live-view label uses a middle dot; the screenshot label must not. + label = screenshot_label("P-0001", "CONFIRMED") + + assert "·" not in label + + def test_annotate_draws_red_border_only_for_confirmed(): image = np.full((60, 80, 3), 30, dtype=np.uint8) confirmed = annotate_frame(image, _confirmed_view())