用两部手机和Python,亲手搭建你的第一个双目视觉系统
几年前,我在一个机器人项目中第一次接触到双目视觉。当时团队预算有限,买不起昂贵的工业相机,我们尝试用两部旧手机来获取立体图像。结果出乎意料地好——虽然过程充满挑战,但最终我们成功实现了基本的深度感知。这个经历让我意识到,双目视觉的门槛并没有想象中那么高,只要有合适的工具和方法,任何人都可以在低成本条件下探索这个领域。
双目视觉的核心思想很简单:模仿人类双眼,通过两个摄像头从不同角度观察同一场景,利用视差计算深度信息。这听起来像是专业实验室的专利,但实际上,你手边的两部智能手机加上Python和OpenCV,就足以开启这段探索之旅。本文面向的是计算机视觉的初学者、学生开发者,或者任何对三维感知感兴趣但硬件预算有限的朋友。我们将一步步解决实际问题:如何用普通手机完成相机标定、如何同步拍摄、如何处理非专业设备的误差,甚至如何用日常物品替代专业标定板。
整个流程我会结合具体代码和实际调试经验,让你不仅能跑通代码,更能理解每一步背后的“为什么”。毕竟,在双目视觉中,理解原理往往比调参更重要。
1. 准备工作:从硬件选择到环境搭建
1.1 硬件选择与配置
双目视觉系统的硬件核心是两个摄像头。专业方案会使用经过精密校准的工业相机,但我们的目标是低成本实践。两部智能手机是目前最理想的平民选择——它们分辨率高、易于获取,而且现代手机的摄像头质量远超普通USB摄像头。
我在实践中测试过多种手机组合,发现几个关键点:
- 尽量选择同型号手机:不同型号的摄像头焦距、畸变特性差异较大,会增加标定和校正的难度
- 关闭所有自动调整功能:包括自动对焦、自动曝光、自动白平衡。这些功能会导致左右图像亮度、对比度不一致,严重影响匹配精度
- 固定焦距到无限远:如果手机相机应用支持手动模式,将焦距锁定到最远;如果不支持,可以拍摄距离较远的场景
注意:有些手机在默认相机应用中无法完全关闭自动调整,可以尝试使用第三方相机应用,如Open Camera(Android)或ProCam(iOS),它们提供完整的手动控制。
关于手机固定方式,我尝试过几种方案:
| 固定方案 | 优点 | 缺点 | 推荐指数 |
|---|---|---|---|
| 三脚架+手机夹 | 稳定性好,角度可调 | 需要额外设备 | ★★★★★ |
| 硬纸板+胶带 | 零成本,快速搭建 | 稳定性差,易晃动 | ★★★☆☆ |
| 3D打印支架 | 定制化,精度高 | 需要3D打印机和设计能力 | ★★★★☆ |
我最推荐的是使用两个小型三脚架,配合手机夹固定。这样既能保证稳定性,又方便调整两个摄像头之间的基线距离(两个摄像头光心之间的距离)。基线距离一般在5-15厘米之间,距离太近视差小,距离太远则共同视野区域变小。
1.2 软件环境配置
Python环境建议使用Anaconda,它能很好地管理包依赖。以下是完整的依赖包列表:
# 创建新的conda环境
conda create -n stereo_vision python=3.8
conda activate stereo_vision
# 安装核心包
pip install opencv-python==4.5.5.64
pip install opencv-contrib-python==4.5.5.64
pip install numpy==1.21.5
pip install matplotlib==3.5.1
pip install scipy==1.7.3
# 验证安装
python -c "import cv2; print(f'OpenCV版本: {cv2.__version__}')"
这里需要特别注意:必须安装opencv-contrib-python,因为双目视觉中的一些高级功能(如StereoSGBM_create)在基础版本中不可用。如果你遇到cv2.ximgproc模块找不到的错误,通常就是因为没有安装contrib版本。
环境配置完成后,创建一个项目目录结构:
stereo_vision_project/
├── calibrate.py # 标定相关函数
├── capture.py # 图像采集脚本
├── reconstruct.py # 三维重建主流程
├── utils.py # 工具函数
├── images/
│ ├── left/ # 左摄像头图像
│ └── right/ # 右摄像头图像
├── calibration_data/ # 标定结果保存
└── results/ # 输出结果
2. 标定板替代方案与图像采集技巧
2.1 自制标定板:从棋盘格到日常物品
专业标定板价格昂贵,但我们可以用打印的棋盘格替代。不过,这里有个更实用的技巧:许多日常物品本身就具有规则的角点特征。
我测试过几种替代方案:
- 打印的棋盘格:在A4纸上打印,贴在硬纸板上保持平整。格子数建议用9×6或7×5(内角点数)
- 方格子笔记本:如果格子大小均匀,可以直接使用
- 瓷砖墙面:确保瓷砖接缝清晰、直线度好
- 国际象棋棋盘:标准比赛棋盘是8×8,正好合适
import cv2
import numpy as np
from glob import glob
def check_calibration_pattern(image_path, pattern_size=(7, 5)):
"""
检查图像中是否能检测到标定板角点
参数:
image_path: 图像路径
pattern_size: 棋盘格内角点数量 (列数, 行数)
返回:
success: 是否检测成功
corners: 检测到的角点坐标
debug_img: 带角点标记的图像(用于调试)
"""
img = cv2.imread(image_path)
if img is None:
print(f"无法读取图像: {image_path}")
return False, None, None
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# 尝试检测棋盘格角点
ret, corners = cv2.findChessboardCorners(gray, pattern_size, None)
if ret:
# 亚像素级精确化
criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001)
corners_refined = cv2.cornerSubPix(gray, corners, (11, 11), (-1, -1), criteria)
# 绘制角点
debug_img = img.copy()
cv2.drawChessboardCorners(debug_img, pattern_size, corners_refined, ret)
return True, corners_refined, debug_img
else:
# 如果标准方法失败,尝试调整参数
# 有时需要调整棋盘格大小或使用不同的检测方法
print(f"标准检测失败: {image_path}")
# 尝试调整图像对比度
clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8,8))
enhanced = clahe.apply(gray)
ret, corners = cv2.findChessboardCorners(enhanced, pattern_size, None)
if ret:
print(f"增强后检测成功: {image_path}")
return True, corners, img
else:
print(f"所有方法均失败: {image_path}")
return False, None, None
实际使用中,我发现光照条件对角点检测影响极大。最好在均匀的漫射光下拍摄,避免强烈的阴影或反光。如果使用打印的棋盘格,确保纸张平整,没有褶皱。
2.2 同步拍摄与数据采集策略
双手机拍摄的最大挑战是同步。即使同时按下快门,由于手机处理速度不同,图像也可能有几十毫秒的延迟。在动态场景中,这会导致左右图像不匹配。
我总结了几种同步策略:
- 声音同步法:拍手或使用声音信号,后期根据声音波形对齐
- 物理同步法:用一根木棍同时按下两个快门(需要练习)
- 软件同步法:使用IP摄像头应用,通过脚本控制同时拍摄
- 后期筛选法:拍摄视频后提取同步帧
import cv2
import time
from threading import Thread
import requests
class DualCameraCapture:
"""双手机摄像头同步采集类"""
def __init__(self, left_cam_url, right_cam_url):
"""
初始化双摄像头
参数:
left_cam_url: 左摄像头视频流URL(如IP摄像头地址)
right_cam_url: 右摄像头视频流URL
"""
self.left_url = left_cam_url
self.right_url = right_cam_url
self.left_frame = None
self.right_frame = None
self.latest_timestamp = None
self.capturing = False
def start_capture(self):
"""开始同步采集"""
self.capturing = True
# 创建左右摄像头的采集线程
left_thread = Thread(target=self._capture_left, daemon=True)
right_thread = Thread(target=self._capture_right, daemon=True)
left_thread.start()
right_thread.start()
print("双摄像头采集已启动")
def _capture_left(self):
"""采集左摄像头图像"""
cap = cv2.VideoCapture(self.left_url)
while self.capturing:
ret, frame = cap.read()
if ret:
self.left_frame = frame
self.latest_timestamp = time.time()
time.sleep(0.033) # 约30fps
cap.release()
def _capture_right(self):
"""采集右摄像头图像"""
cap = cv2.VideoCapture(self.right_url)
while self.capturing:
ret, frame = cap.read()
if ret:
self.right_frame = frame
time.sleep(0.033)
cap.release()
def get_synchronized_pair(self):
"""获取一对同步的图像"""
if self.left_frame is not None and self.right_frame is not None:
return self.left_frame.copy(), self.right_frame.copy()
return None, None
def capture_calibration_set(self, num_pairs=20, save_dir="./calibration_images"):
"""
采集多对标定图像
参数:
num_pairs: 需要采集的图像对数
save_dir: 保存目录
"""
import os
os.makedirs(f"{save_dir}/left", exist_ok=True)
os.makedirs(f"{save_dir}/right", exist_ok=True)
print("开始采集标定图像...")
print("请在不同位置、不同角度展示标定板")
print("按's'保存当前帧,按'q'退出")
saved_count = 0
last_save_time = 0
while saved_count < num_pairs:
left, right = self.get_synchronized_pair()
if left is None or right is None:
continue
# 显示图像
display = cv2.hconcat([left, right])
cv2.putText(display, f"已保存: {saved_count}/{num_pairs}",
(10, 30), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 0), 2)
cv2.imshow("双摄像头视图", display)
key = cv2.waitKey(1) & 0xFF
if key == ord('s'):
# 防止连续快速保存
current_time = time.time()
if current_time - last_save_time > 1.0: # 至少间隔1秒
left_path = f"{save_dir}/left/calib_{saved_count:03d}.jpg"
right_path = f"{save_dir}/right/calib_{saved_count:03d}.jpg"
cv2.imwrite(left_path, left)
cv2.imwrite(right_path, right)
saved_count += 1
last_save_time = current_time
print(f"保存第{saved_count}对图像")
elif key == ord('q'):
break
cv2.destroyAllWindows()
print(f"采集完成,共保存{saved_count}对图像")
采集标定图像时,有几个关键要点:
- 至少需要10-20对图像,覆盖整个图像区域的不同位置
- 标定板需要倾斜、旋转,覆盖各种姿态
- 确保标定板在两张图像中都完整可见
- 避免模糊图像,手机要稳定或使用三脚架
3. 相机标定:从理论到实践
3.1 理解相机模型与畸变校正
相机标定的目的是建立三维世界点与二维图像点之间的数学关系。对于手机摄像头,我们需要考虑两种主要的畸变:
径向畸变:图像边缘的直线变弯曲,像透过鱼眼镜头看世界。这通常由镜头形状引起,可以用以下公式校正:
x_corrected = x(1 + k1*r² + k2*r⁴ + k3*r⁶)
y_corrected = y(1 + k1*r² + k2*r⁴ + k3*r⁶)
其中r是点到图像中心的距离,k1、k2、k3是径向畸变系数。
切向畸变:镜头与传感器不平行导致的畸变,可以用以下公式校正:
x_corrected = x + [2*p1*x*y + p2*(r²+2*x²)]
y_corrected = y + [p1*(r²+2*y²) + 2*p2*x*y]
其中p1、p2是切向畸变系数。
在实际标定中,OpenCV的calibrateCamera函数会同时估计这些参数。但手机摄像头的问题在于,它们的畸变特性往往比工业相机更复杂,特别是广角镜头。
import numpy as np
import cv2
import pickle
import os
from tqdm import tqdm
class StereoCalibrator:
"""双目相机标定类"""
def __init__(self, pattern_size=(7, 5), square_size=0.025):
"""
初始化标定器
参数:
pattern_size: 棋盘格内角点数量 (列数, 行数)
square_size: 每个方格的实

&spm=1001.2101.3001.5002&articleId=152680970&d=1&t=3&u=83d7ef3bfca245a1aa049cb9487652e1)
391

被折叠的 条评论
为什么被折叠?



