3D视觉基础:点云处理、深度估计、立体视觉详解

引言

想让自动驾驶汽车看清路面行人、用手机摄像头精准测量家具尺寸、在VR中1:1复刻真实场景?这些“魔法”背后都离不开一项技术——3D视觉
它让机器像人一样理解三维世界:从一张或多张平面照片中还原物体的深度、形状和空间位置关系。

📂 所属阶段:第二阶段 — 深度学习视觉基础(CNN 篇)
🔗 相关章节:Vision-Language 多模态 · 模型轻量化


1. 3D视觉基础概念

1.1 3D视觉到底在解决什么问题?

简单来说,3D视觉的核心任务就是:用算法重建一个可以交互的三维世界模型
它必须回答三个核心问题:

  • 深度从哪来?——如何从一张或多张2D图像中推算出每个像素离相机有多远?
  • 多视角怎么关联?——不同位置拍的照片,图像中的同一个物体如何对应起来?(涉及对极几何、相机标定等)
  • 3D数据怎么表示?——用什么数据结构存储点、面、体积等信息,才能方便后续分析?

这些难题一攻克,3D视觉就直接落地到了自动驾驶(激光雷达+相机融合)、工业检测(3D缺陷定位)、医疗影像(CT重建)等刚需领域。

1.2 3D数据的5种主流表示

3D数据不像图片那样是整齐的像素网格,它有很多“形态”,不同表示各有优劣:

表示类型形象理解适用场景
点云 (Point Cloud)像把物体表面撒上无数个点,每个点有x,y,z坐标,可附带颜色、法向量。无序、数量可变激光雷达扫描、快速3D快照
网格 (Mesh)用三角形面片拼接成的三维模型,类似游戏里的角色模型。细节丰富、渲染高效电影特效、CAD工业设计
体素 (Voxel)把3D空间切成小方块,就像《我的世界》里的方块世界。结构规则,但高分辨率时数据量爆炸医学CT/MRI重建、3D体素游戏
深度图 (Depth Map)与普通照片一样大的灰度图,像素值代表该点的距离(远近)。制作简单,但只有单个视角的信息深度相机输出、单目/双目深度估计
隐式场 (Implicit Field)用一个神经网络来“记忆”整个场景的颜色和不透明度,查询任一空间点即可获得结果。无需存储显式几何,但计算量大NeRF、高保真新视角合成

2. 点云处理技术

2.1 从零开始玩转点云:Open3D实战

点云是3D视觉传感器(如激光雷达)最原始的输出,但它往往带着噪声、密度不均、离群点。因此,预处理是第一步,也是决定后续分析效果的关键。

以下代码使用Open3DNumpy搭建了一个简洁的点云处理器,你可以直接运行体验:

import numpy as np
import open3d as o3d

class PointCloudProcessor:
    """封装常用点云处理流程,方便调用"""
    def create_from_numpy(self, points: np.ndarray) -> o3d.geometry.PointCloud:
        """从(N,3)的numpy数组生成Open3D点云"""
        pcd = o3d.geometry.PointCloud()
        pcd.points = o3d.utility.Vector3dVector(points)
        return pcd

    def preprocess(self, pcd: o3d.geometry.PointCloud, 
                   voxel_size=0.05, nb_neighbors=20, std_ratio=2.0) -> o3d.geometry.PointCloud:
        """
        一站式预处理:下采样 → 法向量估计 → 离群点去除
        - voxel_size:体素下采样的立方体边长,越大点数越少
        - nb_neighbors & std_ratio:统计滤波参数,保留正常点,去除远处噪声
        """
        # 体素下采样:降低密度,同时保留整体几何外形
        down_pcd = pcd.voxel_down_sample(voxel_size=voxel_size)
        # 估计法向量:为后续分割、重建做准备
        down_pcd.estimate_normals()
        # 统计滤波:剔除孤立的离群点(扫描噪声)
        filtered_pcd, _ = down_pcd.remove_statistical_outlier(
            nb_neighbors=nb_neighbors, std_ratio=std_ratio
        )
        return filtered_pcd

# 快速测试
if __name__ == "__main__":
    # 随机生成10000个高斯分布点,模拟带噪声的扫描数据
    noisy_points = np.random.randn(10000, 3) * 0.5
    pcd_raw = PointCloudProcessor().create_from_numpy(noisy_points)
    pcd_filtered = PointCloudProcessor().preprocess(pcd_raw)
    # 可视化对比(本地运行时可取消注释)
    # o3d.visualization.draw_geometries([pcd_raw, pcd_filtered], window_name="Raw vs Filtered")
    print(f"原始点数: {len(pcd_raw.points)}, 预处理后点数: {len(pcd_filtered.points)}")

关键步骤解读:

  • 体素下采样:把空间划分为一个个小立方体,每个立方体只保留一个点。能有效降低数据量,同时不丢失整体形状。
  • 法向量估计:计算每个点所在局部表面的朝向,是点云分割、配准的基础。
  • 统计滤波:检测那些离邻居太远的“孤点”,它们通常是传感器误差造成的。

2.2 让神经网络直接“吃”点云:轻量级PointNet

点云有两个让传统卷积网络头疼的特性:点的顺序无关性(点怎么排列都能表示同一物体)和点数不固定
PointNet用两个巧妙的设计解决了问题:

  1. 共享MLP(多层感知机):对每一个点独立提取特征,点序改变不影响输出。
  2. 对称最大池化:从所有点特征中选取最大值,得到固定维度的全局特征,不怕点数变化。

下面我们用PyTorch实现一个简化版PointNet分类器(不含T-net空间变换模块),直接处理(batch, 点数, 3)的输入:

import torch
import torch.nn as nn

class SimplePointNet(nn.Module):
    """简化版PointNet分类器,理解核心思想"""
    def __init__(self, num_classes=10):
        super().__init__()
        # 共享MLP:对各点独立做1x1卷积(其实就是逐点全连接)
        self.point_mlp = nn.Sequential(
            nn.Conv1d(3, 64, kernel_size=1),
            nn.BatchNorm1d(64),
            nn.ReLU(),
            nn.Conv1d(64, 128, kernel_size=1),
            nn.BatchNorm1d(128),
            nn.ReLU(),
            nn.Conv1d(128, 1024, kernel_size=1),
            nn.BatchNorm1d(1024)
        )
        # 全局特征聚合:对所有点取最大值,得到(batch,1024)
        self.global_pool = nn.MaxPool1d(kernel_size=1024)
        # 分类头
        self.classifier = nn.Sequential(
            nn.Linear(1024, 512),
            nn.BatchNorm1d(512),
            nn.ReLU(),
            nn.Dropout(0.3),
            nn.Linear(512, 256),
            nn.BatchNorm1d(256),
            nn.ReLU(),
            nn.Dropout(0.3),
            nn.Linear(256, num_classes)
        )

    def forward(self, x: torch.Tensor) -> torch.Tensor:
        """
        Args:
            x: (batch_size, num_points, 3)
        Returns:
            logits: (batch_size, num_classes)
        """
        # 维度调整 (B,N,3) → (B,3,N) 以适配Conv1d
        x = x.transpose(2, 1)
        # 逐点特征提取
        x = self.point_mlp(x)
        # 最大池化得到全局特征 (B,1024,N) → (B,1024)
        x = self.global_pool(x).squeeze(-1)
        # 分类预测
        return self.classifier(x)

# 快速测试维度
if __name__ == "__main__":
    test_input = torch.randn(8, 1024, 3)  # batch=8, 每个物体1024个点
    model = SimplePointNet(num_classes=40)  # 假设使用ModelNet40的40类
    output = model(test_input)
    print(f"输入维度: {test_input.shape}, 输出维度: {output.shape}")

核心优势: PointNet凭借对称函数(最大池化)实现了对点云无序性、不变性的天然适配,是后续PointNet++等工作的基石。


3. 深度估计与立体视觉

3.1 如何让照片“变厚”:深度估计技术分类

深度估计就是给普通照片每个像素赋予一个距离值,生成深度图。根据成本和精度,主流方法如下:

方法类型输入原理通俗解释成本精度
单目深度估计单张彩色图AI学习场景规律:比如近大远小、纹理渐变、遮挡关系,直接“猜”出深度
双目立体视觉两张彩色图模仿人眼:左右图同一物体位置的微小偏移(视差),用几何公式换算成深度中高
硬件传感器Lidar/ToF物理测量光的飞行时间,直接给出精确距离

双目立体视觉因其“成本低、精度还不错”的特点,成为机器人、辅助驾驶的常用方案。

3.2 动手实现:OpenCV双目立体匹配

传统方法中,SGBM(半全局匹配)算法在效果和速度之间取得了很好的平衡。下面我们用OpenCV快速计算视差图和深度图:

import cv2
import numpy as np
import matplotlib.pyplot as plt

def sgbm_stereo_depth(left_path: str, right_path: str, 
                       baseline=0.2, focal_length=720):
    """
    使用SGBM计算双目图像的视差图和深度图。
    baseline: 相机基线距离(单位:米)
    focal_length: 相机焦距(单位:像素)
    """
    # 1. 读取左右灰度图(减少计算量)
    left = cv2.imread(left_path, cv2.IMREAD_GRAYSCALE)
    right = cv2.imread(right_path, cv2.IMREAD_GRAYSCALE)

    # 2. 创建SGBM匹配器(参数可调整)
    stereo = cv2.StereoSGBM_create(
        minDisparity=0,          # 最小视差
        numDisparities=16*5,     # 最大视差范围(16的倍数)
        blockSize=5,             # 匹配的窗口大小
        P1=8*3*5**2,             # 小视差变化惩罚
        P2=32*3*5**2,            # 大视差变化惩罚(保持边缘)
        disp12MaxDiff=1,         # 左右一致性检查允许的最大差异
        uniquenessRatio=15,      # 唯一性阈值
        preFilterCap=63          # 预处理截断值
    )

    # 3. 计算视差图(除以16得到真实视差值)
    disparity = stereo.compute(left, right).astype(np.float32) / 16.0
    # 归一化以便显示
    disparity_normalized = (disparity - disparity.min()) / (disparity.max() - disparity.min() + 1e-5)

    # 4. 视差转深度(Z = F * B / disparity,避开除零)
    depth = (baseline * focal_length) / (disparity + 1e-5)
    depth_normalized = (depth - depth.min()) / (depth.max() - depth.min() + 1e-5)

    # 5. 可视化对比
    plt.figure(figsize=(15, 5))
    plt.subplot(131), plt.imshow(left, cmap='gray'), plt.title('Left Image')
    plt.subplot(132), plt.imshow(disparity_normalized, cmap='jet'), plt.title('Disparity Map')
    plt.subplot(133), plt.imshow(depth_normalized, cmap='jet'), plt.title('Depth Map')
    plt.tight_layout()
    plt.show()

    return disparity, depth

直观理解视差与深度的关系: 物体越近,左右图的位置差异(视差)越大,深度值越小;物体越远,视差越小,深度越大。公式 深度 = (基线 × 焦距) / 视差 将这种反比关系量化。


4. 神经辐射场(NeRF)快速入门

4.1 用神经网络“记住”整个3D场景

NeRF(Neural Radiance Fields)是2020年的明星技术,它彻底改变了场景重建的方式。
传统的3D表示需要存储大量点/面/体素,而NeRF只用一个多层感知机(MLP)就隐式地编码了整个场景。

工作原理可以这样理解:

  • 你告诉网络一个空间点 (x,y,z) 和观察方向,它直接告诉你这个点是透明的还是实心的(体密度 σ),以及它看起来是什么颜色 (r,g,b)
  • 然后,沿着相机射线采样很多点,用体积渲染把这些点的颜色和密度累加起来,就生成了从这个视角看到的图像。

因为网络本身就是一个连续函数,所以我们可以查询任意精细位置的属性,从而渲染出任意新视角的超高清图像。这在VR看房、电影特效、数字孪生等领域有极大的应用潜力。

📌 NeRF虽然效果好,但训练时间长、渲染慢,目前学术界正在积极研究加速版本(如Instant NGP)。


相关教程

3D视觉是计算机视觉的高阶分支,建议先打牢基础: 掌握**图像处理、CNN、PyTorch**,然后从**Open3D点云预处理**和**OpenCV双目立体匹配**这两个可快速上手的实战任务开始,逐步深入到PointNet、NeRF等前沿模型。

总结

整篇下来,我们可以看到3D视觉的三大技术支柱:

  1. 点云处理:Open3D经典预处理 + PointNet等深度学习模型,解决无序数据的分类、分割问题。
  2. 深度估计:利用单目CNN或双目SGBM从图像中恢复场景的“厚度”。
  3. 高保真重建:NeRF等神经渲染技术,用隐式场实现惊艳的新视角合成。

未来的趋势是多传感器融合(激光雷达+相机)实时轻量化重建,这也是自动驾驶、机器人、元宇宙等领域最迫切的需求。不妨现在就动手跑一遍代码,迈出3D视觉的第一步!