ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

RK3588上YOLOv5单目测距的C++工程化实现与标定实践

RK3588上YOLOv5单目测距的C++工程化实现与标定实践 简介面向RK3588平台基于YOLOv5目标检测的C单目摄像头测距源码适合计算机相关专业学生作为毕业设计、课程大作业或初期项目演示需要在边缘设备上完成检测与距离估计的开发者也可直接使用。整套工程共45个文件压缩包约27MB核心包括C源文件.cc、头文件.h/.hpp、RKNN模型.rknn、动态库.so、一键编译脚本.sh及项目说明文档.md模型已从YOLOv5经ONNX转换为RKNN格式省去自行转换的麻烦。源码采用多线程设计支持CPU/NPU定频运行以提升稳定性并附有模型转换与部署应用说明便于在不同场景下二次开发。已有546人学习下载适合想要快速在RK3588上跑通YOLOv5测距流程的入门进阶者。1. 在RK3588上做单目测距为什么C才是能落地的路径拿到一块RK3588开发板想用YOLOv5算目标距离很多人的第一反应是拿Python调cv2跑起一个YOLOv5的detect.py然后在结果上套一个相似三角形公式。这套流程在PC上demo跑通没问题但一旦要部署到板端面对4K视频流、NPU驱动、RGA加速和连续运行的稳定性Python那一套的短板就非常明显推理时延抖动大、内存回收不可控、NPU输出到后处理的数据拷贝路径不够直接。真正在RK3588上干活的方案是用C接RKNN C API把YOLOv5转成rknn模型在NPU上推理再把检测框的底边坐标映射到世界坐标系。但这套流程里有一个最容易让人误判的瓶颈单目测距的精度瓶颈从来不在模型而在标定。YOLOv5能给你检测框但框的底边像素到真实距离之间的映射参数取决于相机安装高度、俯仰角、水平视场角和传感器尺寸。本文按“模型转换→C推理→坐标映射→标定调参”这条线路展开覆盖RK3588上从源码到能读数的完整链路适合已经有YOLOv5训练经验、但第一次把模型落到NPU上做几何测距的工程师。2. 单目测距的底层逻辑YOLOv5检测框与相似三角形映射2.1 单目为什么能测距先认可几何假设再谈精度单目测距的本质是在“地面平面假设”下做透视投影逆变换。所谓“能测距”并不是摄像头能感知深度而是我们假定被测目标的脚点贴着地面且地面是一个平面。在这个前提下像素坐标系中的纵坐标v与目标到相机水平距离d之间存在单调映射关系YOLOv5检测框的底边中点就是目标脚点的最佳近似。这个映射的核心是针孔相机模型。设相机光心到地面的垂直高度为H相机光轴与水平面的夹角为pitch角θ相机垂直方向焦距为f_y单位像素主点纵坐标为v0目标脚点在图像上的纵坐标为v则目标到相机光轴在地面投影点的水平距离为d H / tan(θ arctan((v - v0) / f_y))当θ为0时公式退化为d (H * f_y) / (v - v0)这就是最常见的测距简化式。但实际RK3588开发板上接的摄像头很难保证光轴完全水平所以完整的正切公式比简化式更可靠。YOLOv5在检测线程中输出的box坐标x, y, w, h里底边中点坐标是(x w/2, y h)这个点的v值直接代入上式。提示真正影响测距的并不是检测框的宽度或横坐标而是底边的纵坐标。在调YOLOv5的anchor和置信度阈值时要保证检测框的底边紧贴目标脚点宁可用略大的y坐标也不要用框中心点去算距离否则会偏差几倍。2.2 YOLOv5在RK3588上跑什么形态RKNN与NPU的匹配RK3588的NPU算力是6 TOPS但它只认rknn格式的模型。从YOLOv5的pt权重到能上板推理的rknn模型中间必须经过RKNN-Toolkit2的转换和量化。这个转换步骤决定的是C端推理代码能不能跑通、延时多大、精度掉多少三个问题分别对应三个关键选择第一转换目标平台必须指定为rk3588。RKNN-Toolkit2的config里target_platform参数不写RK3588默认按rv1103之类的小核芯片优化算子和内存布局都会错位。第二量化方式建议用int8RK3588的NPU对int8有专门加速单元fp16虽然部署简单但推理速度会掉一半以上。第三输出张量必须解析为正确的检测结果格式YOLOv5在导出到RKNN时输出层有三个尺度的特征图C端要分别做解码再NMS合并。模型转换的完整命令如下# 在x86主机上安装rknn-toolkit2后执行 python convert_yolov5_to_rknn.py \ --pt_path ./yolov5s.pt \ --rknn_path ./yolov5s_rk3588.rknn \ --target_platform RK3588 \ --quantized_dtype int8 \ --dataset ./dataset_for_quantization.txt转换脚本里对YOLOv5模型的处理本质上就是把PyTorch模型的卷积、C3、SPPF模块逐层映射到RKNN的算子集在映射失败时通常会报不支持算子的名称常见的是Focus层在个别rknn toolkit版本里需要改成普通的卷积加slice拼接建议直接set inputs在导出时把Focus层重参数化掉。dataset文件里放的是几十张从板端摄像头采样的jpg图片路径列表这些图片的亮度、分辨率要贴近真实场景因为int8量化依赖它们完成激活值范围的统计。2.3 C端读NPU输出的数据流设计推理代码用RKNN C API时输入是cv::Mat转成RGB888的连续buffer输出是三个尺度的检测张量。这里有一个新手最容易写错的地方rknn_outputs[0].buf里拿到的数据不是标准的detection格式而是模型原始输出头的raw预测三个尺度的数据分别对应80x80、40x40、20x20的网格每个网格点有85个值x, y, w, h, obj_conf, 80个类别得分或根据类别数调整。C端解析输出的核心代码结构如下// rknn_output解析以scale80x80的一层为例 float* output_data (float*)rknn_outputs[i].buf; int grid_h 80, grid_w 80, num_anchors 3, num_classes 80; int stride 8; // 640输入下80x80特征图对应原图步长8 for (int gy 0; gy grid_h; gy) { for (int gx 0; gx grid_w; gx) { for (int a 0; a num_anchors; a) { int index (a * grid_h gy) * grid_w gx; float obj_conf output_data[index * 85 4]; if (obj_conf conf_threshold) continue; // 反算中心点坐标xy坐标乘以stride还原到640x640尺度 float cx (output_data[index * 85 0] gx) * stride; float cy (output_data[index * 85 1] gy) * stride; // 宽高用anchor的exp映射还原 float w output_data[index * 85 2] * anchors[i][0]; float h output_data[index * 85 3] * anchors[i][1]; boxes.push_back({cx - w/2, cy - h/2, w, h, obj_conf, class_id}); } } }代码里的index偏移计算和anchor值直接对应YOLOv5的模型配置改动类别数或anchor时必须同步修改C常量。坐标输出后先做一次缩放匹配rknn输入分辨率是640x640而原图分辨率可能是1280x720或1920x1080要把检测框坐标按输入尺寸与原图尺寸的比率映射回去才能得到真实画面中的坐标。这一步做错会导致检测框画在错误位置测距自然全错。3. RK3588上YOLOv5的C部署环境从NPU驱动到完整推理代码3.1 开发板端依赖库与交叉编译链准备RK3588运行C推理程序需要三个核心依赖librknnmrt.sorknn运行时库、opencv用于图像读入和显示、pthread用于多线程。开发板系统建议用Ubuntu 22.04或buildroot两者在librknnmrt的调用接口上一致但opencv的安装方式不同。Ubuntu系统直接用apt安装opencv-dev即可buildroot需要在menuconfig里使能opencv模块。交叉编译时需要注意RK3588是aarch64架构不能直接在x86主机上gcc编译。有两种做法一是在板端装gcc和cmake后直接板端编译简单直接但编译大项目慢二是用aarch64-linux-gnu-gcc交叉编译生成的可执行文件拷贝到板端运行。时间充裕的话建议板上编译因为rknn的runtime头文件路径、opencv库路径在板端系统里都能自动找到减少交叉编译的坑。CMakeLists.txt的关键配置如下cmake_minimum_required(VERSION 3.10) project(rk3588_yolov5_distance) set(CMAKE_CXX_STANDARD 14) # rknn runtime头文件和库路径 include_directories(/usr/include/rknn) link_directories(/usr/lib/aarch64-linux-gnu) find_package(OpenCV REQUIRED) add_executable(distance_test src/main.cpp src/yolov5.cpp src/distance.cpp) target_link_libraries(distance_test rknnmrt ${OpenCV_LIBS} pthread)链接rknnmrt库时库文件名是librknnmrt.so但CMake里写rknnmrt即可ld会自动加前缀找库文件。pthread不要漏因为rknn的模型初始化内部会创建线程做算子调度。提示如果板端烧录的是Rockchip官方ubuntu固件librknnmrt.so通常已经预装在/usr/lib目录下不需要重复安装rknn runtime。运行ldconfig -p | grep rknn命令确认是否存在不存在才需要从rknpu2仓库拷贝。3.2 推理算子的初始化与输入输出的内存映射初始化阶段有两个关键代码路径rknn_init加载模型文件rknn_query查询输入输出属性。其中查询输入属性这一步决定了往输入buffer里填什么格式查询输出属性决定了怎么读结果。如果模型输入要求是RGB且分辨率640x640而摄像头给的是NV12的YUV帧就要用RGA做颜色空间转换和缩放不能直接用cv::cvtColor去转因为CPU转4K分辨率一帧要耗费几十毫秒直接拖垮整个推理管线。初始化代码核心// 模型加载与查询 rknn_context ctx; int ret rknn_init(ctx, model_path, model_size, 0, NULL); rknn_input_output_num io_num; rknn_query(ctx, RKNN_QUERY_IN_OUT_NUM, io_num, sizeof(io_num)); rknn_tensor_attr input_attr; input_attr.index 0; rknn_query(ctx, RKNN_QUERY_INPUT_ATTR, input_attr, sizeof(input_attr)); // 打印确认输入格式 printf(input fmt: %d, type: %d\n, input_attr.fmt, input_attr.type); // fmt为RKNN_TENSOR_NCHW时输入buffer按NCHW排列在拿到输入输出属性后建议把输入类型构造为RKNN_TENSOR_NHWC因为RK3588的NPU内部对NHWC布局的处理效率更高从摄像头来的RGB数据天然就是HWC顺序省去一次转置。这个选择在camera输入场景下省掉的耗时大约占整帧处理时间的5%到10%。推理阶段每帧执行三个步骤rknn_inputs_set把Mat数据写入、rknn_run触发NPU计算、rknn_outputs_get取出结果。第2.3节讲的解析代码放在rknn_outputs_get之后整个数据流是从摄像头→Mat→NPU→原始张量→解码框→测距坐标。建议把解码后的boxes用std::vector 封装DetectBox里除了x、y、w、h还要预留一个distance字段测距模块直接操作这个vector。3.3 多线程架构采集、推理、测距显示如何并行RK3588有8个核心但NPU推理过程本身只占用NPU单元CPU核心在做图像采集和预处理。为了让摄像头采集帧率与推理帧率解耦常见做法是双缓冲队列加condition_variable。采集线程负责读camera帧并做RGA转换放入队列推理线程从队列取帧调用rknn_run测距计算直接在推理线程内联完成因为y坐标到距离的公式是一次数学运算耗时微秒级不值得单独开线程。生产者和消费者的队列设计std::mutex mtx; std::condition_variable cv; std::queuecv::Mat frame_queue; const int MAX_QUEUE_SIZE 2; // 采集线程回调 void on_capture(cv::Mat frame) { std::unique_lockstd::mutex lock(mtx); if (frame_queue.size() MAX_QUEUE_SIZE) { frame_queue.pop(); // 丢最旧的帧 } frame_queue.push(frame.clone()); cv.notify_one(); } // 推理线程主循环 while (running) { cv::Mat frame; { std::unique_lockstd::mutex lock(mtx); cv.wait(lock, []{ return !frame_queue.empty(); }); frame frame_queue.front(); frame_queue.pop(); } // 推理测距 process_frame(frame); }队列长度限制为2是为了在推理速度跟不上采集速度时丢弃最旧的帧而不是阻塞采集。这个设计在RK3588上实测能保证显示屏上的画面不会因为推理卡顿而发滞YOLOv5s模型在NPU int8下推理耗时约30到50ms配合这个队列结构可以稳定跑20帧左右。4. 测距核心的相机标定与参数标定焦距、安装高度与俯仰角4.1 内参标定拿到fx和fy别用焦距的毫米值直接算YOLOv5的检测框坐标给的是像素值像素坐标和世界坐标之间的换算需要相机的内参矩阵K。内参矩阵里的fx、fy是焦距的像素度量它的数值等于物理焦距毫米除以单个像素的物理尺寸。同一个摄像头模组接在RK3588上如果设置了不同的分辨率和裁剪模式fx、fy可能不同必须按实际工作分辨率重新标定。标定棋盘格图像时用OpenCV的findChessboardCorners加calibrateCamera标准流程如下std::vectorstd::vectorcv::Point3f object_points; std::vectorstd::vectorcv::Point2f image_points; cv::Size board_size(9, 6); float square_size 0.025f; // 棋盘格边长单位米 // 构造世界坐标 for (int i 0; i board_size.height; i) { for (int j 0; j board_size.width; j) { object_pts.push_back(cv::Point3f(j * square_size, i * square_size, 0)); } } // 对采集到的约20张棋盘图提取角点并收集 cv::Mat gray; cv::cvtColor(img, gray, cv::COLOR_BGR2GRAY); std::vectorcv::Point2f corners; bool found cv::findChessboardCorners(gray, board_size, corners); if (found) { cv::cornerSubPix(gray, corners, cv::Size(11,11), cv::Size(-1,-1), cv::TermCriteria(cv::TermCriteria::EPS cv::TermCriteria::COUNT, 30, 0.01)); image_points.push_back(corners); object_points.push_back(object_pts); } // 统一标定 cv::Mat K, dist_coeffs; std::vectorcv::Mat rvecs, tvecs; cv::calibrateCamera(object_points, image_points, gray.size(), K, dist_coeffs, rvecs, tvecs); // 标定后打印K矩阵取K.atdouble(0,0)为fxK.atdouble(1,1)为fy标定完成后的K矩阵参数直接填入距离公式中的f_y同时如果目标不在图像中心其v坐标相对于主点v0的偏移也会影响距离计算。要注意的是标定相机内参时图像分辨率必须与推理时保持一致因为YOLOv5推理前会把图像resize到640x640此时目标坐标被缩放距离公式里的v也应当是在缩放后的坐标系里的值或者把缩放后的检测框坐标再映射回原始分辨率再计算。4.2 外参标定安装高度与俯仰角是最大的误差来源假设标好了内参如果相机安装俯仰角误差1度在10米距离处测距结果会偏差约0.17米在20米处偏差约0.7米。因此外参标定的精度直接决定测距可信范围。安装高度H用卷尺量即可误差通常在1厘米以内。俯仰角θ的标定有两种方式方式一基于地平线消失点计算。在画面中找到地面上的两条平行线如车道线平移到画面中交于一点该点的像素纵坐标记为v_horizon。根据几何关系有tan(θ) (v_horizon - v0) / f_y解出θ后代入距离公式。这个方法的优点是可以在运行状态下手工校准不用拆装相机。方式二静态已知距离反推。在距离相机已知的多个位置放目标记录检测框底边的v坐标用最小二乘法拟合θ和H的联合值。这个方法更糙但更贴近实际因为最终目标不是物理意义绝对准确而是测量距离输出能匹配标定点的真实距离。我在实际工程中倾向把两种方式结合先用消失点得到一个θ初值再用已知距离的真值反代一次得到微调值。消除安装俯仰角β后最终测距的实现函数double estimate_distance(double pixel_v, double f_y, double v0, double pitch_degree, double camera_height) { double theta pitch_degree * CV_PI / 180.0; double alpha atan2(pixel_v - v0, f_y); double distance camera_height / tan(theta alpha); return distance; }该函数中alpha是目标脚点相对于光轴的竖直角pitch_degree为正值表示相机向下俯视这是绝大多数车载或机器人平视相机的安装形态。如果相机向上仰视theta传负值。4.3 空间误差边界哪些场景下单目测距不可信单目测距的物理模型决定了它在三种场景下必然失效目标在坡度路面上、目标尺寸异常大或异常小导致脚点定位偏移、目标被遮挡导致检测框底边落在遮挡物上。在嵌入式实践中我们通常给distance字段设置一个置信区间例如当检测框底边y坐标值接近图像底部边界时说明目标已经贴近车头或相机正下方此时单目模型失真严重直接输出“无效”标识。误差边界经验数据可以这样估计硬件安装条件良好H为1.2米θ为0~10度标定误差小于0.5度时5米内测距误差约10%5到15米误差约15%到25%15到25米误差可能超过40%。在写代码时要同步输出distance_uncertainty字段用于上层决策模块判断当前测距值是否可信。5. 精准拆解板端Demo的编译、运行与两张误差验证表5.1 在RK3588上编译并执行完整测距程序的三条命令把源码包解压到开发板或交叉编译环境后编译执行步骤通常是顺序执行这三步mkdir build cd build cmake .. -DCMAKE_BUILD_TYPERelease make -j4 ./distance_test --model ../model/yolov5s_rk3588.rknn --camera /dev/video0cmake阶段如果报找不到librknnmrt.so说明runtime库路径没有加入到LD_LIBRARY_PATH在运行前执行export LD_LIBRARY_PATH/usr/lib:$LD_LIBRARY_PATH。make阶段如果报opencv头文件缺失先确认opencv-dev安装包是否安装。运行阶段用--camera指定摄像头节点RK3588上通常接在MIPI-CSI接口时设备节点是media controller形式不能直接用/dev/videoN访问要先用v4l2-ctl --list-devices找到实际生效的video节点。运行后程序窗口会实时显示检测框和距离值同时终端打印每帧的FPS和测距结果。如果窗口画面出现色彩错乱先检查camera的分辨率格式是否设置为V4L2_PIX_FMT_NV12常见的MIPI摄像头在rk3588上默认输出NV12而推理模型输入是RGB888中间的转换通过rknn_tensor_attr里设置RGB格式即可如果漏掉转换YOLOv5检测的置信度会骤降或直接无输出。5.2 用一块已知尺寸的标定板验证测距精度验证测距精度最可靠的方法不用真实行人或车辆而是用一块确定的A4纸或纸箱贴在墙面在距离相机1米、2米、3米、5米、8米、10米处分别摆放记录程序输出的distance字段。汇总后与真实值对比得到误差表。真实距离m检测框底边v坐标像素测距输出m绝对误差m1.08120.940.062.06232.180.183.05183.310.315.03875.620.628.03129.101.1010.026911.851.85这张表的规律符合几何模型距离越远相同像素误差对应的真实距离偏差越大。表格里1米处误差0.06米属于正常水平10米处误差接近18%则说明安装角度或者焦距标定还有优化空间。若10米处误差方向为“偏大”通常意味着θ偏小相机没有实际俯角或俯角比设定小把θ调大0.5度到1度再测一轮。5.3 单目测距的进阶校验用YOLOv5的框宽高辅助修正除了底边纵坐标v和模型H、θYOLOv5检测框的高度h也是一个可用信息。在假设目标真实高度已知的前提下可以通过h反推另一个距离值d_h (H_obj * f_y) / h。将d_h与d_v加权融合能显著减少脚点定位噪声的影响。简单做法是给两个估计值加权权重依据目标的类别设置例如person类别因为高度波动大人体重叠多主要信任v坐标car或truck类别高度一致性较强可以提高h坐标的权重。融合的代码实现double d_v estimate_distance(box.y box.h, fy, v0, pitch, height); double d_h (known_height * fy) / box.h; double fused_d 0.7 * d_v 0.3 * d_h;融合距离输出前再加一个一阶低通滤波抑制单帧检测框抖动带来的距离跳变filterd_d 0.6 * filterd_d 0.4 * fused_d。这个滤波系数在帧率20fps场景下响应时间约0.15秒既能平滑噪声又不至于让输出距离产生明显滞后。经过框高辅助修正和低通后10米内的稳定输出误差基本可以压到10%以内这也是不引入双目和深度相机的前提下RK3588配单目摄像头能做到的上限。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进