RK3588平台YOLOv5单目测距原理与C++工程实践
2026/9/10 11:00:04 网站建设 项目流程

简介:基于RK3588与YOLOv5的C++单目摄像头测距源码包,面向嵌入式智能视觉、边缘计算方向开发者,也适合高校计算机、人工智能、物联网等专业学生作为毕业设计、课程大作业或期末项目参考。项目以C++多线程实现实时单目测距,包含多线程池管理、模型后处理与距离检测封装,覆盖从YOLOv5导出ONNX、用rknn-toolkit2转换为RKNN,再到RK3588部署运行的完整流程;同时提供CPU/NPU定频脚本,便于进行帧率测试与性能调优。压缩包共45个文件,以头文件、C++源文件、动态库、RKNN模型、构建脚本、标签文件及说明文档为主,整体约27MB;其中h/hpp文件约29个,另有4个rknn模型、2个cc源文件及sh/txt/md辅助文件,目录划分清晰,适合直接学习或二次开发。使用说明针对环境依赖、编译命令、模型转换及部署时需要修改的线程池/模型类给出了可操作指引。目前已有546人学习下载,适合希望快速落地RK3588视觉测距方案的开发者参考。

1. RK3588上的单目测距,为什么这个标题值得读三遍

把“RK3588、YOLOv5、C++、单目摄像头测距”放在一起,是一个典型的边缘端视觉项目配置。RK3588提供6 TOPS NPU算力,YOLOv5是工程中复用率最高的检测算法,C++负责把采集、推理、后处理放在一条低延迟管道里,单目摄像头则是成本和安装空间约束下最务实的选择。很多工程师第一次接触类似源码时,以为难点在YOLOv5推理本身,实际跑起来才发现:检测框有了,距离怎么从框上算出来才是问题。

这篇文章从单目相机把3D世界投影到2D图像的基本几何讲起,说明YOLOv5检测框如何变成一个距离估算值;然后梳理RK3588上YOLOv5的C++部署路径,包括RKNN模型转换、librknnrt运行时调用、摄像头取帧和测距函数实现;最后落到标定、滤波、量化误差排查和物理验证这几个真正决定精度的地方。适合正在用RK3588做边缘设备部署的工程师,也适合准备把检测和测距写进同一份C++代码的开发者。

2. 单目测距基础:针孔模型与YOLOv5边界框的数学关系

2.1 针孔相机模型:世界坐标到像素坐标的投影关系

单目测距的根基是针孔相机模型。相机把三维空间中的点投影到二维像平面,在不考虑畸变时,空间点 P(x, y, z) 在相机坐标系下的投影可以写成:

像素坐标 u = fx * x / z + cx,像素坐标 v = fy * y / z + cy

其中 fx、fy 是归一化焦距,单位是像素,cx、cy 是光心在像素坐标系中的位置。从这个公式能看出一个关键信息:一个空间点无论实际在 3D 空间中位于哪里,只要它映射到同一个像素位置,就说明它在同一条射线上,深度 z 被“丢失”了。恢复深度必须借助额外约束,比如已知的物体物理尺寸、已知的地平面关系,或者多视角视差。

实际工程中,标定得到的内参矩阵就包含 fx、fy、cx、cy。RK3588 板子上外接的 USB 摄像头或 MIPI 摄像头,出厂时内参往往不精确,需要自己标定。公式里的 fx 不是镜头焦距毫米数,而是“以像素为单位的焦距”,它由传感器像元尺寸和镜头焦距共同决定,这也是很多人拿镜头参数去算距离却对不上的原因。

2.2 相似三角形公式:用检测框高度反推距离

YOLOv5 输出的检测框,本质上是目标在图像上的外接矩形。假如目标是一个行人,且已知其物理高度约 1.7 米,那么可以用相似三角形模型估算距离:

距离 Z = fy * H_real / h_pixel

H_real 是目标真实物理高度,h_pixel 是目标在图像中占用的像素高度,fy 是标定得到的纵向焦距。这个公式成立的前提是目标基本垂直于相机光轴朝向,且目标的像素高度能准确反映其物理高度。

举个例子:某次检测中行人边界框高度为 180 像素,标定得到 fy 为 1100 像素,取行人物理高度 1.7 米,则 Z = 1100 * 1.7 / 180 ≈ 10.4 米。这个结果可以作为粗略距离输出,但要注意它默认了行人身高恰好是 1.7 米。把 1.7 换成 1.85 米,距离直接变成 11.3 米,误差约 8.6%。单目测距的精度上限,从这一刻起就被物理尺寸先验锁死了。

目标的像素高度从哪来?YOLOv5 后处理解码后得到归一化的框坐标 (center_x, center_y, width, height),像素高度 = height * 输入图像高度。注意如果推理前做了 letterbox 填充,归一化坐标是基于填充后图像的,计算前要把坐标换算回原始图像尺寸,这一步漏掉会导致距离整体偏差。

2.3 地平面约束方法:检测框底部与安装高度的几何关系

基于像素高度的相似三角形法对“目标是否垂直于光轴”很敏感,实际部署中摄像头往往有俯仰角,目标也未必站在光轴正前方。这时更常用的做法是利用地平面约束,适用于行人、车辆这类底部贴地的目标。

设摄像头安装高度为 H_cam,安装俯仰角为 α(光轴向下倾斜为正),目标检测框底边在图像中的纵坐标为 v_bottom。底边对应的视线与光轴的夹角为 β,β = atan((v_bottom - cy) / fy)。则目标沿地面到摄像头的水平距离为:

Z = H_cam / tan(α + β)

这个公式的几何意义是:检测框底边作为目标与地面的接触点,它投影到图像上的位置反映了一条从相机出发、向下指向地面的射线,射线与地面的交点就是目标所在位置。由于使用了摄像头的安装高度和俯仰角,它对行人高度变化不敏感,但对安装角度的准确性要求很高。

实际选型时,行人检测场景建议优先用地平面约束法;如果是无人机对地目标、桌面上的固定规格物体,则更适合像素高度法。两种方法可以同时在代码里实现,通过参数切换,这也是源码里常见的设计。

2.4 单目测距的误差边界:像素量化、框漂移与距离放大

单目测距最容易被低估的是误差放大效应。检测框高度 h_pixel 每偏差 1 个像素,距离误差近似为 f * H_real / h_pixel² * Δh。距离越远,h_pixel 越小,同样 1 个像素偏差造成的距离误差急剧放大。

举例:目标在 5 米处,fy = 1100,H_real = 1.7,则 h_pixel ≈ 374 像素。此时检测框上下偏移 3 个像素,距离变化约 0.04 米。目标在 15 米处,h_pixel ≈ 125 像素,3 像素框偏移造成的距离误差约 0.37 米。也就是说,距离越远,同一个检测框抖动造成的距离跳动越明显。

另一方面,YOLOv5 在 RK3588 上若使用 INT8 量化,边界框回归值会有微小偏移,叠加在远距离目标上会被放大。理解这个误差边界,才能合理设定“最大可靠测距距离”阈值,例如行人目标建议不超过 20 米,车辆目标建议不超过 40 米,超出范围的距离值直接置为无效,而不是输出一个看似精确的数字。

提示:单目测距的定位是“辅助测距”,不是计量级测量。误差在 5% 到 15% 之间都算正常,具体取决于距离、目标类别、标定质量和量化方式。

3. 在RK3588上构建YOLOv5的C++测距管道

3.1 RKNN模型转换步骤:从YOLOv5的pt权重到rknn格式

RK3588 的 NPU 使用 RKNN 格式模型,不能直接加载 PyTorch 的 pt 权重。转换通常在 x86 主机上完成,使用 RKNN-Toolkit2。基本步骤是先导出 ONNX,再转 RKNN。

YOLOv5 官方仓库中导出 ONNX 的命令如下:

python export.py --weights yolov5s.pt --include onnx --opset 12

导出后验证 ONNX 输出的维度是否符合 YOLOv5 的结构:输入为 1x3x640x640,输出为三组,对应 80x80、40x40、20x20 三个特征层。这个结构在后续 C++ 后处理解码时要用到,所以先确认好。

接着用 RKNN-Toolkit2 转换:

from rknn.api import RKNN rknn = RKNN() rknn.config(mean_values=[[0, 0, 0]], std_values=[[255, 255, 255]], target_platform='rk3588') rknn.load_onnx(model='yolov5s.onnx') rknn.build(do_quantization=True, dataset='dataset.txt') rknn.export_rknn('yolov5s.rknn') rknn.release()

转换完成后,把 rknn 文件和后续的 C++ 程序一起部署到 RK3588。dataset.txt 的作用是提供校准图片路径列表,通常放 100 到 300 张包含目标类别的图片;如果校准集分布偏离实际场景,INT8 量化后检测框会偏移,这一点在第四章第四节细讲。

3.2 交叉编译环境与C++工程依赖

RK3588 是 ARMv8 架构,跑的是 Linux 系统(Ubuntu 或 buildroot)。工程依赖主要是 librknnrt.so、OpenCV 和 pthread。常见做法有两种:一是在 RK3588 板子上直接用 g++ 编译,适合板子性能足够、依赖库已装好的情况;二是用 aarch64-linux-gnu-g++ 交叉编译,再拷到板子上运行。

一个最小 CMakeLists.txt 写法如下:

cmake_minimum_required(VERSION 3.10) project(rk3588_yolo_distance) set(CMAKE_CXX_STANDARD 17) find_package(OpenCV REQUIRED) include_directories(${CMAKE_CURRENT_SOURCE_DIR}/include) link_directories(${CMAKE_CURRENT_SOURCE_DIR}/lib) add_executable(yolo_distance main.cpp postprocess.cpp distance.cpp) target_link_libraries(yolo_distance ${OpenCV_LIBS} rknnrt pthread)

这里头文件目录 include 中放 rknn_api.h,lib 目录中放从板子上拿回来的 librknnrt.so 或开发包里自带的版本。链接顺序有讲究:rknnrt 要放在 OpenCV 后面,否则可能出现符号未定义的链接错误。板子上原生编译时,librknnrt.so 通常位于 /usr/lib,可以直接用 -lrknnrt 链接。

3.3 RKNN C++推理核心代码说明

加载模型、设置输入、执行推理、获取输出的完整流程如下:

#include <rknn_api.h> #include <opencv2/opencv.hpp> rknn_context ctx; int ret = rknn_init(&ctx, model_path, 0, 0, NULL); if (ret < 0) { fprintf(stderr, "rknn_init failed: %d\n", ret); return -1; } cv::Mat rgb; cv::cvtColor(img, rgb, cv::COLOR_BGR2RGB); cv::resize(rgb, resized, cv::Size(640, 640)); // 可以在这里做 letterbox,统一原始图和缩放图的对应关系 rknn_input inputs[1]; memset(inputs, 0, sizeof(inputs)); inputs[0].index = 0; inputs[0].type = RKNN_TENSOR_UINT8; inputs[0].size = 640 * 640 * 3; inputs[0].fmt = RKNN_TENSOR_NHWC; inputs[0].buf = resized.data; rknn_inputs_set(ctx, 1, inputs); rknn_run(ctx, NULL); rknn_output outputs[3]; // YOLOv5 三个输出层 memset(outputs, 0, sizeof(outputs)); for (int i = 0; i < 3; i++) { outputs[i].want_float = 1; } rknn_outputs_get(ctx, 3, outputs, NULL); // outputs[i].buf 是 float* 数据,送入后处理函数 float* out0 = (float*)outputs[0].buf; float* out1 = (float*)outputs[1].buf; float* out2 = (float*)outputs[2].buf; // 后处理结束记得释放 rknn_outputs_release(ctx, 3, outputs);

其中want_float设置为 1,让 NPU 输出直接转换成 float,方便后处理计算。若设置 0,拿到的就是量化后的 int8 数据,需要手动反量化,性能更快但代码更复杂。初次调试建议先用 want_float=1 验证正确性,跑通了再考虑用非 float 输出优化性能。

3.4 测距函数实现:边界框到距离的两种计算方式

拿到 YOLOv5 解码后的边界框后,就可以实现距离计算。代码如下:

struct BBox { float x1, y1, x2, y2; // 像素坐标 float score; int class_id; }; float distance_by_height(float fy, float real_height, float bbox_h) { if (bbox_h < 10.0f) return -1.0f; // 框太小不可信 return fy * real_height / bbox_h; } float distance_by_ground(float fy, float cy, float H_cam, float pitch_deg, float v_bottom) { float alpha = pitch_deg * CV_PI / 180.0f; float beta = atan((v_bottom - cy) / fy); return H_cam / tan(alpha + beta); }

distance_by_height 适合目标物理尺寸已知且稳定的场景,比如检测固定高度的货物或某个特定型号的机器人。distance_by_ground 适合行人、车辆等底部贴地目标,它不依赖目标高度,而是依赖摄像头安装高度和俯仰角。

两个函数都加了合理性保护:bbox_h 过小时直接返回 -1,表示距离不可信。实际工程中还要限制最大距离,例如超过 30 米就输出 -1,避免把无效值传给上层逻辑。

3.5 主循环:摄像头取帧、推理、测距的串联

主循环的典型结构如下:

VideoCapture cap(0); if (!cap.isOpened()) { cerr << "camera open failed" << endl; return -1; } while (true) { Mat frame; cap.read(frame); // 1. 缩放或 letterbox Mat input; resize(frame, input, Size(640, 640)); // 2. 推理,获得三个输出层 run_inference(ctx, input, outputs); // 3. 后处理得到 BBox 列表 vector<BBox> dets = postprocess(outputs, conf_thresh, nms_thresh); // 4. 对每个目标按类别计算距离 for (auto& b : dets) { float h = b.y2 - b.y1; float dist = distance_by_ground(fy, cy, H_cam, pitch_deg, b.y2); // 或者 distance_by_height(fy, real_height, h) float real_h = (b.class_id == 0) ? 1.7f : 1.5f; float dist2 = distance_by_height(fy, real_h, h); } }

主循环的性能瓶颈在 resize 和推理本身。RK3588 NPU 对 640x640 输入的 YOLOv5s 推理耗时通常在几十毫秒量级,OpenCV 的 resize 在 4K 输入时也可能成为瓶颈,所以先缩放到较小分辨率再处理是常见优化手段。

注意:摄像头采集分辨率不要盲目追求高分辨率。1080P 和 4K 在测距上的差异,远小于标定误差和检测框抖动带来的影响,而 4K resize 的耗时可能让帧率直接减半。

4. 标定、参数与RK3588量化误差排查

4.1 相机内参标定:棋盘格步骤与fx/fy的来源

测距公式里的 fx、fy、cx、cy 不是镜头规格表上能查到的值,必须通过标定得到。标定最常见的方法是张正友标定法,使用 OpenCV 自带的 findChessboardCorners 和 calibrateCamera 完成。

操作步骤:

// 对每一张棋盘格图像提取角点 vector<Point2f> corners; bool found = findChessboardCorners(gray, boardSize, corners); // boardSize 是内角点数,例如 9x6 vector<vector<Point3f>> obj_points; vector<vector<Point2f>> img_points; calibrateCamera(obj_points, img_points, gray.size(), cameraMatrix, distCoeffs, rvecs, tvecs);

棋盘格建议打印后贴到平整硬板上,采集时从不同角度拍摄 20 到 30 张照片,覆盖画面中心和四角,避免整组照片都是同一姿态。标定结果中 cameraMatrix 的 (0,0) 位置是 fx,(1,1) 位置是 fy。校准完成后用 undistort 或 remap 验证畸变矫正效果,重点看画面边缘的直线是否变直。

标定误差会直接传导到距离计算。fx 偏差 5%,在 distance_by_height 里会直接造成 5% 的距离偏差,而且这个偏差不随距离变化,属于系统性误差。这也是为什么不能跳过标定直接上网找默认内参。

4.2 安装高度、俯仰角与目标物理高度:三个必调参数

在 RK3588 实际部署中,以下参数必须根据现场测量值填写:

参数符号获取方式说明
内参 fx/fyfx, fy棋盘格标定像素单位,与分辨率绑定
光心位置cx, cy棋盘格标定一般接近图像中心
相机安装高度H_cam卷尺量取相机光心到地面的垂直距离
俯仰角pitch量取后反算光轴与水平面的夹角
目标物理高度real_height按类别预设行人和车辆取典型值

俯仰角不容易直接用角度尺量准,常见做法是根据已知距离反算:把一个目标放在 10 米处,微调 pitch 值,直到计算距离和实际距离一致。这样反算出的俯仰角包含了安装时的微小误差,比单独测量更可靠。

目标物理高度建议按类别维护一张表:person 取 1.7,car 取 1.5,bus 取 3.2,truck 取 3.0。实际场景里行人身高差异很大,1.6 到 1.85 之间都正常,单靠固定值会带来较大误差,所以用 distance_by_ground 作为主用方案更合理。

4.3 检测框抖动与测距波动:EMA与中值滤波的实现

YOLOv5 在视频流中逐帧检测,相邻帧的检测框会有几个像素的随机波动,导致距离值来回跳。直接输出原始值在视觉上非常不友好,需要加滤波。

指数移动平均(EMA)是性价比最高的办法:

float ema = 0.0f; float alpha = 0.4f; // 0.1 ~ 0.5 之间调整 // 每帧更新 if (ema == 0.0f) ema = current_dist; else ema = alpha * current_dist + (1 - alpha) * ema;

alpha 越大,响应越快,但噪声也越大;alpha 越小,输出越平滑,但目标突然靠近时反应迟钝。对帧率 30fps 的系统,alpha 取 0.3 到 0.4 比较均衡。更保守的做法是滑动窗口中值滤波,取最近 N 帧(N 为 5 或 7)距离的中位数,能有效剔除单帧误检造成的离群值,但内存占用和计算量略大。

需要警惕的是:滤波会掩盖目标快速靠近时的真实距离变化,这在防碰撞场景中很危险。如果项目需要快速响应,建议用 alpha 偏大的 EMA,或者在检测到距离骤减时对滤波结果做旁路。

4.4 INT8量化后的框偏移:RK3588上的复测方法

RK3588 NPU 对 YOLOv5 使用 INT8 量化后,检测框与 FP32 模型相比通常会有少量偏移,集中表现在小目标和不常见姿态上。先做一次对照实验:分别在 RK3588 上用 INT8 模型、在 x86 上用 FP32 模型跑同一段视频,对比同一帧的边界框尺寸差异。

如果发现 INT8 模型的框系统性偏小几个像素,可以把框的宽高放大 3% 到 5% 再参与测距,也可以用更高的校准集多样性重新量化。校准图片一定要包含实际部署场景的背景和目标角度,如果只用 COCO 官方图片校准,部署现场的地面反光、光照差异会放大量化误差。

另一个排查点是 RKNN 驱动的版本。RK3588 的 librknnrt 版本与 rknn-toolkit2 版本必须匹配,版本错配轻则输出异常,重则初始化失败。检查方式是在板子上运行strings /usr/lib/librknnrt.so | grep version,和高通文档对照确认。

5. 用卷尺验证单目测距误差并让RK3588保持满帧率

5.1 三个距离点的验证操作与误差对照

拿到测距功能后,第一步不是调参,而是做物理验证。选一个开阔场地,用卷尺从摄像头位置拉出 3 米、5 米、10 米三条线,让目标分别站在三个点位上,每个点位记录 10 次测距输出,取平均值填入下表:

实际距离(米)测距平均值(米)误差(米)误差率
33.120.124.0%
55.240.244.8%
1010.850.858.5%

如果误差随距离明显增大,优先检查误检和边界框偏差;如果整体偏差比例近似固定,优先调整 fy 或目标物理高度参数。若 10 米处误差超过 15%,不要急着加大目标高度补偿,先确认分辨率是否足够:目标在 1120P 图像上只有 80 像素高时,任何算法都难保证高精度。

5.2 RK3588的预热与异步推理:让测距输出保持稳定帧率

RK3588 NPU 在首次推理时会经历初始化、模型加载和权重搬运,首帧耗时可能是后续帧的数倍。因此正式测距前,建议用一段空白帧或真实帧跑 3 到 5 次推理完成预热,后面每一帧的推理时间才稳定。

更进一步的优化是异步推理。常见的同步管道中,摄像头等待、推理、后处理串行执行,帧率被三者中最慢的环节拖住。可以开两个线程:线程 A 只负责取帧和预处理,把结果放入缓冲区;线程 B 从缓冲区取数据执行 rknn_run 和后处理。这样帧率不再受摄像头等待时间影响。缓冲区大小建议 2 到 3 帧,超过上限丢弃旧帧,保证测距输出对应的是尽可能新的画面。

另外,rknn_outputs_get 中的 want_float 会触发 NPU 内部数据格式转换,实测中能占到整体后处理时间的 20% 到 30%。如果帧率不达标,可尝试将 want_float 设为 0,在后处理中手动反量化,再结合多线程把解码计算分摊到 CPU 的多个核心上。RK3588 的 4 核 A76 和 4 核 A55 中,后处理任务建议绑在 A76 上,通过 set_affinity 把线程绑定到核心 0 到 3,NMS 计算能快一截。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询