第三章 传感器与视觉感知基础:相机模型与标定、深度相机原理、点云数据简介
各位同学,欢迎来到第三章。这一章我们聊聊视觉感知的“眼睛”——传感器。说白了,机器人要运动、要避障,首先得“看见”世界。怎么看见?靠相机。但相机拍出来的图像,和我们人眼看到的一样吗?不一样。这里面有很多门道。
我个人习惯把这一章分成三块:相机模型与标定、深度相机原理、点云数据简介。这三块是视觉引导的基石。你想想看,如果相机没标定好,后面的运动规划全是错的。我在项目中遇到过好几次,标定参数差一点点,机械臂抓取就偏了十几毫米。嗯,这可不是开玩笑的。
核心观点:视觉感知的精度,决定了运动规划的可靠性。标定是第一步,也是最重要的一步。
3.1 相机模型与标定
先讲相机模型。最常见的模型是针孔模型。你想想看,一个小孔,光线穿过它,在后面的感光元件上成像。这就是最朴素的相机原理。但现实中的相机不是小孔,是透镜。透镜会带来畸变——说白了就是图像变形了。
畸变主要有两种:径向畸变和切向畸变。径向畸变就是图像边缘的直线变弯了,像鱼眼镜头那样。切向畸变呢?是透镜和成像平面不平行导致的。我在项目中遇到过,用廉价的USB摄像头做视觉定位,边缘畸变特别严重,不标定根本没法用。
我的经验:标定相机时,建议至少拍20张不同角度的棋盘格照片。角度要丰富,有俯仰、有旋转。我曾经只拍了10张,结果标定出来的参数误差很大,后来加到25张才稳定下来。
标定的核心是求解内参矩阵和畸变系数。内参矩阵包含焦距(fx, fy)和光心坐标(cx, cy)。外参呢?是相机在世界坐标系中的位置和姿态。标定板(比如棋盘格)就是用来建立这些对应关系的。
代码示例(使用OpenCV):
import cv2
import numpy as np
# 准备棋盘格角点坐标
objp = np.zeros((6*9, 3), np.float32)
objp[:,:2] = np.mgrid[0:9, 0:6].T.reshape(-1,2)
objpoints = [] # 世界坐标系中的点
imgpoints = [] # 图像坐标系中的点
# 遍历标定图片
for fname in images:
img = cv2.imread(fname)
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
ret, corners = cv2.findChessboardCorners(gray, (9,6), None)
if ret:
objpoints.append(objp)
imgpoints.append(corners)
# 标定
ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera(
objpoints, imgpoints, gray.shape[::-1], None, None)
print("内参矩阵:\n", mtx)
print("畸变系数:\n", dist)
注意:标定结果的好坏,很大程度上取决于标定板的质量和拍摄方式。棋盘格要平整,不能有褶皱。拍摄时保证棋盘格在画面中占比合理,不要太小也不要太大。
3.2 深度相机(RGB-D)原理
普通相机只能得到RGB图像,没有深度信息。深度相机就不一样了,它能直接输出每个像素的深度值。目前主流的深度相机有三种技术路线:结构光、ToF(飞行时间)和双目立体视觉。
结构光:投射已知图案(比如红外点阵),通过图案变形计算深度。Kinect v1就是用的这个。优点是精度高,缺点是对环境光敏感。
ToF:发射光脉冲,测量反射时间。Kinect v2和Azure Kinect用的就是ToF。速度快,但分辨率相对较低。
双目立体视觉:用两个普通相机模拟人眼,通过视差计算深度。成本低,但计算量大,对纹理要求高。
| 技术 | 原理 | 优点 | 缺点 | 典型产品 |
|---|---|---|---|---|
| 结构光 | 投射图案,分析变形 | 精度高,室内效果好 | 易受环境光干扰 | Kinect v1, Intel RealSense |
| ToF | 测量光飞行时间 | 速度快,远距离可用 | 分辨率低,多径干扰 | Kinect v2, Azure Kinect |
| 双目 | 视差计算深度 | 成本低,室外可用 | 计算量大,弱纹理失效 | ZED, 大疆双目 |
我个人比较喜欢用结构光相机做室内场景的避障。为什么?因为精度高,能看清小物体。但有一次在强光下做实验,结构光完全失效了。嗯,后来换了ToF才搞定。所以选型时要看应用场景。
RGB-D相机还有一个关键步骤:对齐。深度图和彩色图的分辨率、视角都不一样,需要配准。说白了就是把深度图映射到彩色图的坐标系下。OpenCV和librealsense都提供了现成的API。
3.3 点云数据简介
深度图拿到手,怎么变成点云?很简单,利用相机内参做反投影。每个像素(u, v)加上深度值d,就能算出三维坐标(x, y, z)。一堆这样的点,就构成了点云。
点云数据量很大。一个640×480的深度图,就有30万个点。直接处理?太慢了。所以要做下采样。常用的方法有体素滤波(Voxel Grid Filter),把空间分成小格子,每个格子只保留一个点。
关键点:点云处理的核心是“降维”和“去噪”。降维是为了提高计算效率,去噪是为了提高精度。
点云的常见格式有PCD、PLY、LAS等。我习惯用PCD格式,因为PCL库支持得最好。PCL(Point Cloud Library)是点云处理的标配,滤波、分割、配准、特征提取,应有尽有。
代码示例(点云生成与下采样):
import numpy as np
import open3d as o3d
# 假设 depth_img 是深度图,fx, fy, cx, cy 是内参
h, w = depth_img.shape
u, v = np.meshgrid(np.arange(w), np.arange(h))
z = depth_img / 1000.0 # 假设深度单位是毫米,转成米
x = (u - cx) * z / fx
y = (v - cy) * z / fy
points = np.stack((x, y, z), axis=-1).reshape(-1, 3)
# 去除无效点(深度为0或无穷)
mask = z.reshape(-1) > 0
points = points[mask]
# 创建点云对象
pcd = o3d.geometry.PointCloud()
pcd.points = o3d.utility.Vector3dVector(points)
# 体素下采样
down_pcd = pcd.voxel_down_sample(voxel_size=0.01)
print(f"原始点云: {len(pcd.points)} 点")
print(f"下采样后: {len(down_pcd.points)} 点")
避坑指南:我曾经在处理点云时,忘了去除NaN和Inf值,结果后续的ICP配准直接崩溃。所以,拿到点云的第一步,永远是做滤波。统计滤波可以去除离群点,体素滤波可以降采样。
点云的应用很广。在运动规划中,点云可以用来构建占据网格地图(Occupancy Grid Map),或者直接做碰撞检测。比如,机械臂抓取时,用点云识别物体的位置和姿态,然后规划无碰撞路径。
嗯,这一章的内容就到这里。相机模型是基础,深度相机是工具,点云是数据。三者环环相扣。你想想看,没有准确的标定,深度数据就是错的;没有深度数据,点云就是空的。所以每一步都要扎实。
公众号:蓝海资料掘金营,微信deep3321