视频与三维视觉


文档摘要

视频与三维视觉 视频与三维视觉(video and 3D vision)把图像理解扩展到时间域和空间域。本文件涵盖光流、视频分类(3D CNN、TimeSformer)、目标跟踪(SORT、DeepSORT)、动作识别、深度估计(单目和立体)、点云、NeRF 和三维高斯泼溅(3D Gaussian splatting)。 文件 01 到 04 把图像当作孤立的快照来处理。但视觉世界是连续的:物体会动、场景会变、深度客观存在。本文件把计算机视觉扩展到时间域(视频)和空间域(三维),讨论模型如何理解运动、跟踪物体、估计深度、重建场景。 视频(video)是随时间捕获的一串图像(帧)。30 帧每秒时,一段 10 秒的片段包含 300 帧。

视频与三维视觉

视频与三维视觉(video and 3D vision)把图像理解扩展到时间域和空间域。本文件涵盖光流、视频分类(3D CNN、TimeSformer)、目标跟踪(SORT、DeepSORT)、动作识别、深度估计(单目和立体)、点云、NeRF 和三维高斯泼溅(3D Gaussian splatting)。

  • 文件 01 到 04 把图像当作孤立的快照来处理。但视觉世界是连续的:物体会动、场景会变、深度客观存在。本文件把计算机视觉扩展到时间域(视频)和空间域(三维),讨论模型如何理解运动、跟踪物体、估计深度、重建场景。

  • 视频(video)是随时间捕获的一串图像(帧)。30 帧每秒时,一段 10 秒的片段包含 300 帧。关键挑战在于建模时间维度:物体怎么运动、场景怎么演化、我们怎么把跨帧的信息关联起来?

  • **光流(optical flow)**估计两帧之间像素的表观运动。对第 t 帧的每个像素,光流产出一个二维位移向量 (u, v),指向该像素在第 t+1 帧的位置。结果是一个和图像等大的密集运动场。

连续两帧视频以及它们之间的光流场,用彩色箭头显示像素运动的方向和幅度

  • 光流是在**亮度恒定假设(brightness constancy assumption)**下计算的:一个像素在移动时亮度不变。如果第 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)
  • 做一阶泰勒展开(第 3 章),再除以 \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 个未知量),用最小二乘求解(第 6 章的正规方程):

\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)从成对帧中端到端地学习预测光流。RAFT(Recurrent All-Pairs Field Transforms,Teed 和 Deng,2020)计算两帧所有像素对之间的四维相关体,并用一个基于 GRU 的更新算子迭代地精炼光流估计。RAFT 达到了当时的最佳精度,已成为标准的光流主干。

  • 双流网络(two-stream networks)(Simonyan 和 Zisserman,2014)是视频理解的早期方法。一条流处理单张 RGB 帧(外观),另一条流处理一叠光流帧(运动)。两条流在最后融合(取平均或拼接)。这种架构显式地把"东西长什么样"和"它怎么动"分开了。

  • **三维卷积网络(3D Convolutional Networks)**把二维卷积扩展到时间维度。三维卷积用一个大小为 k \times k \times k_t、同时跨越空间和时间的滤波器,直接学习时空特征。

  • C3D(Tran 等,2015)堆叠了 3x3x3 的三维卷积,表明时间卷积能学到运动特征,无需显式的光流。代价很高:三维卷积比对应的二维卷积多 k_t 倍的参数和计算量。

  • I3D(Inflated 3D,Carreira 和 Zisserman,2017)采取了更实用的做法:从一个预训练的二维 CNN(比如 Inception 或 ResNet)出发,把所有二维滤波器沿时间维度重复权重再除以 k_t 来"膨胀"成三维。这样把 ImageNet 预训练迁移到视频,同时加上了时间建模。一个二维的 k \times k 滤波器变成 k \times k \times k_t 的滤波器,初始化为对所有时间位置 jW_{\text{3D}}[:,:,j] = W_{\text{2D}} / k_t

  • SlowFast 网络(Feichtenhofer 等,2019)用两条在不同时间分辨率上运行的并行通路:

    • **慢通路(Slow pathway)**以低帧率(比如每 16 帧取一帧)处理,空间分辨率高、通道多,捕捉精细的空间细节。
    • **快通路(Fast pathway)**以高帧率(每 2 帧取一帧)处理,空间分辨率降低、通道更少(通常是慢通路的 1/8),捕捉快速的时间变化。
    • 横向连接通过带步长的卷积把信息从快通路融合到慢通路。
  • 这里的洞见是:空间信息和时间信息对带宽的需求不同——物体外观变化慢,但运动可能很快。SlowFast 在设计上就匹配了这种不对称。

  • TimeSformer(Bertasius 等,2021)把视觉 Transformer 应用到视频上。它把全时空注意力(代价高得不可接受:T 帧、每帧 N 个图像块时是 O((T \times N)^2))分解为分离注意力(divided attention):每个块在时间注意力(每个图像块在同一空间位置上跨时间注意)和空间注意力(每个图像块在同一帧内跨空间注意)之间交替。这把代价从 O(T^2 N^2) 降到 O(T^2 + N^2)

  • VideoMAE(Tong 等,2022)把掩码自编码器的想法(文件 04)扩展到视频。它用极高的掩码比例(90-95%),因为视频有很大的时间冗余:相邻帧几乎一样,所以掩掉大部分图像块仍然留下足够信息做重建。VideoMAE 在无标注视频上预训练一个 ViT 主干,再迁移到下游任务。

  • **动作识别(action recognition)**把一段视频片段分到多个动作类别之一(比如"跑步""做饭""弹吉他")。它是图像分类的视频对应物。标准基准包括 Kinetics-400(400 个动作类、约 30 万段片段)、Something-Something(174 个需要时间推理的细粒度动作)和 ActivityNet(200 个类别、长且未剪辑的视频)。

  • **时序动作检测(temporal action detection)**不只是分类:给定一段长的未剪辑视频,找出每个动作的开始时间、结束时间和类别。这是目标检测的时间对应物。ActionFinder 等方法用 Transformer 处理时间特征并预测动作边界。

  • **视频目标跟踪(video object tracking)**指物体在第一帧被识别后,在后续帧中持续跟随它。

  • SORT(Simple Online and Realtime Tracking,Bewley 等,2016)把一个检测模型(在每帧独立地检测物体)和用于运动预测的卡尔曼滤波器(Kalman filter)、用于分配的**匈牙利算法(Hungarian algorithm)**结合起来。

  • 卡尔曼滤波器为每个被跟踪物体维护一个状态估计(位置、速度、大小),并用一个线性运动模型预测它下一帧的位置。当新的检测到来时,卡尔曼滤波器把预测和观测按各自的不确定性加权融合来更新估计。这就是第 5 章贝叶斯更新在跟踪上的应用。

  • 匈牙利算法解决这个二分分配问题:给定 M 个被跟踪物体和 N 个新检测,找到总代价最小的一对一匹配(用文件 03 的 IoU 距离)。未匹配上的检测会启动新轨迹;未匹配上的轨迹在宽限一段时间后被终止。

  • DeepSORT 在 SORT 上加了一个深度外观特征:每个检测到的物体过一个小型 CNN,产生一个外观嵌入(描述子向量)。匹配代价把 IoU 距离和嵌入空间里的余弦距离(第 1 章)结合起来。这能处理遮挡和重识别:即便物体在其他物体后面消失几帧,它的外观嵌入也能在它再次出现时重新匹配。

  • ByteTrack(Zhang 等,2022)通过利用每一个检测(包括低置信度的那些)来改进跟踪。大多数跟踪器会丢弃低于置信度阈值的检测。ByteTrack 先把高置信度检测匹配到已有轨迹,再把剩下的低置信度检测匹配到未匹配的轨迹。这样能找回那些被短暂遮挡或模糊(因此检测置信度低)的物体。

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

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

  • 立体深度(stereo depth)用两个间距为已知基线 b 的相机。同一个点在左右图中的水平位置不同(这个偏移叫视差 disparity d)。深度和视差成反比:

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

  • **单目深度估计(monocular depth estimation)**从单张图像预测深度,这本质上是病态问题(无穷多个三维场景都能产生同一张二维图像)。然而人类凭相对大小、纹理梯度、遮挡、大气雾霾这些线索毫不费力就能做到。深度网络从训练数据里学到这些线索。

  • MiDaSDepth Anything 等模型从单张图像预测相对深度图(排出哪个物体更近)。它们在多样数据集上用尺度不变损失训练,尽管理论上有歧义,仍产生了相当准确的结果。

  • **点云(point cloud)**是三维点 (x, y, z) 的集合,可附带颜色或其他属性,由 LiDAR 传感器或立体重建捕获。和图像不同,点云是无序且不规则分布的。

  • PointNet(Qi 等,2017)直接处理点云:对每个点独立地应用共享 MLP,再用最大池化聚合(最大池化对置换不变,从而解决了排序问题)。PointNet++ 加上层次化分组,在多个尺度上捕捉局部结构。

  • 神经辐射场(Neural Radiance Fields,NeRF)(Mildenhall 等,2020)把一个三维场景表示为一个连续函数:它把三维位置 (x, y, z) 和观察方向 (\theta, \phi) 映射到颜色 (r, g, b) 和密度 \sigma。这个函数由一个 MLP 参数化:

F_\theta: (x, y, z, \theta, \phi) \to (r, g, b, \sigma)
  • 要渲染一个像素,就从相机经过该像素向场景投射一条射线。沿射线采样若干点,MLP 在每个点上预测颜色和密度。像素颜色由**体渲染(volume rendering)**计算:沿射线对颜色按密度加权积分:
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 Gaussian Splatting)(Kerbl 等,2023)通过把场景表示成一组三维高斯原语(而不是一个连续的体积函数)来解决 NeRF 的速度问题。每个高斯有一个三维位置(均值)、一个三维协方差矩阵(控制形状和朝向)、不透明度以及颜色(用球谐函数表示,以产生视角相关的效果)。

  • 渲染时把每个三维高斯投影到成像平面(产生一个二维高斯"泼溅"),按深度排序,再用 alpha 混合从前向后合成。这是一种在 GPU 上实时运行(100+ FPS)的光栅化过程,比 NeRF 的射线步进快好几个数量级。高斯泼溅在质量上匹敌甚至超过 NeRF,同时支持实时渲染。

  • **SLAM(Simultaneous Localisation and Mapping,同时定位与建图)**是在未知环境中构建地图的同时跟踪相机在其中的位置的问题。它对机器人、自动驾驶和 AR 都至关重要。

  • **视觉里程计(visual odometry)通过跨图像跟踪特征来逐帧估计相机运动。特征点(文件 01 的 SIFT、ORB)在连续帧之间匹配,再从这些对应关系用本质矩阵(essential matrix)**估计相机的旋转和平移(本质矩阵编码了两视图之间的几何关系,由文件 01 的内参和外参导出)。

  • **基于特征的 SLAM(feature-based SLAM)**通过维护一张持久的地图来扩展视觉里程计。ORB-SLAM(Mur-Artal 等,2015)是用得最广的基于特征的 SLAM 系统。它有三个并行线程:

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

  • **视觉-惯性 SLAM(Visual-Inertial SLAM)**把相机数据和 IMU(加速度计 + 陀螺仪)的测量融合起来。IMU 提供高频的旋转和加速度估计,填补相机帧之间的空隙,并能应对快速运动或短暂的视觉特征丢失。

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

  • **姿态估计(pose estimation)**从图像确定人体(或人脸、或手)的位置和朝向。身体姿态通常表示成一组二维或三维关键点位置(关节:肩、肘、腕、髋、膝、踝)。OpenPoseMediaPipe 等模型用热力图回归来预测这些关键点:对每个关节,模型输出一张热力图,峰值指示关节位置。

  • **自顶向下(top-down)**方法先用一个边界框检测器(文件 03)检测人,再在每个框内估计姿态。**自底向上(bottom-up)**方法先检测图像中所有的关键点,再用部位亲和场(编码相连关节之间关联的向量场)把它们分组成个体。

  • **场景重建(scene reconstruction)**从传感器数据构建环境的三维模型。在 AR 中,这让我们能把虚拟物体放到真实表面上、用真实物体遮挡虚拟物体、以及投射虚拟阴影。实时场景重建方法(如 ARKit 和 ARCore 中基于深度传感器的系统)构建一张随用户移动而更新的环境稀疏网格。

  • VR 里实时渲染的约束极其苛刻:双眼需要分别以 90+ FPS 渲染(避免晕动症),从头部运动到显示更新的延迟要低于 20 毫秒。注视点渲染(foveated rendering)(只在用户注视的地方用高分辨率渲染,借助眼动追踪)和重投影(reprojection)(根据新的头部位姿扭曲上一帧来填补下一帧渲染时的空隙)等技术对于满足这些约束至关重要。

  • 实时神经渲染(三维高斯泼溅)、鲁棒跟踪(视觉-惯性 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('Frame 1'); axes[0].axis('off') axes[1].imshow(frame2, cmap='gray'); axes[1].set_title('Frame 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('Optical Flow'); axes[2].axis('off') plt.tight_layout(); plt.show() # 检查运动区域里的平均光流 region_u = u[20:40, 15:35] print(f"Average horizontal flow in object region: {region_u[region_u != 0].mean():.2f} pixels")
  1. 实现一个简单的二维卡尔曼滤波器用于目标跟踪。模拟一条带噪声的轨迹,并展示卡尔曼滤波器如何平滑估计。
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='Ground Truth') plt.scatter(obs_x, obs_y, c='#e74c3c', s=20, alpha=0.5, label='Noisy Observations') plt.plot(kalman_x, kalman_y, '#3498db', linewidth=2, label='Kalman Filter') plt.legend(); plt.grid(alpha=0.3) plt.title('Kalman Filter Tracking') 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"Observation RMSE: {obs_error:.2f}") print(f"Kalman filter RMSE: {kalman_error:.2f}") print(f"Error reduction: {(1 - kalman_error/obs_error) * 100:.1f}%")
  1. 实现一个简化的 NeRF 风格体渲染流水线。在一个简单的三维场景(已知颜色和密度的球体)中投射射线,并沿每条射线积分来渲染一张图像。
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-style Volume Rendering\n(3 spheres)') plt.axis('off') plt.tight_layout(); plt.show() print(f"Image shape: {image.shape}") print(f"Rendered {img_h * img_w} rays with 64 samples each")

作者与出处
原作者: HenryNdubuaku
来源:HenryNdubuaku
许可证:Apache-2.0
整理: 灏天文库整理
由灏天文库结构化整理,提供目录导航、全文检索与在线阅读,便于系统化学习
发布者: 作者: HenryNdubuaku 转发
评论区 (0)
U