
1. 项目概述从“看得见”到“量得准”在计算机视觉和机器人领域让机器“看见”只是第一步让机器“看懂”并“量出”距离才是真正迈向智能的关键一步。单目测距顾名思义就是仅用一个摄像头来估算目标物体到相机的距离。这听起来有点像我们人类闭上一只眼睛仅凭经验和参照物来判断远近但机器要做得精确就需要一套严谨的数学模型和算法。传统的单目测距方法比如基于已知物体尺寸的相似三角形法通常假设相机光轴与地面平行。但在实际应用中尤其是在车载、无人机或者机器人导航场景下相机往往是倾斜安装的存在一个俯仰角也就是我们常说的pitch角。忽略这个角度测距结果会随着目标在图像中纵向位置的变化而产生系统性误差距离越远误差越大。这就好比一个测量员如果歪着头看标尺读出来的数肯定不准。这个项目“带pitch角的单目测距”核心就是解决这个“歪头测量”的问题。它通过引入相机坐标系与世界坐标系的旋转变换将pitch角纳入测距模型从而修正了因相机俯仰带来的测量偏差。我结合实践用C实现了这套算法并验证了其有效性。实测下来在640x480分辨率下对于90米处的目标误差能控制在10%左右45米处则能优化到5%左右这对于很多精度要求不是极端苛刻的实时应用如辅助驾驶中的前车距离预警、机器人避障来说已经具备了很高的实用价值。接下来我将彻底拆解这个项目的技术内核从原理推导、代码实现到实操中的各种“坑”为你呈现一份可以直接“抄作业”的完整指南。无论你是正在学习计算机视觉的学生还是需要在项目中集成测距功能的工程师这篇文章都能帮你绕过弯路直达核心。2. 核心原理当相机不再“平视”要理解带pitch角的测距我们必须先回到最基础的相机成像模型并一步步引入坐标变换。2.1 相机模型与坐标系定义我们通常使用针孔相机模型。这里涉及几个关键坐标系像素坐标系 (u, v)图像上的坐标原点在左上角u轴向右v轴向下。图像坐标系 (x, y)以图像中心光心投影点为原点单位是毫米。通过相机内参焦距f、主点cx, cy与像素坐标系关联x (u - cx) / fx,y (v - cy) / fy。其中fx, fy是焦距的像素表示。相机坐标系 (Xc, Yc, Zc)原点在相机光心Zc轴沿光轴指向拍摄方向Xc轴向右Yc轴向下。世界坐标系 (Xw, Yw, Zw)我们定义的一个固定坐标系。在测距场景中通常将地面设为Xw-O-Zw平面地面平面Yw轴垂直地面向上。在不考虑pitch角即相机光轴平行于地面的理想情况下一个地面上的点P其在相机坐标系中的坐标(Xc, Yc, Zc)与图像坐标(x, y)满足简单的相似三角形关系。但一旦相机俯仰这个关系就被打破了。2.2 Pitch角的引入与坐标变换Pitch角θ描述的是相机绕其自身Xc轴的旋转角度。当相机抬头时θ为正低头时θ为负。我们的目标是将一个在世界坐标系地面中的点P(Xw, Yw, Zw)转换到相机坐标系中。由于我们假设地面是平的且地面点满足Yw 0通常将地面平面高度设为0。这个变换包含旋转和平移。旋转就是由pitch角引起的。绕X轴旋转θ的旋转矩阵R为R [1, 0, 0; 0, cos(θ), -sin(θ); 0, sin(θ), cos(θ)]假设相机在世界坐标系中的位置为T [tx, ty, tz]^T。那么从世界坐标到相机坐标的变换为[Xc; Yc; Zc] R * [Xw; Yw; Zw] T由于地面点Yw0且我们关心的是点P相对于相机的距离通常可以简化模型假设相机在世界坐标系中的高度H是已知的即ty H并且相机在Xw和Zw方向上的偏移在测距时可以先不考虑或通过其他传感器补偿即tx≈0, tz≈0。这样T ≈ [0, H, 0]^T。那么对于一个地面点P(Xw, 0, Zw)它在相机坐标系中的坐标为Xc Xw Yc 0 * cosθ - Zw * sinθ H Zc 0 * sinθ Zw * cosθ注意这里假设世界坐标系的Xw轴与相机坐标系的Xc轴平行这是一个合理的简化因为偏航角yaw可以通过其他方式如车道线检测进行粗略对齐或忽略其小角度影响。2.3 测距公式推导现在这个地面点P会成像在图像的某个像素(v)上我们主要关心纵向v坐标因为它决定了俯仰关系。根据针孔模型图像坐标y归一化到米与相机坐标的关系是y Yc / Zc将上面Yc和Zc的表达式代入y (H - Zw * sinθ) / (Zw * cosθ)我们的目标是求Zw即点P到相机光心在地面平面上的投影点的距离欧氏距离为sqrt(Xw^2 Zw^2)但沿光轴方向的Zw是主要分量。从上式解出Zwy * Zw * cosθ H - Zw * sinθZw * (y * cosθ sinθ) HZw H / (y * cosθ sinθ)这就是带pitch角的单目测距核心公式。其中Zw目标点沿相机光轴方向在世界坐标系地面上的距离。H相机光心距离地面的高度需要事先测量。θ相机的pitch角抬头为正需要事先标定。y目标点底部接触地面的那一点在图像坐标系中的归一化纵坐标单位米。y (v - cy) / fyv是该点在图像中的像素纵坐标。对比无pitch角的情况当θ0时公式退化为Zw H / y这就是最基础的相似三角形测距公式。可以看到pitch角θ实际上是对分母进行了一个修正。当相机抬头θ0时对于图像中同一个y值即同一个像素行计算出的Zw会比忽略pitch角时更大这符合直觉相机朝上看同样成像在画面中部的地面点实际物理距离更远。3. 系统设计与关键模块拆解理解了原理我们来看如何构建一个完整的、可运行的测距系统。整个流程可以分解为几个核心模块每个模块都有其需要注意的细节。3.1 模块一相机标定——获取内参和初始外参标定是精度的基础。我们需要得到内参矩阵K包含焦距fx, fy和主点cx, cy。通常还会包含畸变系数k1, k2, p1, p2, k3用于图像去畸变。初始外参——Pitch角θ在系统初始安装时需要标定出相机相对于水平面的俯仰角。标定方法内参标定使用OpenCV的cv::calibrateCamera函数配合棋盘格或Charuco板进行。采集15-20张不同角度、不同位置的标定板图片确保图像四个角落和中心都有覆盖。Pitch角标定这是一个外参标定。有两种实用方法参照物法在相机前方已知距离Zw1和Zw2例如5米和10米处放置两个明显的标记物。在图像中测出它们对应的纵坐标v1, v2并换算成y1, y2。代入核心公式得到两个方程联立可以解出H和θ。这是最直接的方法。惯性传感器辅助如果设备集成了IMU惯性测量单元可以直接读取pitch角。但需要注意IMU坐标系与相机坐标系的安装对齐关系可能存在一个固定的安装偏角需要标定。实操心得标定pitch角时确保相机安装在最终使用的位置和姿态。哪怕微小的变动如车辆负载变化导致车身姿态改变都可能引入误差。对于车载应用建议在标准负载如半箱油、无乘客下进行标定。3.2 模块二目标检测与接地点定位测距公式中的y对应的是目标底部与地面接触点的纵坐标。因此我们需要检测目标使用目标检测算法如YOLO、SSD等获取目标的边界框。定位接地点对于车辆、行人等地面移动物体接地点通常是边界框的底边中心点。这是最常用的假设在大部分平坦路面场景下是有效的。关键难点与处理遮挡如果目标底部被遮挡接地点定位会不准。需要算法有一定的推断能力或结合目标类型如车辆通常宽度固定进行底部边缘补全。非平坦地面如果目标不在水平地面上如上坡、下坡我们的地面平面假设失效会引入误差。这是单目测距的固有局限之一。像素级精度边界框的底边中心坐标是整数像素。为了减少量化误差可以考虑使用亚像素精度例如通过计算边界框底边附近边缘的梯度中心来获得更精确的接地点位置。3.3 模块三图像预处理与坐标转换获取到接地点像素坐标(u, v)后需要一系列转换才能得到公式中的y。图像去畸变使用标定得到的畸变系数和cv::undistort函数对原始图像进行校正得到无畸变图像坐标。这一步能显著提升远处目标的测距精度。像素坐标转归一化图像坐标x (u - cx) / fxy (v - cy) / fy注意这里的(u, v)应该是去畸变后的图像坐标或者直接使用原始坐标并通过cv::undistortPoints函数直接计算得到去畸变后的归一化坐标这样更精确。3.4 模块四距离计算与后处理将得到的y连同已知的H和θ代入核心公式Zw H / (y * cosθ sinθ)即可计算出纵向距离。后处理与滤波单位转换计算出的Zw单位与H一致通常是米。如果需要其他单位在此转换。稳定性滤波单帧检测可能存在抖动导致距离跳变。可以采用简单的滑动平均滤波、卡尔曼滤波或一阶低通滤波器来平滑距离输出使读数更稳定。无效值处理当分母(y * cosθ sinθ)接近或小于零时计算结果无意义。这通常发生在目标点位于地平线以上对于低头安装的相机y为负且绝对值很大时。需要设置合理的阈值进行过滤。4. C代码实现与逐行解析下面是我实现的核心测距类的代码基于OpenCV库包含了上述主要模块的功能。代码力求清晰并附有详细注释。// MonoDistanceEstimator.h #pragma once #include opencv2/opencv.hpp #include vector class MonoDistanceEstimator { public: // 构造函数初始化内参、高度、pitch角 MonoDistanceEstimator(const cv::Mat cameraMatrix, const cv::Mat distCoeffs, double cameraHeight, double pitchAngleRad); // 设置/更新相机高度和俯仰角例如车辆负载变化后重新标定 void setCameraParams(double cameraHeight, double pitchAngleRad); // 核心函数估计单个接地点的距离 // param groundPointPixel: 图像中接地点像素坐标 (u, v) // return: 估计的距离米如果计算无效返回负数 double estimateDistance(const cv::Point2f groundPointPixel) const; // 批量估计输入多个接地点返回对应距离 std::vectordouble estimateDistances(const std::vectorcv::Point2f groundPoints) const; // 实用函数校正图像可选如果输入坐标是原始坐标则需要在计算前调用此函数校正点坐标 cv::Point2f undistortPoint(const cv::Point2f point) const; private: cv::Mat m_cameraMatrix; // 内参矩阵 K cv::Mat m_distCoeffs; // 畸变系数 double m_cameraHeight; // 相机高度 H (米) double m_pitchAngleRad; // 俯仰角 θ (弧度) double m_cosPitch; // cos(θ) 缓存 double m_sinPitch; // sin(θ) 缓存 // 更新三角函数缓存 void updateTrigCache(); };// MonoDistanceEstimator.cpp #include MonoDistanceEstimator.h #include cmath MonoDistanceEstimator::MonoDistanceEstimator(const cv::Mat cameraMatrix, const cv::Mat distCoeffs, double cameraHeight, double pitchAngleRad) : m_cameraMatrix(cameraMatrix.clone()) , m_distCoeffs(distCoeffs.clone()) , m_cameraHeight(cameraHeight) , m_pitchAngleRad(pitchAngleRad) { updateTrigCache(); } void MonoDistanceEstimator::setCameraParams(double cameraHeight, double pitchAngleRad) { m_cameraHeight cameraHeight; m_pitchAngleRad pitchAngleRad; updateTrigCache(); } void MonoDistanceEstimator::updateTrigCache() { m_cosPitch std::cos(m_pitchAngleRad); m_sinPitch std::sin(m_pitchAngleRad); } cv::Point2f MonoDistanceEstimator::undistortPoint(const cv::Point2f point) const { std::vectorcv::Point2f src {point}; std::vectorcv::Point2f dst; cv::undistortPoints(src, dst, m_cameraMatrix, m_distCoeffs, cv::noArray(), m_cameraMatrix); // undistortPoints 输出的是归一化平面上的点需要转换回像素坐标如果需要 // 但为了后续计算y我们通常需要归一化坐标。这里返回的是校正后的像素坐标。 // 更常见的做法是在estimateDistance内部先将像素点通过undistortPoints得到归一化坐标。 // 为了清晰我们采用另一种方式在estimateDistance内部直接计算归一化坐标。 return dst.empty() ? point : dst[0]; } double MonoDistanceEstimator::estimateDistance(const cv::Point2f groundPointPixel) const { // 方法直接使用原始像素坐标通过undistortPoints获取去畸变后的归一化坐标 std::vectorcv::Point2f srcPoints {groundPointPixel}; std::vectorcv::Point2f dstPoints; // cv::undistortPoints 输入输出可以是像素坐标或归一化坐标。 // 我们设置 R 为单位矩阵P 为内参矩阵使其输出归一化坐标 (x, y)。 cv::undistortPoints(srcPoints, dstPoints, m_cameraMatrix, m_distCoeffs, cv::noArray() /* R */, m_cameraMatrix /* P */); if (dstPoints.empty()) { return -1.0; // 处理错误 } cv::Point2f normPoint dstPoints[0]; // (x, y) double y_norm normPoint.y; // 这就是我们需要的归一化平面上的 y 坐标 // 应用带pitch角的测距公式 double denominator y_norm * m_cosPitch m_sinPitch; // 防止除零或接近零的无效情况目标位于地平线以上 const double EPSILON 1e-6; if (std::fabs(denominator) EPSILON) { return -1.0; } double distance m_cameraHeight / denominator; // 距离应为正值 if (distance 0) { return -1.0; } return distance; } std::vectordouble MonoDistanceEstimator::estimateDistances(const std::vectorcv::Point2f groundPoints) const { std::vectordouble distances; distances.reserve(groundPoints.size()); // 批量处理一次性对所有点进行去畸变效率更高 std::vectorcv::Point2f undistortedPoints; if (!groundPoints.empty()) { cv::undistortPoints(groundPoints, undistortedPoints, m_cameraMatrix, m_distCoeffs, cv::noArray(), m_cameraMatrix); } for (size_t i 0; i groundPoints.size(); i) { if (i undistortedPoints.size()) { double y_norm undistortedPoints[i].y; double denominator y_norm * m_cosPitch m_sinPitch; const double EPSILON 1e-6; if (std::fabs(denominator) EPSILON) { distances.push_back(-1.0); } else { double dist m_cameraHeight / denominator; distances.push_back(dist 0 ? dist : -1.0); } } else { distances.push_back(-1.0); } } return distances; }使用示例// 假设已通过标定获得以下参数 cv::Mat cameraMatrix (cv::Mat_double(3,3) 800, 0, 320, 0, 800, 240, 0, 0, 1); cv::Mat distCoeffs (cv::Mat_double(1,5) -0.1, 0.01, 0, 0, 0); // k1, k2, p1, p2, k3 double cameraHeight 1.2; // 米轿车摄像头安装高度 double pitchAngleDeg -3.0; // 度相机略微低头 double pitchAngleRad pitchAngleDeg * CV_PI / 180.0; // 初始化估计器 MonoDistanceEstimator estimator(cameraMatrix, distCoeffs, cameraHeight, pitchAngleRad); // 假设从目标检测器得到了一个车辆的边界框 cv::Rect carBbox(300, 400, 100, 50); // (x, y, width, height) cv::Point2f groundPoint(carBbox.x carBbox.width / 2.0f, // 底边中心x carBbox.y carBbox.height); // 底边y // 估计距离 double distance estimator.estimateDistance(groundPoint); if (distance 0) { std::cout Estimated distance: distance meters std::endl; } else { std::cout Distance estimation failed. std::endl; }5. 误差分析与精度提升实战任何测量系统都有误差理解误差来源是优化精度的前提。5.1 主要误差来源参数标定误差相机高度H测量不准。1厘米的高度误差在50米处可能导致约0.5米的距离误差误差比例约1%。需要使用高精度测距仪如激光测距仪在安装位置仔细测量。Pitch角θ标定不准。0.5度的角度误差在50米处可能引入超过1米的误差。角度标定需要尽量使用远距离的参照物来减小误差。相机内参特别是焦距f和主点c。标定板的质量、拍摄张数和角度分布直接影响内参精度。使用高精度标定板并确保覆盖整个视野。接地点定位误差检测框抖动目标检测算法输出的边界框存在帧间抖动导致接地点坐标波动。需要对检测结果进行滤波或对计算出的距离进行时序滤波。接地假设失效目标底部被遮挡、目标不在水平地面如车辆在上坡、目标非刚性行人腿部弯曲等情况都会导致接地点判断错误。模型假设误差地面平坦假设这是最大的误差来源之一。道路有坡度、起伏时模型失效。可以通过结合惯性传感器IMU实时估计路面坡度动态修正pitch角但这会大大增加系统复杂度。忽略偏航角Yaw我们假设世界坐标系X轴与相机X轴平行。如果车辆正在转弯这个假设不成立。在高速场景下小角度的偏航对侧向距离影响大对纵向距离影响相对较小但也不能完全忽略。5.2 精度提升技巧与实操心得高精度标定流程内参标定在温度、光照稳定的室内进行。使用背面是磨砂亚光面的高精度棋盘格避免反光。拍摄时标定板要充满画面各个区域并且要有明显的倾斜、旋转姿态至少20张有效图片。OpenCV的calibrateCamera函数返回的重投影误差reprojection error应小于0.3像素。外参H, θ联合标定准备找一段长而平直的路面。测量用激光测距仪精确测量相机光心到地面的垂直距离H。标定θ在距离相机Z120米和Z240米处距离越远标定对角度越敏感放置两个高对比度标记物如锥桶。采集相机拍照精确获取两个标记物底部在图像中的v1, v2坐标可使用图像处理软件手动点击获取亚像素坐标。计算将(Z1, v1), (Z2, v2)以及H代入公式可以建立两个方程。由于方程非线性可以使用最小二乘法或简单的迭代法求解出最优的θ。也可以使用多个点如5个不同距离的点进行拟合精度更高。接地点优化对于车辆不要直接用检测框的底边中心。可以尝试在检测框底边附近一小块区域内使用边缘检测如Canny寻找最强的水平边缘线取这条线的中点作为接地点精度更高。多帧融合对同一个目标连续跟踪多帧将其接地点的像素坐标进行卡尔曼滤波再用滤波后的状态进行测距能有效平滑抖动。实时pitch角补偿对于车载应用车辆在刹车、加速时会有“点头”和“抬头”动作导致瞬时pitch角变化。如果车辆装有高精度IMU可以实时读取车辆姿态角并与初始标定的安装角叠加得到实时的相机pitch角动态更新到测距模型中能显著提升动态场景下的精度。距离滤波简单的滑动平均窗口滤波如取最近5帧的中值或均值就能滤除大部分异常跳动。对于有运动模型的目标如前方车辆可以使用匀速模型或卡尔曼滤波器对距离进行预测和更新输出会更平滑、更可靠。6. 常见问题排查与调试指南在实际部署中你肯定会遇到各种问题。下面是我踩过的一些坑和解决方法。6.1 问题速查表问题现象可能原因排查步骤与解决方案计算出的距离全是负值或极大值1. Pitch角θ符号错误。2. 归一化坐标y计算错误符号或数值。3. 相机高度H单位错误如用成了厘米。1. 检查θ的符号定义抬头为正。用一个小角度如3度代入手动计算一个近点验证。2. 打印出归一化坐标y的值。对于图像下半部分地面y应为正值因为像素v cy且相机模型通常y轴向下。确认(v - cy) / fy计算正确。3. 确认H是以米为单位输入。距离随目标远近变化但整体偏大或偏小1. 相机高度H测量不准。2. 焦距f标定不准。1. 重新精确测量H。在已知距离处如15米放置标记物反推H校准参数。2. 重新进行内参标定确保标定板姿态多样重投影误差小。距离在近处较准远处偏差越来越大1. Pitch角θ误差。这是典型症状。2. 镜头畸变校正不充分远处像素畸变影响大。1. 使用更远距离的参照物重新标定θ。2. 检查标定用的畸变系数是否正确应用。确保undistortPoints函数被调用或图像已进行去畸变处理。同一距离目标在图像左侧和右侧测距结果不同1. 忽略了偏航角Yaw。2. 相机存在严重的切向畸变未校正。1. 如果车辆是直线行驶此差异应很小。如果差异大检查相机安装是否水平无偏转。对于弯道此误差是模型局限。2. 确保标定包含了切向畸变系数p1, p2并正确应用了去畸变。帧间距离输出跳动剧烈1. 目标检测框抖动。2. 没有进行滤波处理。1. 优化目标检测算法或对检测框进行跟踪和滤波。2. 对计算出的距离结果施加滑动平均滤波或卡尔曼滤波。对非地面目标如交通标志测距完全错误模型假设失效。本模型仅适用于底部接触地面的目标。对于悬空目标需要完全不同的测距方法如基于已知尺寸。在应用中需先判断目标类型。6.2 调试与验证流程当你写完代码第一件事不是直接上线而是建立一个可靠的验证流程。静态场景验证在平坦场地上从5米开始每隔5米放置一个标记物直到50米或更远。记录每个标记物在图像中的精确接地点像素坐标可以写个简单的鼠标点击取坐标的程序。运行你的测距程序对比计算距离与实际测量距离。绘制误差曲线误差 vs 距离。理想情况应该是误差随距离线性增长相对误差恒定。如果曲线出现系统性偏移或非线性重点检查H和θ。动态视频验证录制一段包含已知距离参照物如路灯间距已知的驾驶视频。离线运行程序将测距结果可视化在视频上在目标框上方显示距离。观察距离数值的稳定性和准确性。特别注意在车辆加减速pitch角变化时测距值的漂移情况。参数敏感性分析微调H±0.02米和θ±0.5度观察不同距离下输出结果的变化量。这能让你理解每个参数的误差会带来多大影响从而明确标定工作需要达到的精度等级。6.3 性能优化小技巧批量处理如代码所示使用cv::undistortPoints对多个点进行一次性去畸变比循环调用单点处理效率高得多。查表法LUT对于固定相机参数的应用距离Zw与像素坐标v的关系是确定的。可以预先计算一个查找表LUT将图像每一行v坐标对应的距离预先算好。实际测距时只需根据接地点所在的像素行从LUT中读取距离省去了实时三角计算和去畸变计算极大提升速度。这是工程部署时的常用优化手段。异步处理图像采集、目标检测、测距计算、结果发布可以采用流水线并行充分利用多核CPU保证系统整体帧率。实现一个鲁棒、精准的单目测距系统三分靠算法七分靠细节处理和工程实践。从精确的标定开始到谨慎的接地点选择再到实时的滤波和误差补偿每一步都需要耐心调试。这套带pitch角的模型为单目测距提供了一个坚实的理论基础和实用的工程起点。当你看到屏幕上稳定、准确的距离数字时那种把数学公式和代码变成真实感知能力的成就感正是驱动我们不断深入探索的动力。