首页/知识库/3D 视觉:点云、NeRF、3D 重建

3D 视觉:点云、NeRF、3D 重建

✍️ AI Master📅 创建 2026-04-12📖 20 min 阅读
💡

文章摘要

从 2D 到 3D,掌握三维视觉的核心技术

13D 数据表示:点云、网格、体素与 NeRF

3D 视觉的第一步是理解计算机如何表示三维世界。最直观的方式是点云——大量无序的 (x, y, z) 坐标点集合,每个点还可以附带颜色、法向量、反射强度等属性。点云的优势在于结构简单、存储稀疏,但缺乏拓扑信息。网格(Mesh)在点云基础上增加了面片连接关系(三角形面),形成了连续的表面表示,是 3D 渲染和建模的标准格式。体素(Voxel)则将 3D 空间离散化为规则的 3D 网格,类似 2D 像素的立体扩展,适合用 3D 卷积处理,但内存消耗随分辨率立方增长。NeRF(神经辐射场)代表了最新方向——用一个 MLP 隐式编码整个 3D 场景,输入 3D 坐标和视角方向,输出颜色和体密度,实现了照片级的新视角合成。这四种表示方式各有适用场景:点云适合传感器原始数据,网格适合渲染和编辑,体素适合体素级深度学习,NeRF 适合高质量渲染和场景重建。

python
import numpy as np

# 点云基础数据结构
class PointCloud:
    """最简 3D 点云表示"""
    def __init__(self, points, colors=None, normals=None):
        """
        points: (N, 3) xyz 坐标
        colors: (N, 3) RGB 颜色, 范围 0-1
        normals: (N, 3) 法向量, 已归一化
        """
        self.points = np.asarray(points, dtype=np.float32)
        self.colors = colors
        self.normals = normals

    @property
    def num_points(self):
        return self.points.shape[0]

    @property
    def centroid(self):
        return self.points.mean(axis=0)

    def normalize(self):
        """将点云归一化到单位球内"""
        center = self.centroid
        points_centered = self.points - center
        max_dist = np.max(np.linalg.norm(points_centered, axis=1))
        self.points = points_centered / max_dist
        return self

    def downsample_voxel(self, voxel_size=0.01):
        """体素下采样:均匀降采样点云"""
        # 将点量化到体素网格
        voxel_indices = np.floor(self.points / voxel_size).astype(int)
        # 按体素索引分组,取每个体素内点的均值
        unique_voxels, inverse = np.unique(
            voxel_indices, axis=0, return_inverse=True)
        downsampled = np.zeros((len(unique_voxels), 3), dtype=np.float32)
        np.add.at(downsampled, inverse, self.points)
        counts = np.bincount(inverse, minlength=len(unique_voxels))
        return PointCloud(downsampled / counts[:, None])
python
import numpy as np

# 网格与体素转换
class Mesh:
    """三角网格表示"""
    def __init__(self, vertices, faces):
        """
        vertices: (V, 3) 顶点坐标
        faces: (F, 3) 三角形面片索引
        """
        self.vertices = np.asarray(vertices, dtype=np.float32)
        self.faces = np.asarray(faces, dtype=np.int32)

    def compute_face_normals(self):
        """计算每个三角形面片的法向量"""
        v0 = self.vertices[self.faces[:, 0]]
        v1 = self.vertices[self.faces[:, 1]]
        v2 = self.vertices[self.faces[:, 2]]
        edge1 = v1 - v0
        edge2 = v2 - v0
        normals = np.cross(edge1, edge2)
        norms = np.linalg.norm(normals, axis=1, keepdims=True)
        return normals / (norms + 1e-8)

class VoxelGrid:
    """规则 3D 体素网格"""
    def __init__(self, resolution=32, bounds=None):
        self.resolution = resolution
        self.bounds = bounds or np.array([[-1, 1], [-1, 1], [-1, 1]])
        self.grid = np.zeros((resolution, resolution, resolution),
                              dtype=np.float32)

    def points_to_voxels(self, points):
        """将点云转换为占用体素网格"""
        mins = self.bounds[:, 0]
        ranges = self.bounds[:, 1] - mins
        normalized = (points - mins) / ranges
        indices = (normalized * (self.resolution - 1)).astype(int)
        indices = np.clip(indices, 0, self.resolution - 1)
        self.grid[indices[:, 0], indices[:, 1], indices[:, 2]] = 1.0
        return self.grid
表示方式数据结构内存消耗适用场景

点云

无序点集 (N,3)

O(N) 稀疏

传感器数据、大规模场景

网格

顶点+面片

O(V+F)

渲染、3D 打印、建模

体素

规则 3D 网格

O(R^3) 立方增长

3D 卷积、占用预测

NeRF

MLP 权重

O(MLP参数量)

新视角合成、高质量渲染

SDF

符号距离场

O(网格) 或 MLP

几何重建、物理仿真

入门 3D 视觉建议先掌握点云和网格的基本操作,NeRF 和 SDF 可以后续深入

体素表示的内存消耗随分辨率呈立方增长,64^3 的体素需要 256MB,128^3 则需 2GB

2点云深度学习:PointNet 革命

2017 年斯坦福大学 Qi 等人提出的 PointNet 开创性地将深度学习直接应用于原始点云数据,绕过了传统的体素化或投影预处理步骤。PointNet 的核心挑战在于点云的三个特性:无序性(点集没有固定顺序)、局部不变性(点的排列不影响语义)、旋转不变性(旋转后的点云应得到相同结果)。PointNet 的解决方案简洁而优雅:使用共享 MLP 独立处理每个点,然后通过全局最大池化(max pooling)聚合所有点的特征。最大池化天然具备对称性和无序不变性——无论输入点的顺序如何,pooling 后的结果都是一样的。PointNet 同时输出了点级特征(用于分割)和全局特征(用于分类),在 ModelNet40 分类和 ShapeNet 分割任务上取得了当时的最优结果。后续的 PointNet++ 进一步引入了层次化分组和集合抽象,解决了原始 PointNet 缺乏局部特征建模的问题。

python
import torch
import torch.nn as nn
import torch.nn.functional as F

class PointNetClassifier(nn.Module):
    """PointNet 分类网络"""
    def __init__(self, num_classes=40, num_points=1024):
        super().__init__()
        self.num_points = num_points

        # 输入变换网络 (T-Net)
        self.input_transform = nn.Sequential(
            nn.Conv1d(3, 64, 1), nn.BatchNorm1d(64), nn.ReLU(),
            nn.Conv1d(64, 128, 1), nn.BatchNorm1d(128), nn.ReLU(),
            nn.Conv1d(128, 1024, 1), nn.BatchNorm1d(1024), nn.ReLU(),
            nn.MaxPool1d(num_points),
            nn.Linear(1024, 512), nn.ReLU(),
            nn.Linear(512, 256), nn.ReLU(),
            nn.Linear(256, 9)  # 3x3 变换矩阵
        )

        # 特征提取 MLP
        self.mlp1 = nn.Sequential(
            nn.Conv1d(3, 64, 1), nn.BatchNorm1d(64), nn.ReLU(),
            nn.Conv1d(64, 64, 1), nn.BatchNorm1d(64), nn.ReLU(),
        )

        # 特征变换
        self.feature_transform = nn.Sequential(
            nn.Conv1d(64, 64, 1), nn.BatchNorm1d(64), nn.ReLU(),
            nn.Conv1d(64, 128, 1), nn.BatchNorm1d(128), nn.ReLU(),
            nn.Conv1d(128, 1024, 1), nn.BatchNorm1d(1024), nn.ReLU(),
            nn.MaxPool1d(num_points),
            nn.Linear(1024, 512), nn.ReLU(),
            nn.Linear(512, 256), nn.ReLU(),
            nn.Linear(256, 64*64)  # 64x64 变换矩阵
        )

        # 分类头
        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):
        # x: (B, 3, N)
        B = x.size(0)
        trans = self.input_transform(x).view(B, 3, 3)
        x = torch.bmm(x.transpose(2, 1), trans).transpose(2, 1)

        x = self.mlp1(x)
        trans_feat = self.feature_transform(x).view(B, 64, 64)
        x = torch.bmm(x.transpose(2, 1), trans_feat).transpose(2, 1)

        # 全局最大池化:核心操作!
        x = torch.max(x, 2, keepdim=True)[0].view(B, -1)
        return self.classifier(x)
python
import torch
import torch.nn as nn

# 为什么全局最大池化有效?
def demonstrate_maxpool_invariance():
    """演示 max pooling 对点顺序的不变性"""
    # 模拟 4 个点的特征 (每个点 3 维特征)
    features_a = torch.tensor([
        [0.1, 0.5, 0.3],  # 点 1
        [0.9, 0.2, 0.1],  # 点 2
        [0.4, 0.8, 0.6],  # 点 3
        [0.2, 0.1, 0.9],  # 点 4
    ]).unsqueeze(0)  # (1, 4, 3)

    # 打乱顺序
    perm = torch.randperm(4)
    features_b = features_a[:, perm, :]

    # 最大池化 (沿点的维度)
    global_a = torch.max(features_a, dim=1)[0]
    global_b = torch.max(features_b, dim=1)[0]

    print(f"原始顺序 max: {global_a}")
    print(f"打乱顺序 max: {global_b}")
    print(f"相等: {torch.allclose(global_a, global_b)}")  # True!
    # 最大池化天然对点序不变,
# 这是 PointNet 处理无序点集的关键设计

demonstrate_maxpool_invariance()
组件输入维度输出维度作用

输入 T-Net

(B, 3, N)

(B, 3, 3) 矩阵

对齐输入点云到规范空间

共享 MLP1

(B, 3, N)

(B, 64, N)

逐点特征提取

特征 T-Net

(B, 64, N)

(B, 64, 64) 矩阵

对齐特征空间

共享 MLP2

(B, 64, N)

(B, 1024, N)

扩展到高维特征

全局 MaxPool

(B, 1024, N)

(B, 1024)

聚合全局点云特征

分类 MLP

(B, 1024)

(B, num_classes)

输出分类概率

PointNet 的 T-Net 是可学习的空间变换器,类似 STN,让网络自动学习最佳对齐方式

PointNet 对每个点独立处理,缺乏局部上下文建模——小物体或精细结构容易被忽略

3多视图立体 MVS:从多张照片到稠密深度

多视图立体(Multi-View Stereo, MVS)是 3D 重建中从已知相机位姿的多张照片恢复稠密 3D 几何的核心技术。与 SfM(仅恢复稀疏点云和相机位姿)不同,MVS 的目标是生成稠密的、表面连续的 3D 模型。MVS 的基本思路是:对于参考视图中的每个像素,在相邻视图中搜索对应的匹配点,通过三角测量计算深度值。传统的 MVS 方法依赖于光度一致性假设——同一 3D 点在不同视角下的像素值应该相近。深度学习方法(如 MVSNet)则将这一过程端到端可微化:使用 2D CNN 提取多视图特征,通过单应性变换将特征 warp 到参考视图,构建代价体(cost volume),最后用 3D CNN 回归深度图。MVSNet 的关键创新在于可微的单应性变换和正则化的 3D 代价体,使得整个管线可以通过反向传播联合优化。

python
import torch
import torch.nn as nn

def build_homography_warp(ref_features, src_features,
                           ref_intrinsics, src_intrinsics,
                           ref_poses, src_poses, depths):
    """
    构建单应性变换代价体 (MVSNet 风格)
    ref_features: (B, C, H, W) 参考视图特征
    src_features: (B, N, C, H, W) N 个源视图特征
    depths: (D,) 候选深度值
    返回: (B, C, D, H, W) 代价体
    """
    B, N, C, H, W = src_features.shape
    D = len(depths)
    volume = torch.zeros(B, C, D, H, W,
                         device=ref_features.device)

    for d_idx, depth in enumerate(depths):
        # 为每个深度假设构建参考平面
        # 参考平面法向量 n=[0,0,1], 距离 d=depth
        n = torch.tensor([0, 0, 1], dtype=torch.float32)
        ref_K = ref_intrinsics  # (B, 3, 3)
        ref_Rt = ref_poses       # (B, 3, 4)

        for src_idx in range(N):
            src_K = src_intrinsics[:, src_idx]
            src_Rt = src_poses[:, src_idx]

            # 单应性矩阵 H = K_src * (R - t*n^T/d) * K_ref^{-1}
            R_rel = src_Rt[:, :, :3] @ ref_Rt[:, :, :3].transpose(1, 2)
            t_rel = src_Rt[:, :, :3] @ ref_Rt[:, :, 3:4]
            H = src_K @ (R_rel - t_rel @ n.unsqueeze(0).unsqueeze(0) / depth) @ torch.inverse(ref_K)

            # 使用 H 将参考特征 warp 到源视图
            warped = homography_warp(ref_features, H, H_size=(H, W))
            # 与源视图特征计算相似度 (L1 距离)
            cost = torch.mean(
                torch.abs(warped - src_features[:, src_idx]), dim=1)
            volume[:, :, d_idx] += cost

    return volume / N  # 平均多视图代价
python
import torch
import torch.nn as nn

class DepthRegression(nn.Module):
    """MVSNet 深度回归模块"""
    def __init__(self, num_depths=192):
        super().__init__()
        self.num_depths = num_depths
        # 3D UNet 正则化代价体
        self.regularization = nn.Sequential(
            nn.Conv3d(1, 16, 3, padding=1),
            nn.BatchNorm3d(16), nn.ReLU(),
            nn.Conv3d(16, 8, 3, padding=1),
            nn.BatchNorm3d(8), nn.ReLU(),
            nn.Conv3d(8, 1, 3, padding=1),
        )
        # 候选深度 (线性或非线性分布)
        self.min_depth = 0.5
        self.max_depth = 15.0

    def forward(self, cost_volume, ref_images):
        """
        cost_volume: (B, C, D, H, W) 原始代价体
        返回: (B, H, W) 深度图 + (B, H, W) 置信度
        """
        # 沿通道维度压缩为单通道代价体
        prob_volume = self.regularization(
            cost_volume.mean(dim=1, keepdim=True))
        prob_volume = torch.softmax(
            -prob_volume.squeeze(1), dim=1)  # (B, D, H, W)

        # 构建候选深度值
        depths = torch.linspace(self.min_depth, self.max_depth,
                                self.num_depths, device=prob_volume.device)

        # 软回归:期望深度 = sum(p_d * d)
        depth_map = torch.sum(
            prob_volume * depths.view(1, -1, 1, 1), dim=1)

        # 置信度 = 最大概率 (不确定性估计)
        confidence = torch.max(prob_volume, dim=1)[0]

        return depth_map, confidence
方法输入输出核心创新

传统 PatchMatch

多张标定照片

稠密深度图

高效随机搜索匹配

MVSNet

多视图 + 相机参数

深度图 + 置信度

可微单应性 + 3D 代价体

R-MVSNet

多视图 + 相机参数

逐像素深度

循环网络逐像素回归

CVP-MVSNet

多视图 + 相机参数

级联深度图

级联代价体 + 曲率先验

Vis-MVSNet

多视图 + 相机参数

可见性感知的深度

可见性感知代价体构建

MVS 的深度假设范围(min_depth/max_depth)需要根据场景尺度调整,设置过窄会导致截断

MVS 在弱纹理区域(白墙、天空)和反光表面(玻璃、水面)上表现很差,这些区域缺乏可靠的匹配线索

4NeRF 神经辐射场:隐式 3D 表示的突破

2020 年 Mildenhall 等人提出的 NeRF(Neural Radiance Fields)彻底改变了新视角合成的范式。与传统的显式 3D 表示(点云、网格)不同,NeRF 用一个多层感知机(MLP)隐式地编码整个 3D 场景。网络接受 5D 输入——3D 空间坐标 (x, y, z) 和 2D 观察方向 (θ, φ)——输出该点的颜色 (r, g, b) 和体密度 σ。渲染过程通过体积渲染积分实现:从相机出发发射光线,沿光线采样多个点,将每个点的颜色和密度按照体积渲染方程累积,得到最终像素颜色。NeRF 的关键技巧包括位置编码(positional encoding)——将输入坐标映射到高维傅里叶特征空间,使网络能够学习高频细节;以及层次化采样策略——先用粗网络确定大致区域,再用细网络在重要区域密集采样。原始 NeRF 的训练需要数百张多视角照片,渲染一张 800×800 的图片需要数分钟,但其视觉质量远超传统方法。

python
import torch
import torch.nn as nn

class NeRF(nn.Module):
    """原始 NeRF 网络结构"""
    def __init__(self, num_layers=8, hidden_dim=256,
                 skips=[4], pos_freq=10, view_freq=4):
        super().__init__()
        self.skips = skips
        self.pos_freq = pos_freq
        self.view_freq = view_freq
        # 位置编码维度: 3 * (2*10 + 1) = 63
        pos_enc_dim = 3 * (2 * pos_freq + 1)
        view_enc_dim = 3 * (2 * view_freq + 1)

        # 几何 MLP (仅依赖位置)
        layers = []
        prev_dim = pos_enc_dim
        for i in range(num_layers):
            if i in skips:
                prev_dim += pos_enc_dim  # 跳跃连接
            layers.extend([
                nn.Linear(prev_dim, hidden_dim),
                nn.ReLU()
            ])
            prev_dim = hidden_dim
        self.geometry = nn.Sequential(*layers)

        # 密度输出
        self.density = nn.Linear(hidden_dim, 1)

        # 外观 MLP (依赖位置+方向)
        self.appearance = nn.Sequential(
            nn.Linear(hidden_dim + view_enc_dim, hidden_dim // 2),
            nn.ReLU(),
            nn.Linear(hidden_dim // 2, 3),
            nn.Sigmoid()  # 颜色输出 [0, 1]
        )

    def positional_encoding(self, x, num_freq):
        """位置编码: 将坐标映射到高频傅里叶特征"""
        freqs = 2.0 ** torch.arange(num_freq,
                                     device=x.device)
        encoding = torch.cat([
            torch.sin(x[..., None] * freqs),
            torch.cos(x[..., None] * freqs)
        ], dim=-1)
        return torch.cat([x, encoding.reshape(x.shape[:-1] + (-1,))],
                         dim=-1)

    def forward(self, points, directions):
        # points: (N_rays, N_samples, 3)
        # directions: (N_rays, N_samples, 3)
        pos_enc = self.positional_encoding(
            points, self.pos_freq)  # (N, S, 63)
        view_enc = self.positional_encoding(
            directions, self.view_freq)  # (N, S, 25)

        # 几何网络
        h = pos_enc
        for i, layer in enumerate(self.geometry):
            if i == 0 and 0 in self.skips:
                h = torch.cat([h, pos_enc], dim=-1)
            h = layer(h)

        density = self.density(h)  # (N, S, 1)

        # 外观网络
        rgb = self.appearance(
            torch.cat([h, view_enc], dim=-1))  # (N, S, 3)
        return rgb, density.squeeze(-1)
python
import torch

def volume_rendering(rays, points, densities, rgbs,
                      noise_std=1e-4):
    """
    体积渲染:沿光线累积颜色和透明度
    rays: (N_rays, 3) 光线原点
    points: (N_rays, N_samples, 3) 采样点
    densities: (N_rays, N_samples) 体密度 sigma
    rgbs: (N_rays, N_samples, 3) 颜色
    """
    # 计算步长 (相邻采样点距离)
    dists = torch.norm(
        points[:, 1:] - points[:, :-1], dim=-1)
    # 最后一个采样点的步长设为无穷大
    dists = torch.cat(
        [dists, torch.full((dists.shape[0], 1),
                           1e10, device=dists.device)], dim=-1)

    # 添加噪声使训练更稳定
    if noise_std > 0:
        densities = densities + torch.randn_like(densities) * noise_std

    # 透明度: alpha_i = 1 - exp(-sigma_i * delta_i)
    alpha = 1.0 - torch.exp(-densities * dists)

    # 透射率: T_i = product(1 - alpha_j) for j < i
    cumprod = torch.cumprod(
        1.0 - alpha + 1e-10, dim=-1)
    transmittance = torch.cat(
        [torch.ones_like(cumprod[:, :1]),
         cumprod[:, :-1]], dim=-1)

    # 权重: w_i = T_i * alpha_i
    weights = transmittance * alpha  # (N_rays, N_samples)

    # 渲染: C(r) = sum(w_i * c_i)
    rendered_rgb = torch.sum(
        weights[..., None] * rgbs, dim=-2)  # (N_rays, 3)

    # 深度图 (可选输出)
    rendered_depth = torch.sum(
        weights * dists, dim=-1)  # (N_rays,)

    return rendered_rgb, rendered_depth, weights
NeRF 组件输入维度输出维度关键参数

位置编码

(x,y,z) → 3D

63D 特征

10 个频率 (2^0 ~ 2^9)

几何 MLP

63D 位置特征

256D 隐藏 + 密度

8 层, 跳跃连接在第 4 层

外观 MLP

256D 几何 + 25D 方向

RGB 颜色 [0,1]

2 层, 128D 隐藏

体积渲染

N_rays x N_samples

像素颜色 + 深度

粗 64 + 细 128 采样

训练策略

200K 步, batch 4096 光线

PSNR 指标

Adam, lr=5e-4 衰减

NeRF 的位置编码频率选择至关重要——低频捕获大尺度结构,高频捕获精细纹理

NeRF 训练非常慢(数小时到数天),且每场景独立训练,无法直接泛化到未见过的场景

53D 重建管线:SfM + MVS 完整流程

完整的 3D 重建管线通常分为两个阶段:运动恢复结构(Structure from Motion, SfM)和多视图立体(MVS)。SfM 负责从无序的照片集合中恢复相机位姿和稀疏 3D 点云。它首先通过特征匹配(SIFT/SuperPoint)找到照片之间的对应点,然后利用 RANSAC 估计基础矩阵和本质矩阵,逐步恢复每张照片的相机位置和方向,同时通过三角测量得到稀疏的 3D 点。SfM 的输出包括:所有相机的内外参数(K, R, t)和一个稀疏点云。MVS 阶段则以 SfM 的相机参数为输入,通过稠密匹配生成深度图,融合多视图深度得到稠密点云,最后通过表面重建算法(泊松重建、 marching cubes)得到连续的三角网格。整个管线中,SfM 提供了几何约束的骨架,MVS 填充了丰富的表面细节。COLMAP 是目前最流行的开源实现,它集成了完整的 SfM 和 MVS 流程。

python
import subprocess
import os

def run_colmap_sfm(image_dir, output_dir):
    """
    使用 COLMAP 进行 SfM 重建
    返回: 相机参数 + 稀疏点云
    """
    os.makedirs(output_dir, exist_ok=True)

    # 1. 特征提取
    subprocess.run([
        "colmap", "feature_extractor",
        "--database_path", f"{output_dir}/database.db",
        "--image_path", image_dir,
        "--ImageReader.single_camera", "1",
        "--SiftExtraction.use_gpu", "1"
    ])

    # 2. 特征匹配 (暴力匹配或词汇树匹配)
    subprocess.run([
        "colmap", "exhaustive_matcher",
        "--database_path", f"{output_dir}/database.db"
    ])

    # 3. 重建 (增量式 SfM)
    subprocess.run([
        "colmap", "mapper",
        "--database_path", f"{output_dir}/database.db",
        "--image_path", image_dir,
        "--output_path", f"{output_dir}/sparse"
    ])

    # 4. 光束法平差优化
    subprocess.run([
        "colmap", "bundle_adjuster",
        "--input_path", f"{output_dir}/sparse/0",
        "--output_path", f"{output_dir}/sparse/0"
    ])
    print(f"SfM 完成! 输出: {output_dir}/sparse/0")

def run_colmap_mvs(sparse_dir, image_dir, output_dir):
    """使用 COLMAP MVS 进行稠密重建"""
    os.makedirs(output_dir, exist_ok=True)

    # 1. 图像去畸变
    subprocess.run([
        "colmap", "image_undistorter",
        "--image_path", image_dir,
        "--input_path", sparse_dir,
        "--output_path", output_dir,
        "--output_type", "COLMAP"
    ])

    # 2. 立体匹配 (PatchMatch)
    subprocess.run([
        "colmap", "patch_match_stereo",
        "--workspace_path", output_dir,
        "--workspace_format", "COLMAP",
        "--PatchMatchStereo.geom_consistency", "true"
    ])

    # 3. 深度图融合 → 点云
    subprocess.run([
        "colmap", "stereo_fusion",
        "--workspace_path", output_dir,
        "--workspace_format", "COLMAP",
        "--input_type", "geometric",
        "--output_path", f"{output_dir}/fused.ply"
    ])
    print(f"MVS 完成! 稠密点云: {output_dir}/fused.ply")
python
import numpy as np
import open3d as o3d

def reconstruct_mesh_from_cloud(pcd_path, output_path):
    """从稠密点云重建三角网格"""
    # 加载点云
    pcd = o3d.io.read_point_cloud(pcd_path)
    print(f"点云: {len(pcd.points)} 个点")

    # 1. 下采样
    pcd_down = pcd.voxel_down_sample(voxel_size=0.005)

    # 2. 法向量估计 (用于泊松重建)
    pcd_down.estimate_normals(
        search_param=o3d.geometry.KDTreeSearchParamHybrid(
            radius=0.02, max_nn=30))

    # 3. 离群点去除
    cl, ind = pcd_down.remove_statistical_outlier(
        nb_neighbors=20, std_ratio=2.0)
    pcd_clean = pcd_down.select_by_index(ind)
    print(f"清理后: {len(pcd_clean.points)} 个点")

    # 4. 泊松表面重建
    mesh, densities = o3d.geometry.TriangleMesh.create_from_point_cloud_poisson(
        pcd_clean, depth=9)

    # 5. 根据密度裁剪低质量区域
    densities = np.asarray(densities)
    vertices_to_remove = densities < np.quantile(densities, 0.05)
    mesh.remove_vertices_by_mask(vertices_to_remove)

    # 6. 简化网格
    mesh = mesh.simplify_quadric_decimation(100000)

    # 7. 保存
    o3d.io.write_triangle_mesh(output_path, mesh)
    print(f"网格已保存: {output_path}")
    print(f"  顶点数: {len(mesh.vertices)}")
    print(f"  面片数: {len(mesh.triangles)}")

reconstruct_mesh_from_cloud("fused.ply", "reconstructed.obj")
阶段输入输出核心算法

特征提取

原始照片

SIFT/SuperPoint 特征

DoG 检测 + 描述子

特征匹配

特征描述子

图像对匹配

FLANN 最近邻 + 比率测试

SfM 重建

匹配 + 相机模型

相机位姿 + 稀疏点云

增量式 SfM + BA 优化

MVS 稠密

SfM 结果 + 照片

稠密点云

PatchMatch + 深度图融合

表面重建

稠密点云 + 法线

三角网格

泊松重建 / Marching Cubes

SfM 阶段的照片覆盖率至关重要——相邻照片之间至少需要 60% 的重叠区域才能保证可靠匹配

POW 重建对法向量方向敏感,需要确保所有法向量朝向一致(可以使用 PCA 或传播法进行法线统一)

63D 目标检测:从点云中理解场景

3D 目标检测是自动驾驶和机器人领域的核心技术——在点云中定位并分类 3D 物体(车辆、行人、障碍物等)。与 2D 检测不同,3D 检测需要预测物体的 3D 边界框(中心坐标 x, y, z,尺寸 w, h, l,以及朝向角 θ)。主流方法分为三类:基于点的方法(直接在点云上操作,如 PointRCNN)、基于体素的方法(将点云体素化后用 3D 卷积处理,如 VoxelNet)、以及基于鸟瞰图的方法(将点云投影到 BEV 平面用 2D 卷积处理,如 PointPillars)。PointPillars 通过将点云编码为伪图像(Pillars),在保持精度的同时将推理速度提升到了实时级别(>60 FPS)。近年来,基于 Transformer 的方法(如 CenterPoint、VoxelNeXt)进一步提升了检测精度,通过引入中心点预测和特征金字塔,实现了对小物体和遮挡物体的鲁棒检测。

python
import torch
import torch.nn as nn
import numpy as np

class PillarFeatureNet(nn.Module):
    """PointPillars 的 Pillar 特征编码"""
    def __init__(self, in_channels=4, num_features=64,
                 pillar_size=(0.16, 0.16), max_points=100,
                 max_pillars=40000):
        super().__init__()
        self.pillar_size = pillar_size
        self.max_points = max_points
        self.max_pillars = max_pillars

        # 简化的 Pillar VFE (Virtual Feature Encoding)
        self.pillar_vfe = nn.Sequential(
            nn.Linear(in_channels + 3, num_features),  # +3 为 (x_c, y_c, z_c) 偏移
            nn.BatchNorm1d(num_features),
            nn.ReLU(),
            nn.Linear(num_features, num_features),
            nn.BatchNorm1d(num_features),
            nn.ReLU(),
        )

    def points_to_pillars(self, points):
        """
        将点云编码为 Pillar 伪图像
        points: (N, 4) [x, y, z, intensity]
        返回: (B, C, H_p, W_p) 伪图像
        """
        # 将点分配到 Pillar 网格
        x = points[:, 0]
        y = points[:, 1]
        x_pillar = ((x + 40) / self.pillar_size[0]).long()
        y_pillar = ((y + 40) / self.pillar_size[1]).long()

        # 限制在网格范围内 (假设 512x512)
        x_pillar = torch.clamp(x_pillar, 0, 511)
        y_pillar = torch.clamp(y_pillar, 0, 511)

        # 创建伪图像 (简化版,实际使用稀疏卷积)
        pillar_idx = x_pillar * 512 + y_pillar
        unique_pillars = torch.unique(pillar_idx)

        pillar_features = torch.zeros(
            len(unique_pillars), 64, device=points.device)

        for i, p_idx in enumerate(unique_pillars):
            mask = pillar_idx == p_idx
            pillar_points = points[mask]

            # 计算 Pillar 中心偏移
            pillar_center = pillar_points[:, :3].mean(dim=0)
            offsets = pillar_points[:, :3] - pillar_center

            # 拼接: [x, y, z, intensity, x_c, y_c, z_c]
            pillar_input = torch.cat(
                [pillar_points, offsets], dim=1)

            # 通过 VFE 网络
            features = self.pillar_vfe(pillar_input)
            pillar_features[i] = features.max(dim=0)[0]  # Max pooling

        return pillar_features  # (num_pillars, 64)
python
import torch
import numpy as np

def non_max_suppression_3d(boxes, scores, iou_threshold=0.5):
    """
    3D NMS: 按 IoU 去除冗余检测框
    boxes: (N, 7) [x, y, z, w, l, h, theta]
    scores: (N,) 置信度
    """
    if len(boxes) == 0:
        return []

    # 按分数排序
    order = scores.argsort()[::-1]
    keep = []

    while len(order) > 0:
        i = order[0]
        keep.append(i)

        if len(order) == 1:
            break

        # 计算 3D IoU (简化为 2D BEV IoU)
        remaining = order[1:]
        boxes_i = boxes[i]
        boxes_r = boxes[remaining]

        # 2D BEV IoU 计算 (x, y, w, l, theta)
        ious = compute_bev_iou(boxes_i[:5], boxes_r[:, :5])
        # 移除 IoU 过高的框
        inds = torch.where(ious <= iou_threshold)[0]
        order = remaining[inds]

    return keep

def compute_bev_iou(box1, boxes2):
    """计算 2D BEV 平面的 IoU"""
    # 简化实现: 使用旋转矩形的近似 IoU
    # 实际应用中建议使用 shapely 或自定义 CUDA 核
    area1 = box1[2] * box1[3]
    area2 = boxes2[:, 2] * boxes2[:, 3]
    # 这里仅作演示,实际需要精确的旋转矩形交集计算
    return torch.zeros(len(boxes2))
方法输入表示骨干网络检测头速度 (FPS)

VoxelNet

体素

3D CNN + RPN

Anchor-based

~10

PointPillars

Pillar 伪图像

2D CNN (ResNet)

Anchor-based

~60

PointRCNN

原始点云

PointNet++ 2 阶段

Proposal 级精化

~15

CenterPoint

体素 BEV

CenterNet 风格

Center 点预测

~25

VoxelNeXt

体素

Voxel 特征

NeXt 预测头

~30

PointPillars 是自动驾驶实时检测的首选方案——精度和速度达到了最佳平衡

3D 检测的朝向角 θ 存在 180 度歧义(前后对称),需要使用朝向分类(direction classification)来解决

7实战:Open3D + NeRF 入门工作流

掌握 3D 视觉理论后,实战是最有效的学习方式。Open3D 是一个开源的 3D 数据处理库,支持点云和网格的读取、可视化、配准、重建等操作,是 3D 视觉入门的首选工具。本章节提供一个完整的实战工作流:首先使用 Open3D 读取和可视化点云数据,进行基本的预处理操作(下采样、法线估计、离群点去除);然后介绍如何使用 Instant-NGP(NVIDIA 的神经图形瞬时普适表示框架)快速训练一个 NeRF 模型。Instant-NGP 通过多分辨率哈希编码和 CUDA 加速,将 NeRF 的训练时间从数小时缩短到数秒,是目前最实用的 NeRF 实现。我们将展示从数据准备到模型训练再到新视角渲染的完整流程,让你能够快速上手 3D 视觉的两个核心技术。

python
import open3d as o3d
import numpy as np

def point_cloud_pipeline(pcd_path):
    """Open3D 点云处理完整管线"""
    # 1. 读取点云
    pcd = o3d.io.read_point_cloud(pcd_path)
    print(f"原始点云: {len(pcd.points)} 点")

    # 2. 可视化
    o3d.visualization.draw_geometries([pcd])

    # 3. 体素下采样
    pcd_down = pcd.voxel_down_sample(voxel_size=0.01)
    print(f"下采样后: {len(pcd_down.points)} 点")

    # 4. 法向量估计
    pcd_down.estimate_normals(
        search_param=o3d.geometry.KDTreeSearchParamHybrid(
            radius=0.03, max_nn=50))

    # 5. 统计离群点去除
    cl, ind = pcd_down.remove_statistical_outlier(
        nb_neighbors=30, std_ratio=2.0)
    pcd_clean = pcd_down.select_by_index(ind)
    print(f"清理后: {len(pcd_clean.points)} 点")

    # 6. ICP 配准 (如果有两个点云需要对齐)
    source = pcd_clean
    target = pcd_clean  # 实际使用时加载另一个点云

    # 初始化变换 (恒等变换)
    init_transform = np.eye(4)

    # 执行 ICP
    reg = o3d.pipelines.registration.registration_icp(
        source, target, max_correspondence_distance=0.05,
        init=init_transform,
        estimation_method=o3d.pipelines.registration
            .TransformationEstimationPointToPoint())

    print(f"ICP 适应度: {reg.fitness:.4f}")
    print(f"ICP 变换矩阵:
{reg.transformation}")

    # 7. 保存处理后的点云
    o3d.io.write_point_cloud("processed.ply", pcd_clean)
    return pcd_clean

pcd_clean = point_cloud_pipeline("input.ply")
python
import subprocess
import json
import os

def train_instant_ngp(data_dir, output_dir,
                       n_steps=30000):
    """
    使用 Instant-NGP 训练 NeRF 模型
    需要安装: https://github.com/NVlabs/instant-ngp
    """
    os.makedirs(output_dir, exist_ok=True)

    # 1. 准备 transforms.json (COLMAP 格式转 Instant-NGP 格式)
    # transforms.json 格式:
    transforms = {
        "camera_model": "OPENCV",
        "frames": [],
        "aabb_scale": 4  # 场景包围盒缩放
    }

    # 假设已有 COLMAP 输出,转换为 Instant-NGP 格式
    # 实际使用 colmap2nerf.py 脚本自动转换
    # subprocess.run(["python", "colmap2nerf.py",
    #     "--colmap_matcher", "exhaustive",
    #     "--run_colmap", "False",
    #     "--n_images", "-1"])

    # 2. 训练 NeRF
    train_cmd = [
        "./instant-ngp/build/testbed",
        "--mode", "training",
        "--scene", f"{output_dir}/transforms.json",
        "--network", "instant-ngp/configs/nerf/base.json",
        "--n_steps", str(n_steps),
        "--save_snapshot", f"{output_dir}/nerf.ingp",
        "--screenshot", f"{output_dir}/result.png",
    ]

    # 3. 渲染新视角
    render_cmd = [
        "./instant-ngp/build/render",
        "--scene", f"{output_dir}/transforms.json",
        "--load_snapshot", f"{output_dir}/nerf.ingp",
        "--n_frames", "60",
        "--output", f"{output_dir}/render_frames/",
    ]

    print(f"训练命令: {' '.join(train_cmd)}")
    print(f"渲染命令: {' '.join(render_cmd)}")

    # 实际执行 (取消注释)
    # subprocess.run(train_cmd)
    # subprocess.run(render_cmd)
    print(f"
Instant-NGP 训练完成! 约 30s 收敛到 ~30 PSNR")
    print(f"模型保存: {output_dir}/nerf.ingp")
    print(f"渲染结果: {output_dir}/result.png")
工具/框架语言核心功能适用场景

Open3D

Python/C++

点云/网格处理、可视化、配准

3D 数据预处理和可视化

Instant-NGP

C++/CUDA

NeRF 训练(数秒级)

高质量新视角合成

COLMAP

C++

SfM + MVS 完整管线

照片级 3D 重建

PyTorch3D

Python

3D 深度学习组件

3D 模型训练和研究

Kaolin

Python

3D 深度学习库 (NVIDIA)

3D GAN / 生成模型

Instant-NGP 的训练只需几十秒——准备好 transforms.json 和照片后,运行一条命令即可开始

NeRF 训练数据质量直接决定结果——照片需要覆盖目标物体的所有角度,且光照条件尽量一致

继续你的 AI 学习之旅

浏览更多 AI 知识库文章,或者探索 GitHub 上的优质 AI 项目