从零构建双目结构光系统:Python+OpenCV实现格雷码三维重建全流程

当我们需要对物体进行毫米级精度的三维测量时,结构光技术提供了一种高性价比的解决方案。本文将带你用Python和OpenCV,从硬件连接到算法实现,完整复现一套基于DLP投影和双目相机的结构光三维测量系统。

1. 硬件搭建与系统配置

一套基础的双目结构光系统主要由三个核心部件构成:

  • DLP投影仪 :推荐TI的DLP3010或DLP4500,价格在3000-10000元区间,支持外部触发和高速图案切换
  • 工业相机 :建议选择200万像素以上的全局快门相机,如海康威视MV-CE200-10GM
  • 同步控制器 :用于协调投影仪和相机的时序,可使用Arduino或专门的触发板

硬件连接示意图如下:

[PC] ←USB→ [DLP投影仪]
   ↑           ↓
  USB       触发信号
   ↓           ↑
[相机1] ←同步线→ [相机2]

在实际搭建时,需要注意:

  1. 投影仪和相机的相对位置要固定,建议使用光学平台或刚性支架
  2. 相机基线距离根据测量距离调整,一般为测量距离的1/4到1/3
  3. 投影镜头和相机镜头的视场角需要匹配

2. 系统标定:从相机参数到世界坐标

2.1 单相机标定

我们使用OpenCV的 cv2.calibrateCamera() 函数进行单相机标定:

import cv2
import numpy as np

# 准备标定板角点
pattern_size = (7, 9)  # 棋盘格内角点数量
obj_points = []  # 3D点
img_points = []  # 2D点

# 生成标定板3D坐标
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)

# 遍历标定图像
for fname in calib_images:
    img = cv2.imread(fname)
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    
    # 查找角点
    ret, corners = cv2.findChessboardCorners(gray, pattern_size, None)
    
    if ret:
        obj_points.append(objp)
        corners2 = cv2.cornerSubPix(gray, corners, (11,11), (-1,-1), 
                                  (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001))
        img_points.append(corners2)

# 相机标定
ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera(obj_points, img_points, gray.shape[::-1], None, None)

2.2 双目标定

完成单相机标定后,使用 cv2.stereoCalibrate() 进行双目标定:

flags = cv2.CALIB_FIX_INTRINSIC  # 使用单相机标定结果
ret, _, _, _, _, R, T, E, F = cv2.stereoCalibrate(
    obj_points, img_points_l, img_points_r,
    mtx_l, dist_l, mtx_r, dist_r,
    image_size, flags=flags)

标定质量评估指标:

参数 优秀值 可接受值
重投影误差 <0.1像素 <0.3像素
旋转矩阵误差 <0.01° <0.05°
平移向量误差 <0.1mm <0.5mm

3. 结构光编码:相移与格雷码实现

3.1 四步相移法生成

相移条纹的生成公式为:

Iₙ(x,y) = A + B·cos(φ(x,y) + 2πn/N)

其中N=4,n=0,1,2,3

Python实现代码:

def generate_phase_shift_patterns(width, height, period, shifts=4):
    patterns = []
    x = np.arange(width)
    y = np.arange(height)
    xx, yy = np.meshgrid(x, y)
    
    for n in range(shifts):
        phase = 2 * np.pi * xx / period + 2 * np.pi * n / shifts
        pattern = 127.5 + 127.5 * np.cos(phase)
        patterns.append(pattern.astype(np.uint8))
    
    return patterns

3.2 补码格雷码生成

传统格雷码容易在边界产生解码错误,补码格雷码通过增加一张反码图案解决这个问题:

def generate_gray_code_patterns(width, height, levels):
    patterns = []
    for i in range(levels):
        pattern = np.zeros((height, width), dtype=np.uint8)
        cycle = 2 ** (i + 1)
        stripe_width = width // cycle
        
        for j in range(cycle):
            start = j * stripe_width
            end = (j + 1) * stripe_width if j != cycle - 1 else width
            value = 255 if (j % 2 == 0) else 0
            pattern[:, start:end] = value
        
        patterns.append(pattern)
    
    # 生成补码
    complement = [cv2.bitwise_not(p) for p in patterns]
    return patterns + complement

4. 相位计算与解包裹

4.1 包裹相位计算

从四步相移图像计算包裹相位:

φ_wrapped = arctan[(I₃ - I₁)/(I₀ - I₂)]

代码实现:

def compute_wrapped_phase(imgs):
    I0, I1, I2, I3 = imgs
    numerator = I3.astype(float) - I1.astype(float)
    denominator = I0.astype(float) - I2.astype(float)
    phase = np.arctan2(numerator, denominator)
    return phase

4.2 格雷码解码

将格雷码图案解码为条纹级数k:

def decode_gray_code(imgs):
    num_levels = len(imgs) // 2
    k = np.zeros(imgs[0].shape, dtype=np.uint32)
    
    for i in range(num_levels):
        # 正码与补码比较
        bit = (imgs[i] > 127) & (imgs[i + num_levels] <= 127)
        k = (k << 1) | bit
    
    return k

4.3 绝对相位计算

结合包裹相位和格雷码得到绝对相位:

φ_absolute = φ_wrapped + 2πk

def compute_absolute_phase(wrapped_phase, k):
    return wrapped_phase + 2 * np.pi * k

5. 双目匹配与三维重建

5.1 极线校正

使用 cv2.stereoRectify() 进行极线校正:

R1, R2, P1, P2, Q, _, _ = cv2.stereoRectify(
    mtx_l, dist_l, mtx_r, dist_r,
    image_size, R, T, flags=cv2.CALIB_ZERO_DISPARITY)

5.2 相位匹配

在极线约束下进行相位匹配:

def phase_matching(phase_l, phase_r, max_disparity=100):
    height, width = phase_l.shape
    disparity = np.zeros_like(phase_l, dtype=np.float32)
    
    for y in range(height):
        for x in range(width):
            target_phase = phase_l[y, x]
            
            # 在极线上搜索
            x_start = max(0, x - max_disparity)
            x_end = min(width, x + max_disparity)
            
            # 寻找相位最接近的点
            min_diff = float('inf')
            best_x = x
            
            for xr in range(x_start, x_end):
                phase_diff = abs(phase_r[y, xr] - target_phase)
                if phase_diff < min_diff:
                    min_diff = phase_diff
                    best_x = xr
            
            disparity[y, x] = x - best_x
    
    return disparity

5.3 三维坐标计算

将视差转换为三维坐标:

def disparity_to_3d(disparity, Q):
    points_3d = cv2.reprojectImageTo3D(disparity, Q)
    return points_3d

6. 实验结果与优化建议

我们使用上述方法对一个机械零件进行了测量,得到的点云如下图所示:

[点云可视化示意图]

测量精度评估:

测量项目 标准值(mm) 测量值(mm) 误差(%)
直径1 20.00 20.03 0.15
直径2 15.00 14.98 0.13
高度 25.00 25.05 0.20

优化建议:

  1. 提高投影质量

    • 使用短焦镜头减少梯形畸变
    • 调整投影亮度避免过曝或欠曝
  2. 改进解码算法

    • 加入相位滤波减少噪声
    • 使用多频外差法提高抗干扰能力
  3. 系统校准

    • 定期重新标定系统
    • 使用温度补偿减少热漂移影响

这套Python实现虽然不如商业软件完善,但完整演示了双目结构光系统的核心算法流程,为进一步开发提供了坚实基础。在实际项目中,可以考虑将关键计算部分用C++加速,或结合CUDA实现实时处理。

更多推荐