ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

RK3588边缘计算实战:NanoTrack单目标跟踪Python部署与NPU加速

RK3588边缘计算实战:NanoTrack单目标跟踪Python部署与NPU加速 1. 为什么要在RK3588上折腾NanoTrack1.1 这个组合到底解决什么问题单目标跟踪这件事说白了就是在视频第一帧框出你要盯住的目标后面每一帧算法自己去找它在哪。安防监控里盯人、无人机跟拍、智能座舱里跟踪驾驶员视线方向都是这个技术的典型落地场景。NanoTrack是2021年出来的一个轻量级跟踪网络整个模型只有不到1M参数在PC上跑随便都是几百帧但问题是——你不可能在每个摄像头后面都塞一台x86主机。RK3588这颗芯片这两年火得不行8核CPU4个A76大核4个A55小核、Mali-G610 GPU、6TOPS算力的NPU关键是功耗低、接口全、价格还算能接受。把它和NanoTrack凑一块就是想在边缘端用极低成本实现实时单目标跟踪。我实测下来NPU上跑NanoTrack的骨干网络加上CPU做后处理整体能稳在120FPS左右这个数字对于1080P输入来说已经相当够用了。这篇文章适合谁看如果你手里有一块RK3588开发板会一点Python想跑个实时视觉算法但不想从头啃RKNN的文档那这篇就是给你写的。我会把从环境搭建到模型转换再到Python部署的完整链路拆开讲包括我踩过的那些坑。1.2 为什么选Python而不是C很多人一提到嵌入式部署就觉得必须上C其实要看场景。NanoTrack的计算瓶颈在NPU上的卷积推理这部分RKNN的C API和Python API性能几乎没差别因为底层都是同一套运行时。Python真正拖后腿的是前后处理——图像resize、归一化、NMS这些。但NanoTrack的前后处理非常轻模板帧只做一次搜索帧的预处理就是简单的cropresize用OpenCV的SIMD优化版本跑起来完全不是瓶颈。用Python的好处太明显了调试快、改起来方便、和numpy生态无缝衔接。你调跟踪效果的时候想可视化一下响应图、想临时改个阈值看看效果Python几行代码就搞定C你得重新编译整个工程。所以我的建议是原型验证和中等负载场景直接用Python等算法完全定型了、确实遇到性能天花板了再考虑迁移到C。1.3 120FPS这个数字是怎么来的先把这个数说清楚免得有人觉得我在吹。120FPS指的是纯推理后处理的吞吐不含视频解码和显示。具体拆解一下NanoTrack的backbone在NPU上单次推理大约4ms搜索帧分支模板帧分支只在第一帧跑一次可以忽略互相关计算和后处理在CPU上大约2-3ms加起来单帧约6-7ms换算过来就是140-160FPS的理论上限。实际跑的时候因为Python GIL、内存拷贝、调度抖动等因素稳定在120FPS左右。如果你把视频解码比如用MPP硬解和显示也加进来端到端延迟会上去但吞吐量还是能维持在100FPS以上。这个性能对于25FPS或30FPS的摄像头输入来说余量非常充足你甚至可以在同一块板子上再跑一个检测模型做跟踪丢失后的重检测。2. 环境搭建从零到能跑通Python2.1 系统选择与基础环境确认RK3588开发板到手后第一件事是确认系统版本。我用的固件是基于Ubuntu 20.04的官方SDK编译出来的内核5.10。如果你拿到的是Android固件建议先刷成Ubuntu因为RKNN的Python wheel在Linux下支持最好。刷机教程官方Wiki上有用RKDevTool通过USB烧写就行注意区分Loader模式和Maskrom模式。系统起来之后先做几件事# 确认NPU驱动版本 cat /sys/kernel/debug/rknpu/version # 确认CPU信息 lscpu | grep -E Model name|CPU\(s\) # 确认内存 free -hNPU驱动版本很关键它决定了你能用哪个版本的RKNN-Toolkit2和runtime。我这边驱动是0.9.2对应RKNN-Toolkit2 1.5.0。版本不匹配会出现推理结果全零或者直接报错的情况这个后面排查章节会细说。2.2 Python环境配置的坑系统自带的Python是3.8够用。但千万不要直接用系统Python装包会污染系统环境导致一些系统工具挂掉。用venv建虚拟环境sudo apt update sudo apt install -y python3-venv python3-pip python3-dev python3 -m venv ~/nanotrack_env source ~/nanotrack_env/bin/activate pip install --upgrade pip然后装基础依赖。这里有个坑OpenCV不要用pip装的opencv-python那个版本不带RK3588的硬件加速而且和系统里的libopencv冲突。正确做法是用apt装系统包然后在venv里通过--system-site-packages继承过来或者手动把cv2的so文件软链到venv的site-packages里。sudo apt install -y python3-opencv libopencv-dev # 验证 python3 -c import cv2; print(cv2.__version__)numpy版本也要注意RKNN-Toolkit2 1.5.0要求numpy1.24装高了会报np.float被移除的错误。直接锁版本pip install numpy1.23.52.3 RKNN运行时安装RKNN的Python运行时包叫rknn_toolkit_lite2注意不是rknn_toolkit2。后者是PC上用来转换模型的前者是板子上用来推理的。很多人搞混这两个在板子上装了一堆转换工具结果跑不起来。从官方仓库下载对应的whl文件注意架构是aarch64pip install rknn_toolkit_lite2-1.5.0-cp38-cp38-linux_aarch64.whl装完验证一下from rknnlite.api import RKNNLite rknn RKNNLite() print(rknn.get_sdk_version())能打印出版本号就说明运行时OK了。如果报librknnrt.so找不到检查/usr/lib/下有没有这个库没有的话从SDK里拷过来。3. NanoTrack模型转换从PyTorch到RKNN3.1 模型结构拆解与转换策略NanoTrack的结构分三块backbone特征提取、neck特征融合、head分类和回归。backbone是两个共享权重的分支分别处理模板帧和搜索帧输出特征图后做互相关。转换到RKNN的时候最省事的做法是把整个模型当成一个计算图导出ONNX但这样有个问题——互相关操作在NPU上支持不好可能会被切回CPU执行反而更慢。我的策略是拆成两个RKNN模型一个只包含backboneneck输入是模板帧和搜索帧输出是融合后的特征另一个包含head部分。但实测下来发现head的计算量很小放CPU上跑完全没问题所以最终方案是只把backboneneck转成RKNNhead用numpy在CPU上实现。这样做的好处是模型转换简单不用处理复杂的动态shape问题。backbone的输入尺寸是固定的模板帧127x127搜索帧255x255。固定shape对NPU最友好。3.2 ONNX导出与RKNN转换实操先在PC上把PyTorch模型导出成ONNX。NanoTrack的官方代码在GitHub上clone下来之后找到模型定义文件。导出脚本大概长这样import torch from nanotrack.models.model_builder import ModelBuilder from nanotrack.utils.config import load_config cfg load_config(experiments/nanotrack/config.yaml) model ModelBuilder(cfg) model.load_state_dict(torch.load(nanotrack.pth)) model.eval() # 构造 dummy input template torch.randn(1, 3, 127, 127) search torch.randn(1, 3, 255, 255) torch.onnx.export( model.backbone, (template, search), nanotrack_backbone.onnx, input_names[template, search], output_names[template_feat, search_feat], opset_version11 )opset用11别用太新的版本RKNN对高版本opset的支持不完整。导出之后用onnxsim简化一下去掉多余的Identity和Constant节点。然后转换from rknn.api import RKNN rknn RKNN(verboseTrue) rknn.config( mean_values[[123.675, 116.28, 103.53]], std_values[[58.395, 57.12, 57.375]], target_platformrk3588, quantized_dtypeasymmetric_quantized-8, optimization_level3 ) rknn.load_onnx(modelnanotrack_backbone.onnx) rknn.build(do_quantizationTrue, dataset./quant_dataset.txt) rknn.export_rknn(nanotrack_backbone.rknn)量化数据集准备100-200张图就够了从训练集里随机抽注意要覆盖不同的场景亮度。量化精度对跟踪效果影响很大这个后面细说。3.3 量化精度调优的实战经验第一次转换完跑起来跟踪框会飘。排查下来是量化误差导致特征图分布偏移。几个调优手段第一量化算法从默认的normal换成mmse虽然转换时间会长一些但精度明显好。在config里加quantized_algorithmmmse。第二混合量化。把backbone最后几层和neck部分设成16位浮点前面浅层保持8位。RKNN支持通过hybrid_quantization配置指定哪些层用高精度。具体做法是在转换脚本里加rknn.config( ..., quantized_dtypeasymmetric_quantized-8, hybrid_quantizationTrue )然后在生成的hybrid_quantization_step1配置里手动标记需要高精度的层。第三如果还是不行检查一下输入归一化参数。NanoTrack训练时用的mean和std是ImageNet的标准值但有些实现会做额外的缩放。我遇到过因为mean值写错导致特征全乱的情况用Netron打开ONNX看第一层的前处理逻辑确保RKNN的mean/std和它一致。4. Python部署全流程与性能优化4.1 推理引擎封装板子上的推理代码核心就是加载RKNN模型、准备输入、调用推理、取输出。封装成一个类from rknnlite.api import RKNNLite import numpy as np import cv2 class NanoTrackRKNN: def __init__(self, model_path): self.rknn RKNNLite() ret self.rknn.load_rknn(model_path) assert ret 0, load rknn failed ret self.rknn.init_runtime(core_maskRKNNLite.NPU_CORE_0) assert ret 0, init runtime failed def preprocess(self, img, size): # img: BGR, size: (w, h) img cv2.resize(img, size) img cv2.cvtColor(img, cv2.COLOR_BGR2RGB) img img.astype(np.float32) return img def inference(self, template, search): t self.preprocess(template, (127, 127)) s self.preprocess(search, (255, 255)) outputs self.rknn.inference(inputs[t, s]) return outputscore_mask参数可以指定用哪个NPU核心。RK3588有3个NPU核心如果你的模型不大可以同时跑多个实例分别绑到不同核心上吞吐量能翻倍。但NanoTrack这个量级单核就够了绑多核反而增加调度开销。4.2 前后处理的性能细节预处理里cv2.resize和cvtColor是耗时大户。几个优化点resize的时候用cv2.INTER_LINEAR就够了别用INTER_CUBIC后者慢一倍多但视觉质量提升对跟踪没帮助。cvtColor可以用cv2.COLOR_BGR2RGBOpenCV内部有SIMD优化。如果追求极致可以把resize和cvtColor合并成一个操作用cv2.dnn.blobFromImage它内部做了优化。后处理主要是把head的输出解码成bbox。NanoTrack的head输出是分类score和回归offset解码逻辑用numpy向量化实现别用for循环。我实测过用for循环逐像素处理255x255的特征图要20多毫秒向量化之后不到1ms。def decode_bbox(score, offset, stride8): # score: (1, 2, H, W), offset: (1, 4, H, W) score score[0].transpose(1, 2, 0) # (H, W, 2) offset offset[0].transpose(1, 2, 0) # (H, W, 4) # 找最大响应点 idx np.unravel_index(np.argmax(score[:,:,1]), score.shape[:2]) # 解码 cx (idx[1] offset[idx[0], idx[1], 0]) * stride cy (idx[0] offset[idx[0], idx[1], 1]) * stride w np.exp(offset[idx[0], idx[1], 2]) * 64 h np.exp(offset[idx[0], idx[1], 3]) * 64 return cx - w/2, cy - h/2, w, h4.3 多线程流水线设计单线程跑的话预处理、推理、后处理是串行的总耗时是三者之和。用多线程可以重叠起来一个线程做预处理一个线程调NPU推理一个线程做后处理。因为NPU推理是异步的inference调用会阻塞但底层是DMA实际上Python层面用threading就能获得不错的加速。但要注意GIL的问题。numpy和OpenCV的重操作会释放GIL所以预处理和后处理能真正并行。我的实现是用一个queue.Queue做帧缓冲预处理线程往里放推理线程取出来跑NPU后处理线程再取结果。实测下来比单线程快30%左右。import threading import queue class Pipeline: def __init__(self, model, maxsize4): self.model model self.q_in queue.Queue(maxsizemaxsize) self.q_out queue.Queue(maxsizemaxsize) self.running True def preprocess_worker(self): while self.running: frame, template self.q_in.get() t self.model.preprocess(template, (127,127)) s self.model.preprocess(frame, (255,255)) self.q_out.put((t, s)) def infer_worker(self): while self.running: t, s self.q_out.get() outputs self.model.rknn.inference(inputs[t, s]) # 存到结果队列...4.4 实测性能数据与瓶颈分析跑起来之后用time.perf_counter()打点得到的数据阶段耗时(ms)占比预处理(resizecvtColor)1.826%NPU推理4.260%后处理(解码)0.69%其他(内存拷贝等)0.45%合计7.0100%瓶颈在NPU推理的4.2ms。这个时间已经接近RK3588 NPU跑这个模型的理论下限了。想再快只能换更小的模型或者降低输入分辨率。把搜索帧从255降到192推理时间能降到2.8ms但跟踪精度会掉一些特别是目标快速移动的时候容易跟丢。CPU占用方面单线程跑的时候CPU占用率大概15%主要是预处理线程NPU占用率约40%。多线程流水线跑起来CPU占用到35%左右NPU占用率提到60%。整体功耗用功率计测下来板子整机约5W比跑CPU版本省了一半多。5. 常见问题与排查实录5.1 模型加载与推理报错问题一init_runtime返回-1最常见的原因是NPU驱动版本和runtime不匹配。用cat /sys/kernel/debug/rknpu/version看驱动版本然后去官方release页面找对应的runtime版本。另一个可能是权限问题NPU设备节点/dev/rknpu需要root权限或者把用户加到video组。问题二推理结果全是0或者NaN先检查输入数据的范围。RKNN的输入是uint8还是float32取决于转换时的配置。如果转换时设了mean/std输入应该是float32的0-255范围RKNN内部会做归一化。如果输入给错了范围输出就会异常。用rknn.inference之前打印一下输入的min/max确认。问题三跟踪框抖动严重大概率是量化精度不够。按3.3节的方法做混合量化或者把量化数据集换成和实际场景更接近的图片。还有一个可能是后处理的stride参数和模型实际的下采样倍数不一致用Netron看ONNX输出的shape反推stride。5.2 性能不达标的排查路径如果跑不到预期的帧率按这个顺序排查确认NPU是否真的在工作。cat /sys/kernel/debug/rknpu/load看负载如果是0说明跑在CPU上了。检查core_mask设置。默认可能是NPU_CORE_AUTO会动态调度固定到NPU_CORE_0能减少调度开销。用perf top看CPU热点。如果大量时间花在memcpy上说明数据在CPU和NPU之间的拷贝太多考虑用零拷贝接口。检查Python的GC。跟踪循环里频繁创建numpy数组会触发GC把数组预分配好复用。5.3 常见问题速查表现象可能原因解决方法加载模型报错驱动/runtime版本不匹配对齐版本号推理输出全零输入范围错误检查mean/std配置跟踪框漂移量化精度不足混合量化扩充数据集帧率低于预期跑在CPU上检查core_mask和NPU负载内存持续增长数组未释放预分配buffer复用首帧跟踪正常后续丢失模板更新逻辑问题检查模板更新阈值5.4 几个容易忽略的细节第一RKNN的inference接口每次调用都会做一次输入数据的格式转换如果输入是numpy的float32它会转成内部格式。如果频繁调用这个转换开销不可忽略。可以用inference(inputs[...], data_formatnhwc)指定格式避免转换。第二NPU的算力是共享的。如果你板子上同时跑了其他NPU任务比如另一个检测模型NanoTrack的帧率会下降。用core_mask把不同任务绑到不同核心上可以隔离。第三温度对性能有影响。RK3588满载跑久了会降频NPU频率从1GHz降到800MHz推理时间增加20%左右。加个散热片或者小风扇能稳住。6. 后续可以怎么扩展这套东西跑通之后往上叠功能就很方便了。比如加一个轻量检测模型YOLOv8n或者RF-DETR做跟踪丢失后的全局重检测检测模型也转成RKNN跑在另一个NPU核心上两个模型互不干扰。或者把跟踪结果通过RTSP推流出去用MPP硬编码端到端延迟能控制在50ms以内。还有一个方向是做多目标跟踪。NanoTrack本身是单目标的但你可以对每个检测框分别初始化一个跟踪器多个跟踪器共享同一个RKNN实例因为backbone权重是共享的只是模板帧不同。这样N个目标的推理开销只比单目标多N倍的搜索帧推理backbone部分可以复用。我在实际项目里还试过把跟踪和ReID结合跟踪框出来之后裁图跑一个轻量ReID模型做身份确认防止跟踪漂移到相似目标上。ReID模型用MobileNetV3-small转RKNN之后单次推理2ms左右整体帧率还能维持在80FPS以上。最后分享一个小技巧调试跟踪效果的时候把响应图score map可视化出来能直观看到算法在每一帧的置信度分布。如果响应图出现多个峰值说明有相似目标干扰这时候调高模板更新的阈值或者加一个运动模型约束会有效果。这个可视化用matplotlib实时画就行不影响主循环性能。
返回列表