双目视觉技术模仿人类双眼的立体感知机制,通过两个摄像头从不同角度采集图像,计算视差来获取深度信息。这就像我们闭上一只眼睛时很难准确判断物体的距离,但睁开双眼后就能轻松感知远近。在ROS仿真环境中,我们可以通过Gazebo等工具创建虚拟双目摄像头,省去了物理设备标定的麻烦。
OpenCV作为计算机视觉领域的瑞士军刀,提供了完整的立体匹配算法实现。常用的算法包括BM(Block Matching)、SGBM(Semi-Global Block Matching)和BM3D等。我实测下来发现,BM算法虽然速度较快但精度一般,而SGBM在保持不错速度的同时能获得更平滑的深度图。对于仿真环境来说,SGBM通常是个不错的折中选择。
深度图本质上是一个二维矩阵,每个像素值代表该点与摄像头的距离。在OpenCV中,这个距离通常以毫米为单位。需要注意的是,深度图的精度会受到多种因素影响,包括摄像头分辨率、基线距离(两个摄像头的间距)以及匹配算法的参数设置等。
在Gazebo中创建双目摄像头需要修改URDF文件。下面是一个典型的双目摄像头配置示例:
xml复制<gazebo reference="stereo_camera">
<sensor type="camera" name="left_camera">
<update_rate>30</update_rate>
<camera>
<horizontal_fov>1.047</horizontal_fov>
<image>
<width>640</width>
<height>480</height>
</image>
</camera>
</sensor>
<sensor type="camera" name="right_camera">
<pose>0.1 0 0 0 0 0</pose> <!-- 基线距离10cm -->
<update_rate>30</update_rate>
<camera>
<horizontal_fov>1.047</horizontal_fov>
<image>
<width>640</width>
<height>480</height>
</image>
</camera>
</sensor>
</gazebo>
这里的关键参数是<pose>标签,它定义了右摄像头相对于左摄像头的位置。我建议基线距离设置在5-15cm之间,太小会导致深度精度不足,太大则可能造成匹配困难。
在仿真环境中获取相机参数比实物方便得多。启动仿真后,可以通过以下命令查看相机内参:
bash复制rostopic echo /camera/left/camera_info
rostopic echo /camera/right/camera_info
你会得到类似这样的输出:
yaml复制K: [381.36, 0.0, 320.5, 0.0, 381.36, 240.5, 0.0, 0.0, 1.0]
D: [0.0, 0.0, 0.0, 0.0, 0.0]
R: [1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0]
P: [381.36, 0.0, 320.5, 0.0, 0.0, 381.36, 240.5, 0.0, 0.0, 0.0, 1.0, 0.0]
其中K矩阵包含焦距和光学中心参数,D是畸变系数,R是旋转矩阵,P是投影矩阵。这些参数在后续深度计算中都会用到。
在ROS中获取双目图像数据需要同时订阅左右摄像头的topic。这里有个坑我踩过:直接使用rospy.spin()会导致程序阻塞,无法同时处理两个话题。解决方案是使用rospy.wait_for_message()非阻塞式获取:
python复制import rospy
from sensor_msgs.msg import Image
def get_stereo_images():
rospy.init_node('stereo_reader')
left_img = rospy.wait_for_message('/camera/left/image_raw', Image)
right_img = rospy.wait_for_message('/camera/right/image_raw', Image)
return left_img, right_img
ROS的Image消息需要转换为OpenCV格式。使用cv_bridge可以方便地完成这个转换:
python复制from cv_bridge import CvBridge
bridge = CvBridge()
cv_image = bridge.imgmsg_to_cv2(image_msg, "bgr8")
但要注意,仿真环境中的图像有时会使用不同的编码格式。如果遇到转换错误,可以尝试"rgb8"或"mono8"等格式。
即使是在仿真环境中,为了获得更好的深度图效果,也需要进行立体校正。这包括去畸变和行对齐两个步骤:
python复制# 使用相机标定参数进行校正
left_map1, left_map2 = cv2.initUndistortRectifyMap(
camera_matrix, dist_coeffs, R1, P1, image_size, cv2.CV_16SC2)
right_map1, right_map2 = cv2.initUndistortRectifyMap(
camera_matrix, dist_coeffs, R2, P2, image_size, cv2.CV_16SC2)
img_left_rect = cv2.remap(img_left, left_map1, left_map2, cv2.INTER_LINEAR)
img_right_rect = cv2.remap(img_right, right_map1, right_map2, cv2.INTER_LINEAR)
校正后的图像应该满足极线约束,即对应点位于同一水平线上。这可以大幅提高后续立体匹配的准确性。
OpenCV提供了多种立体匹配算法,这里以SGBM为例:
python复制window_size = 3
min_disp = 0
num_disp = 16*5
stereo = cv2.StereoSGBM_create(
minDisparity=min_disp,
numDisparities=num_disp,
blockSize=window_size,
P1=8*3*window_size**2,
P2=32*3*window_size**2,
disp12MaxDiff=1,
uniquenessRatio=15,
speckleWindowSize=100,
speckleRange=32
)
disparity = stereo.compute(img_left_gray, img_right_gray)
关键参数说明:
numDisparities:视差搜索范围,必须是16的整数倍blockSize:匹配块大小,奇数且在3-11之间效果较好P1/P2:平滑性约束参数,P2通常比P1大3-4倍得到视差图后,可以通过以下公式转换为深度图:
code复制depth = (f * B) / disparity
其中f是焦距,B是基线距离。OpenCV提供了直接转换的函数:
python复制depth_map = cv2.reprojectImageTo3D(disparity, Q)
这里的Q是重投影矩阵,可以从立体校正的结果中获得。
在仿真环境中调试深度图时,我总结了几个实用技巧:
numDisparities和blockSize可以提速,但会牺牲精度cv2.medianBlur或cv2.bilateralFilter可以去除噪声如果遇到深度图更新延迟的问题,可以尝试以下方法:
在实际项目中,我遇到过几个典型问题:
问题1:深度图全是噪声
可能原因:
问题2:深度值不连续
解决方案:
speckleWindowSize和speckleRangemode=cv2.STEREO_SGBM_MODE_HH获取更高质量结果问题3:边缘区域深度错误
这是正常现象,因为:
cv2.ximgproc.createDisparityWLSFilter进行后处理对于实时性要求高的应用,我建议先在仿真环境中找到合适的参数组合,再移植到真实系统。仿真环境的最大优势是可以快速迭代,不必担心损坏设备或等待漫长的标定过程。