Python+OpenCV实战:用手机搭建简易双目视觉系统(附完整代码)

用两部手机和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 同步拍摄与数据采集策略

双手机拍摄的最大挑战是同步。即使同时按下快门,由于手机处理速度不同,图像也可能有几十毫秒的延迟。在动态场景中,这会导致左右图像不匹配。

我总结了几种同步策略:

  1. 声音同步法:拍手或使用声音信号,后期根据声音波形对齐
  2. 物理同步法:用一根木棍同时按下两个快门(需要练习)
  3. 软件同步法:使用IP摄像头应用,通过脚本控制同时拍摄
  4. 后期筛选法:拍摄视频后提取同步帧
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: 每个方格的实
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值