前言上一篇我们把PaDiM模型的 backbone 导出成了 ONNX统计参数也存好了。接下来就是把 ONNX 转成 高通QCS9100平台上跑的格式再在板子上把完整推理链路跑通。说实话QNN 这套工具链第一次用会觉得有点绕但只要按步骤来问题不大。这篇会尽量把命令和路径写清楚你照着做能少踩点坑。1. QNN SDK 安装与路径说明1.1 下载 QNN SDK去高通开发者官网下 QNN SDK选和你 QCS9100 系统匹配的版本比如 2.18、2.20 等。解压后目录大概长这样qnn-v2.18.0/ ├── bin/ │ ├── x86_64-linux-clang/ # x86 Linux 工具 │ └── aarch64-android/ # Android 目标 ├── lib/ ├── include/ └── ...我们主要用qnn-onnx-converter和qnn-model-lib-generator这两个在 bin 下面。1.2 设置环境变量export QNN_SDK_ROOT/path/to/qnn-v2.18.0 export PATH$QNN_SDK_ROOT/bin/x86_64-linux-clang:$PATH export LD_LIBRARY_PATH$QNN_SDK_ROOT/lib/x86_64-linux-clang:$LD_LIBRARY_PATH建议写进~/.bashrc每次开终端自动生效。1.3 检查工具是否可用qnn-onnx-converter --help qnn-model-lib-generator --help能打出帮助信息说明环境 OK。2. ONNX 转 QNN 模型2.1 准备 ONNX 模型上一篇导出的padim_resnet18_backbone.onnx直接拿来用。如果输入是动态 batch建议先固定成 1避免板端一些奇奇怪怪的问题# fix_onnx_batch.py import onnx from onnx import shape_inference model onnx.load(padim_resnet18_backbone.onnx) # 固定 batch1 for inp in model.graph.input: for d in inp.type.tensor_type.shape.dim: if d.dim_param batch: d.ClearField(dim_param) d.dim_value 1 onnx.save(model, padim_resnet18_backbone_fixed.onnx)2.2 运行转换qnn-onnx-converter \ --input_network padim_resnet18_backbone.onnx \ --output_path padim_qnn \ --input_list input_names.txtinput_names.txt内容根据你 ONNX 的实际输入名调整image 1 3 256 256格式是输入名 batch channel height width。2.3 量化QCS9100 的 NPU 跑 INT8 比 FP32 快很多建议做量化。准备一些校准图片正常样本裁成 256×256列个清单# calibration_list.txt /path/to/img1.png /path/to/img2.png ...然后qnn-onnx-converter \ --input_network padim_resnet18_backbone.onnx \ --output_path padim_qnn_quant \ --input_list input_names.txt \ --quantization_overrides quantization_overrides.jsonquantization_overrides.json示例按需调整{ ConvertFp16ToFp32: [Conv, Gemm], Quantization: { image: { quantization_scheme: quantization_scheme_range_unsigned_symmetric, bit_width: 8, calibration_method: calibration_method_per_channel } } }量化这块不同版本 QNN 参数可能不一样以官方文档为准。转完之后会得到.bin和.cpp等文件这些就是板端要用的。3. 生成可执行库qnn-model-lib-generator \ --model padim_qnn/padim_resnet18_backbone.cpp \ --model padim_qnn/padim_resnet18_backbone.bin \ --output_dir padim_lib生成完会有一坨.so或静态库这就是在 QCS9100 上要链接的推理库。4. 板端部署思路4.1 文件准备把下面这些拷到板子padim_lib/下的库文件padim_mean.npy、padim_cov_inv.npyQNN 的 runtime 库在 QNN SDK 的lib里按目标架构选4.2 推理流程伪代码初始化QNN context加载 backbone 模型加载padim_mean.npy、padim_cov_inv.npy循环a. 从摄像头/文件读一帧图像b. 预处理resize 到 256×256减均值除方差和训练时一致c. 调用 QNN 执行 backbone 前向 → 得到 layer1/2/3 特征d. 按 PaDiM 论文把多层特征拼接、reshapee. 用马氏距离公式算异常分数f. 大于阈值判异常可选画热力图4.3 Python 板端示例有 QNN Python 绑定时# 伪代码具体 API 以 QNN Python 包为准 from qnn import QNNRuntime import numpy as np rt QNNRuntime(model_pathpadim_backbone.qnn) mean np.load(padim_mean.npy) cov_inv np.load(padim_cov_inv.npy) def infer(image): # image: 256x256x3, 预处理后 feat rt.execute(image) # 按 PaDiM 公式算马氏距离 score mahalanobis(feat, mean, cov_inv) return score4.4 C 板端高通一般推荐用 C 调 QNN性能更好。流程就是用QnnContext加载模型QnnInterface::graphExecute()跑推理取输出 buffer在 CPU 上做马氏距离和热力图这部分代码量不少建议直接参考 QNN SDK 里的 sample把输入输出改成 PaDiM 的就行。5. 性能与阈值 tuning时延backbone 在 NPU 上大概几毫秒到十几毫秒看量没量化、输入尺寸马氏距离在 CPU 上很快整体可以做到几十毫秒一帧。阈值用验证集或测试集正常/异常样本算一遍分数画个分布挑个合适的阈值让召回率和误报率平衡一下。6. 小结这篇把 ONNX → QNN → 板端推理的主线走通了重点是QNN SDK装好环境变量设对qnn-onnx-converter把 backbone 转成 QNN 格式能做量化尽量做板端QNN 跑 backboneCPU 算统计部分两者结合就是完整的 PaDiM 推理该系列完。