
35 块钱能买到什么两杯奶茶或者一块带 NPU 的 Linux 开发板。凌智视觉模块背后的 RV1106 芯片第一次出现在货架上的时候我就盯上了它单核 Cortex-A7、0.5 TOPS INT8 算力、支持 MIPI 摄像头价格压到了消费级。这个配置最适合拿来做嵌入式视觉里的轻活而人像分割正是其中最有代表性的一个。这个决定最后落地在一个智能终端项目上设备要实时把摄像头画面中的人像抠出来再叠加到虚拟背景上呈现给用户——过去这活儿至少得上 RK3588 或者用 PC 端跑。换到 RV1106 PP-HumanSeg 这个组合之后硬件成本直接降了一个数量级。整个过程踩了不少坑从模型导出到量化再到板端调试每一环都重新认识了边缘部署这四个字。这篇文章记录的就是从零到实机的完整链路硬件评估、SDK 选型、模型转换、C 推理代码、性能调优和故障排查。如果你也在评估这个算力档位能不能跑分割模型或者手上正好有凌智板子想复现可以直接照着做。1. 0.5 TOPS 算力底细RV1106 能跑什么不能跑什么先别急着刷固件拿到一块新板子之前得先搞清楚这颗芯片到底有多少家底。RV1106 是瑞芯微面向 IPC 和视觉模组市场出的 SoCCPU 只有一颗 ARM Cortex-A7主频 1.2GHz没有 GPU。真正干活的是内置的 0.5 TOPS NPU支持 INT8/INT16 量化还能跑一点点 INT4。这个算力放在手机 SoC 面前是不值一提但在 35 块钱的价位上它就是同级别里能买到的最强视觉硬件。0.5 TOPS 是什么概念换算一下大概就是每秒能完成 5000 亿次 int8 乘加运算。跑 MobileNetV3 这种轻量分类网络可以做到实时跑 YOLOv5s 这种检测模型稍微吃力但也能到 5~10 FPS。而像 PP-HumanSeg 这种人像分割模型本身就做了极致的轻量化设计Lite 版本的骨干网络是 PPLCNet计算量控制得很低输入分辨率压到 192x192 之后在 RV1106 的 NPU 上单次推理能控制在 20ms 以内。这就在硬件能力边界内站住脚了。1.1 算力之外容易被忽略的 RGA 和 ISP很多人只看 NPU 数字忽略了 RV1106 外围还藏着两个对视觉管线至关重要的硬件模块ISP 和 RGA。ISP 负责图像信号处理接 MIPI 摄像头时RAW 数据通过 ISP 输出 YUV 或 RGB这个流程是硬件跑的不占 CPU。凌智视觉模块板载的摄像头接口就是走这条链路开发时要善用它别自己去写软件做 debayer 之类的事情。RGA 是一个 2D 图形加速单元能做缩放、格式转换、旋转、裁剪。预处理里最重的 RGB 图像缩放、NV12 转 RGB都可以交给 RGACPU 全程不参与。这一点在实时管线里非常重要因为我实测下来如果用 CPU 软缩放大尺寸图延迟立刻多出 30ms直接把整个帧率拖垮。1.2 为什么选 PP-HumanSeg 而不是其他分割模型市面上能跑的人像分割模型不少但大部分是给 GPU 设计的参数量动辄几十 MB部署到 0.5 TOPS 的 NPU 上就是灾难。UNet 结构虽然经典但卷积通道数太大量化到 int8 后精度衰减也明显。DeepLabV3 有 ASPP 模块感受野大但计算量更大。PP-HumanSeg 是飞桨 PaddleSeg 里的一个专门做人体分割的系列分三个档位PP-HumanSegV1、PP-HumanSegV2、PP-HumanSeg-Lite。其中 Lite 版本就是为移动端和边缘设备准备的模型文件大小只有几 MBINT8 量化后更是压缩到不到 5MB输入 192x192 的单次前向计算量大概只有 0.2 GFLOPs 出头。这个量级放在 RV1106 的 NPU 上基本时间都在传输和调度上计算本身反而不是瓶颈。更重要的是PaddleSeg 官方针对这个模型提供了非常成熟的部署链路Paddle 推理模型可以转 ONNXONNX 可以转 RKNN每一环的工具链都是现成的。而且 PP-HumanSeg 在各类人像场景上的泛化效果经过了大规模数据验证比自己在小数据集上从头训练一个 UNet 要靠谱得多。1.3 凌智视觉模块的几款 SKU 怎么选凌智视觉模块的产品线分好几个型号核心区别主要在内存和 Flash 上。入门款一般是 64MB DDR 128MB SPI NAND高配款能做到 256MB DDR。内存大小直接影响能跑什么分辨率、能不能开较大的帧缓冲和模型缓冲区。我的建议是如果只跑 PP-HumanSeg Lite 192x192 输入64MB 版本勉强够用但系统可用内存会非常紧张跑起来容易触发 OOM。256MB 版本就从容很多还能顺便做点 UI 或者网络传输。如果这个项目要加 OpenCV、RTSP 推流、或者跑多个模型直接上最高内存版本别在几十块钱上抠后面调试的时间更值钱。2. 环境搭配与模型导出从 PaddleSeg 拿到一个干净的推理模型硬件评估清楚了下一步就是搭环境。这个环节最容易翻车因为瑞芯微的工具链、飞桨的模型库、Linux SDK 这三者各自都有自己的版本要求组合在一起稍不注意就出现莫名其妙的问题。2.1 主机上的 Python 环境模型转换这一步在 x86 的 Ubuntu 主机上完成推荐用 Ubuntu 20.04 或者 22.04Python 版本选 3.8 或 3.10。rv1106 对应的 RKNN-Toolkit2 版本在 1.6.0 之后才正式加入支持所以装的时候别选太老的版本。创建一个虚拟环境安装依赖python3 -m venv rknn_env source rknn_env/bin/activate pip install rknn-toolkit21.6.0这里有个坑rknn-toolkit2 的 pip 包依赖的 numpy、onnx、PaddlePaddle 版本可能会冲突。建议先装 rknn-toolkit2再根据它的报错去补装其他包不要一上来就把最新版 numpy 装上最新版 numpy 有时候会和 rknn 的 C 扩展不兼容导致导入直接闪退。2.2 克隆 PaddleSeg 并导出推理模型接下来在主机上克隆 PaddleSeg 仓库git clone https://github.com/PaddlePaddle/PaddleSeg.git cd PaddleSeg pip install -r requirements.txt然后从 PaddleSeg 的模型库下载 PP-HumanSeg-Lite 的预训练权重。这里有一个关键决策导出推理模型时要不要带 argmax。命令如下python tools/export.py \ --config configs/pp_humanseg_lite/pp_humanseg_lite.yml \ --model_path output/pretrain/pp_humanseg_lite/model.pdparams \ --save_dir inference_model/pp_humanseg_lite默认导出的模型输出是原始 logits形状为 [1, 2, H, W]两个通道分别对应背景和人像。不要加--output_op argmax原因有二一是 argmax 在 RKNN 转换时可能会被拆成多个算子增加转换失败的概率二是 argmax 拿到的是离散索引后处理如果想做边缘羽化就没素材了。我们在板端拿 logits 自己做 softmax灵活得多。导出完成后inference_model/pp_humanseg_lite/目录下有两个文件model.pdmodel和model.pdiparams。可以用 Netron 打开看一眼确认输入节点名称和输出节点名称。输入通常叫x输出通常叫softmax_0.tmp_0或者sigmoid_0.tmp_0具体以 Netron 显示为准这个信息在下一步转 ONNX 时要用。2.3 输入尺寸的取舍为什么我选 192x192PP-HumanSeg-Lite 官方支持 192x192 和 256x256 两种输入。我两个尺寸都试过实测下来 256x256 的 NPU 推理时间是 192x192 的 1.7 倍左右换来的分割边缘质量提升却很有限。原因在于人像分割是一个结构性很强的任务人形轮廓的信息量主要集中在低频和中频192 分辨率已经能覆盖大部分人形骨架和轮廓特征继续提高分辨率主要改善的是发丝、手指这些高频细节而这类细节在 int8 量化后本来就容易丢提升幅度并不成正比。如果你的应用是绿幕直播这种对边缘细腻度要求非常高的场景可以用 256x256配合后处理里的边缘羽化来补偿量化误差。但普通的人机交互、人数统计、会议背景替换192x192 完全够用而且能把宝贵的 NPU 算力留给其他任务。3. Paddle → ONNX → RKNN每一步都有人在栽跟头的转换链路模型转换是整条部署链路里最容易磨人心态的一环问题通常不是出在某个大方向上而是散落在各种小细节里。这一节我把三个转换步骤完整走一遍顺便把容易踩的坑都标出来。3.1 paddle2onnx 转换与输出节点检查PaddleSeg 导出的是 Paddle 推理模型第一步先转 ONNXpaddle2onnx \ --model_dir inference_model/pp_humanseg_lite \ --model_filename model.pdmodel \ --params_filename model.pdiparams \ --save_file model.onnx \ --opset_version 11 \ --enable_onnx_checker Trueopset_version 选 11 是基于 RKNN 工具链的兼容性考虑太新的 opset 瑞芯微还不一定完全支持。转换完用 Netron 再确认一遍重点看 Resize、ArgMax 这类算子是否存在。PaddleSeg 导出的模型里偶尔会出现Flip算子如果看到它别慌通常是数据增强残留用 onnxsim 可以顺手清理掉。3.2 ONNX 简化python -m onnxsim model.onnx model_sim.onnxonnxsim 会做常量折叠、冗余节点消除、算子融合。对于 PP-HumanSeg 这种结构sim 完之后节点数能减少 20% 左右。这一步强烈建议做不仅能让模型更干净还能减少 RKNN 转换时算子不支持的风险。3.3 RKNN 转换脚本与量化数据集接下来写 RKNN 转换脚本核心配置如下from rknn.api import RKNN rknn RKNN(verboseTrue) ret rknn.config( mean_values[[127.5, 127.5, 127.5]], std_values[[127.5, 127.5, 127.5]], target_platformrv1106, quantized_dtypeasymmetric_quantized-8, quantized_algorithmnormal, optimization_level3, ) assert ret 0, config failed ret rknn.load_onnx(modelmodel_sim.onnx) assert ret 0, load_onnx failed ret rknn.build(do_quantizationTrue, datasetdataset.txt) assert ret 0, build failed ret rknn.export_rknn(pp_humanseg_lite.rknn) assert ret 0, export failedmean_values和std_values填127.5是因为 PaddleSeg 训练时的归一化方式就是把 [0,255] 像素映射到 [-1,1]这个配置必须和训练时保持一致否则量化后的模型精度会明显下降。asymmetric_quantized-8是 RKNN 的默认量化方式对分割模型来说效果比较稳定。量化数据集是关键。dataset.txt里每行写一张图片的路径这些图片在量化时会被用来统计激活值的分布范围。我强烈建议采集 50~100 张和真实场景分布一致的图片比如你这个项目是在室内就用室内不同光照、不同距离、不同人数的人像照片。如果随便网上拉几张自然风景图凑数量化出来的模型在真机上分割效果会非常随机边缘可能糊成一片。3.4 模拟器与真机结果的一致性验证转换完成后先用模拟器做一次推理验证ret rknn.init_runtime(targetNone) img cv2.imread(test.jpg) img cv2.resize(img, (192, 192)) outputs rknn.inference(inputs[img])模拟器是跑在 x86 主机上的速度比真机快注意不要把它当性能基准。它主要用来验证转换后的模型逻辑是否正确。我通常会把模拟器输出的浮点结果和原始 Paddle 模型的输出做一个对比计算一下最大绝对误差。误差在 1e-2 量级是正常的如果误差到了 1e-1 以上就要怀疑量化配置或者数据集是不是出了问题。真机验证需要把 .rknn 文件推到板子上用 rknn-toolkit2 自带的板端推理 demo 跑一遍确认能正常加载和推理再进下一步写正式的业务代码。4. 板端 C 程序图像进来、掩码出去的完整实现模型文件拿到手接下来进入真机开发阶段。我建议用 C 语言写推理程序不要用板端的 Python 运行时。RV1106 的 CPU 性能有限Python 解释器本身就吃掉不少资源而且 rknn python 接口在板端封装得比较厚内存拷贝开销明显。C 接口虽然写起来麻烦一点但每个环节的资源消耗都是可控的。4.1 RKNN 推理流程的 API 调用顺序RKNN C API 的核心调用流程非常固定rknn_init加载 .rknn 模型初始化上下文rknn_query查询输入输出 tensor 的属性比如形状、格式、名称rknn_inputs_set设置输入数据rknn_run执行推理rknn_outputs_get获取输出rknn_outputs_release释放输出rknn_destroy销毁上下文一个最常见错误是忽略rknn_query这一步直接写死输入输出的形状。我建议无论模型多简单都先查询一下 tnesor 属性把宽高和通道数打印出来确认和预期一致再往下写。加载模型的代码样例#include rknn_api.h static rknn_context ctx; int load_model(const char *path) { FILE *fp fopen(path, rb); if (!fp) return -1; fseek(fp, 0, SEEK_END); int model_size ftell(fp); fseek(fp, 0, SEEK_SET); unsigned char *model_data malloc(model_size); fread(model_data, 1, model_size, fp); fclose(fp); int ret rknn_init(ctx, model_data, model_size, 0, NULL); free(model_data); return ret; }4.2 用 RGA 做图像缩放和格式转换摄像头采集到的原始帧通常是 NV12 格式分辨率是摄像头传感器的原生分辨率比如 1280x720而模型输入是 192x192 RGB这个转换如果用 CPU 做每一帧会多消耗不少时间直接掉帧。正确姿势是用 RGA。Luckfox SDK 里提供了 librga可以这样用#include rga.h static RgaInfo src_info, dst_info; int rga_resize_convert(void *src_buf, int src_w, int src_h, void *dst_buf, int dst_w, int dst_h) { memset(src_info, 0, sizeof(src_info)); memset(dst_info, 0, sizeof(dst_info)); src_info.fd -1; src_info.virtualAddr src_buf; src_info.width src_w; src_info.height src_h; src_info.wstride src_w; src_info.hstride src_h; src_info.format RK_FORMAT_YCbCr_420_SP; // NV12 dst_info.fd -1; dst_info.virtualAddr dst_buf; dst_info.width dst_w; dst_info.height dst_h; dst_info.wstride dst_w; dst_info.hstride dst_h; dst_info.format RK_FORMAT_RGB_888; return rga_convert(src_info, dst_info, 0, 0, 0); }这里有几个容易踩的点。第一wstride和width的区别要搞清楚如果摄像头输出的行存在对齐wstride不一定等于width。第二RGA 的缓冲区要求对齐到 16 字节直接用 malloc 分配可能有风险建议用posix_memalign。4.3 从 logits 到 Alpha 遮罩的后处理进入 NPU 之前输入数据是一个 192x192x3 的 uint8 数组按 NHWC 排列。推理结束后输出数据我们设置为want_float1拿到的是浮点 logits形状为 [1, 2, 192, 192]通道 0 是背景 logits通道 1 是人像 logits。后处理的核心是把这对 logits 转成前景概率再量化到 0~255 输出float *out0 (float *)outputs[0].buf; // 背景通道 float *out1 out0 192 * 192; // 人像通道 uint8_t *alpha (uint8_t *)malloc(192 * 192); for (int i 0; i 192 * 192; i) { float max_val out0[i] out1[i] ? out0[i] : out1[i]; float exp0 expf(out0[i] - max_val); float exp1 expf(out1[i] - max_val); float prob exp1 / (exp0 exp1); // softmax 后取人像概率 alpha[i] (uint8_t)(prob * 255.0f); }后面如果想输出到屏幕或者叠加背景就把 alpha 值映射到 UI 层做 alpha blending。如果只是做人流统计直接对 alpha 做阈值二值化统计人像像素占比就行。4.4 一个可以落地的单线程主循环这里给出一个最简单但能跑通的单线程架构从 V4L2 持帧RGA 缩放转换送入 NPU取回后处理渲染显示。逻辑如下while (running) { // 1. 从摄像头取一帧 NV12 void *nv12_buf vi_get_frame(); // 2. RGA 转换到 192x192 RGB rga_resize_convert(nv12_buf, 1280, 720, rgb_buf, 192, 192); // 3. 设置 RKNN 输入 rknn_input inputs[1] {0}; inputs[0].index 0; inputs[0].type RKNN_TENSOR_UINT8; inputs[0].fmt RKNN_TENSOR_NHWC; inputs[0].buf rgb_buf; inputs[0].size 192 * 192 * 3; rknn_inputs_set(ctx, 1, inputs); // 4. 推理 rknn_run(ctx, NULL); // 5. 获取输出 rknn_output outputs[1] {0}; outputs[0].want_float 1; rknn_outputs_get(ctx, 1, outputs, NULL); // 6. 后处理 post_process(outputs[0].buf, alpha_buf); // 7. alpha 叠加渲染 render_composite(alpha_buf); rknn_outputs_release(ctx, 1, outputs); }这个单线程版本一帧里所有操作是串行执行的优点是逻辑清晰、适合先跑通流程。实测下来帧率大约在 12~18 FPS具体取决于摄像头分辨率和 RGA 耗时。如果业务上需要更高的帧率下一节讲怎么拆线程。5. 实测帧率与三个行之有效的调优方向跑通一个 demo 很简单真正难的是在 0.5 TOPS 的算力上压出稳定可用的实时帧率。这一节给出我实测的数据和一些经过验证的调优方法。5.1 帧率实测数据测试环境凌智视觉模块 256MB 版本MIPI 摄像头输出 1280x72030FPS模型输入 192x192单线程串行逻辑。用time统计各环节耗时如下环节耗时V4L2 取帧5~8 msRGA 缩放格式转换4~6 msNPU 推理18~25 ms后处理 softmax alpha3~5 ms渲染合成2~3 ms总周期35~47 ms实际帧率21~28 FPS这个数据说明NPU 推理确实是主要瓶颈但还没有占到绝对主导地位。如果能把取帧、渲染这些外设操作和推理并行起来帧率提升空间还很大。5.2 CPU 与 NPU 的负载分布用top观察板子的实时负载跑推理时 CPU 占用大概在 35%~50%其中相当一部分被后处理的 softmax 循环吃掉了。RGA 不占 CPU这一点在数据上体现得很明显同样的管线如果不走 RGA 而用 CPU 软缩放CPU 占用会直接飙升到 80% 以上总帧率掉到 12 FPS 以下。5.3 提升实时性的三个方向第一个方向是把取帧和渲染拆到独立线程。摄像头采集是阻塞操作如果和推理串行一帧的采集等待时间会被重复计入。用一个双缓冲机制采集线程往 buffer A 写入推理线程在 buffer B 上跑推理两帧轮流切换这样取帧的等待基本被隐藏。第二个方向是减小后处理的尺寸。当前后处理是对 192x192 的整幅图做逐像素 softmax如果用查表法把 exp 计算换成预计算的概率表后处理耗时能从 4ms 降到 1ms 以内。具体做法是把两个 logits 的差值映射到 0~255 的整数区间用差值查表得到概率。第三个方向是调整 NPU 的频率策略。RV1106 的 NPU 时钟可以在驱动层面进行配置把 NPU 频率从默认值拉高一档推理时间能缩短 10% 左右。代价是功耗上升、发热变大如果产品是长期运行的建议在温升允许范围内小幅上调。我的实测方案是把采集线程和推理线程拆开之后帧率稳定在了 28~30 FPS代价是代码复杂度上升了不少但换来的流畅度提升非常明显。6. 真机部署中我反复遇见的四类问题部署阶段的问题往往不是逻辑层面的而是环境、格式、资源这些看似不起眼的细节。以下四个问题是我在多个项目中反复踩过的写出来给你省点排查时间。6.1 rknn_init 返回非零的排查链路模型加载失败第一反应该是版本不匹配。RKNN 模型文件和 rknn-toolkit2 的版本强相关用 toolkit 1.6.0 生成的 .rknn 文件如果板端 runtime 库是 1.5.0 的加载大概率失败。排查链路先在板端确认 runtime 库版本再确认生成的 .rknn 文件是用同一个版本工具链打包的。另外注意RV1106 和 RV1103 的 NPU 驱动和 runtime 库是共用的但不同 chip 的模型不能混用。如果你之前给 RV1103 转换过模型千万记得target_platform要改成 rv1106。6.2 量化后分割边缘粗糙的根因量化后模型的分割质量下降是正常现象但如果边缘粗糙到轮廓都看不清回头查两件事。第一是量化数据集是否覆盖了真实场景中的人体姿态多样性——如果数据集里全是直立全身照到了真机上遇到坐姿、侧卧、手部遮挡就会崩。第二是检查mean_values和std_values是否和训练配置一致这个参数错了量化统计的激活值范围整体偏移精度直接崩盘。6.3 画面发绿或者颜色错乱RGA 转换后的颜色不对多半是 NV12 的 UV 顺序搞错了。NV12 是 YUV 420 半平面排列U 和 V 的起始位置不同如果采集线程拿到的是 NV21RGA 里的格式却配成了 NV12输出的 RGB 就是偏色的。排查方法是先用一张纯色画面做对齐测试确认 RGA 输出的颜色空间正确后再接推理链路。6.4 偶发掉帧与输入缓冲区未及时刷新一跑就是几十分钟偶发卡顿几帧这种问题最难定位。我遇到过一次是 V4L2 缓冲队列溢出采集速率偶尔高于消费速率驱动丢帧。解决方案是把 V4L2 的 buffer 数量从默认的 4 个调大到 8 个同时在取帧之后主动释放上一次的 buffer避免队列堆积。这件事排查了很久最后是照着 V4L2 文档逐字段核对才发现 buffer 参数没配置到位。最后再说两句整个项目做下来我的体会是在 RV1106 这种低算力平台上做部署模型转换只是门槛系统集成才是真正的大头。模型本身只花了几天就转完了后面花在 RGA 格式对齐、线程同步、性能调优上的时间反而占了七成。这个经验也直接改变了我的技术选型习惯——现在评估一个新的嵌入式视觉方案我会先算清楚 ISP、RGA、NPU 这三者的协同关系而不是只盯着 NPU 的 TOPS 数字。如果你也打算在凌智视觉模块上跑 pp-humseg建议从这一篇文章里提到的环境版本开始不要一上来就追最新版工具链。新版工具链可能会修复一些 bug但也可能引入新的兼容问题。用一套经过验证的版本组合先把整条链路跑通再去考虑升级。这样即使遇到问题排查范围也能收窄很多。