news 2026/6/18 16:35:35

【OpenCV实战】单目相机 + 条纹结构光三维重建:从条纹图到点云

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
【OpenCV实战】单目相机 + 条纹结构光三维重建:从条纹图到点云

前言

单目相机本身只能获取二维图像,无法直接得到真实深度信息。
如果想用单个相机做三维重建,常见做法是引入主动光源,比如投影仪投射条纹图案。

这种方法通常称为:

结构光三维重建

其中,投影仪投射编码条纹,单目相机采集物体表面的条纹变形图像。通过分析条纹相位变化,再结合相机和投影仪的标定参数,就可以恢复物体表面的三维坐标。

本文主要介绍单目相机 + 条纹图重建的基本流程,包括:

  • 单目相机标定
  • 条纹图生成
  • 条纹图采集
  • 相位计算
  • 相位展开
  • 投影仪坐标恢复
  • 三维点云重建
  • Python 和 C++ 核心代码

一、单目相机 + 条纹图重建是什么?

普通单目相机只能看到图像中的像素点:

(u, v)

但是无法知道这个像素点对应的真实空间深度。

加入条纹投影后,投影仪会向物体表面投射一组有规律的条纹。物体表面的高度变化会导致条纹发生弯曲或偏移。

相机拍摄到这些变形条纹后,可以根据条纹相位反推出物体表面的位置。

可以简单理解为:

相机负责拍摄 投影仪负责给物体表面编码 条纹相位负责建立像素对应关系 标定参数负责恢复三维坐标

在结构光系统中,投影仪通常可以看作一个“反向相机”。
因此,单目相机 + 投影仪实际上可以形成一个类似双目的几何系统。

二、整体重建流程

完整流程如下:

1. 标定单目相机 2. 标定投影仪参数 3. 标定相机和投影仪之间的外参 4. 生成正弦条纹图 5. 投影条纹到物体表面 6. 相机采集条纹图 7. 计算包裹相位 8. 相位展开,得到绝对相位 9. 根据相位恢复投影仪像素坐标 10. 通过三角测量恢复三维点云

如果只想看物体表面的相对形变,可以不做完整投影仪标定。
但如果要得到真实毫米级三维坐标,就必须完成相机、投影仪以及二者外参的标定。

三、正弦条纹图原理

常用的条纹图是正弦条纹:

I(x, y) = A + B * cos(phase + delta)

其中:

  • A:背景亮度
  • B:条纹对比度
  • phase:相位
  • delta:相移量

常见方法是四步相移法,分别投影四张相位不同的条纹图:

0 π / 2 π 3π / 2

相机采集到四张图后,可以计算包裹相位:

phase = atan2(I4 - I2, I1 - I3)

这个相位范围通常在:

[-π, π]

所以它叫包裹相位。
要进行三维重建,还需要进一步做相位展开。

四、Python 生成条纹图

下面代码生成一组竖直方向的正弦条纹图。

import cv2 import numpy as np import os width = 1280 height = 720 period = 64 output_dir = "fringe_patterns" os.makedirs(output_dir, exist_ok=True) phase_shifts = [0, np.pi / 2, np.pi, 3 * np.pi / 2] x = np.arange(width) xx = np.tile(x, (height, 1)) for i, shift in enumerate(phase_shifts): img = 127.5 + 127.5 * np.cos(2 * np.pi * xx / period + shift) img = img.astype(np.uint8) cv2.imwrite(f"{output_dir}/vertical_{i}.png", img)

生成后的图片可以通过投影仪依次投射到物体表面,然后用相机同步采集。

如果要得到完整的投影仪二维坐标,一般还需要生成水平方向条纹:

y = np.arange(height) yy = np.tile(y.reshape(-1, 1), (1, width)) for i, shift in enumerate(phase_shifts): img = 127.5 + 127.5 * np.cos(2 * np.pi * yy / period + shift) img = img.astype(np.uint8) cv2.imwrite(f"{output_dir}/horizontal_{i}.png", img)

竖直条纹主要用于恢复投影仪的x坐标,水平条纹主要用于恢复投影仪的y坐标。

五、Python 计算包裹相位

假设相机已经采集到四张竖直条纹图:

capture_vertical_0.png capture_vertical_1.png capture_vertical_2.png capture_vertical_3.png

计算相位代码如下:

import cv2 import numpy as np imgs = [] for i in range(4): img = cv2.imread(f"capture_vertical_{i}.png", cv2.IMREAD_GRAYSCALE) imgs.append(img.astype(np.float32)) I1, I2, I3, I4 = imgs wrapped_phase = np.arctan2(I4 - I2, I1 - I3) phase_show = cv2.normalize( wrapped_phase, None, 0, 255, cv2.NORM_MINMAX ).astype(np.uint8) cv2.imwrite("wrapped_phase.png", phase_show)

这一步得到的是包裹相位图。
从图像上看,相位会呈现周期性跳变,这属于正常现象。

六、相位展开

包裹相位只能表示一个周期内的相位,无法区分当前点位于第几个条纹周期。
因此需要进行相位展开。

简单场景下,可以使用numpy.unwrap()做一维展开:

unwrapped_phase = np.unwrap(wrapped_phase, axis=1) phase_unwrap_show = cv2.normalize( unwrapped_phase, None, 0, 255, cv2.NORM_MINMAX ).astype(np.uint8) cv2.imwrite("unwrapped_phase.png", phase_unwrap_show)

不过在真实项目中,单纯unwrap()很容易受到噪声、阴影、反光和断裂区域影响。

更稳定的方式是:

格雷码 + 相移法 多频相移法 时间相位展开 质量引导相位展开

工程里比较常用的是“格雷码 + 四步相移”。
格雷码负责确定条纹周期编号,相移法负责提供高精度亚像素相位。

七、由相位恢复投影仪坐标

假设投影条纹周期为period,展开后的相位为unwrapped_phase,则可以近似恢复投影仪横坐标:

projector_x = unwrapped_phase * period / (2 * np.pi)

如果同时采集了水平条纹,也可以恢复投影仪纵坐标:

projector_y = unwrapped_phase_y * period / (2 * np.pi)

最终可以建立这样的对应关系:

相机像素点: (camera_x, camera_y) 投影仪像素点: (projector_x, projector_y)

有了这组对应关系,就可以把相机和投影仪当作一个双目系统进行三角测量。

八、Python 三角测量生成点云

假设已经得到:

camera_matrix 相机内参 projector_matrix 投影仪内参 R 投影仪相对于相机的旋转矩阵 T 投影仪相对于相机的平移向量

可以构造两个投影矩阵:

import cv2 import numpy as np P_camera = camera_matrix @ np.hstack((np.eye(3), np.zeros((3, 1)))) P_projector = projector_matrix @ np.hstack((R, T))

然后对相机像素和投影仪像素进行三角测量:

camera_points = np.array([ camera_x, camera_y ], dtype=np.float32) projector_points = np.array([ projector_x, projector_y ], dtype=np.float32) points_4d = cv2.triangulatePoints( P_camera, P_projector, camera_points, projector_points ) points_3d = points_4d[:3] / points_4d[3] points_3d = points_3d.T

这里的points_3d就是恢复出来的三维点云。

如果要保存为 PLY 文件,可以使用下面的简单函数:

def save_ply(filename, points): with open(filename, "w") as f: f.write("ply\n") f.write("format ascii 1.0\n") f.write(f"element vertex {len(points)}\n") f.write("property float x\n") f.write("property float y\n") f.write("property float z\n") f.write("end_header\n") for p in points: f.write(f"{p[0]} {p[1]} {p[2]}\n") save_ply("result.ply", points_3d)

生成的result.ply可以用 CloudCompare、MeshLab 等软件打开查看。

九、C++ 版本相位计算代码

下面是 C++ 版本的四步相移法相位计算代码。

#include <opencv2/opencv.hpp> #include <iostream> int main() { cv::Mat I1 = cv::imread("capture_vertical_0.png", cv::IMREAD_GRAYSCALE); cv::Mat I2 = cv::imread("capture_vertical_1.png", cv::IMREAD_GRAYSCALE); cv::Mat I3 = cv::imread("capture_vertical_2.png", cv::IMREAD_GRAYSCALE); cv::Mat I4 = cv::imread("capture_vertical_3.png", cv::IMREAD_GRAYSCALE); if (I1.empty() || I2.empty() || I3.empty() || I4.empty()) { std::cout << "条纹图读取失败" << std::endl; return -1; } I1.convertTo(I1, CV_32F); I2.convertTo(I2, CV_32F); I3.convertTo(I3, CV_32F); I4.convertTo(I4, CV_32F); cv::Mat numerator = I4 - I2; cv::Mat denominator = I1 - I3; cv::Mat wrappedPhase; cv::phase(denominator, numerator, wrappedPhase, false); cv::Mat phaseShow; cv::normalize(wrappedPhase, phaseShow, 0, 255, cv::NORM_MINMAX); phaseShow.convertTo(phaseShow, CV_8U); cv::imwrite("wrapped_phase_cpp.png", phaseShow); return 0; }

cv::phase()计算的是:

atan2(y, x)

所以这里传入:

x = denominator = I1 - I3 y = numerator = I4 - I2

十、C++ 三角测量核心代码

当已经得到相机点和投影仪点的匹配关系后,可以使用 OpenCV 的triangulatePoints()进行三维重建。

cv::Mat Pcamera = cameraMatrix * cv::Mat::eye(3, 4, CV_64F); cv::Mat Rt; cv::hconcat(R, T, Rt); cv::Mat Pprojector = projectorMatrix * Rt; cv::Mat cameraPoints(2, pointCount, CV_64F); cv::Mat projectorPoints(2, pointCount, CV_64F); // cameraPoints 第 0 行是相机 x,第 1 行是相机 y // projectorPoints 第 0 行是投影仪 x,第 1 行是投影仪 y cv::Mat points4D; cv::triangulatePoints( Pcamera, Pprojector, cameraPoints, projectorPoints, points4D ); std::vector<cv::Point3f> points3D; for (int i = 0; i < points4D.cols; i++) { double w = points4D.at<double>(3, i); double x = points4D.at<double>(0, i) / w; double y = points4D.at<double>(1, i) / w; double z = points4D.at<double>(2, i) / w; points3D.emplace_back(x, y, z); }

实际工程中,还需要对无效点进行过滤,例如:

亮度过低的点 反光区域 相位不连续区域 深度异常点 超出有效测量范围的点

十一、重建效果怎么判断?

1. 看包裹相位图

正常情况下,相位图应该呈现连续、规律的周期变化。
如果有大量断裂、噪声或黑块,说明采集质量可能存在问题。

2. 看展开相位图

展开相位应该整体连续。
如果突然出现大面积跳变,通常说明相位展开失败。

3. 看点云形状

打开 PLY 文件后,观察点云是否有明显畸变:

  • 平面是否弯曲
  • 边缘是否破碎
  • 深度是否抖动
  • 是否存在大量飞点

4. 看实际尺寸误差

如果重建的是一个已知尺寸物体,可以测量点云中的距离,与真实尺寸进行对比。

十二、常见问题

1. 单目相机加条纹图为什么能重建三维?

因为条纹图给物体表面增加了主动编码。
相机像素和投影仪像素建立对应关系后,相机和投影仪就可以组成类似双目的几何系统。

2. 只用一组竖直条纹可以重建吗?

可以做一定程度的重建,但信息不完整。
工程中通常会同时投影竖直条纹和水平条纹,获得投影仪的二维坐标,重建结果更稳定。

3. 为什么需要相位展开?

因为atan2()得到的相位只能落在一个周期范围内。
如果不展开,就无法判断当前点属于第几个条纹周期。

4. 为什么重建点云有很多飞点?

常见原因包括:

  • 条纹图过曝
  • 物体表面反光
  • 投影亮度不足
  • 相机和投影仪标定不准确
  • 相位展开错误
  • 阴影区域没有有效条纹信息

总结

本文介绍了单目相机 + 条纹结构光三维重建的基本流程。整体可以概括为:

  1. 使用单目相机采集投影条纹图
  2. 通过四步相移法计算包裹相位
  3. 通过相位展开得到绝对相位
  4. 根据相位恢复投影仪像素坐标
  5. 结合相机和投影仪标定参数进行三角测量
  6. 最终生成物体表面的三维点云

需要注意的是,单目相机本身无法直接获得深度。
条纹图的作用是给场景增加主动编码,而投影仪在几何上可以看作一个“反向相机”。因此,真正完成三维重建的关键不是单张图片,而是“相机 + 投影仪 + 条纹编码 + 标定参数”共同构成的结构光系统。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/6/18 16:34:17

星盘接口开发文档:年运语料接口指南

星盘接口开发文档&#xff1a;年运语料接口指南 1. 引言 本文档详细介绍了占星系统的年运语料接口的使用方法&#xff0c;包括请求参数详解、响应数据结构、错误处理机制以及最佳实践建议。 2. 接口基础信息 接口名称: 年运语料 请求方式: POSTContent-Type: application/x-www…

作者头像 李华
网站建设 2026/6/18 16:29:00

CPRI驱动架构与SmartDSP OS集成:设计原理与工程实践

1. CPRI驱动架构与SmartDSP OS集成深度解析在无线通信系统的开发中&#xff0c;尤其是分布式基站&#xff08;D-RAN&#xff09;和集中式无线接入网&#xff08;C-RAN&#xff09;架构下&#xff0c;基带处理单元&#xff08;BBU&#xff09;与远端射频单元&#xff08;RRU&…

作者头像 李华
网站建设 2026/6/18 16:24:24

【MyBatis】从入门到精通(下):缓存机制与MyBatis-Plus

目录 六、缓存机制 6.1 为什么需要缓存 6.2 一级缓存&#xff08;Local Cache&#xff09; 6.3 二级缓存&#xff08;Global Cache&#xff09; 6.4 缓存执行顺序 6.5 什么时候用缓存 七、MyBatis-Plus增强 7.1 是什么 7.2 核心特性 7.3 快速集成 7.4 BaseMapper — …

作者头像 李华
网站建设 2026/6/18 16:22:42

Win11Debloat终极指南:一键清理Windows系统臃肿的免费工具

Win11Debloat终极指南&#xff1a;一键清理Windows系统臃肿的免费工具 【免费下载链接】Win11Debloat A simple, lightweight PowerShell script that allows you to remove pre-installed apps, disable telemetry, as well as perform various other changes to declutter an…

作者头像 李华
网站建设 2026/6/18 16:18:43

MAE掩码自编码器:从视觉自监督到ViT大模型预训练实战

1. 项目概述&#xff1a;从“看图补画”到视觉大模型的新范式如果你玩过“你画我猜”或者小时候做过“根据局部猜整体”的题目&#xff0c;那你已经对“掩码自编码器”的核心思想有了最朴素的直觉。在计算机视觉领域&#xff0c;我们一直希望机器能像人一样&#xff0c;通过观察…

作者头像 李华
网站建设 2026/6/18 16:14:01

从零构建个人知识管理系统:轻量级信息抓取与聚合实践

1. 项目概述&#xff1a;从“Claw”到个人知识管理系统的蜕变最近在整理自己的数字生活时&#xff0c;发现了一个普遍存在的痛点&#xff1a;信息碎片化。我们每天在微信、浏览器、Kindle、PDF文档、甚至聊天记录里&#xff0c;会接触到大量有价值的信息片段——一句深刻的观点…

作者头像 李华