ARTICLE DETAIL

资讯详情

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

单目测距实战:从相机标定到距离计算的完整指南

单目测距实战:从相机标定到距离计算的完整指南

1. 项目概述:从一张照片到真实距离

“基于相机模型的单目测距——普通相机”,这个标题听起来有点学术,但它的核心目标非常直接:只用一台普通的、没有深度传感器的摄像头(比如你的手机摄像头),通过一张照片,估算出画面中某个物体离你到底有多远。这听起来像是科幻电影里的技术,但实际上,它背后的数学原理(相机模型)和工程实践(OpenCV等工具)已经相当成熟,是计算机视觉领域一个经典且实用的课题。

我最初接触这个需求,是在一个机器人导航项目中。当时预算有限,没法上昂贵的激光雷达或多目立体视觉系统,但机器人又需要判断前方障碍物的距离来决定是否绕行。于是,单目测距就成了一个极具性价比的解决方案。它不要求特殊的硬件,仅凭一个普通的USB摄像头或手机摄像头就能工作,核心在于对相机本身的精确“理解”和对场景的合理假设。

简单来说,这个过程可以拆解为三个关键步骤:首先,你得知道你的相机“看”世界的方式,这通过“相机标定”来获取相机的内参;其次,你需要识别并定位图像中的目标物体;最后,结合已知的物体尺寸或场景几何约束,利用透视投影原理反推出距离。整个流程的精度,很大程度上取决于第一步——相机标定的准确性。这也是为什么相关热搜词里,“相机标定”和“相机内参”会如此高频地出现。

这篇文章,我将以一个从业者的角度,带你完整走一遍用普通相机实现单目测距的实战流程。我们会从最基础的相机模型讲起,手把手完成相机标定,然后探讨在实际场景中(比如已知物体尺寸、或地面假设)如何计算距离,并分享我在调试过程中踩过的坑和积累的经验。无论你是做机器人、自动驾驶、AR应用,还是单纯对技术好奇,这篇内容都能给你提供一套可直接复现的代码和清晰的思路。

2. 核心原理:透视投影与相机模型拆解

为什么一张2D的图片能反推出3D的距离?这背后的基石是针孔相机模型透视投影原理。理解了这个,你才能明白后续所有操作的意义,而不是机械地调用几个OpenCV函数。

2.1 针孔相机模型:世界如何被压扁在传感器上

想象一下一个暗箱,前端有一个小孔。外界的光线穿过这个小孔,在暗箱后端的感光平面上形成一个倒立的像。这就是最简化的针孔相机模型。在这个模型里,有几个关键点:

  1. 光心(Optical Center):可以近似理解为那个小孔的位置,是所有光线汇聚的虚拟点。
  2. 图像平面(Image Plane):就是感光元件(CMOS/CCD)所在的位置。
  3. 焦距(Focal Length):光心到图像平面的垂直距离。

一个现实世界中的点P(X, Y, Z)(以相机光心为原点的坐标系下),通过光心投影到图像平面上,形成一个点p(u, v)。它们之间的关系,就是透视投影的核心公式(齐次坐标下):s * [u, v, 1]^T = K * [X, Y, Z]^T其中,s是一个任意的尺度因子(因为从3D到2D丢失了深度信息),K就是我们要标定的相机内参矩阵

2.2 内参矩阵K:相机的“身份证”

内参矩阵K描述了相机自身的属性,与它的摆放位置和朝向无关。一个常用的形式是:

K = [ fx, 0, cx; 0, fy, cy; 0, 0, 1 ]
  • fx, fy:以像素为单位的焦距。fx = f / dx,fy = f / dy。其中f是物理焦距,dx, dy是传感器每个像素的物理尺寸。由于制造工艺,fxfy可能略有不同。
  • cx, cy:主点坐标,通常是图像的中心点(width/2, height/2)。它表示光轴与图像平面的交点。
  • 0:这里假设图像坐标系的两个轴是严格垂直的,对于大多数现代相机,这个假设是合理的。如果存在严重的镜头畸变导致轴不垂直,这里会有一个倾斜参数s,但通常可以忽略。

为什么内参如此重要?因为它建立了图像像素坐标(u, v)与相机坐标系下方向射线之间的映射关系。知道了K,给定一个图像点,我们就能知道在相机前方,有一条特定的空间射线对应着这个像素。测距问题,就变成了如何在这条射线上确定一个具体的点(即物体的真实位置)。

2.3 从2D回推3D:为什么需要额外假设?

从公式s * p = K * P可以看出,我们想要求解P(X, Y, Z),但一个方程有X, Y, Z, s四个未知数,而p(u, v)只提供了两个约束(方程)。这就是单目视觉固有的尺度不确定性:你无法区分一个物体是小的但很近,还是大的但很远。

因此,单目测距必须引入额外的先验信息或假设来固定这个尺度。最常见的有两种思路:

  1. 已知物体尺寸:如果你知道目标物体在现实世界中的实际大小(比如一个边长为15cm的立方体,一个身高1.7米的人),那么通过它在图像中占据的像素大小,就可以根据相似三角形原理计算出距离。
  2. 已知场景几何:比如假设目标物体放置在一个水平地面上(地面假设),并且我们知道相机离地面的高度。那么物体接地点在图像中的位置,就可以唯一确定其到相机的水平距离。

注意:镜头畸变(径向畸变和切向畸变)会破坏理想的针孔模型,导致直线在图像中变弯。因此,在利用内参进行几何计算前,必须先对图像进行畸变校正,将图像点校正到符合针孔模型的位置。OpenCV的标定过程会同时计算出畸变系数,用于后续校正。

3. 实战第一步:高精度相机标定详解

标定的目的是精确获取相机的内参矩阵K和畸变系数dist。OpenCV提供了非常完善的工具链,但魔鬼藏在细节里。标定的精度直接决定了后续测距的可靠性。

3.1 工具与材料准备

  • 相机:任何普通的USB摄像头、网络摄像头、手机摄像头均可。建议使用焦距固定的镜头,变焦镜头会增加复杂度。
  • 标定板:推荐使用棋盘格。因为它角点检测稳定、精度高。打印一张棋盘格(例如9x6个内角点,每个方格边长建议在20-30mm),并将其贴在一个非常平整的硬质表面(如亚克力板、厚卡纸)。平整度至关重要,翘曲的纸会引入标定误差。
  • OpenCV库:确保安装了包含contrib模块的OpenCV,用于更稳定的角点检测。可以通过pip install opencv-contrib-python安装。
  • 拍摄环境:光线均匀,避免反光和阴影覆盖角点。从不同角度、不同距离拍摄标定板,覆盖整个图像区域。

3.2 标定流程与代码实操

标定的本质是求解一个最优化问题:寻找一组内参和畸变系数,使得根据这些参数将三维标定板角点投影到二维图像上时,与实际检测到的图像角点之间的误差最小。

以下是使用OpenCV进行标定的核心代码步骤和解释:

import cv2 import numpy as np import glob # 1. 定义棋盘格尺寸(内角点数量,非方格数) pattern_size = (8, 5) # 例如,棋盘格有9x6个方格,则内角点为8x5 # 2. 准备物体点(世界坐标系中的角点3D坐标,Z=0) objp = np.zeros((pattern_size[0] * pattern_size[1], 3), np.float32) objp[:, :2] = np.mgrid[0:pattern_size[0], 0:pattern_size[1]].T.reshape(-1, 2) # 假设每个方格实际边长为25mm square_size = 25.0 # 单位:毫米 objp *= square_size # 3. 用于存储所有图像的对象点和图像点 objpoints = [] # 3d点 in real world space imgpoints = [] # 2d点 in image plane. # 4. 读取所有标定图片 images = glob.glob('./calibration_imgs/*.jpg') for fname in images: img = cv2.imread(fname) gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # 查找棋盘格角点 ret, corners = cv2.findChessboardCorners(gray, pattern_size, None) # 如果找到,添加对象点和图像点(细化后) if ret: objpoints.append(objp) # 亚像素级角点检测,提升精度 criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001) corners_refined = cv2.cornerSubPix(gray, corners, (11,11), (-1,-1), criteria) imgpoints.append(corners_refined) # 可视化(可选) cv2.drawChessboardCorners(img, pattern_size, corners_refined, ret) cv2.imshow('Corners Found', img) cv2.waitKey(500) cv2.destroyAllWindows() # 5. 相机标定 ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera(objpoints, imgpoints, gray.shape[::-1], None, None) print("相机内参矩阵 K:\n", mtx) print("\n畸变系数 (k1, k2, p1, p2, k3):\n", dist.ravel()) # 6. 评估重投影误差(衡量标定质量) mean_error = 0 for i in range(len(objpoints)): imgpoints2, _ = cv2.projectPoints(objpoints[i], rvecs[i], tvecs[i], mtx, dist) error = cv2.norm(imgpoints[i], imgpoints2, cv2.NORM_L2) / len(imgpoints2) mean_error += error print(f"\n平均重投影误差: {mean_error/len(objpoints):.6f} 像素")

关键操作解析:

  • findChessboardCorners: 自动检测棋盘格角点的初始位置。
  • cornerSubPix:这是提升精度的关键一步。初始角点坐标是整数像素级,此函数通过迭代搜索,将其优化到亚像素级别(如0.1像素),显著提高标定精度。
  • calibrateCamera: 执行核心的标定优化计算。返回内参矩阵mtx、畸变系数dist、以及每张图片的旋转向量rvecs和平移向量tvecs
  • 重投影误差:这是标定质量的黄金指标。它计算的是,用标定出的参数将3D点重新投影到2D图像,与真实检测到的2D点之间的平均像素误差。一般来说,误差小于0.5像素可以认为是优秀,小于1像素可以接受。如果误差大于2像素,就需要检查标定板平整度、图片数量和质量、角点检测是否正确。

3.3 标定经验与避坑指南

  1. 图片数量与质量:至少需要10-15张不同位姿的清晰图片。太少会导致过拟合或解不稳定。确保标定板覆盖图像的各个角落(尤其是边缘,畸变更明显)和不同深度。
  2. 标定板平整度:这是最容易被忽视的误差源。一张A4纸打印后很容易弯曲。务必贴在硬板上,拍摄时也确保其平整。
  3. 光照均匀:避免局部高光或阴影,这会导致角点检测失败或位置偏移。
  4. 保存与使用参数:标定一次后,将mtxdist保存为文件(如np.save),后续测距程序直接加载使用,无需重复标定。
  5. 畸变校正验证:标定后,用cv2.undistort()函数校正一张图片,观察原本弯曲的直线(如图像边缘的门框)是否被拉直,这是检验畸变系数是否有效的直观方法。

4. 单目测距的两种核心实现方法

拿到精确的内参和畸变系数后,我们就可以进行测距了。下面介绍两种最常用、也最实用的方法。

4.1 方法一:基于已知物体尺寸的测距

这是最直观的方法。前提是:你知道目标物体的真实物理尺寸(如宽度W或高度H)

原理:物体在图像中成像的像素宽度w_pixel,物体的实际物理宽度W_real,物体的距离Z(沿光轴方向),以及焦距f(物理焦距,但我们可以用像素焦距fx代替,并假设物体正面平行于图像平面)满足相似三角形关系:W_real / Z = w_pixel / fx。因此,Z = (W_real * fx) / w_pixel

实操步骤:

  1. 图像预处理与目标检测:使用你的方法(如YOLO、颜色分割、轮廓检测等)定位目标物体,并获取其像素级宽度或高度。例如,用一个矩形框住物体,框的宽度就是w_pixel
  2. 畸变校正:将检测到的像素坐标(或整个图像)用cv2.undistort()进行校正,确保使用的是无畸变的坐标进行计算。
  3. 距离计算
    import cv2 import numpy as np # 加载之前标定的参数 mtx = np.load('camera_mtx.npy') # 内参矩阵 dist = np.load('camera_dist.npy') # 畸变系数 # 假设已知物体的真实宽度(米) OBJECT_REAL_WIDTH = 0.15 # 例如,一个杯子的宽度是0.15米 # 读取图像并校正畸变 img = cv2.imread('test_image.jpg') h, w = img.shape[:2] newcameramtx, roi = cv2.getOptimalNewCameraMatrix(mtx, dist, (w,h), 1, (w,h)) dst = cv2.undistort(img, mtx, dist, None, newcameramtx) # 假设通过某种检测,我们得到了物体边界框的像素坐标 (x_min, y_min, x_max, y_max) # 这里用模拟数据代替 bbox = [320, 180, 480, 420] # [x1, y1, x2, y2] pixel_width = bbox[2] - bbox[0] # 计算距离 (使用内参矩阵中的fx) fx = mtx[0, 0] distance = (OBJECT_REAL_WIDTH * fx) / pixel_width print(f"检测到的像素宽度: {pixel_width}") print(f"估算距离: {distance:.3f} 米")

注意事项:

  • 物体朝向:公式Z = (W_real * fx) / w_pixel成立的前提是物体正面完全平行于图像平面。如果物体有旋转,其成像宽度会变窄,导致距离估算偏大。对于非刚体或姿态变化的物体,此方法误差较大。
  • 尺寸准确性OBJECT_REAL_WIDTH必须尽可能准确。对于同一类物体(如易拉罐),可以使用平均尺寸。
  • 检测框精度:目标检测框的边界是否紧贴物体实际边缘,直接影响pixel_width的准确性。

4.2 方法二:基于地面假设的测距(单目逆透视变换)

这在机器人、自动驾驶中非常常见。前提是:相机固定安装,且其离地面的高度H已知,目标物体与地面有接触点

原理:我们建立两个坐标系:图像坐标系(u, v)和真实世界的地面坐标系(X, Y),其中Y是相机前进方向,X是横向方向,地面为Z=0平面。通过相机内参和外参(主要是相机高度和俯仰角),可以建立一个从图像像素到地面坐标的映射关系,即逆透视变换(IPM)

推导与实现:

  1. 定义坐标系:相机坐标系原点在光心,Z轴沿光轴向前,Y轴向下,X轴向右。地面点P_g = (X, 0, Z)在相机坐标系下(这里Z是距离,注意与地面坐标系区分)。
  2. 投影关系:地面点投影到图像点p=(u,v),满足s * [u, v, 1]^T = K * [X, H, Z]^T。其中H是相机光心离地高度(绝对值,因为Y轴向下)。
  3. 求解距离:我们可以消去尺度因子s,得到两个方程。对于地面上的点(Y = H),可以解出:Z = (H * fx) / (v - cy)(这是一个简化公式,假设相机没有俯仰角且主点准确)。 更通用的,需要先通过标定确定相机相对于地面的旋转矩阵R和平移向量tt[1]即高度H),然后通过cv2.undistortPointscv2.solvePnP等函数进行坐标转换。

简化版地面测距代码示例(假设相机无俯仰,光轴平行地面):

def estimate_distance_to_ground_point(img_point_v, camera_matrix, camera_height): """ 估算图像中一个地面接触点到相机的纵向距离。 :param img_point_v: 地面接触点在图像中的v坐标(行坐标)。 :param camera_matrix: 相机内参矩阵K。 :param camera_height: 相机光心离地面的高度,单位米。 :return: 沿光轴方向的估计距离,单位米。 """ fx = camera_matrix[0, 0] cy = camera_matrix[1, 2] # 注意:这个公式是简化版,假设主点在图像中心且无俯仰。 # 更精确的做法需要结合相机外参(俯仰角)。 distance = (camera_height * fx) / (img_point_v - cy) return distance # 使用示例 camera_height = 1.2 # 米,相机离地高度 # 假设检测到前方一个物体的接地点在图像中的v坐标为400 v_coord = 400 dist = estimate_distance_to_ground_point(v_coord, mtx, camera_height) print(f"物体接地点的估计距离: {dist:.3f} 米")

注意事项:

  • 相机外参标定:简化公式误差大。实践中,需要精确标定相机相对于地面的姿态(俯仰角、滚转角)。这可以通过在已知尺寸的地面上放置标定板,用cv2.solvePnP求解。
  • 地面平坦假设:必须假设物体所在区域地面是平坦的,与相机标定时的地面是同一平面。
  • 接地点检测:如何准确找到物体与地面的接触点(如车轮底部、行人脚底)是另一个挑战,通常需要结合目标检测和地面分割。

5. 工程实践:构建一个完整的单目测距demo

理论和方法都有了,我们来串联一个完整的、可运行的示例。这个Demo将使用已知尺寸的物体(比如一个A4纸)进行测距。

5.1 系统流程设计

  1. 初始化:加载预标定的相机内参和畸变系数。
  2. 图像采集:从摄像头或视频文件读取一帧图像。
  3. 畸变校正:对原始图像进行去畸变处理。
  4. 目标检测与定位:使用图像处理技术(轮廓检测)定位A4纸,并获取其四个顶点的像素坐标。
  5. 像素尺寸计算:计算A4纸在图像中的像素宽度或高度。
  6. 距离解算:代入已知的A4纸真实尺寸(0.21m x 0.297m),利用相似三角形公式计算距离。
  7. 可视化输出:在图像上绘制检测框和计算出的距离。

5.2 完整代码实现

import cv2 import numpy as np class MonocularDistanceEstimator: def __init__(self, camera_matrix, dist_coeffs, object_real_width=0.21, object_real_height=0.297): """ 初始化单目测距器。 :param camera_matrix: 相机内参矩阵 :param dist_coeffs: 畸变系数 :param object_real_width: 已知物体的真实宽度(米),例如A4纸短边 :param object_real_height: 已知物体的真实高度(米),例如A4纸长边 """ self.mtx = camera_matrix self.dist = dist_coeffs self.obj_width = object_real_width self.obj_height = object_real_height self.fx = camera_matrix[0, 0] self.fy = camera_matrix[1, 1] def _undistort_frame(self, frame): """校正图像畸变""" h, w = frame.shape[:2] newcameramtx, roi = cv2.getOptimalNewCameraMatrix(self.mtx, self.dist, (w,h), 1, (w,h)) undistorted = cv2.undistort(frame, self.mtx, self.dist, None, newcameramtx) return undistorted def _find_a4_paper(self, image): """使用轮廓检测寻找图像中类似A4纸的矩形""" gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) blurred = cv2.GaussianBlur(gray, (5,5), 0) edged = cv2.Canny(blurred, 50, 150) contours, _ = cv2.findContours(edged.copy(), cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE) contours = sorted(contours, key=cv2.contourArea, reverse=True)[:5] # 取面积最大的5个轮廓 for contour in contours: peri = cv2.arcLength(contour, True) approx = cv2.approxPolyDP(contour, 0.02 * peri, True) # 寻找有四个顶点的轮廓 if len(approx) == 4: # 计算轮廓的宽高比,A4纸比例约为1:1.414 (sqrt(2)) x, y, w, h = cv2.boundingRect(approx) aspect_ratio = w / float(h) if 0.6 < aspect_ratio < 0.8 or 1.2 < aspect_ratio < 1.5: # 宽松的比例范围 # 对顶点进行排序:左上,右上,右下,左下 rect = self._order_points(approx.reshape(4, 2)) return rect, (x, y, w, h) return None, None def _order_points(self, pts): """对矩形四个顶点进行排序(左上,右上,右下,左下)""" rect = np.zeros((4, 2), dtype="float32") s = pts.sum(axis=1) rect[0] = pts[np.argmin(s)] # 左上角,x+y最小 rect[2] = pts[np.argmax(s)] # 右下角,x+y最大 diff = np.diff(pts, axis=1) rect[1] = pts[np.argmin(diff)] # 右上角,y-x最小 rect[3] = pts[np.argmax(diff)] # 左下角,y-x最大 return rect def _calculate_distance(self, pixel_width, pixel_height): """根据像素尺寸计算距离(使用宽度和高度,取平均或根据姿态选择)""" # 简单平均,更复杂的做法可以判断物体姿态选择更可靠的维度 distance_via_width = (self.obj_width * self.fx) / pixel_width distance_via_height = (self.obj_height * self.fy) / pixel_height # 返回平均值,也可以根据宽高比判断哪个更可靠 return (distance_via_width + distance_via_height) / 2.0 def process_frame(self, frame): """处理一帧图像,返回带标注的图像和距离""" undistorted_frame = self._undistort_frame(frame) paper_contour, bbox = self._find_a4_paper(undistorted_frame) distance = None output_frame = undistorted_frame.copy() if paper_contour is not None: # 绘制轮廓和边框 cv2.drawContours(output_frame, [paper_contour.astype(int)], -1, (0, 255, 0), 2) x, y, w, h = bbox cv2.rectangle(output_frame, (x, y), (x+w, y+h), (255, 0, 0), 2) # 计算像素尺寸(使用轮廓顶点计算更准确) (tl, tr, br, bl) = paper_contour pixel_width = np.linalg.norm(tr - tl) # 上边宽度 pixel_height = np.linalg.norm(br - tr) # 右边高度 # 计算距离 distance = self._calculate_distance(pixel_width, pixel_height) # 在图像上显示距离 cv2.putText(output_frame, f"Dist: {distance:.2f}m", (x, y-10), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 0, 255), 2) return output_frame, distance # 主程序 if __name__ == "__main__": # 加载标定参数(假设已保存为npy文件) try: mtx = np.load('camera_mtx.npy') dist = np.load('camera_dist.npy') except FileNotFoundError: print("未找到标定文件,请先运行相机标定程序。") exit() # 初始化测距器,目标设为A4纸(宽0.21m,高0.297m) estimator = MonocularDistanceEstimator(mtx, dist, object_real_width=0.21, object_real_height=0.297) # 打开摄像头 cap = cv2.VideoCapture(0) if not cap.isOpened(): print("无法打开摄像头") exit() print("按 'q' 键退出程序。") while True: ret, frame = cap.read() if not ret: break processed_frame, dist = estimator.process_frame(frame) cv2.imshow('Monocular Distance Estimation', processed_frame) if dist: print(f"当前估算距离: {dist:.3f}米", end='\r') if cv2.waitKey(1) & 0xFF == ord('q'): break cap.release() cv2.destroyAllWindows()

5.3 代码要点与调试技巧

  • 轮廓检测的鲁棒性:示例中使用Canny边缘检测和轮廓近似来寻找矩形。在实际环境中,光照变化、背景杂乱会影响检测。可以尝试:
    • 使用自适应阈值代替固定阈值的Canny。
    • 在检测前先进行颜色过滤(如果目标颜色已知)。
    • 使用更稳定的角点检测方法(如Shi-Tomasi)来定位纸张的四个角。
  • 距离计算的稳定性:单帧计算可能受噪声影响。可以加入简单的滤波(如移动平均滤波)来平滑距离输出,避免数值跳动。
    # 简单的移动平均滤波 class MovingAverageFilter: def __init__(self, window_size=5): self.window = [] self.size = window_size def update(self, value): if value is None: return None self.window.append(value) if len(self.window) > self.size: self.window.pop(0) return sum(self.window) / len(self.window)
  • 多维度融合:示例中同时使用了宽度和高度来计算距离并取平均。更好的策略是判断物体的姿态。如果物体宽度方向更正对相机,则用宽度计算更准;反之用高度。可以通过计算轮廓的宽高比和最小外接矩形的角度来粗略判断。

6. 误差分析与性能优化实战

单目测距的误差来源是多方面的,理解并尽可能减少这些误差,是提升系统实用性的关键。

6.1 主要误差来源剖析

误差类别具体原因影响程度缓解措施
标定误差标定板不平、图片数量不足、角点检测不准确保标定板平整,采集15-20张高质量图片,使用亚像素优化。
内参漂移相机对焦/变焦后内参变化中-高固定焦距拍摄,若使用变焦镜头需重新标定或建立查找表。
物体尺寸误差先验尺寸不准确(如人的身高估算)使用更精确的平均值或在线估计(如基于统计模型)。
检测定位误差目标检测框不紧、角点定位偏差优化检测算法,使用亚像素级边缘/角点定位。
姿态假设误差物体不平行于图像平面(方法一)改用基于接地点的方法,或引入姿态估计。
地面假设误差地面不平、相机俯仰角标定不准(方法二)定期标定外参,或结合IMU数据动态估计姿态。
镜头畸变残余标定未完全校正,尤其是边缘使用更高阶的畸变模型(如k3, k4, k5),确保标定覆盖全图。

6.2 提升测距精度的实用技巧

  1. 融合多尺度信息:不要只依赖一个尺寸。如果物体有多个已知尺寸的特征(如车牌的长和宽、人脸的瞳距和鼻尖到下巴的距离),可以同时利用它们进行计算,并通过RANSAC等算法剔除 outliers,得到更鲁棒的距离估计。
  2. 利用消失点约束:在结构化场景(如道路、室内走廊)中,平行线的消失点可以提供强大的几何约束。结合消失点和已知的物体尺寸或相机高度,可以更稳定地计算距离,对物体姿态的依赖性更低。
  3. 引入滤波与跟踪:对于视频流,不要孤立地处理每一帧。使用卡尔曼滤波(Kalman Filter)或粒子滤波(Particle Filter)对目标的位置和距离进行跟踪。这可以平滑噪声,并在目标短暂丢失时进行预测,大大提升体验的连贯性和稳定性。
  4. 在线标定与自适应:对于固定场景的应用,可以在场景中放置一个或多个已知尺寸的参考物。系统运行时自动检测这些参考物,用它们来动态微调距离计算的尺度因子,补偿内参漂移或安装误差。
  5. 深度学习辅助:传统的几何方法依赖于精确的特征提取和假设。可以结合深度学习:
    • 使用更鲁棒的目标检测和实例分割网络(如YOLOv8, Mask R-CNN)来获取精确的像素级掩码,比矩形框更准。
    • 直接训练一个网络进行单目深度估计(如MiDaS),虽然它是相对深度,但如果在固定场景下用少量真实距离数据进行微调,可以得到不错的绝对距离估计,作为几何方法的补充或验证。

6.3 常见问题排查清单

当你发现测距结果不准时,可以按以下清单逐一排查:

  1. 标定是否可靠?
    • 检查重投影误差是否< 1像素。
    • cv2.undistort看校正后的图像直线是否笔直。
  2. 目标检测是否准确?
    • 可视化检测框,看是否紧密贴合物体边缘。
    • 尝试手动标注一个框,用这个框计算距离是否准确?如果准确,问题在检测算法。
  3. 已知尺寸是否正确?
    • 用尺子实际测量一下目标物体,确认输入的程序的数据无误。
    • 考虑物体的平均尺寸是否有代表性(比如“成年人”的身高用1.7米可能偏差较大)。
  4. 物体姿态是否满足假设?
    • 对于“已知尺寸”法,物体是否正对相机?如果倾斜,计算的距离会偏大。可以尝试从不同角度拍摄同一距离的物体,观察距离读数变化。
    • 对于“地面假设”法,物体是否确实接触地面?相机高度和俯仰角参数是否准确?
  5. 光照与环境影响?
    • 强光、反光、阴影是否影响了目标特征的提取(如轮廓、角点)?
    • 尝试在不同光照条件下测试,观察结果稳定性。

单目测距是一个平衡成本、复杂度和精度的技术方案。它无法达到双目或深度相机的精度,但在许多对精度要求不极端、成本控制严格的场景下,通过精细的标定、合理的假设和巧妙的算法设计,完全可以达到实用水平。理解其原理的每一个环节,并系统地分析误差来源,是将其成功应用于实际项目的关键。

返回列表