做智能摄像头项目最容易栽跟头的其实不是算法本身而是辛辛苦苦训练好的模型到了板子上根本不跑。我手里这块嘉立创泰山派RK3566配合RKNN工具链部署MobileNetV3做实时图像分类从模型转换、量化到摄像头取流、NPU推理、屏幕显示全部打通前后折腾了一个多星期。今天这篇实战分享就把这套技术路线和踩过的坑完整写出来给准备在泰山派上做视觉项目的朋友作参考。不管你是想做个桌面识别小玩意还是想评估RK3566的NPU到底能不能用这篇都应该能省你不少时间。1. 为什么是泰山派RK3566算力账、成本账和折腾成本1.1 智能摄像头项目的核心需求先说需求。我做的这个智能摄像头要求其实很朴素插上摄像头本地实时识别画面内容屏幕上直接看到结果。不需要联网不需要云端延迟还得低最好还能顺便学点RKNN相关的部署经验。这几个要求放在一起传统方案就不太行了。最简单的是纯CPU跑OpenCV加深度学习模型可一旦模型稍微复杂点帧率立马掉到个位数体验很糟糕。另一种思路是全部丢云端设备只负责推流这又引入网络延迟和带宽成本而且很多场景下摄像头画面根本不应该出本地网络。所以核心诉求是终端设备要有足够的算力同时得支持成熟、易用的模型部署链路。这正是我选择瑞芯微RK3566这条路线的原因。1.2 为什么在RK3566上跑MobileNetV3而不是云方案RK3566这板子NPU算力虽然没法跟高端产品比但跑MobileNetV3这个级别的轻量网络完全够用。MobileNetV3是Google用神经架构搜索做出来的结构上大量使用深度可分离卷积和SE注意力模型体积小、计算量低尤其是INT8量化后在NPU上执行效率非常高特别适合这种端侧摄像头场景。对比一下其他备选方案就能看出这套组合的合理性。树莓派4B用的是CPU推理跑MobileNetV3-Large一次推理得好几百毫秒做实时识别比较吃力加上价格也不便宜。Jetson Nano算力是强但价格翻了至少两倍而且现在不太好买。云方案前面说了延迟和隐私都是问题。最后落到RK3566它内置的NPU虽然只有不到1TOPS的水平但胜在工具链成熟RKNN-Toolkit2对PyTorch/ONNX导出支持完善官方还提供了详细的文档和示例代码。在几百块的板子上实现本地实时分类这套路线的性价比相当能打。1.3 泰山派这块板子的硬件底子嘉立创泰山派是基于RK3566做的一块开发板硬件接口给的相当全四核Cortex-A55处理器、NPU、MIPI-CSI摄像头接口、MIPI-DSI和LVDS屏幕接口、HDMI输出、千兆网口、USB3.0/2.0、UART串口调试引脚都有。最实在的是嘉立创把原理图、PCB、设备树源码、固件这些资料都开源了这意味着遇到问题时可以往底层查不用对着黑盒猜。内存方面我建议直接上2GB以上的版本。板端Linux系统加上OpenCV、摄像头采集这些常驻内存1GB会显得紧张。存储建议用自带的eMMC起步后期跑模型、存校准集都方便。如果你手头有MIPI摄像头模组可以直接接板载的CSI座子没有的话USB摄像头也行代码层面差距不大。2. 开发环境搭建从系统烧录到RKNN-Toolkit2跑通2.1 系统选择与烧录细节泰山派官方支持Debian、Ubuntu和Android几套系统。做AI视觉项目我推荐用Debian这类板级Linux环境干净Python生态好装也能直接操作V4L2摄像头设备。Android虽然也能跑但各种权限、SELinux问题会白白消耗时间。烧录时我用的是Windows下的瑞芯微开发工具。板子先完全断电按住板上的loader/recovery按键不松再用USB线连电脑这时候工具会识别到设备进入Loader模式。选择编译好的镜像点升级固件等进度条跑完就OK。这里有个小经验烧录前把USB线换个好点的劣质线在烧到一半时断开大概率会变砖别问我怎么知道的。2.2 RKNN-Toolkit2版本匹配老生常谈但必须谈RKNN的工具链分两侧PC端负责模型转换的工具叫RKNN-Toolkit2跑在x86的Ubuntu上板子端跑推理用的是RKNNLite和它自带的runtime库librknnrt.so。这两侧的版本必须匹配否则转换出来的模型在板子上加载会直接报错。我这次踩的坑是PC端装了最新版RKNN-Toolkit2板子固件里带的runtime还是几个月前的旧版本结果模型在PC端验证一切正常拷到板子上后load_rknn直接抛版本不匹配的异常。解决办法有两个要么把PC端工具降到跟固件runtime对应的版本要么去官方GitHub找到匹配的runtime库更新到板子上。我图省事直接降了PC端版本后续开发稳定不少。2.3 我的环境配置清单以下是我最终跑通的环境组合给同样用泰山派的朋友一个参照。位置项目版本/配置PC端操作系统Ubuntu 22.04 x86_64PC端Python3.10虚拟环境PC端RKNN-Toolkit21.6.0PC端PyTorch2.1.0仅用于导出ONNXPC端onnx1.14.0板端系统泰山派DebianBullseye板端Python3.9板端RKNNLite1.6.0配套版摄像头USB摄像头640x48030fps安装RKNN-Toolkit2只需要一条pip命令但建议在虚拟环境里装避免污染系统Python。装完执行rknn-toolkit2 -V能输出版本号就算第一步通了。3. MobileNetV3转换RKNN全链路导出、量化、部署3.1 从PyTorch到ONNXinput_tensor和opset这两个参数是关键RKNN不能直接吃PyTorch模型中间必须先转到ONNX再转RKNN。PyTorch导出ONNX这一步看似简单实际有两个参数很关键。第一个是opset_version我们用的torchvision的MobileNetV3建议设为12。opset版本太高会引入一些新算子RKNN的ONNX解析器不一定支持版本太低又可能丢失一些结构信息。第二个是推理模式导出前必须model.eval()否则BN层和Dropout的行为会是训练态的导出的模型结果完全不对。import torch from torchvision.models import mobilenet_v3_large, MobileNet_V3_Large_Weights model mobilenet_v3_large(weightsMobileNet_V3_Large_Weights.IMAGENET1K_V2) model.eval() dummy_input torch.randn(1, 3, 224, 224) torch.onnx.export( model, dummy_input, mobilenet_v3_large.onnx, input_names[input], output_names[output], opset_version12, do_constant_foldingTrue, )这里dummy_input的shape是(1, 3, 224, 224)对应batch1、RGB三通道、分辨率224x224。RKNN转换要求输入shape固定别开dynamic_axes否则转出来模型在NPU上的内存布局会很不可控。3.2 ONNX转RKNN配置文件就是你的说明书ONNX转RKNN用的是PC端的RKNN-Toolkit2代码很短但config这一步直接决定转换是否成功。内置的MobileNetV3导出后输出是1000类的logits直接用就行。from rknn.api import RKNN rknn RKNN() rknn.config( mean_values[[0, 0, 0]], std_values[[255, 255, 255]], target_platformrk3566, ) rknn.load_onnx(modelmobilenet_v3_large.onnx) rknn.build(do_quantizationFalse) rknn.export_rknn(mobilenet_v3_large_fp16.rknn)mean和std是模型输入的归一化参数。如果训练时用的是ImageNet的标准归一化mean0.485, 0.456, 0.406std0.229, 0.224, 0.225按道理这里应该填这组数。但我这次图省事直接在代码端把输入图片resize后除以255相当于把0到1的RGB直接送进去所以config里mean全0、std全255。这种方式在某些模型上精度会有小损失MobileNetV3这种鲁棒性强的网络基本无感。target_platform必须写rk3566写错了模型也能转出来但在板子上NPU运行时会异常。3.3 INT8量化校准集决定成败第一次我图省事直接用FP16跑效果也不错。但既然这块板子的NPU对INT8有加速优势我还是花了时间做量化。量化这一步最核心的是校准集——一组有代表性的真实图片用来统计每层激活值的分布从而确定量化scale。校准集的准备方式很简单找几十张到几百张和实际场景相近的图片统一缩放到224x224按每行一个路径的格式写进dataset.txt。calib/001.jpg calib/002.jpg calib/003.jpg ...然后build时打开量化开关rknn.build(do_quantizationTrue, datasetdataset.txt)这里容易出问题的点在于校准集图片必须和预处理方式保持一致。如果图片不是RGB顺序、没缩放到224x224或者压根就是一张超大图喂进去量化统计出来的分布就是错的模型输出的精度会崩。另外校准集数量也不用贪多100张左右足够超过500张收益就不明显了反而增加转换时间。量化后生成的mobilenet_v3_large_int8.rknn文件体积大约是FP16的四分之一NPU上推理也有可感知的加速。对MobileNetV3这种本身就很轻量的模型来说INT8量化后的精度损失通常不到1%。3.4 板端RKNNLite推理API板子上的部署代码和PC端不一样用的是RKNNLiteAPI更轻量。逻辑上就四步初始化解锁NPU、加载rknn模型、初始化runtime、推理。from rknnlite.api import RKNNLite rknn_lite RKNNLite() rknn_lite.load_rknn(mobilenet_v3_large_int8.rknn) rknn_lite.init_runtime(core_maskRKNNLite.NPU_CORE_0) # 假设img是预处理好的224x224 RGB numpy数组 outputs rknn_lite.inference(inputs[img])core_mask可以指定用哪个NPU核心有NPU_CORE_0、NPU_CORE_1、NPU_CORE_2和AUTO。实测MobileNetV3这种小模型绑单核心就行AUTO模式反而可能引入线程切换开销。还有一个容易被忽略的坑RKNNLite的输入是RGB还是BGR取决于你模型训练时的输入约定。torchvision的MobileNetV3是在RGB数据上训练的所以板端从OpenCV读到BGR图像后必须先cv2.cvtColor(img, cv2.COLOR_BGR2RGB)否则识别结果会明显变差。4. 摄像头采集与推理代码从V4L2到实时识别4.1 V4L2取帧三种方案对比在做智能摄像头时取帧是绕不开的一环。Linux下摄像头设备走的是V4L2框架常见三种方式v4l2-ctl命令行适合调试v4l2-ctl --list-devices能列出设备--stream-mmap可以测试采集但不可能在正式程序里用。OpenCV的VideoCapture最省事底层把V4L2的mmap封装好了直接读帧就行。缺点是多一层封装CPU开销略高但对这个项目级别完全够用。直接写V4L2 ioctl最底层也最灵活需要自己处理缓冲区队列适合对CPU占用特别敏感的场景。我选的是OpenCV方案成熟稳定而且后续画框、显示、resize都在OpenCV生态里能少写很多代码。import cv2 cap cv2.VideoCapture(0) cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640) cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480)如果板子上同时挂了USB摄像头和MIPI CSI摄像头VideoCapture(0)的设备号不一定是你要的那一个。先跑v4l2-ctl --list-devices确认一下或者直接用/dev/video1这种显式路径去打开。4.2 推理线程与显示线程分离如果按最简单的单线程流程——取帧、推理、画框、显示——你会发现帧率上不去而且画面一卡一卡的。原因是imshow这个窗口刷新操作是阻塞式的在窗口环境下会等待垂直同步尤其在板子通过HDMI或者远程显示的时候这个延迟极其明显。我的做法是把整个管道拆成两个线程采集线程只负责从摄像头取帧、resize、送进队列推理线程从队列拿图执行NPU推理、后处理、画框、显示。这两个线程之间用一个有最大长度的队列隔开队列满了就直接丢旧帧。import threading import queue import cv2 frame_queue queue.Queue(maxsize3) def capture_loop(cap): while True: ret, frame cap.read() if not ret: continue if frame_queue.full(): try: frame_queue.get_nowait() except queue.Empty: pass frame_queue.put(frame) threading.Thread(targetcapture_loop, args(cap,), daemonTrue).start()这样即使某个时刻推理慢了一拍也不会导致采集线程在内核缓冲区里越堆越多延迟不会一直累积。4.3 画框与标注的实现细节因为我们用的是MobileNetV3分类模型它本身不会输出目标的坐标框。想有个框来辅助定位我采取了一个取巧方案在画面中心区域画一个固定的ROI框实际送进NPU的是框内裁剪并缩放到224x224的图像。这样摄像头对准哪里框里就是模型识别的内容交互上很直观。h, w frame.shape[:2] x1, y1 int(w * 0.25), int(h * 0.25) x2, y2 int(w * 0.75), int(h * 0.75) cv2.rectangle(frame, (x1, y1), (x2, y2), (0, 255, 0), 2) roi frame[y1:y2, x1:x2] roi_resized cv2.resize(roi, (224, 224)) rgb cv2.cvtColor(roi_resized, cv2.COLOR_BGR2RGB)推理得到1000类的概率后按置信度排序取前5个类别用cv2.putText写到画面左上角。字体选FONT_HERSHEY_SIMPLEX厚度和大小按分辨率调整就行。prob softmax(outputs[0]) top5_idx np.argsort(prob)[::-1][:5] for i, idx in enumerate(top5_idx): text f{imagenet_classes[idx]}: {prob[idx]:.2f} cv2.putText(frame, text, (10, 30 i * 30), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 0), 2)如果你真的需要检测物体的位置那分类模型这条路就是绕远了直接上YOLO这类检测模型是正解。我第6节会专门讲YOLO26转RKNN的思路。5. 上板实测与踩坑帧率、LVDS屏幕和一次救砖5.1 实测性能数据整个系统跑起来之后我记录了这么一组数据。项目数值备注采集分辨率640x480USB摄像头模型输入224x224中心ROI裁剪后缩放MobileNetV3-Large INT8推理耗时约15ms纯NPU推理不含预处理MobileNetV3-Large FP16推理耗时约20ms同上总体流水线帧率25~30fps双线程瓶颈在显示和预处理CPU占用单核中等偏高主要在OpenCV的resize、BGR2RGB、画字这说明RK3566的NPU跑MobileNetV3-Large是完全没问题的推理时间占比很小真正吃掉CPU的是图像处理那一堆OpenCV操作。如果你把预处理也想办法优化比如用板子的RGA硬件缩放代替cv2.resize帧率还有上升空间。5.2 点亮LVDS屏幕设备树里的一处改动泰山派上有LVDS屏幕接口很多朋友想把画面输出到一块工业屏上。这个环节我没少折腾核心问题就是设备树配置。系统固件默认的设备树很多是给HDMI或MIPI-DSI用的LVDS屏幕需要你自己去改dts。关键节点有两个一个是panel节点里面要声明屏幕的compatible、尺寸、时序参数另一个是route_lvds节点必须显式status okay同时把对应的display path打开。我当时就是因为只改了LVDS的route没管panel的时序配置导致上电后屏幕只亮背光画面黑屏。查dmesg | grep -i lvds看到panel probe失败说明时序参数没对上。后来对照屏幕手册把clock频率、前后肩、同步极性问题解决屏幕才正常点亮。这块建议先用官方文档里验证过的屏参类型别上来就用自己的野屏不然排错成本很高。5.3 一次救砖经历maskrom模式刷机说到设备树就不得不提那次差点把板子刷成砖的经历。当时改了dts后编译出新的boot.img烧进去直接起不来板子串口完全没输出工具也识别不到Loader模式心里一凉。后来查到泰山派是有MaskRom模式的完全断电按住板上的MaskRom按键不松上电后再插USB线PC端瑞芯微开发工具就能识别出Maskrom设备。这时选择loader文件进行烧写先恢复基础引导再重新烧完整镜像板子就救回来了。这次经历的教训有三条第一改设备树之前备份好原始可用镜像第二测试新固件时准备一张SD卡系统作为备援启动方案避免每次都动eMMC第三MaskRom模式是最后的救命稻草但前提是硬件没坏别把电源和地接反。5.4 串口ch340调试的设置板级开发离不开串口。泰山派的调试串口引脚定义在网络上有接USB转串口模块时用的是CH340这类芯片接线要仔细板子的TX接模块的RX板子的RX接模块的TX电源和地共地。连接好后在Linux下用ls /dev/ttyUSB*确认设备有没有被识别再用picocom /dev/ttyUSB0 -b 1500000或者minicom进去。这里的波特率是个隐藏坑RK3566的SDK默认调试串口波特率经常是1500000而不是常见的115200。如果你打开全是乱码先检查是不是波特率不对再接错线也会导致完全无输出。CH340的驱动在Linux内核里一般都有插上就能识别。Windows下需要装一下官方驱动装好后设备管理器里能看到对应的COM口。6. 后续扩展从YOLO26到DINOv3RKNN吞得下吗6.1 YOLO26转RKNN的注意点这个项目跑通后很多朋友问我能不能换YOLO26来做目标检测。YOLO系列一直是边缘部署的热门但转RKNN时要特别注意输出层的处理。YOLO26这类检测器模型原生的输出往往已经包含了NMS后处理逻辑或者在导出ONNX时会带有大量解码分支。这些后处理算子不是NPU擅长的硬转过来要么算子不支持要么效率极低。建议的做法是导出ONNX时只保留backbone和检测头的原始输出NMS、decode这些全部放到板端的CPU上用numpy实现。这样RKNN模型只是纯粹的计算图转换成功率和板端推理速度都会好很多。另外YOLO的输出shape通常是动态batch或者自带anchor网格的RKNN对固定shape的支持最稳。导出前把输入固定到实际使用的分辨率比如640x640能减少很多麻烦。6.2 DINOv3一类的注意力模型迁移思路除了YOLO现在很多人想把Transformer结构的模型往RKNN上搬热词里就有DINOv3转RKNN。这类模型转RKNN最大的不确定性在于算子支持度比如LayerNorm、Multi-Head Attention在RKNN的算子列表里虽然有了但不同版本实现有差异。如果遇到不支持的算子我会先试这几个方向把ONNX用onnx-simplifier过一遍消除冗余节点固定所有输入shape包括序列长度把一些特殊激活函数替换为等价的组合算子。实在不行就只能把模型拆两部分一部分在NPU上跑另一部分回退到CPU。DINOv3这种大模型全量跑在RK3566上还是挺吃力的更适合提取特征后接一个小分类头而不是直接端到端推理。6.3 还有哪些模型可以替换上去基于现在这套摄像头推理框架换模型其实很便宜。想做人形检测、手部关键点、车牌识别只需要替换rknn模型文件和对应的预处理、后处理代码采集、显示、线程架构完全不用动。我接下来的计划是在这块泰山派上跑一个轻量目标检测模型把中心ROI框升级成真正的目标框再联动舵机做个简单的自动追踪云台。RK3566的NPU虽然小但处理这类轻量任务是够用的关键是工具链已经跑通后续扩展就是换模型和调后处理的问题了。最后分享一个个人经验先别急着把模型量化成INT8先用FP16把整条数据和代码链路跑通确认流程没问题后再上INT8。否则一旦出问题你很难判断是量化引入的精度损失还是前处理或代码逻辑的bug。做端侧AI项目稳定的核心链路比单点的极致性能重要得多。
