Files
flykhan 2536c937e3 feat: 完整中文翻译 maths-cs-ai-compendium(数学·计算机科学·AI 知识大全)
翻译自英文原版 maths-cs-ai-compendium,共 20 章全部完成。

第01章 向量 | 第02章 矩阵 | 第03章 微积分
第04章 统计学 | 第05章 概率论 | 第06章 机器学习
第07章 计算语言学 | 第08章 计算机视觉 | 第09章 音频与语音
第10章 多模态学习 | 第11章 自主系统 | 第12章 图神经网络
第13章 计算与操作系统 | 第14章 数据结构与算法
第15章 生产级软件工程 | 第16章 SIMD与GPU编程
第17章 AI推理 | 第18章 ML系统设计
第19章 应用人工智能 | 第20章 前沿人工智能

翻译说明:
- 所有数学公式 $...$ / $$...$$、代码块、图片引用完整保留
- mkdocs.yml 配置中文导航 + language: zh
- README.md 已翻译为中文(兼 docs/index.md)
- docs/ 目录包含指向各章文件的 symlink
- 约 29,000 行中文内容,排除 .cache/ 构建缓存
2026-05-03 10:23:20 +08:00

22 KiB
Raw Permalink Blame History

视频与3D视觉

视频与3D视觉将图像理解扩展到时间域和空间域。本文涵盖光流、视频分类(3D卷积网络、TimeSformer)、目标跟踪(SORT、DeepSORT)、动作识别、深度估计(单目与立体)、点云、神经辐射场(NeRF)和3D高斯泼溅。

  • 文件01-04将图像视为孤立快照。但视觉世界是连续的:物体在运动,场景在变化,深度真实存在。本文将计算机视觉扩展到时间域(视频)和空间域(3D),涵盖模型如何理解运动、跟踪目标、估计深度和重建场景。

  • 视频是一系列随时间捕获的图像(帧)。以30帧/秒计算,一段10秒的片段包含300帧。关键挑战在于建模时间维度:物体如何运动,场景如何演变,以及如何跨帧关联信息。

  • 光流估计两帧连续图像之间像素的表观运动。对于帧$t$中的每个像素,光流产生一个二维位移向量$(u, v)$,指向该像素在帧$t+1$中的位置。结果是一个与图像大小相同的稠密运动场。

两帧连续视频帧及其之间的光流场,以彩色箭头可视化显示像素运动方向和大小

  • 光流在亮度恒常性假设下计算:像素的强度在其移动时不变。如果帧$t$中位置$(x, y)$处的像素强度为$I(x, y, t)$,并在小时间间隔$\delta t$内移动了$(u, v)$
I(x + u\delta t, \, y + v\delta t, \, t + \delta t) = I(x, y, t)
  • 进行一阶泰勒展开(见第03章)并除以$\delta t$
I_x u + I_y v + I_t = 0
  • 其中$I_x, I_y$是空间梯度(Sobel算子,见文件01),$I_t$是时间梯度(相邻帧的差值)。这就是光流约束方程。一个方程,两个未知数$(u, v)$:我们需要额外的约束条件。

  • Lucas-Kanade假设光流在一个小窗口内(例如5x5像素)是恒定的。这给出了一个超定系统(25个方程,2个未知数),通过最小二乘法求解(第06章的正规方程):

\begin{bmatrix} u \\ v \end{bmatrix} = \begin{bmatrix} \sum I_x^2 & \sum I_x I_y \\ \sum I_x I_y & \sum I_y^2 \end{bmatrix}^{-1} \begin{bmatrix} -\sum I_x I_t \\ -\sum I_y I_t \end{bmatrix}
  • 这个2x2矩阵就是文件01中的结构张量(与Harris角点检测中使用的矩阵相同)。Lucas-Kanade适用于小运动,但当物体在帧间移动超过几个像素时会失效。

  • Farneback方法对每个像素邻域进行多项式展开,并估计最能解释帧间变化的位移场。它产生稠密光流(每个像素一个向量),能处理比Lucas-Kanade更大的运动。

  • 现代深度学习光流方法(FlowNet、RAFT)学习从帧对端到端预测光流。RAFTRecurrent All-Pairs Field TransformsTeed和Deng2020)计算两帧中所有像素对之间的4D相关体,并使用基于GRU的更新算子迭代优化光流估计。RAFT达到了最先进的精度,并已成为标准的光流骨干网络。

  • 双流网络Simonyan和Zisserman2014)是视频理解的早期方法。一个流处理单帧RGB图像(外观),另一个流处理光流帧的堆叠(运动)。两个流在末端融合(通过平均或拼接)。这种架构明确区分了"事物看起来像什么"与"它们如何运动"。

  • 3D卷积网络将2D卷积扩展到时间维度。3D卷积使用大小为$k \times k \times k_t$的滤波器,同时跨越空间和时间维度,直接学习时空特征。

  • C3DTran等人,2015)堆叠了3x3x3滤波器的3D卷积,展示了时间卷积可以在没有显式光流的情况下学习运动特征。代价是高昂的:3D卷积的参数和计算量是其2D对应物的$k_t$倍。

  • I3DInflated 3DCarreira和Zisserman2017)采用了一种更实用的方法:从预训练的2D CNN(如Inception或ResNet)开始,将所有2D滤波器沿时间维度"膨胀"为3D,重复权重并除以$k_t$。这将ImageNet预训练迁移到视频,同时增加了时间建模能力。一个2D的$k \times k$滤波器变为$k \times k \times k_t$的滤波器,初始化为$W_{\text{3D}}[:,:,j] = W_{\text{2D}} / k_t$,对所有时间位置$j$。

  • SlowFast网络Feichtenhofer等人,2019)使用两条并行的路径,以不同的时间分辨率运行:

    • Slow路径以低帧率(例如每16帧)处理帧,具有高空间分辨率和更多通道,捕获精细的空间细节。
    • Fast路径以高帧率(每2帧)处理帧,空间分辨率降低且通道数较少(通常为Slow路径的$1/8$),捕获快速的时间变化。
    • 侧向连接通过步长卷积将信息从Fast融合到Slow。
  • 其核心洞见是:空间和时间信息具有不同的带宽需求——物体外观变化缓慢,但运动可以很迅速。SlowFast通过设计匹配这种非对称性。

  • TimeSformerBertasius等人,2021)将Vision Transformer应用于视频。它将完整的时空注意力(代价过高:$O((T \times N)^2)$,其中$T$为帧数,$N$为每帧的块数)分解为分块注意力:每个块在时间注意(每个块在相同空间位置跨时间进行注意力)和空间注意(每个块在同一帧内跨空间进行注意力)之间交替。这使代价从$O(T^2 N^2)$降低到$O(T^2 + N^2)$。

  • VideoMAE(Tong等人,2022)将掩码自编码器思想(见文件04)扩展到视频。使用极高的掩码比例(90-95%),因为视频具有高度的时间冗余性:相邻帧看起来几乎相同,因此掩码大部分块后仍然留有足够的信息进行重建。VideoMAE在无标签视频上预训练ViT骨干网络,并迁移到下游任务。

  • 动作识别将视频片段分类为多种动作类别之一(例如"跑步"、"烹饪"、"弹吉他")。它是图像分类的视频对应任务。标准基准数据集包括Kinetics-400(400个动作类别,约30万个片段)、Something-Something174个需要时间推理的细粒度动作)和ActivityNet(200个类别,包含长时未裁剪视频)。

  • 时间动作检测超越了分类:给定一个长段未裁剪的视频,找到每个动作的开始时间、结束时间和类别。这是目标检测的时间对应任务。ActionFormer等方法使用Transformer处理时间特征并预测动作边界。

  • 视频目标跟踪在第一帧识别出特定目标后,跨帧跟踪该目标。

  • SORTSimple Online and Realtime TrackingBewley等人,2016)将检测模型(独立检测每帧中的目标)与卡尔曼滤波器(用于运动预测)和匈牙利算法(用于分配)相结合。

  • 卡尔曼滤波器为每个跟踪的目标维护一个状态估计(位置、速度、大小),并使用线性运动模型预测它在下一帧中的位置。当新的检测结果到达时,卡尔曼滤波器通过结合预测值和观测值(按各自的不确定性加权)来更新其估计。这是贝叶斯更新(第05章)在跟踪中的应用。

  • 匈牙利算法解决双线性分配问题:给定$M$个已跟踪目标和$N$个新检测结果,找到使总代价最小化的最优一对一匹配(使用文件03中的IoU距离)。未匹配的检测结果开始新的轨迹;未匹配的轨迹在宽限期后被终止。

  • DeepSORT通过添加深度外观特征扩展了SORT:每个检测到的目标经过一个小型CNN,产生一个外观嵌入(描述子向量)。匹配代价结合了IoU距离和嵌入空间中的余弦距离(第01章)。这处理了遮挡和重识别:即使一个目标在其他目标后消失数帧,其外观嵌入允许在重新出现时重新匹配。

  • ByteTrack(Zhang等人,2022)通过使用所有检测结果(包括低置信度的)来改进跟踪。大多数跟踪器会丢弃低于置信度阈值的检测结果。ByteTrack首先将高置信度检测结果与现有轨迹匹配,然后将剩余的低置信度检测结果与未匹配的轨迹匹配。这恢复了暂时被遮挡或模糊(因此检测置信度低)的目标。

  • 3D视觉恢复在2D图像投影中丢失的第三个空间维度(见文件01)。

  • 深度估计预测从相机到场景中每个点的距离。

  • 立体深度使用两个相距基线距离$b$的相机。同一个点在左右图像中出现在不同的水平位置(这个偏移称为视差$d$)。深度与视差成反比:

Z = \frac{f \cdot b}{d}
  • 其中$f$是焦距,$b$是基线距离。计算视差需要找到两个图像之间的对应点(立体匹配),这是沿水平扫描线的一维搜索(因为相机水平对齐,3D中同一高度的点投影到两幅图像的同一行)。

  • 单目深度估计从单张图像预测深度,这本质上是病态问题(无限多个3D场景可以产生相同的2D图像)。然而,人类利用相对大小、纹理梯度、遮挡和大气雾霾等线索毫不费力地做到这一点。深度学习网络从训练数据中学习这些线索。

  • MiDaSDepth Anything等模型从单张图像预测相对深度图(排序哪些物体更近)。它们使用尺度不变损失在各种数据集上训练,尽管理论上存在歧义,但仍能产生非常准确的结果。

  • 点云是3D点$(x, y, z)$的集合,可选地带有颜色或其他属性,由LiDAR传感器或立体重建捕获。与图像不同,点云是无序且不规则间隔的。

  • PointNet(Qi等人,2017)通过独立地对每个点应用共享MLP,然后使用最大池化聚合(这是置换不变的,解决了排序问题),直接处理点云。**PointNet++**增加了层次化分组,以捕获多尺度的局部结构。

  • 神经辐射场(NeRFMildenhall等人,2020)将3D场景表示为一个连续函数,将3D位置$(x, y, z)$和视角方向$(\theta, \phi)$映射到颜色$(r, g, b)$和密度$\sigma$。该函数由一个MLP参数化:

F_\theta: (x, y, z, \theta, \phi) \to (r, g, b, \sigma)
  • 为了渲染一个像素,从相机穿过该像素向场景投射一条射线。沿射线采样点,MLP预测每个点的颜色和密度。像素颜色通过体渲染计算:沿射线按密度加权积分颜色:
C(\mathbf{r}) = \int_{t_n}^{t_f} T(t) \cdot \sigma(\mathbf{r}(t)) \cdot \mathbf{c}(\mathbf{r}(t), \mathbf{d}) \, dt
  • 其中$T(t) = \exp(-\int_{t_n}^{t} \sigma(\mathbf{r}(s)) , ds)$是累积透射率(已吸收的光总量)。在实际中,该积分通过沿射线采样$N$个点并求和来近似:
\hat{C} = \sum_{i=1}^{N} T_i \cdot (1 - \exp(-\sigma_i \delta_i)) \cdot c_i
  • NeRF通过最小化渲染像素与一组带位姿照片的真实像素之间的MSE来训练。训练完成后,NeRF可以从任何相机位置渲染逼真的新视角。其局限性在于速度:渲染需要对MLP进行数百万次评估(每个像素每个采样点一次),这使得实时渲染变得困难。

  • 3D高斯泼溅(Kerbl等人,2023)通过将场景表示为3D高斯原语的集合(而非连续的体积函数)来解决NeRF的速度限制。每个高斯原语有一个3D位置(均值)、一个3D协方差矩阵(控制形状和朝向)、不透明度及颜色(表示为球谐函数以实现视角相关效果)。

  • 渲染将每个3D高斯投影到图像平面(产生一个2D高斯"泼溅"),按深度排序,并使用alpha混合从前往后合成。这是一个在GPU上实时运行的栅格化过程(100+ FPS),比NeRF的射线步进快几个数量级。高斯泼溅达到或超过NeRF的质量,同时实现实时渲染。

  • SLAM(同时定位与地图构建)是在未知环境中构建地图同时跟踪相机自身位置的问题。这是机器人、自动驾驶和AR的基础。

  • 视觉里程计通过跨图像跟踪特征来估计相机从一帧到另一帧的运动。特征点(SIFT、ORB,见文件01)在连续帧之间匹配,并利用这些匹配关系通过本质矩阵(编码两视图之间的几何关系,由文件01的内参和外参推导)估计相机的旋转和平移。

  • 基于特征的SLAM通过维护持久地图来扩展视觉里程计。ORB-SLAMMur-Artal等人,2015)是使用最广泛的基于特征的SLAM系统。它有三个并行线程:

    1. 跟踪:将每帧中的ORB特征与地图匹配,使用PnPPerspective-n-Point)和RANSAC估计相机位姿。
    2. 局部建图:从匹配的特征三角化新的地图点,通过光束法平差(最小化所有观察到每个点的视图的重投影误差)优化其位置。
    3. 闭环检测:检测相机何时重新访问先前建图的区域(使用视觉词袋),然后通过全局优化地图来校正累积漂移。
  • LiDAR SLAM使用来自LiDAR传感器的3D点云替代(或补充)相机图像。LiDAR提供直接的深度测量,使几何估计更鲁棒,但硬件成本更高。LOAMLiDAR Odometry and Mapping)等方法使用迭代最近点(ICP)配准来对齐连续扫描之间的点云。

  • 视觉-惯性SLAM融合相机数据与IMU(加速度计+陀螺仪)的测量结果。IMU提供高频的旋转和加速度估计,弥补相机帧之间的间隙,并处理快速运动或临时视觉特征丢失的情况。

  • VR/AR应用是计算机视觉最苛刻的消费者之一。

  • 姿态估计从图像中确定人体(或面部、手部)的位置和朝向。身体姿态通常表示为一组2D或3D关键点位置(关节点:肩膀、肘部、手腕、髋部、膝盖、脚踝)。OpenPoseMediaPipe等模型使用热图回归预测这些关键点:对于每个关节点,模型输出一个热图,其中峰值指示关节点的位置。

  • 自上而下的方法首先使用边界框检测器(见文件03)检测人物,然后在每个框内估计姿态。自下而上的方法首先检测图像中的所有关键点,然后使用部位亲和场(编码连接关节点之间关联的向量场)将它们分组为个体。

  • 场景重建从传感器数据构建环境的3D模型。在AR中,这使得可以将虚拟物体放置在真实表面上、遮挡真实物体后面的虚拟物体以及投射虚拟阴影。实时场景重建方法(如ARKit和ARCore中基于深度传感器的系统)构建环境的稀疏网格,并随着用户移动而更新。

  • VR中的实时渲染约束极为苛刻:双眼需要独立渲染90+ FPS(以避免晕动症),从头部位移到显示更新的延迟需低于20毫秒。注视点渲染(仅渲染用户注视位置的高分辨率,使用眼动追踪)和重投影(基于新头部位姿扭曲上一帧以填补下一帧渲染间隙)等技术对于满足这些约束至关重要。

  • 实时神经渲染(3D高斯泼溅)、鲁棒跟踪(视觉-惯性SLAM)和高效姿态估计的融合,正使逼真的交互式AR/VR体验变得越来越可行。

编程任务(使用CoLab或Notebook

  1. 从头实现Lucas-Kanade光流算法。计算一个方块向右移动的两帧合成图像之间的光流。
import jax.numpy as jnp
import matplotlib.pyplot as plt

def lucas_kanade(frame1, frame2, window_size=5):
    """Lucas-Kanade光流。"""
    # 计算梯度
    Ix = jnp.zeros_like(frame1)
    Iy = jnp.zeros_like(frame1)
    It = frame2 - frame1

    # Sobel风格梯度
    Ix = Ix.at[1:-1, :].set((frame1[2:, :] - frame1[:-2, :]) / 2)
    Iy = Iy.at[:, 1:-1].set((frame1[:, 2:] - frame1[:, :-2]) / 2)

    H, W = frame1.shape
    half_w = window_size // 2
    u = jnp.zeros_like(frame1)
    v = jnp.zeros_like(frame1)

    for i in range(half_w, H - half_w):
        for j in range(half_w, W - half_w):
            Ix_win = Ix[i-half_w:i+half_w+1, j-half_w:j+half_w+1].ravel()
            Iy_win = Iy[i-half_w:i+half_w+1, j-half_w:j+half_w+1].ravel()
            It_win = It[i-half_w:i+half_w+1, j-half_w:j+half_w+1].ravel()

            A = jnp.stack([Ix_win, Iy_win], axis=1)
            ATA = A.T @ A
            ATb = -A.T @ It_win

            # 检查系统是否良态
            det = ATA[0,0] * ATA[1,1] - ATA[0,1] * ATA[1,0]
            if jnp.abs(det) > 1e-6:
                flow = jnp.linalg.solve(ATA, ATb)
                u = u.at[i, j].set(flow[0])
                v = v.at[i, j].set(flow[1])

    return u, v

# 创建两帧:一个向右移动的白色方块
frame1 = jnp.zeros((64, 64))
frame1 = frame1.at[20:40, 15:35].set(1.0)

frame2 = jnp.zeros((64, 64))
frame2 = frame2.at[20:40, 20:40].set(1.0)  # 向右移动5个像素

u, v = lucas_kanade(frame1, frame2, window_size=7)

# 可视化
fig, axes = plt.subplots(1, 3, figsize=(14, 4))
axes[0].imshow(frame1, cmap='gray'); axes[0].set_title('帧1'); axes[0].axis('off')
axes[1].imshow(frame2, cmap='gray'); axes[1].set_title('帧2'); axes[1].axis('off')

# 光流的箭矢图(为清晰起见降采样)
step = 4
Y, X = jnp.mgrid[0:64:step, 0:64:step]
axes[2].imshow(frame1, cmap='gray', alpha=0.5)
axes[2].quiver(X, Y, u[::step, ::step], v[::step, ::step],
               color='#e74c3c', scale=50, width=0.005)
axes[2].set_title('光流'); axes[2].axis('off')

plt.tight_layout(); plt.show()

# 检查运动区域的平均光流
region_u = u[20:40, 15:35]
print(f"物体区域的平均水平光流: {region_u[region_u != 0].mean():.2f} 像素")
  1. 实现一个用于2D目标跟踪的简单卡尔曼滤波器。模拟一个带噪声的轨迹,并展示卡尔曼滤波器如何平滑估计。
import jax
import jax.numpy as jnp
import matplotlib.pyplot as plt

def kalman_predict(x, P, F, Q):
    """卡尔曼滤波器预测步骤。"""
    x_pred = F @ x
    P_pred = F @ P @ F.T + Q
    return x_pred, P_pred

def kalman_update(x_pred, P_pred, z, H, R):
    """卡尔曼滤波器更新步骤。"""
    y = z - H @ x_pred                        # 创新
    S = H @ P_pred @ H.T + R                  # 创新协方差
    K = P_pred @ H.T @ jnp.linalg.inv(S)      # 卡尔曼增益
    x_updated = x_pred + K @ y
    P_updated = (jnp.eye(len(x_pred)) - K @ H) @ P_pred
    return x_updated, P_updated

# 状态: [x, y, vx, vy]
dt = 1.0
F = jnp.array([[1, 0, dt, 0],    # 状态转移
                [0, 1, 0, dt],
                [0, 0, 1, 0],
                [0, 0, 0, 1]])
H = jnp.array([[1, 0, 0, 0],     # 观测:测量 x, y
                [0, 1, 0, 0]])
Q = jnp.eye(4) * 0.01            # 过程噪声
R = jnp.eye(2) * 4.0             # 测量噪声(有噪声的检测器)

# 模拟真实轨迹:圆周运动
n_steps = 50
t = jnp.linspace(0, 2 * jnp.pi, n_steps)
true_x = 10 * jnp.cos(t) + 20
true_y = 10 * jnp.sin(t) + 20

# 带噪声的观测
key = jax.random.PRNGKey(42)
noise = jax.random.normal(key, (n_steps, 2)) * 2.0
obs_x = true_x + noise[:, 0]
obs_y = true_y + noise[:, 1]

# 运行卡尔曼滤波器
x = jnp.array([obs_x[0], obs_y[0], 0.0, 0.0])  # 初始状态
P = jnp.eye(4) * 10.0                             # 初始不确定性

kalman_x, kalman_y = [], []
for i in range(n_steps):
    x, P = kalman_predict(x, P, F, Q)
    z = jnp.array([obs_x[i], obs_y[i]])
    x, P = kalman_update(x, P, z, H, R)
    kalman_x.append(x[0])
    kalman_y.append(x[1])

kalman_x = jnp.array(kalman_x)
kalman_y = jnp.array(kalman_y)

# 可视化
plt.figure(figsize=(8, 8))
plt.plot(true_x, true_y, 'k-', linewidth=2, label='真实轨迹')
plt.scatter(obs_x, obs_y, c='#e74c3c', s=20, alpha=0.5, label='带噪声的观测')
plt.plot(kalman_x, kalman_y, '#3498db', linewidth=2, label='卡尔曼滤波')
plt.legend(); plt.grid(alpha=0.3)
plt.title('卡尔曼滤波跟踪')
plt.xlabel('x'); plt.ylabel('y')
plt.axis('equal'); plt.show()

obs_error = jnp.mean(jnp.sqrt((obs_x - true_x)**2 + (obs_y - true_y)**2))
kalman_error = jnp.mean(jnp.sqrt((kalman_x - true_x)**2 + (kalman_y - true_y)**2))
print(f"观测RMSE: {obs_error:.2f}")
print(f"卡尔曼滤波RMSE: {kalman_error:.2f}")
print(f"误差降低: {(1 - kalman_error/obs_error) * 100:.1f}%")
  1. 实现一个简化的NeRF风格体渲染管线。通过一个简单的3D场景(已知颜色和密度的球体)投射射线,并沿每条射线积分来渲染图像。
import jax
import jax.numpy as jnp
import matplotlib.pyplot as plt

def render_ray(origin, direction, spheres, n_samples=64, t_near=1.0, t_far=6.0):
    """穿过球体场景对单条射线进行体渲染。"""
    t_vals = jnp.linspace(t_near, t_far, n_samples)
    deltas = jnp.concatenate([jnp.diff(t_vals), jnp.array([1e-3])])

    colour = jnp.zeros(3)
    transmittance = 1.0

    for i in range(n_samples):
        point = origin + t_vals[i] * direction

        # 计算该点的密度和颜色
        density = 0.0
        point_colour = jnp.zeros(3)

        for center, radius, col, sigma in spheres:
            dist = jnp.linalg.norm(point - center)
            # 软球体:密度随距表面的距离指数衰减
            d = jnp.exp(-jnp.maximum(0, dist - radius) * sigma) * sigma
            density += d
            point_colour += d * jnp.array(col)

        # 按总密度归一化颜色
        point_colour = jnp.where(density > 1e-6, point_colour / density, point_colour)

        # 体渲染方程
        alpha = 1.0 - jnp.exp(-density * deltas[i])
        colour += transmittance * alpha * point_colour
        transmittance *= (1.0 - alpha)

    return colour

# 场景:三个彩色球体
spheres = [
    (jnp.array([0.0, 0.0, 4.0]), 0.8, [1.0, 0.2, 0.2], 5.0),   # 红色
    (jnp.array([1.5, 0.5, 5.0]), 0.6, [0.2, 1.0, 0.2], 5.0),   # 绿色
    (jnp.array([-1.0, -0.5, 3.5]), 0.5, [0.2, 0.2, 1.0], 5.0), # 蓝色
]

# 相机设置
img_h, img_w = 64, 64
focal = 60.0
origin = jnp.array([0.0, 0.0, 0.0])

image = jnp.zeros((img_h, img_w, 3))
for i in range(img_h):
    for j in range(img_w):
        # 计算射线方向
        px = (j - img_w / 2) / focal
        py = -(i - img_h / 2) / focal
        direction = jnp.array([px, py, 1.0])
        direction = direction / jnp.linalg.norm(direction)

        colour = render_ray(origin, direction, spheres)
        image = image.at[i, j].set(jnp.clip(colour, 0, 1))

plt.figure(figsize=(6, 6))
plt.imshow(image)
plt.title('NeRF风格体渲染\n(3个球体)')
plt.axis('off')
plt.tight_layout(); plt.show()
print(f"图像形状: {image.shape}")
print(f"渲染了 {img_h * img_w} 条射线,每条 {64} 个采样点")