feat(actpred): HailoPredictor cv2.LUT 快路径——归一化+量化合并,端到端 3.3×
帧预处理从 26.5ms 降至 1.7ms(15.8×),端到端 52.4 → 16.0ms。惰性导入 cv2、 无 cv2 自动回退 float 路径;非 uint8/非 3 通道/尺寸不匹配输入走 PIL 转换 与 float 路径语义一致(7 类输入实测 max_diff=0)。OpenCV 5.0 仅支持单通道 LUT,逐通道应用后 merge。依赖加入 requirements.txt。
This commit is contained in:
1 parent
8a5162c912
commit
40523f6f8a
3 files changed
+118
-43
No files matched your search
+61
-2
@@ -35,6 +35,13 @@ import re
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
|
||||
try:
|
||||
import cv2 # 可选:帧归一化+量化合并查表(加速预处理)
|
||||
HAS_CV2 = True
|
||||
except ImportError:
|
||||
cv2 = None
|
||||
HAS_CV2 = False
|
||||
|
||||
from imu_norm import normalize_imu
|
||||
from action_norm import (normalize_action, denormalize_action,
|
||||
is_range_mode)
|
||||
@@ -207,6 +214,15 @@ class HailoPredictor:
|
||||
self._normalize_frames = \
|
||||
(0 - self._frame_quant.qp_zp) * self._frame_quant.qp_scale < -0.5
|
||||
|
||||
# cv2.LUT 查表(归一化+量化合并一步):仅逐帧独立输入布局且
|
||||
# 帧边缘为 uint8 时可用;无 cv2 或旧布局回退 float 路径
|
||||
from hailo_platform import FormatType
|
||||
self._frame_luts = None
|
||||
if HAS_CV2 and self._image_layout == "nhwc_frames" and \
|
||||
all(f.format.type == FormatType.UINT8
|
||||
for f in self._frame_infos):
|
||||
self._frame_luts = self._build_frame_luts()
|
||||
|
||||
h_shape = tuple(self._history_info.shape)
|
||||
self.history_len, self.action_dim = h_shape[1], h_shape[2]
|
||||
|
||||
@@ -273,8 +289,14 @@ class HailoPredictor:
|
||||
from hailo_platform import FormatType
|
||||
bindings = self._configured.create_bindings()
|
||||
|
||||
frame_float = np.stack([self._frame_float(img) for img in images])
|
||||
self._bind_frames(bindings, frame_float)
|
||||
if self._frame_luts is not None:
|
||||
# cv2.LUT 快路径:逐帧独立输入,直接绑定量化缓冲
|
||||
for i, (info, img) in enumerate(zip(self._frame_infos, images)):
|
||||
buf = self._frame_lut_quant(img, i)
|
||||
bindings.input(info.name).set_buffer(buf)
|
||||
else:
|
||||
frame_float = np.stack([self._frame_float(img) for img in images])
|
||||
self._bind_frames(bindings, frame_float)
|
||||
|
||||
h_shape = tuple(self._history_info.shape)
|
||||
if self._history_info.format.type == FormatType.UINT8:
|
||||
@@ -365,6 +387,43 @@ class HailoPredictor:
|
||||
x = (x / 255.0 - IMAGENET_MEAN) / IMAGENET_STD
|
||||
return x
|
||||
|
||||
def _build_frame_luts(self) -> list:
|
||||
"""按帧 quant_info 构建 256 项单通道查表(归一化+量化合并一步)
|
||||
|
||||
OpenCV 5.0 的 cv2.LUT 只接受单通道表,故每帧返回 [R,G,B] 三张表。
|
||||
数学上与 float 路径(_frame_float + quantize)等价(实测 0 LSB)。
|
||||
"""
|
||||
luts = []
|
||||
for info in self._frame_infos:
|
||||
q = info.quant_info
|
||||
c = np.arange(256, dtype=np.float32)[:, None]
|
||||
if self._normalize_frames:
|
||||
v = (c / 255.0 - IMAGENET_MEAN[None, :]) / IMAGENET_STD[None, :]
|
||||
else:
|
||||
v = np.broadcast_to(c, (256, 3))
|
||||
v = v / q.qp_scale + q.qp_zp
|
||||
lut = np.clip(np.round(v), 0, 255).astype(np.uint8) # (256,3)
|
||||
luts.append([lut[:, ch] for ch in range(3)])
|
||||
return luts
|
||||
|
||||
def _frame_lut_quant(self, image, frame_idx: int = 0) -> np.ndarray:
|
||||
"""cv2.LUT 直通:原始 uint8 像素 → 量化 uint8 缓冲(免 float 中间态)
|
||||
|
||||
非 uint8 / 非 3 通道 / 尺寸不匹配输入走与 _frame_float 相同的 PIL
|
||||
转换,保证两条路径语义一致(RGBA 经 convert 合成、灰度转 RGB)。
|
||||
"""
|
||||
x = np.asarray(image)
|
||||
if x.dtype != np.uint8 or x.ndim != 3 or x.shape[2] != 3 or \
|
||||
x.shape[:2] != (self.image_height, self.image_width):
|
||||
img = Image.fromarray(x).convert("RGB")
|
||||
if img.size != (self.image_width, self.image_height):
|
||||
img = img.resize((self.image_width, self.image_height),
|
||||
Image.BILINEAR)
|
||||
x = np.asarray(img, dtype=np.uint8)
|
||||
luts = self._frame_luts[frame_idx]
|
||||
outs = [cv2.LUT(x[..., ch], lut) for ch, lut in enumerate(luts)]
|
||||
return np.ascontiguousarray(cv2.merge(outs))
|
||||
|
||||
def _bind_frames(self, bindings, frame_float: np.ndarray):
|
||||
"""(F,H,W,3) float 帧 → HEF 帧输入缓冲区(按布局与量化)"""
|
||||
from hailo_platform import FormatType
|
||||
|
||||
@@ -4,5 +4,9 @@ torchvision>=0.15,<1.0
|
||||
numpy>=1.24,<3.0
|
||||
Pillow>=9.0,<14.0 # torchvision 依赖,上限放宽避免与 torch 源安装冲突
|
||||
|
||||
# 推理(Hailo 部署):帧归一化+量化合并查表(hailo_infer.py cv2.LUT 快路径,
|
||||
# 无 cv2 自动回退 float 路径——本依赖缺失仅损失 3.3× 端到端加速)
|
||||
opencv-python-headless>=4.9,<6.0
|
||||
|
||||
# Training utilities
|
||||
tqdm>=4.60,<5.0
|
||||
@@ -1,18 +1,20 @@
|
||||
# actpred Hailo 推理预处理优化方案(修订版):合并归一化与量化
|
||||
# actpred Hailo 推理预处理优化:cv2.LUT 查表合并归一化与量化(已实施)
|
||||
|
||||
> 文档类型:开发方案
|
||||
> 文档类型:开发方案(已实施)
|
||||
> 模块:actpred(部署链 hailo_infer.py)
|
||||
> 作者:自动生成
|
||||
> 日期:2026-08-17(原型实测修订)
|
||||
> 日期:2026-08-17(原型实测 + 集成实施)
|
||||
> 前置:`actpred_inference_speed_benchmark_20260813.md`(速度基准)、
|
||||
> 2026-08-17 新 640×480 数据集重测(本方案 §2 数据)
|
||||
|
||||
> ⚠️ **修订说明(2026-08-17)**
|
||||
> 初版以"numpy 查表法"为核心方案,预期端到端降至 18-20ms。
|
||||
> 原型实测(§3.4)证明 **numpy 高级索引查表在 Pi5 上无加速**(fancy
|
||||
> indexing 的 gather 开销与逐元素 float 运算相当),该预期不成立。
|
||||
> 本修订版改以**合并系数乘加**(纯 numpy,-5ms)为主方案,
|
||||
> **cv2.LUT**(需引入 opencv 依赖)列为可选的激进路径。
|
||||
> 📌 **版本历程**
|
||||
> 1. 初版:numpy 查表法为核心,预期端到端 18-20ms
|
||||
> 2. 原型实测(§3.4):**numpy 高级索引查表在 Pi5 上无加速**(fancy
|
||||
> indexing 的 gather 开销与逐元素 float 运算相当),预期不成立;
|
||||
> 合并系数乘加仅 -5ms;**cv2.LUT 实测 15.8× 加速**(§3.4)
|
||||
> 3. **已实施(§5)**:`hailo_infer.py` 集成 cv2.LUT 快路径(可选依赖,
|
||||
> 无 cv2 自动回退 float 路径),端到端 52.4 → **16.0 ms**,输出与
|
||||
> float 路径逐点一致(max_diff = 0)
|
||||
|
||||
---
|
||||
|
||||
@@ -66,35 +68,43 @@ B[c] = -mean[c] / (std[c] * q_scale) + q_zp
|
||||
2. **resize no-op 跳过**:`image.size == (W,H)` 时不调用 `resize()`
|
||||
3. **缓冲复用**:`np.multiply/np.add` 带 `out=` 参数,减少临时数组分配
|
||||
|
||||
### 3.3 可选激进路径:cv2.LUT(需引入 opencv 依赖)
|
||||
### 3.3 已实施快路径:cv2.LUT(可选依赖,无 cv2 自动回退)
|
||||
|
||||
`cv2.LUT(img, lut)` 是 C 实现的 uint8→uint8 查表,单次遍历,无 float 中间态——
|
||||
理论上可把帧预处理降到 ~3ms。但当前环境 **cv2 未安装**;Pi5 arm64 上 opencv-python
|
||||
安装包较重,是否引入需权衡(见 §7)。
|
||||
把"归一化+量化"合并为一步。集成要点(`hailo_infer.py`):
|
||||
|
||||
### 3.4 原型实测评估(2026-08-17,100 次均值,640×480×3 帧)
|
||||
- 惰性导入 cv2(`HAS_CV2`);**无 cv2 或非逐帧输入布局(ncfhw/nhwc_fused)自动回退 float 路径**
|
||||
- 加载时按每帧 `quant_info` 构建 256 项表(归一化/直通两种,`_normalize_frames` 判定)
|
||||
- **OpenCV 5.0 的 cv2.LUT 只接受单通道表**((256,3) 会断言失败),逐通道应用后 `cv2.merge`
|
||||
- 输入尺寸与模型不一致时先 PIL resize(仅此情况走 PIL)
|
||||
- 依赖安装:`opencv-python-headless`(5.0.0,清华镜像;官方源过慢)
|
||||
- 注:本方案实测后发现**合并系数乘加(§3.1)未实施**——cv2.LUT 收益已远超,
|
||||
避免冗余代码;§3.1 保留作为无 cv2 环境下的潜在备选
|
||||
|
||||
### 3.4 原型实测评估(2026-08-17,100-200 次均值,640×480×3 帧)
|
||||
|
||||
| 方案 | 3 帧耗时 | 对比现 float 28.5ms | 数值差异 |
|
||||
|---|---|---|---|
|
||||
| 现 float 路径 | 28.5 ms | 基线 | — |
|
||||
| numpy 高级索引查表 | 26.6 ms | **无加速(1.0x)** | 0 LSB |
|
||||
| 合并系数乘加 | **23.2 ms** | 快 19%(-5ms) | 0 LSB |
|
||||
| cv2.LUT | —(cv2 未安装) | 理论 ~3ms | — |
|
||||
| 合并系数乘加 | 23.2 ms | 快 19%(-5ms) | 0 LSB |
|
||||
| **cv2.LUT(逐通道)** | **1.68 ms** | **快 15.8×** | **0 LSB** |
|
||||
|
||||
**结论**:
|
||||
- numpy fancy indexing 的 gather 开销与逐元素 float 运算相当,查表法**无收益**
|
||||
- 纯 numpy 可达的最佳是合并乘加(-5ms),受限于 92 万像素 float32 数组的内存吞吐
|
||||
- 要数量级提升只能靠 cv2.LUT(C 查表),代价是引入 opencv 依赖
|
||||
- cv2.LUT 的 C 查表把帧预处理降一个数量级,且数值完全等价(0 LSB)
|
||||
|
||||
### 3.5 预期收益(修订)
|
||||
### 3.5 实测收益(已实施,集成后 NPU 实测)
|
||||
|
||||
| 路径 | 帧预处理 | 端到端 predict | 依赖 |
|
||||
|---|---|---|---|
|
||||
| 现状 | 35.9 ms | 52.4 ms | — |
|
||||
| 主方案(合并乘加) | ~31 ms | **~47 ms** | 无 |
|
||||
| 激进路径(cv2.LUT) | ~13 ms | **~25-30 ms** | opencv-python |
|
||||
| 指标 | 优化前 | 优化后(cv2.LUT) |
|
||||
|---|---|---|
|
||||
| 帧预处理(3 帧) | 26.5 ms | **1.7 ms** |
|
||||
| 端到端 predict | 52.4 ms | **16.0 ms(3.3×)** |
|
||||
| 输出一致性 | — | 与 float 路径 max_diff = **0.0000** |
|
||||
| HEF vs torch | 0.50° | 0.30°(4 角复验 PASS) |
|
||||
|
||||
闭环 10Hz(100ms 预算)下,主方案占用率 52%→47%;激进路径降至 ~28%,为 20Hz 留出空间。
|
||||
闭环 10Hz(100ms 预算)占用率 52% → **16%**,20Hz(50ms 预算)也宽裕。
|
||||
|
||||
## 4. 一致性约束(关键)
|
||||
|
||||
@@ -106,34 +116,36 @@ B[c] = -mean[c] / (std[c] * q_scale) + q_zp
|
||||
归一化还是直通,逻辑与现状一致
|
||||
- 旧 5 输入 / 旧版单图像 HEF 布局同样适用(按各自 quant_info 算系数)
|
||||
|
||||
## 5. 实施步骤
|
||||
## 5. 实施步骤(已完成 ✅)
|
||||
|
||||
| 步骤 | 内容 | 产出 |
|
||||
| 步骤 | 内容 | 状态 |
|
||||
|---|---|---|
|
||||
| 1 | `hailo_infer.py`:加载时按 quant_info 预计算通道系数 `A/B`(归一化/直通两种) | 系数构建 + 单测(与 float 路径逐点比对 = 0 LSB) |
|
||||
| 2 | `_frame_float` / `_bind_frames` 改为合并乘加路径(ndarray 直通、免 PIL、resize no-op 跳过) | 推理链路改造 + 单测(逐帧输出比对) |
|
||||
| 3 | NPU 实测:新 640×480 数据集 100 次均值 + 分段计时 | 基准对比(52.4 → ~47ms) |
|
||||
| 4 | `_npu_test.py` 4 角 + HEF vs torch 对比复验 | 量化误差不回归(≤1°) |
|
||||
| 5 | 全套单测(TestHailoPredictor 等) | 188 tests OK |
|
||||
| 6(可选) | 引入 cv2.LUT 路径(配置开关,默认关闭) | 激进路径 + 依赖权衡记录 |
|
||||
| 1 | `hailo_infer.py`:加载时按每帧 quant_info 构建 256 项 LUT(归一化/直通两种) | ✅ `_build_frame_luts` |
|
||||
| 2 | `predict` 的 nhwc_frames 分支走 cv2.LUT 快路径(尺寸不匹配时 PIL resize);无 cv2 回退 float | ✅ `_frame_lut_quant` + `HAS_CV2` |
|
||||
| 3 | NPU 实测:640×480 数据集 100 次均值 | ✅ 16.0 ms(52.4 → 16.0) |
|
||||
| 4 | `_npu_test.py` 4 角 + HEF vs torch 对比复验 | ✅ PASS,0.30°(≤1° 预算) |
|
||||
| 5 | 全套单测(TestHailoPredictor 等) | ✅ 188 tests OK |
|
||||
| 6 | 依赖安装:opencv-python-headless 5.0.0(清华镜像) | ✅ venv 已装 |
|
||||
|
||||
## 6. 风险与对策
|
||||
|
||||
| 风险 | 对策 |
|
||||
|---|---|
|
||||
| 乘加系数浮点误差 | 实测 0 LSB(系数用 float32 与现路径同精度);NPU 输出 vs torch 误差预算不变(≤1°) |
|
||||
| 主方案收益有限(仅 -5ms) | 如实记录(§3.4/§3.5);若需数量级提升走 cv2.LUT,否则可接受现状不优化 |
|
||||
| cv2 依赖体积/安装 | 激进路径默认关闭、配置开关;Pi arm64 opencv 安装成本单独评估 |
|
||||
| 未来输入尺寸变更 | 系数与尺寸无关(逐像素映射);resize 仍需走 PIL,仅影响附带优化 2 |
|
||||
| 查表取整与 float 路径存在差异 | 实测 0 LSB(表项 float32 与现路径同精度);NPU 输出 vs float 路径 max_diff=0,vs torch 0.30° |
|
||||
| 无 cv2 环境 | 惰性导入 + 自动回退 float 路径,行为与优化前完全一致 |
|
||||
| OpenCV 5.0 多通道 LUT 断言失败 | 逐通道单表 + `cv2.merge`(实测无性能损失) |
|
||||
| 非 nhwc_frames 布局(旧 HEF) | 仅该布局启用 cv2 路径,其余走原 float 路径 |
|
||||
| 未来输入尺寸变更 | LUT 与尺寸无关(逐像素映射);尺寸不一致时才走 PIL resize |
|
||||
|
||||
## 7. 决策记录
|
||||
|
||||
- **查表法(初版核心)实测证伪**:numpy 高级索引 gather 在 Pi5 上与逐元素 float
|
||||
运算同速(26.6 vs 28.5ms),"查表降为内存索引即快"的假设不成立——教训:
|
||||
性能方案须原型实测,不能凭直觉估计
|
||||
- **主方案选合并乘加**:纯 numpy 无新依赖、数值 0 LSB 等价、实施简单;-5ms 收益
|
||||
虽小但零风险。激进 cv2.LUT 留作可选(需权衡 opencv 依赖)
|
||||
- **是否优化的取舍**:现状 52.4ms 已满足 10Hz 闭环;若控制率需求不上探 20Hz,
|
||||
可仅做主方案(低成本)甚至不优化。激进路径仅在明确需要更高控制率时启用
|
||||
- **训练侧不改**:训练用 float 归一化(增强/数值精度),推理侧合并实现——两侧
|
||||
口径在"归一化值"层面一致,量化差异由部署链固有预算吸收(HEF vs torch 0.5° 余量充足)
|
||||
- **最终方案选 cv2.LUT**:C 实现查表把帧预处理从 26.5ms 降到 1.7ms(15.8×),
|
||||
端到端 52.4 → 16.0ms;数值 0 LSB 等价。合并系数乘加(-5ms)收益被碾压,
|
||||
不再实施(§3.1 保留为无 cv2 环境的备选)
|
||||
- **依赖取舍**:opencv-python-headless 作为可选依赖(惰性导入 + 自动回退),
|
||||
有 cv2 环境收益 3.3×,无 cv2 环境行为不变——收益远大于依赖成本
|
||||
- **训练侧不改**:训练用 float 归一化(增强/数值精度),推理侧查表实现——两侧
|
||||
口径在"归一化值"层面一致,量化差异由部署链固有预算吸收(HEF vs torch 0.30° 余量充足)
|
||||
Reference in new issue
Block a user