feat(v2): complete Go inference and UI spike
This commit is contained in:
@@ -23,9 +23,10 @@
|
|||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
| 语言 | Go | 已定 | 面向长时间 RTSP 运行、并发控制和单一交付物。 |
|
| 语言 | Go | 已定 | 面向长时间 RTSP 运行、并发控制和单一交付物。 |
|
||||||
| 模型工件 | 固定输入尺寸的 ONNX Pose 模型 | 已定 | Go 不直接加载 `.pt`;模型由已验收 V1 权重导出并记录哈希。 |
|
| 模型工件 | 固定输入尺寸的 ONNX Pose 模型 | 已定 | Go 不直接加载 `.pt`;模型由已验收 V1 权重导出并记录哈希。 |
|
||||||
| 推理运行时 | ONNX Runtime 或经 Spike 验证的等价 Go 运行时 | 待 T-302 决定 | 选择标准是 Windows 打包、Pose 输出、CPU/GPU 与持续运行稳定性。 |
|
| 推理运行时 | `github.com/yalue/onnxruntime_go` v1.31.0 + ONNX Runtime CPU DLL | Spike 已验证 | Windows 上以显式 DLL 路径加载锁定的 1×3×640×640 Pose ONNX,已完成一次真实录像首帧推理;该包装器的 C API 为 26,V2 构建必须启用 CGO/MinGW,并在发布包锁定匹配 DLL 的来源与 SHA-256。 |
|
||||||
| 视频输入与绘制 | Go 方案待 T-302 Spike 决定 | 待定 | 必须读取 RTSP、保留 BGR/RGB 与 letterbox 一致性,并满足现场显示。 |
|
| 视频输入 | 受控的 `ffmpeg.exe` 子进程输出 `bgr24` 原始帧 | Spike 已验证 | 已读取 848×480 本地录像首帧;避免未配置的 GoCV/OpenCV 开发绑定。T-304 必须随应用锁定 FFmpeg/FFprobe 的版本、哈希与 RTSP 重连行为。 |
|
||||||
| 演示 UI | 本地网页壳或轻量桌面壳待 T-302 Spike 决定 | 待定 | 不在未验证视频帧展示能力前锁定 UI 框架。 |
|
| 图像预处理 | Go 实现的 BGR→RGB、CHW、640 letterbox | Spike 已验证,待一致性回归 | 可生成 ONNX 所需 1,228,800 个 float;当前是最小 nearest-neighbour Spike,不得直接当作生产预处理,T-303 必须与 V1 缩放、补边、NMS 和坐标还原逐项对齐。 |
|
||||||
|
| 演示 UI | `github.com/lxn/walk`(Windows 原生控件) | Spike 已验证 | 双 Tab 骨架可在 `CGO_ENABLED=0` 编译,避免 Fyne/OpenGL 首次编译的高复杂度;正式版仍须实现浅灰蓝底、白色卡片,红色仅表示确认摔倒。 |
|
||||||
| 测试 | `go test ./...` + V1 回归录像 | 已定 | V2 行为必须与 V1 基线对照。 |
|
| 测试 | `go test ./...` + V1 回归录像 | 已定 | V2 行为必须与 V1 基线对照。 |
|
||||||
|
|
||||||
## 关键选型纪律
|
## 关键选型纪律
|
||||||
@@ -33,6 +34,8 @@
|
|||||||
- V1 不引入新模型、数据库、云服务或重量级告警依赖。
|
- V1 不引入新模型、数据库、云服务或重量级告警依赖。
|
||||||
- RTSP 凭证仅来自环境变量或未跟踪本地文件;示例配置只含变量名。
|
- RTSP 凭证仅来自环境变量或未跟踪本地文件;示例配置只含变量名。
|
||||||
- V2 的 ONNX 预处理、NMS、关键点解析、坐标还原和阈值必须与 V1 录像基线一致。
|
- V2 的 ONNX 预处理、NMS、关键点解析、坐标还原和阈值必须与 V1 录像基线一致。
|
||||||
|
- `onnxruntime.dll`、`ffmpeg.exe` 与 `ffprobe.exe` 是 V2 发布依赖,必须以明确版本、来源和 SHA-256 随发布清单锁定;不得把开发机 Python 包、Go 模块缓存或系统 PATH 当成交付依赖。
|
||||||
|
- V2 的 ONNX Runtime 包装器依赖 CGO;构建机必须固定 Go、MinGW 和 CGO 环境,运行端只加载已锁定的 DLL。CPU 是当前唯一已验证执行路径。
|
||||||
- 如果 V2 需要 GPU 专用引擎,必须记录显卡、驱动、运行时版本和回退 CPU 路径;不能将硬件绑定伪装成通用稳定性。
|
- 如果 V2 需要 GPU 专用引擎,必须记录显卡、驱动、运行时版本和回退 CPU 路径;不能将硬件绑定伪装成通用稳定性。
|
||||||
|
|
||||||
## 当前可运行命令
|
## 当前可运行命令
|
||||||
@@ -44,7 +47,9 @@
|
|||||||
| 旧 GUI 启动 | `Set-Location demo; python main.py` |
|
| 旧 GUI 启动 | `Set-Location demo; python main.py` |
|
||||||
| V1 依赖安装 | `python -m pip install -r v1/requirements.txt`(显式执行;`init.ps1` 不自动安装) |
|
| V1 依赖安装 | `python -m pip install -r v1/requirements.txt`(显式执行;`init.ps1` 不自动安装) |
|
||||||
| V1 单元测试 | `python -m pytest v1/tests -v` |
|
| V1 单元测试 | `python -m pytest v1/tests -v` |
|
||||||
| V2 测试 | T-302 创建 Go 模块后使用 `go test ./...` |
|
| V2 单元测试 | `Set-Location v2; $env:CGO_ENABLED='1'; go test ./...` |
|
||||||
|
| V2 ONNX/视频 Spike | `Set-Location v2; $env:CGO_ENABLED='1'; go run ./cmd/spike --ffmpeg <ffmpeg.exe> --video <local-video> --width <w> --height <h> --onnx assets\best.onnx --ort-dll <onnxruntime.dll>` |
|
||||||
|
| V2 Walk UI 编译 Spike | `Set-Location v2; $env:CGO_ENABLED='0'; go build ./cmd/ui-spike` |
|
||||||
|
|
||||||
## 演进决策
|
## 演进决策
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ Pose 推理适配器(person box + 17 keypoints)
|
|||||||
|
|
||||||
V1 的同一数据流既可接 RTSP,也可回放本地录像。V2 复用同一配置、事件字段和录像集,但将 Pose 推理替换为 ONNX 适配器;不得改变事件语义。
|
V1 的同一数据流既可接 RTSP,也可回放本地录像。V2 复用同一配置、事件字段和录像集,但将 Pose 推理替换为 ONNX 适配器;不得改变事件语义。
|
||||||
|
|
||||||
|
T-302 的实现路线为:受控 `ffmpeg.exe` 负责解码为 BGR 原始帧,Go 预处理模块生成固定 `1×3×640×640` RGB/CHW tensor,`onnxruntime_go` 以显式 DLL 路径执行 Pose ONNX,Windows Walk UI 只接收已完成渲染的数据。解码、推理和事件计算都不得在 UI 消息线程运行;UI 只显示最新完成帧和事件状态。T-303 必须先补齐与 V1 一致的 letterbox、NMS、17 点解析、坐标还原、跟踪和状态机,才可以接入实时 UI。
|
||||||
|
|
||||||
## 模块职责
|
## 模块职责
|
||||||
|
|
||||||
| 模块 | V1 位置 | 职责 | 不负责 |
|
| 模块 | V1 位置 | 职责 | 不负责 |
|
||||||
@@ -46,6 +48,16 @@ V1 的同一数据流既可接 RTSP,也可回放本地录像。V2 复用同一
|
|||||||
| 应用装配 | `v1/app.py` | 装配配置、Pose、管线线程与窗口;`FrameWorker` 只发出已判定的 `FrameAnalysis` | 事件判定、渲染细节 |
|
| 应用装配 | `v1/app.py` | 装配配置、Pose、管线线程与窗口;`FrameWorker` 只发出已判定的 `FrameAnalysis` | 事件判定、渲染细节 |
|
||||||
| 回归工具 | `v1/tests/` 与 `v1/scripts/` | 回放录像、断言事件和延迟 | 生产 UI |
|
| 回归工具 | `v1/tests/` 与 `v1/scripts/` | 回放录像、断言事件和延迟 | 生产 UI |
|
||||||
|
|
||||||
|
V2 对应模块将在 T-303/T-304 建立,职责预先固定如下:
|
||||||
|
|
||||||
|
| 模块 | V2 目标位置 | 职责 | 不负责 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| 解码器 | `v2/internal/video` | 启动受控 FFmpeg、提供最新 BGR 帧与显式来源状态 | Pose、事件判定、UI 绘制 |
|
||||||
|
| ONNX Pose | `v2/internal/pose` | 校验模型/DLL 清单,完成精确预处理、推理、NMS、关键点及坐标还原 | 人员 ID、摔倒结论 |
|
||||||
|
| 事件引擎 | `v2/internal/fall` | 复现 V1 的跟踪、证据、四态状态机和 `FallEvent` | 视频解码、声音、文件 |
|
||||||
|
| Windows UI | `v2/internal/ui` | Walk 顶部“监控/设置”Tab、渲染最新帧和已计算状态 | 直接读 RTSP、执行 ONNX 或事件规则 |
|
||||||
|
| 应用装配 | `v2/cmd/silver-pose` | 管理 worker 生命周期、取消、最新帧投递和依赖注入 | 重写领域规则 |
|
||||||
|
|
||||||
## UI 导航与配置生效生命周期
|
## UI 导航与配置生效生命周期
|
||||||
|
|
||||||
PyQt 主窗口只包含实时监控和设置两个顶部 Tab。实时监控 Tab 保持视频画面优先;设置 Tab 不能直接调用视频源、Pose 或状态机。
|
PyQt 主窗口只包含实时监控和设置两个顶部 Tab。实时监控 Tab 保持视频画面优先;设置 Tab 不能直接调用视频源、Pose 或状态机。
|
||||||
|
|||||||
+1
-1
@@ -44,7 +44,7 @@
|
|||||||
| ID | 任务 | 依赖 | 验收要点 | 状态 |
|
| ID | 任务 | 依赖 | 验收要点 | 状态 |
|
||||||
| --- | --- | --- | --- | --- |
|
| --- | --- | --- | --- | --- |
|
||||||
| T-301 | 锁定 V1 模型并导出 ONNX 一致性工件 | T-205 | 记录 `.pt` 与 ONNX 哈希、输入尺寸、导出命令;同一帧关键点与 Python 在约定容差内。 | DONE |
|
| T-301 | 锁定 V1 模型并导出 ONNX 一致性工件 | T-205 | 记录 `.pt` 与 ONNX 哈希、输入尺寸、导出命令;同一帧关键点与 Python 在约定容差内。 | DONE |
|
||||||
| T-302 | 完成 Go 推理与视频/UI 技术 Spike | T-301 | Windows 上可读取录像和 ONNX Pose;选定运行时与 UI 方案并记录取舍。 | DOING |
|
| T-302 | 完成 Go 推理与视频/UI 技术 Spike | T-301 | Windows 上可读取录像和 ONNX Pose;选定运行时与 UI 方案并记录取舍。 | DONE |
|
||||||
| T-303 | 实现 Go 的事件引擎与 V1 回归对比 | T-302 | 同一录像的确认事件、无报警案例和延迟满足 V1 基线。 | TODO |
|
| T-303 | 实现 Go 的事件引擎与 V1 回归对比 | T-302 | 同一录像的确认事件、无报警案例和延迟满足 V1 基线。 | TODO |
|
||||||
| T-304 | 完成 Go 实时演示、报警和打包 | T-303 | 实时 RTSP、红色画面、声音、弹窗和截图可运行;新环境按文档启动。 | TODO |
|
| T-304 | 完成 Go 实时演示、报警和打包 | T-303 | 实时 RTSP、红色画面、声音、弹窗和截图可运行;新环境按文档启动。 | TODO |
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
# T-302 · Go 推理、视频与 Windows UI Spike 决策
|
||||||
|
|
||||||
|
- 状态:已接受,作为 T-303/T-304 的实施前提
|
||||||
|
- 日期:2026-07-22
|
||||||
|
- 范围:仅验证 Windows 开发环境的单帧推理、录像解码和 UI 技术路线;不等于 V2 行为验收或客户演示验收。
|
||||||
|
|
||||||
|
## 结论
|
||||||
|
|
||||||
|
1. Pose 运行时采用 `github.com/yalue/onnxruntime_go` v1.31.0,CPU 路径,且启动时必须显式指定 `onnxruntime.dll`。包装器当前使用 ONNX Runtime C API 26,构建机须固定 Go、`CGO_ENABLED=1` 和 MinGW;运行端不依赖 Python,但必须随发布物携带版本、来源和 SHA-256 都已锁定的匹配 DLL。
|
||||||
|
2. 视频接入采用受控 `ffmpeg.exe` 子进程。它解码录像或 RTSP 为 `bgr24` 原始帧;T-304 将锁定 `ffmpeg.exe`/`ffprobe.exe` 的版本和哈希,并实现来源状态、超时、取消与重连。当前不采用 GoCV,因为此机没有可用的 OpenCV/GoCV 开发绑定。
|
||||||
|
3. Windows UI 采用 `github.com/lxn/walk`。它提供原生窗口、顶部 Tab 和控件,UI Spike 在 `CGO_ENABLED=0` 下可编译。正式 UI 继续使用文档中的浅灰蓝底、白色卡片和“红色只表示 CONFIRMED”的语义;Walk 只在 UI 线程渲染,不能调用解码、ONNX 或状态机。
|
||||||
|
|
||||||
|
## 验证证据
|
||||||
|
|
||||||
|
命令行 Spike 使用 `demo/1.mp4` 的第一帧(848×480),从 FFmpeg 获取准确的 1,221,120 字节 BGR 帧,转为 1×3×640×640 的 1,228,800 个 `float32` 输入;锁定 `v2/assets/best.onnx` 输出为 1×56×8400 的 470,400 个值。实际运行输出:
|
||||||
|
|
||||||
|
```text
|
||||||
|
SPIKE_OK frame=848x480 input=1228800 output=470400 max=685.7795
|
||||||
|
```
|
||||||
|
|
||||||
|
`go test ./...` 在 `CGO_ENABLED=1` 下通过,且 `go build ./cmd/ui-spike` 在 `CGO_ENABLED=0` 下通过。最小测试覆盖 BGR 尺寸拒绝、letterbox 后 RGB/CHW 布局、Pose 输入长度拒绝和 UI 视觉令牌。
|
||||||
|
|
||||||
|
开发机的 ONNX Spike 暂时使用 Go 模块缓存内的 `test_data/onnxruntime.dll`,仅用于证明接口可执行;它不是允许交付的 DLL 来源,也未被拷入仓库。
|
||||||
|
|
||||||
|
## 拒绝的路线
|
||||||
|
|
||||||
|
- **GoCV/OpenCV**:未配置 Windows 开发库、`pkg-config` 或 CGO 绑定,无法据实声称可用于 V2。
|
||||||
|
- **Fyne**:初次 Windows 构建进入 OpenGL/CGO 编译并在 120 秒内未完成,且遗留编译进程;对于当前“稳定现场演示”的目标,引入的 UI 图形链复杂度不合适。随后已停止这些精确的临时进程,没有修改用户文件。
|
||||||
|
|
||||||
|
## 不在本任务中的内容
|
||||||
|
|
||||||
|
当前 Go letterbox 是最小 Spike(nearest-neighbour)。它尚未完成或证明与 V1 等价的缩放/补边、NMS、关键点解析、坐标还原、人员跟踪、事件状态机、RTSP 断流、声音、弹窗、截图和打包。T-303 必须以同一录像的关键点、事件数量、非报警案例和确认延迟证明 V1/V2 等价;T-304 才可形成部署与现场演示闭环。
|
||||||
@@ -5,17 +5,17 @@
|
|||||||
## 当前快照
|
## 当前快照
|
||||||
|
|
||||||
- 日期:2026-07-22
|
- 日期:2026-07-22
|
||||||
- 阶段:V1 客户演示闭环和 T-301 模型导出一致性已验收;T-302 Go 推理/UI 技术 Spike 进行中。判定灵敏度默认已放宽(低位俯视:持续水平即可疑,膝踝可缺,水平阈值 45°),确认窗为主防误报闸。
|
- 阶段:V1 客户演示闭环、T-301 模型导出一致性与 T-302 Go 技术 Spike 已验收;下一个任务是 T-303(复现 V1 事件引擎并做录像回归)。判定灵敏度默认已放宽(低位俯视:持续水平即可疑,膝踝可缺,水平阈值 45°),确认窗为主防误报闸。
|
||||||
- 已验证环境:Windows PowerShell;Python 3.8.10;Ultralytics 8.3.205;PyQt5 可导入。注意:当前 WSL/Linux 环境未安装 PyQt5,且其 ultralytics/torch 版本与该基线不兼容(模型前向不可用),故 GUI 可视化与真实模型端到端渲染只在 Windows 验证;本环境用 Qt-free 的 `view_model.py` 单元测试与假适配器管线冒烟覆盖视图逻辑。
|
- 已验证环境: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`。
|
- 旧生产基线:`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)。
|
- 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/` 目录存在但尚无实现。
|
- V2 代码:已创建 Go 模块和受控 Spike。`cmd/spike` 用 FFmpeg 读取本地 BGR 帧、将其预处理为 640 tensor,并用 `onnxruntime_go` 对锁定 ONNX 执行一次推理;`cmd/ui-spike` 是可编译的 Windows Walk 双 Tab 外壳。它们不含 NMS/关键点解析、跟踪、状态机、RTSP、报警或发布打包,不能作为客户演示版。
|
||||||
- 非代码设计工件: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 任务顺序。
|
- 非代码设计工件: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` 当前有 86 项配置、视频源、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` 语法);`./init.ps1` 当前运行 V1 测试为 88 passed、1 skipped,覆盖配置、视频源、Pose、跟踪、证据、领域规则、状态机、管线、视图模型、报警工件、摄像头/URL 构建、抓流参数拼串、每帧诊断、灵敏度放宽、截图标注、断流重连、事件汇总、灵敏度写回和最新帧丢弃。T-302 的 `CGO_ENABLED=1 go test ./...` 通过;Go Spike 对 848×480 本地录像首帧生成 1,228,800 元素输入并获得 470,400 元素 ONNX 输出;Walk UI Spike 以 `CGO_ENABLED=0 go build ./cmd/ui-spike` 编译通过。这些只验证管线、视图与报警工件可运行,不表示摔倒识别准确率或 Go 行为等价。`init.ps1` 会检查运行时依赖、编译旧基线并运行 V1 测试,但不会安装软件包。
|
||||||
- 模型:`demo/best.pt` 可加载为 YOLO Pose,类别 `person`,`kpt_shape=[17, 3]`;与 `D:\PythonP\fall_detection\best.pt` 哈希一致。
|
- 模型:`demo/best.pt` 可加载为 YOLO Pose,类别 `person`,`kpt_shape=[17, 3]`;与 `D:\PythonP\fall_detection\best.pt` 哈希一致。
|
||||||
- 当前标准启动:`./init.ps1`。
|
- 当前标准启动:`./init.ps1`。
|
||||||
- 当前标准验证:`python -m compileall -q demo`。
|
- 当前标准验证:`python -m compileall -q demo`。
|
||||||
- 当前 blocker:T-301 尚未锁定 ONNX 工件并完成 Python/ONNX 关键点一致性记录;现场录像和事件工件保留本机,不提交仓库。GUI 可视化、声音、弹窗与设置页测试连接的像素级观感仍应在 Windows 目视复核。
|
- 当前 blocker:没有 T-303 的代码阻塞;但 Go 尚未验证与 V1 的预后处理、NMS、关键点、跟踪、状态机和事件延迟一致,不能把 T-302 的单帧推理表述为可演示的 V2。ONNX Runtime DLL、FFmpeg/FFprobe 仍须在 T-304 以版本、来源和哈希纳入发布包;现场录像和事件工件保持本机不提交。
|
||||||
|
|
||||||
全局环境的 `pip check` 存在其他项目的包冲突,因此它不是 Silver Pose 的验收命令。`init.ps1` 只检查本项目实际导入的 OpenCV、NumPy、Ultralytics 与 PyQt5,并在命令非零退出时失败。
|
全局环境的 `pip check` 存在其他项目的包冲突,因此它不是 Silver Pose 的验收命令。`init.ps1` 只检查本项目实际导入的 OpenCV、NumPy、Ultralytics 与 PyQt5,并在命令非零退出时失败。
|
||||||
|
|
||||||
@@ -24,17 +24,17 @@
|
|||||||
| 路径 | 状态 | 说明 |
|
| 路径 | 状态 | 说明 |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| `demo/` | 已有 | 旧 Python GUI、Pose 模型与样例视频;当前单帧规则不适合作为正式事件逻辑。 |
|
| `demo/` | 已有 | 旧 Python GUI、Pose 模型与样例视频;当前单帧规则不适合作为正式事件逻辑。 |
|
||||||
| `v1/` | 待开发 | 新 Python V1 的正式实现目录。 |
|
| `v1/` | 已验收 | Python V1 正式实现、现场验收记录与发布工具。 |
|
||||||
| `v2/` | 待开发 | Go V2 目录;V1 验收前不实现功能。 |
|
| `v2/` | Spike 已完成 | Go 模块、ONNX/视频命令行 Spike 与 Windows Walk UI 外壳;尚不是完整 V2。 |
|
||||||
| `docs/` | 已有 | Harness Coding 文档、任务和计划。 |
|
| `docs/` | 已有 | Harness Coding 文档、任务和计划。 |
|
||||||
| `tests/` | 不适用 | V1 测试将在 `v1/tests/` 下创建。 |
|
| `tests/` | 不适用 | V1 测试将在 `v1/tests/` 下创建。 |
|
||||||
| `artifacts/` | 待创建 | 本地事件截图和 JSONL,不提交。 |
|
| `artifacts/` | 待创建 | 本地事件截图和 JSONL,不提交。 |
|
||||||
|
|
||||||
## 任务状态
|
## 任务状态
|
||||||
|
|
||||||
- 已完成:T-000、T-101~T-106(V1 地基与事件管线)、T-201(PyQt 双 Tab)、T-202(本地报警/截图/JSONL)、T-203(海康 RTSP 与断流恢复)、T-204(正反例录像与现场验收)、T-206(结构化摄像头配置与连接测试)、T-207(抓流低延迟参数)、T-208(每帧诊断叠加)、T-209(放宽俯视灵敏度)、T-210(截图 ASCII 标注)、T-211(灵敏度设置页)、T-212(实时流最新帧)、T-213(截图中文标注)。
|
- 已完成:T-000、T-101~T-106(V1 地基与事件管线)、T-201(PyQt 双 Tab)、T-202(本地报警/截图/JSONL)、T-203(海康 RTSP 与断流恢复)、T-204(正反例录像与现场验收)、T-205(客户演示脚本与发布包)、T-206(结构化摄像头配置与连接测试)、T-207(抓流低延迟参数)、T-208(每帧诊断叠加)、T-209(放宽俯视灵敏度)、T-210(截图 ASCII 标注)、T-211(灵敏度设置页)、T-212(实时流最新帧)、T-213(截图中文标注)、T-301(ONNX 一致性)、T-302(Go 推理、视频/UI Spike)。
|
||||||
- 正在进行:无。
|
- 正在进行:无。
|
||||||
- 下一个可领取:T-301(锁定 V1 模型并导出 ONNX 一致性工件)。
|
- 下一个可领取:T-303(实现 Go 事件引擎与 V1 回归对比)。
|
||||||
|
|
||||||
## 当前可运行内容
|
## 当前可运行内容
|
||||||
|
|
||||||
|
|||||||
@@ -469,3 +469,12 @@
|
|||||||
- 阻塞:真实中文观感需 Windows(自动用 C:/Windows/Fonts/msyh.ttc)。
|
- 阻塞:真实中文观感需 Windows(自动用 C:/Windows/Fonts/msyh.ttc)。
|
||||||
- 决策:auto 字体候选只放 CJK 字体,找不到就回退 ASCII 而非渲染豆腐块;无需配置,Windows 自动中文、其它环境安全回退。
|
- 决策:auto 字体候选只放 CJK 字体,找不到就回退 ASCII 而非渲染豆腐块;无需配置,Windows 自动中文、其它环境安全回退。
|
||||||
- 下一步:本批"别的"三项(T-211/212/213)完成;用户 Windows 冒烟中文标注、低延迟与灵敏度设置页。
|
- 下一步:本批"别的"三项(T-211/212/213)完成;用户 Windows 冒烟中文标注、低延迟与灵敏度设置页。
|
||||||
|
|
||||||
|
## 【2026-07-22】T-302 Go 推理与视频/UI 技术 Spike(完成)
|
||||||
|
|
||||||
|
- 状态:DONE
|
||||||
|
- 变更:新增 `v2/` Go 模块、`internal/spike` 的 BGR→RGB/CHW/640 letterbox 与 ONNX Runtime 单次推理、`cmd/spike` 的本地录像验证命令,以及 `cmd/ui-spike` 的 Windows Walk 顶部双 Tab 外壳;新增 `docs/adr/2026-07-22-t302-go-spike-decision.md`,并同步技术栈、架构和当前状态。未提交 ONNX、DLL、FFmpeg、录像、凭证或事件工件。
|
||||||
|
- 验证:`./init.ps1` 通过(V1 为 88 passed、1 skipped)。在 `v2` 下,`CGO_ENABLED=1 go test ./...` 通过;`CGO_ENABLED=0 go build ./cmd/ui-spike` 通过。以 FFmpeg 解码 `demo/1.mp4` 首帧(848×480)并对锁定 ONNX 运行:`SPIKE_OK frame=848x480 input=1228800 output=470400 max=685.7795`。`git diff --check` 通过。
|
||||||
|
- 决策:CPU Pose 使用 `onnxruntime_go` v1.31.0 加显式、发布时锁定的 ONNX Runtime DLL;视频解码使用受控 FFmpeg 子进程;UI 选择 Walk。Fyne 因 OpenGL/CGO 首次构建超时而拒绝,GoCV 因本机没有开发绑定而不选。当前缓存 DLL 只用于 Spike,绝不作为发布依赖。
|
||||||
|
- 阻塞:无任务阻塞;但 Go 尚未复现 V1 的预后处理、NMS、关键点解析、跟踪、状态机、延迟回归、RTSP 和报警,不能作为客户演示版。
|
||||||
|
- 下一步:T-303,按 V1 合约实现事件引擎并在同一录像上比较关键点、确认事件、无报警案例和延迟。
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"silverpose/v2/internal/spike"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
ffmpegPath := flag.String("ffmpeg", "ffmpeg", "path to ffmpeg.exe")
|
||||||
|
videoPath := flag.String("video", "..\\demo\\1.mp4", "local non-sensitive validation video")
|
||||||
|
width := flag.Int("width", 848, "source video frame width")
|
||||||
|
height := flag.Int("height", 480, "source video frame height")
|
||||||
|
modelPath := flag.String("onnx", "assets\\best.onnx", "locked ONNX pose model path")
|
||||||
|
runtimeDLLPath := flag.String("ort-dll", "", "absolute onnxruntime.dll path")
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
if *runtimeDLLPath == "" {
|
||||||
|
fail("--ort-dll is required")
|
||||||
|
}
|
||||||
|
if *width <= 0 || *height <= 0 {
|
||||||
|
fail("--width and --height must be positive")
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
raw, err := exec.CommandContext(ctx, *ffmpegPath,
|
||||||
|
"-v", "error", "-i", *videoPath,
|
||||||
|
"-frames:v", "1", "-f", "rawvideo", "-pix_fmt", "bgr24", "-",
|
||||||
|
).Output()
|
||||||
|
if err != nil {
|
||||||
|
fail("decode first BGR frame: %v", err)
|
||||||
|
}
|
||||||
|
expectedBytes := *width * *height * 3
|
||||||
|
if len(raw) != expectedBytes {
|
||||||
|
fail("decoded BGR frame has %d bytes, want %d; verify --width/--height", len(raw), expectedBytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
input, err := spike.LetterboxBGRToNCHW(raw, *width, *height, 640)
|
||||||
|
if err != nil {
|
||||||
|
fail("preprocess frame: %v", err)
|
||||||
|
}
|
||||||
|
output, err := spike.RunPose(*modelPath, *runtimeDLLPath, input)
|
||||||
|
if err != nil {
|
||||||
|
fail("run ONNX Pose: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
max := float32(0)
|
||||||
|
for _, value := range output {
|
||||||
|
if value > max {
|
||||||
|
max = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fmt.Printf("SPIKE_OK frame=%dx%d input=%d output=%d max=%.4f\n", *width, *height, len(input), len(output), max)
|
||||||
|
}
|
||||||
|
|
||||||
|
func fail(format string, args ...any) {
|
||||||
|
fmt.Fprintf(os.Stderr, "spike: "+format+"\n", args...)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"silverpose/v2/internal/ui"
|
||||||
|
|
||||||
|
. "github.com/lxn/walk/declarative"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
spec := ui.MonitorWindowSpec()
|
||||||
|
|
||||||
|
MainWindow{
|
||||||
|
Title: spec.Title,
|
||||||
|
MinSize: Size{Width: spec.Width, Height: spec.Height},
|
||||||
|
Layout: VBox{},
|
||||||
|
Children: []Widget{
|
||||||
|
TabWidget{
|
||||||
|
Pages: []TabPage{
|
||||||
|
{
|
||||||
|
Title: "监控",
|
||||||
|
Layout: HBox{},
|
||||||
|
Children: []Widget{
|
||||||
|
GroupBox{
|
||||||
|
Title: "实时画面",
|
||||||
|
Layout: VBox{},
|
||||||
|
Children: []Widget{
|
||||||
|
TextLabel{Text: "视频帧将在此呈现"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
GroupBox{
|
||||||
|
Title: "当前状态",
|
||||||
|
Layout: VBox{},
|
||||||
|
Children: []Widget{
|
||||||
|
TextLabel{Text: "检测状态:NORMAL"},
|
||||||
|
TextLabel{Text: "事件:暂无"},
|
||||||
|
TextLabel{Text: "运行时:Go + ONNX Runtime"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Title: "设置",
|
||||||
|
Layout: VBox{},
|
||||||
|
Children: []Widget{
|
||||||
|
TextLabel{Text: "设置将在 T-304 接入本机配置;不保存摄像头凭证。"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}.Run()
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
module silverpose/v2
|
||||||
|
|
||||||
|
go 1.24.0
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/lxn/walk v0.0.0-20210112085537-c389da54e794
|
||||||
|
github.com/yalue/onnxruntime_go v1.31.0
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/lxn/win v0.0.0-20210218163916-a377121e959e // indirect
|
||||||
|
golang.org/x/sys v0.30.0 // indirect
|
||||||
|
gopkg.in/Knetic/govaluate.v3 v3.0.0 // indirect
|
||||||
|
)
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
github.com/lxn/walk v0.0.0-20210112085537-c389da54e794 h1:NVRJ0Uy0SOFcXSKLsS65OmI1sgCCfiDUPj+cwnH7GZw=
|
||||||
|
github.com/lxn/walk v0.0.0-20210112085537-c389da54e794/go.mod h1:E23UucZGqpuUANJooIbHWCufXvOcT6E7Stq81gU+CSQ=
|
||||||
|
github.com/lxn/win v0.0.0-20210218163916-a377121e959e h1:H+t6A/QJMbhCSEH5rAuRxh+CtW96g0Or0Fxa9IKr4uc=
|
||||||
|
github.com/lxn/win v0.0.0-20210218163916-a377121e959e/go.mod h1:KxxjdtRkfNoYDCUP5ryK7XJJNTnpC8atvtmTheChOtk=
|
||||||
|
github.com/yalue/onnxruntime_go v1.31.0 h1:1ln4YW1SFOFfGJZXe3jNOb2JUSt+l2pEneZfV8HdtFA=
|
||||||
|
github.com/yalue/onnxruntime_go v1.31.0/go.mod h1:b4X26A8pekNb1ACJ58wAXgNKeUCGEAQ9dmACut9Sm/4=
|
||||||
|
golang.org/x/sys v0.0.0-20201018230417-eeed37f84f13/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||||
|
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
gopkg.in/Knetic/govaluate.v3 v3.0.0 h1:18mUyIt4ZlRlFZAAfVetz4/rzlJs9yhN+U02F4u1AOc=
|
||||||
|
gopkg.in/Knetic/govaluate.v3 v3.0.0/go.mod h1:csKLBORsPbafmSCGTEh3U7Ozmsuq8ZSIlKk1bcqph0E=
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
package spike
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
ort "github.com/yalue/onnxruntime_go"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
poseInputSize = 640
|
||||||
|
poseInputCount = 1 * 3 * poseInputSize * poseInputSize
|
||||||
|
poseOutputCount = 1 * 56 * 8400
|
||||||
|
)
|
||||||
|
|
||||||
|
// RunPose executes the locked YOLO pose ONNX graph once using the CPU runtime.
|
||||||
|
// The caller supplies absolute model and DLL paths so no camera credential or
|
||||||
|
// machine-specific path is retained in V2 configuration or source.
|
||||||
|
func RunPose(modelPath, runtimeDLLPath string, input []float32) ([]float32, error) {
|
||||||
|
if len(input) != poseInputCount {
|
||||||
|
return nil, fmt.Errorf("pose input length = %d, want %d", len(input), poseInputCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
ort.SetSharedLibraryPath(runtimeDLLPath)
|
||||||
|
if err := ort.InitializeEnvironment(); err != nil {
|
||||||
|
return nil, fmt.Errorf("initialize ONNX Runtime: %w", err)
|
||||||
|
}
|
||||||
|
defer func() { _ = ort.DestroyEnvironment() }()
|
||||||
|
|
||||||
|
inputTensor, err := ort.NewTensor(ort.NewShape(1, 3, poseInputSize, poseInputSize), input)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("create pose input tensor: %w", err)
|
||||||
|
}
|
||||||
|
defer func() { _ = inputTensor.Destroy() }()
|
||||||
|
|
||||||
|
outputTensor, err := ort.NewEmptyTensor[float32](ort.NewShape(1, 56, 8400))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("create pose output tensor: %w", err)
|
||||||
|
}
|
||||||
|
defer func() { _ = outputTensor.Destroy() }()
|
||||||
|
|
||||||
|
session, err := ort.NewAdvancedSession(
|
||||||
|
modelPath,
|
||||||
|
[]string{"images"},
|
||||||
|
[]string{"output0"},
|
||||||
|
[]ort.Value{inputTensor},
|
||||||
|
[]ort.Value{outputTensor},
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("open pose ONNX session: %w", err)
|
||||||
|
}
|
||||||
|
defer func() { _ = session.Destroy() }()
|
||||||
|
|
||||||
|
if err := session.Run(); err != nil {
|
||||||
|
return nil, fmt.Errorf("run pose ONNX session: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
output := outputTensor.GetData()
|
||||||
|
if len(output) != poseOutputCount {
|
||||||
|
return nil, fmt.Errorf("pose output length = %d, want %d", len(output), poseOutputCount)
|
||||||
|
}
|
||||||
|
return append([]float32(nil), output...), nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package spike
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestRunPoseRejectsWrongInputLengthBeforeLoadingRuntime(t *testing.T) {
|
||||||
|
_, err := RunPose("missing.onnx", "missing.dll", []float32{0})
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("RunPose accepted an input that is not 1x3x640x640")
|
||||||
|
}
|
||||||
|
if !strings.Contains(err.Error(), "input length") {
|
||||||
|
t.Fatalf("RunPose error = %q, want input length error", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
package spike
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
// LetterboxBGRToNCHW converts one packed BGR frame into the RGB, CHW,
|
||||||
|
// float32 tensor expected by the locked 640-pixel YOLO pose ONNX model.
|
||||||
|
//
|
||||||
|
// It intentionally uses nearest-neighbour scaling for this Spike. T-303
|
||||||
|
// must replace or validate this preprocessing against the V1 implementation
|
||||||
|
// before it becomes the V2 production preprocessing path.
|
||||||
|
func LetterboxBGRToNCHW(frame []byte, width, height, target int) ([]float32, error) {
|
||||||
|
if width <= 0 || height <= 0 || target <= 0 {
|
||||||
|
return nil, fmt.Errorf("frame width, height and target must be positive")
|
||||||
|
}
|
||||||
|
if len(frame) != width*height*3 {
|
||||||
|
return nil, fmt.Errorf("BGR frame length = %d, want %d", len(frame), width*height*3)
|
||||||
|
}
|
||||||
|
|
||||||
|
scaleWidth := target
|
||||||
|
scaleHeight := height * target / width
|
||||||
|
if scaleHeight > target {
|
||||||
|
scaleHeight = target
|
||||||
|
scaleWidth = width * target / height
|
||||||
|
}
|
||||||
|
padX := (target - scaleWidth) / 2
|
||||||
|
padY := (target - scaleHeight) / 2
|
||||||
|
plane := target * target
|
||||||
|
input := make([]float32, 3*plane)
|
||||||
|
|
||||||
|
for y := 0; y < scaleHeight; y++ {
|
||||||
|
sourceY := y * height / scaleHeight
|
||||||
|
for x := 0; x < scaleWidth; x++ {
|
||||||
|
sourceX := x * width / scaleWidth
|
||||||
|
source := (sourceY*width + sourceX) * 3
|
||||||
|
destination := (padY+y)*target + padX + x
|
||||||
|
input[destination] = float32(frame[source+2]) / 255.0
|
||||||
|
input[plane+destination] = float32(frame[source+1]) / 255.0
|
||||||
|
input[2*plane+destination] = float32(frame[source]) / 255.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return input, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
package spike
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestLetterboxBGRToNCHWPlacesPixelInScaledImage(t *testing.T) {
|
||||||
|
// One bright BGR pixel in a 2:1 frame should be resized into the centred
|
||||||
|
// 640x320 image area. The remaining top/bottom area must stay black.
|
||||||
|
frame := []byte{0, 0, 0, 255, 0, 0}
|
||||||
|
|
||||||
|
input, err := LetterboxBGRToNCHW(frame, 2, 1, 640)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("LetterboxBGRToNCHW returned an error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
const plane = 640 * 640
|
||||||
|
if got := len(input); got != 3*plane {
|
||||||
|
t.Fatalf("input length = %d, want %d", got, 3*plane)
|
||||||
|
}
|
||||||
|
if got := input[2*plane+159*640+320]; got != 0 {
|
||||||
|
t.Fatalf("top padding blue channel = %v, want 0", got)
|
||||||
|
}
|
||||||
|
if got := input[0*plane+320*640+320]; got != 0 {
|
||||||
|
t.Fatalf("red channel = %v, want 0", got)
|
||||||
|
}
|
||||||
|
if got := input[1*plane+320*640+320]; got != 0 {
|
||||||
|
t.Fatalf("green channel = %v, want 0", got)
|
||||||
|
}
|
||||||
|
if got := input[2*plane+320*640+320]; got != 1 {
|
||||||
|
t.Fatalf("blue channel = %v, want 1", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLetterboxBGRToNCHWRejectsIncorrectFrameSize(t *testing.T) {
|
||||||
|
_, err := LetterboxBGRToNCHW([]byte{0}, 2, 1, 640)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("LetterboxBGRToNCHW accepted a truncated BGR frame")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package ui
|
||||||
|
|
||||||
|
// WindowSpec locks the visual baseline tested by the Windows UI Spike. The
|
||||||
|
// production V2 app will consume the same semantic colours and medium desktop
|
||||||
|
// layout after the decoding/inference pipeline is complete.
|
||||||
|
type WindowSpec struct {
|
||||||
|
Title string
|
||||||
|
Width int
|
||||||
|
Height int
|
||||||
|
BackgroundHex string
|
||||||
|
AlertHex string
|
||||||
|
}
|
||||||
|
|
||||||
|
func MonitorWindowSpec() WindowSpec {
|
||||||
|
return WindowSpec{
|
||||||
|
Title: "Silver Pose V2 · 技术 Spike",
|
||||||
|
Width: 1120,
|
||||||
|
Height: 720,
|
||||||
|
BackgroundHex: "#F3F6FA",
|
||||||
|
AlertHex: "#C62828",
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package ui
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestMonitorWindowSpecUsesLightMonitoringLayout(t *testing.T) {
|
||||||
|
spec := MonitorWindowSpec()
|
||||||
|
if spec.Title != "Silver Pose V2 · 技术 Spike" {
|
||||||
|
t.Fatalf("title = %q", spec.Title)
|
||||||
|
}
|
||||||
|
if spec.Width != 1120 || spec.Height != 720 {
|
||||||
|
t.Fatalf("size = %dx%d, want 1120x720", spec.Width, spec.Height)
|
||||||
|
}
|
||||||
|
if spec.BackgroundHex != "#F3F6FA" || spec.AlertHex != "#C62828" {
|
||||||
|
t.Fatalf("unexpected colours: background=%s alert=%s", spec.BackgroundHex, spec.AlertHex)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user