空间坐标系 · 交互讲解

从"一个点有两套坐标"讲起,一直讲到 C 臂(DiffDRR / GeoReg)和 R2-Gaussian 的相机。全部用简单形状,没有真实数据;每章都有能拖的东西和一道小题。

页面自检:计算中… 每章右上角的"自检"是打开页面时自动跑的数值断言(比如 RᵀR = I、两条代码路径落点一致),点开能看到每一条。

全篇统一的约定

整条链一张图

一个体素的值最后变成探测器上一个像素,中间要换四次坐标。点方框跳到对应章节。

体素下标(i, j, k)
affine
第 5 章
世界坐标(x, y, z) mm
w2c
第 6·9·10 章
相机坐标(x_c, y_c, z_c)
× DSD ⁄ z_c
第 7 章
探测器平面(u, v) mm
÷ 像素间距
第 7 章
像素(row, col)

反方向(像素 → 射线 → 沿射线采样体素)就是 DRR 的做法,在第 8 章。

目录

  1. 坐标系是什么
  2. 旋转矩阵:列就是新的坐标轴
  3. 齐次坐标与 4×4:补一个 1、顺序、逆
  4. 欧拉角:用 3 个数描述一个旋转
  5. 体素下标 ↔ 世界坐标
  6. 相机外参:一步步把局部坐标系变成相机
  7. 投影:相机坐标 → 像素(含 c2w → 像素完整数例)
  8. 反过来:像素 → 射线 → 体素
  9. C 臂(DiffDRR / GeoReg)
  10. R2-Gaussian 的相机与投影
  11. 对照表与常见坑
第 1 章

坐标系是什么

自检…

坐标系 = 一个原点 + 几根有方向的轴 + 单位长度。坐标不是点本身的属性,而是"从某个原点出发,沿这几根轴各走几步能到这个点"。换一个坐标系,同一个点的数字就变了,点本身一动没动。

下图有两个坐标系:固定的世界坐标系(深色 x、y),和一个可以拖动、旋转的局部坐标系(浅色 x′、y′)。蓝色的 F 是"钉"在局部坐标系上的,跟着它走。

拖白色圆圈 = 平移局部原点 O′;拖 x′ 轴尖上的红点 = 旋转;拖橙色点 P = 换一个点看坐标。
这一章只需要记住一个式子:局部坐标 p′ → 世界坐标 p 是 p = R · p′ + t。
  • R 的每一列 = 一根局部轴在世界里的方向(第 1 列是 x′,第 2 列是 y′)。
  • t = 局部原点 O′ 在世界里的位置。
  • 反过来 p′ = Rᵀ · (p − t):先减掉原点,再用转置"转回去"。
后面的 affine(体素 → 世界)、c2w(相机 → 世界)全都是这个形状,只是从 2 维变成 3 维。
第 2 章

旋转矩阵:列就是新的坐标轴

自检…

到 3D 还是同一件事。一个 3×3 旋转矩阵 R 不用背公式,只要会读它的三列:第 1 列是物体自己的 x′ 轴转完以后在世界里指向哪,第 2、3 列是 y′、z′。

所以"矩阵乘一个点"其实就是:R · (a, b, c) = a × 第 1 列 + b × 第 2 列 + c × 第 3 列——沿着三根新轴依次走 a、b、c 步。右边打开"分解",3D 里会画出这三段路。

深色长箭头 = 世界轴;浅色短箭头 = 积木自己的轴 x′ y′ z′。鼠标停在右边矩阵的某一列上,对应的轴会变粗。
要点
  • R 的列 = 旋转后的坐标轴。三列长度都是 1、两两垂直,所以 RᵀR = I,R⁻¹ = Rᵀ:求逆只要转置。
  • det R = +1 是纯旋转;把某一列取反,det 变成 −1,物体就被镜像了(F 会变成反的 F)。第 5 章 affine 的行列式带负号,说的就是这件事。
第 3 章

齐次坐标与 4×4:补一个 1,就冒出了"顺序"

自检…

第 1 章的 p = R·p′ + t 里有两种运算:旋转是乘,平移是加。只做一次变换没问题;可一旦要连着做好几次(体素 → 世界 → 配准 → 相机……),乘法和加法交替出现,公式很快就乱了。这一章按顺序讲三件事:

  1. 补一个 1:把点 (x, y) 写成 (x, y, 1),平移也就能写成矩阵,变成乘法。→ 下面 ①
  2. 好处:旋转、平移全是矩阵了,连着做几次 = 几个矩阵相乘,可以先乘成一个矩阵。
  3. 代价:矩阵乘法不能交换(A·B ≠ B·A)。"先做哪个"只体现在谁在左、谁在右,写反了照样能算、不报错。→ 下面 ②

③④ 是由此而来的两个常见错误。2D 用 3×3,3D 用 4×4,道理完全一样;为了看得清楚,这一章用 2D。

要点
  • 补上 1 以后,平移矩阵第三列的 (tₓ, t_y) 乘的正是点的那个 1,结果里就多出 +tₓ、+t_y:加法被写成了乘法。
  • 从右往左读:p′ = B·A·p 里 A 离 p 最近,先做 A。顺序不同,合起来矩阵的平移列就不同:B·A 的平移列还是 B 的 t;A·B 的平移列是 R·t(平移向量自己也被转了)。
  • 对到 C 臂:DiffDRR 的 E = [R | R·tra] 拆开就是 R·T(tra),先平移 tra,再旋转 R,所以源在 R·tra 而不在 tra(第 9 章)。
  • 自己手写合成公式时,平移列是 R₂·t₁ + t₂,不是 t₁ + t₂(③);刚体变换的逆是 [Rᵀ, −Rᵀ·t],不是 [Rᵀ, −t](④)。第 6 章 w2c 的第 4 列就是 −Rᵀ·t。
第 4 章

欧拉角:用 3 个数描述一个旋转

自检…

从前两章接过来

于是有个实际问题:怎么用 3 个数说清楚一个旋转?C 臂的主角度、副角度,DiffDRR 的 rot,优化器要调的参数,都需要这 3 个数,不可能让人去填 9 个互相牵制的矩阵元素。

欧拉角的办法:把任意旋转拆成"绕坐标轴转 3 次"。绕某一根坐标轴转,矩阵特别简单,只有一个角度:

三个乘起来就是 R。既然是连乘,第 3 章的教训马上用上:先绕哪根轴,结果不一样。所以欧拉角一定要带顺序名,比如 ZYX 表示 R = Rz(α)·Ry(β)·Rx(γ)。只说"三个角是 30°、20°、10°"而不说顺序,是还原不出旋转的。

再多一个坑:绕的是"哪根"轴?

转第二次时,物体已经转过一次了。"绕 y 轴转"指的是世界的 y 轴,还是物体自己转过之后的 y′ 轴?两种说法都有人用,而且同一个乘积 R = Rz(α)·Ry(β)·Rx(γ) 两种都能读:

两种读法的终点完全一样。最简单的例子:α = 90°、β = 90°、γ = 0,也就是 R = Rz(90°)·Ry(90°):

读法第一步第二步终点(积木三根轴指向)
外旋,从右往左绕世界 Y 转 90°绕世界 Z 转 90°x′ → 世界 −z
y′ → 世界 −x
z′ → 世界 +y
内旋,从左往右绕 Z 转 90°
此时积木的 y′ 指向世界 −x
绕积木自己的 y′(= 世界 −x)转 90°

下面先点"用这个例子",再分别点两个播放按钮:黄色粗箭头标出每一步绕的那根轴,两次播放停在同一个姿态。

注意第一步。积木还没转过的时候,它自己的轴和世界的轴是重合的,所以不管哪种读法,第一步看起来都像"绕世界轴转"。两种读法的区别从第二步才开始:内旋第二步绕的是积木转过一次之后的 y′,它已经不是世界 y 了。播放时灰色细箭头会把"另一种读法此刻会绕的轴"画出来对比。
蓝色积木 = 当前顺序的结果;半透明灰色积木 = 换成另一种乘法顺序的结果。黄色粗箭头 = 播放时正在绕的轴;灰色细箭头 = 对比用的另一根轴(外旋时画积木自己的同名轴,内旋时画世界轴)。
对到代码
  • DiffDRR:convert(rot, tra, parameterization="euler_angles", convention="ZYX") 算的正是 R = Rz(rot[0])·Ry(rot[1])·Rx(rot[2])。GeoReg 用 rot = [主角度, 0, 副角度],所以 R = Rz(主)·Rx(副)。
  • R2-Gaussian:angle2pose 里写着 "rotate … (fixed axis)",是外旋:rot = R3·R2·R1,先 R1 后 R3。第 10 章会分步播放它。
  • 万向锁:ZYX 且 β = ±90° 时,α 和 γ 绕的变成同一根轴,只剩它们的差有意义,少了一个自由度。优化器在这附近会很难受。
第 5 章

体素下标 ↔ 世界坐标

自检…

CT 体数据在文件里只是一个三维数组,下标 (i, j, k) 是整数,没有单位、也不知道头朝哪。把它放进世界里的,是 4×4 的 affine:

[x, y, z, 1]ᵀ = A · [i, j, k, 1]ᵀ = i × 第1列 + j × 第2列 + k × 第3列 + 第4列

和第 1 章一模一样:前三列 = 下标每加 1 在世界里往哪走、走几毫米;第 4 列 = 体素 (0, 0, 0) 的中心在世界里的位置。下面的格子是一个 5×6×3 的小体积,蓝色体素拼成一个 F(i 向右、j 向上时读起来是正的)。

点任意一个格子 = 选中这个体素,右边算它的世界坐标。红色小球是一个世界点,右边反算它的"连续下标"。
要点
  • det(A 的前 3×3) 的绝对值 = 一个体素的体积(mm³);负号 = 下标坐标系相对世界是镜像的。
  • 方向码(nibabel 的 aff2axcodes)说的是下标增大那一端朝哪:第 1 列是 (−20, 0, 0) → i 增大时往 −x 走 → 在 RAS 世界里 −x 是病人左侧 → 码是 L。
  • canonicalize(DiffDRR read() 默认做)只改第 4 列,把体积中心挪到世界原点;数组一个字节不动,前三列也不动。中心 = A·[(n−1)/2, 1]。
  • 下标顺序:nibabel 读出来是 arr[i, j, k];SimpleITK 和很多 CT 代码是 arr[k, j, i]。不少重建代码里的 volume_shape 也写成 (Z, Y, X)。同一个体素,两种写法下标顺序正好反过来。
第 6 章

相机外参:一步步把局部坐标系变成相机(c2w、w2c)

自检…

回顾:世界坐标系和"物体自己的"坐标系

世界坐标系:从这一章开始,所有东西都放进第 5 章 canonicalize 之后的那个世界里。原点 = 体积(头)的中心,x 向病人右、y 向前、z 向上,单位 mm。下面 3D 图里的"头"就摆在原点,三个小球标出方向:R 在 +x、A 在 +y、S 在 +z。头一直不动。

物体自己的(局部)坐标系:第 1–3 章讲过,一个局部坐标系一开始和世界系重合;把它转一下(R)、挪一下(t),它里面的点换到世界就是 p = R·p′ + t。R 的三列是它三根轴在世界里的方向(第 2 章),t 是它的原点在世界里的位置,补一个 1 拼成 4×4 就是 [R | t](第 3 章)。

相机就是这样一个局部坐标系,没有任何特殊。拍片时要回答的问题是"站在相机那里看,这个点偏右多少、偏下多少、离我多远",这三个数就是点在这个局部坐标系里的坐标。下面不直接给定义,而是从世界原点出发,一步步把一个局部坐标系转、移成相机。

六步:从世界原点到相机

步骤做什么这一步的矩阵做完以后
① 起点局部系和世界系重合,放在原点IM = I
② 转绕世界 x 转 −90°Rx(−90°)z′ 指向 +y,y′ 指向 −z
③ 转绕世界 z 转 +90°Rz(+90°)z′ 指向 −x、x′ 指向 +y、y′ 朝下:得到基准朝向 R₀,给三根轴起名 x_c / y_c / z_c
④ 转绕世界 y 转 −el(仰角)Ry(−el)视线往下压 el
⑤ 转绕世界 z 转 az(方位角)Rz(az)旋转定下来:R = Rz(az)·Ry(−el)·R₀
⑥ 移沿自己的视线往后退 dT(C),C = R·(0, 0, −d)M = [R | C],这就是 c2w

②–⑤ 每一步都只转不移,而且都绕世界的固定轴转,所以新的一步都乘在左边:M ← S·M(第 4 章外旋读法)。先抬仰角、再转方位角,是为了让每一步都绕世界的固定轴;反过来先转方位角也行,但那时仰角要绕"跟着转过去的水平轴"(第 4 章内旋读法),结果一样。

下面按 ①–⑥ 一步步点,或者点"▶ 从头播放"。3D 里浅色箭头是这个局部坐标系,半透明的是它上一步的位置,黄色粗箭头是这一步绕的世界轴。

黑球 = 局部坐标系的原点(做完第 ⑥ 步就是相机位置);浅色箭头 = 局部系三根轴;半透明箭头 = 上一步的位置;黄色粗箭头 = 这一步绕的世界轴;灰色框 = 成像平面(第 ③ 步起名以后才画)。第 ⑥ 步后:蓝色虚线圆 = 只拖方位角时相机走的路,紫色虚线弧 = 只拖仰角时走的路。

做完第 ⑥ 步:M 就叫 c2w

这个 M 做的事和第 1 章的 [R | t] 一样:把局部系里的点送到世界。局部系现在就是相机系,所以它叫 c2w(camera-to-world)。两个最直观的检验:

手算一个例子(仰角 0、方位角 0、d = 500,下面点"代入手算例子"):第 ③ 步得到 R₀,第 ④⑤ 步角度都是 0,什么都不转;第 ⑥ 步沿视线 z_c = (−1, 0, 0) 往后退 500,到 C = (500, 0, 0)。所以 x_c = (0, 1, 0)、y_c = (0, 0, −1)、z_c = (−1, 0, 0)。相机正前方 300 mm 的点:C + 300 × z_c = (200, 0, 0),正好在相机和原点之间。

w2c:反过来,世界 → 相机(投影真正要用的方向)

拍片时方向反过来:已知世界里的点,要算它在相机坐标里是多少。所以要 c2w 的逆,叫 w2c = [Rᵀ, −Rᵀ·C](第 3 章 ④ 的逆矩阵)。它做的事很直观:先减去相机位置,再分别点乘相机的三根轴。

接着上面的例子:小球 R 在世界 (52, 0, 8)。p − C = (−448, 0, 8);x_c = (0, 1, 0)·(−448, 0, 8) = 0;y_c = (0, 0, −1)·(−448, 0, 8) = −8;z_c = (−1, 0, 0)·(−448, 0, 8) = 448。意思是:R 在相机正前方 448 mm,水平居中,比画面中心高 8 mm(y_c 为负就是向上)。第 ⑥ 步之后右边会显示任意点的这套换算。

这台相机有几个自由度

要点
  • 世界原点 = 头(体积)的中心,头不动;相机是一个局部坐标系,从原点出发先转(②–⑤)、再沿视线往后退(⑥)。
  • 这几步乘起来就是 c2w:前三列是相机三根轴在世界里的方向,第 4 列是相机位置。w2c 的第 4 列 −Rᵀ·C 不是相机位置,直接当位置用是常见 bug。
  • w2c = "先减相机位置,再点乘三根相机轴"。z_c 是深度,第 7 章的透视就是拿 x_c、y_c 除以它。
第 7 章

投影:相机坐标 → 像素

自检…

换到相机坐标以后,X 光(锥形束)成像只剩一件事:相似三角形。源在原点,探测器在视线方向距离 DSD 处,一个点 (x_c, y_c, z_c) 落在探测器上的位置是

u = DSD · x_c ⁄ z_c  v = DSD · y_c ⁄ z_c  (单位 mm,探测器中心为 0)

除以 z_c 就是"近大远小"。这一步没法写成 3×3 乘法,所以才需要第 3 章说的齐次坐标:先乘矩阵,再除以最后一个分量。

① 侧面看:相似三角形

左边黑点是源,右边竖线是探测器。橙色箭头是物体,虚线是经过箭头尖的那条射线。

② 正面看:从 3D 点到像素

下面是模拟头的 DRR(每个像素 = 那条射线上密度的积分,算法在第 8 章),十字是用矩阵算出来的三个小球中心的投影。十字落在亮斑正中间,就说明矩阵算对了。右边对比两条算像素的路:相机内参矩阵 K,以及 R2-Gaussian 光栅化器用的投影矩阵 P → ndc → 像素。

鼠标移到图上看像素下标。

③ 从 c2w 到像素:代入数字一步步算

把前面几章串成一条完整的计算:给定扫描角 θ(R2-Gaussian 的 angle2pose,仰角 0)、DSO、DSD 和世界里的一个点 p,算出它落在探测器的第几行、第几列。左边俯视图(从 +z 往下看)里可以拖光源(改扫描角)、拖点 p(改它的 x、y);右边按步骤列出每一步的数字,用"显示到第几步"一次只看前几步,俯视图也会跟着一步步把线画出来。

俯视图(从 +z 往下看,单位 mm):灰圈 = 头;虚线圆 = 光源轨道(半径 DSO);黑点 = 光源 C(可拖);橙点 = 点 p(可拖);粗黑线 = 探测器;浅红 / 浅蓝箭头 = 相机的 x_c、z_c(y_c 朝下,垂直于纸面)。第 ③ 步画出 p 在 z_c、x_c 方向上的分量,第 ④ 步画出射线打到探测器的位置。
要点
  • 放大率 = DSD ⁄ DSO:等中心处 1 mm 的东西在探测器上是 DSD ⁄ DSO mm。离源越近的东西越大。
  • mm → 像素:col = u ⁄ 像素间距 + (W − 1) ⁄ 2。把这两步合在一起就是内参矩阵 K = [[DSD⁄du, 0, cx], [0, DSD⁄dv, cy], [0, 0, 1]]。
  • R2-Gaussian 的 P 矩阵把 x_c ⁄ z_c 先缩放到 ndc ∈ [−1, 1],再用 ndc2Pix(v, S) = ((v + 1)·S − 1) ⁄ 2 变成像素;那个 −1 是半像素修正,让像素中心落在整数上。两条路结果逐位相同。
  • 平行束是 DSO → ∞ 的极限:不除以 z_c,放大率恒为 1。R2-Gaussian 平行束模式里 P 直接是单位阵。
第 8 章

反过来:像素 → 射线 → 体素

自检…

第 7 章是"点 → 像素"。DRR(包括 DiffDRR)走的是反方向:对每个像素,从源画一条穿过它的射线,沿射线在体积里采样,把密度加起来。这一步会把前面几章的矩阵全部用一遍:

像素 (row, col) → 探测器上一点(相机坐标)→ c2w → 世界坐标 → 射线 → 采样点 → A⁻¹ → 体素下标 → 取密度 → 求和
灰色立方体 = 体积范围(20×20×20 个 10 mm 体素);探测器上贴的是 DRR;橙色线是选中像素的射线,线上的点是采样点。
要点
  • 像素中心在相机坐标里是 ((col − (W−1)⁄2)·du, (row − (H−1)⁄2)·dv, DSD),乘 c2w 就到世界里;射线方向 = 它减去源的位置再归一化。
  • 采样点的下标一般不是整数(A⁻¹ 算出来是 12.37 这种),所以要插值(DiffDRR 用三线性插值,也可以用 Siddon 算法精确求每个体素里的长度)。
  • 求和 Σ ρ·Δt 是线积分的近似。采样越密越接近真值;太稀时细小结构会被整个跳过。
第 9 章

C 臂(DiffDRR / GeoReg)

自检…

这一章把 DiffDRR 0.6 里真正执行的几行代码逐步复现出来(对照 DiffDRR 0.6 源码写的)。世界坐标是 RAS(x 向病人右、y 向前、z 向上),read() 已经把体积中心挪到原点(第 5 章的 canonicalize)。

rot = [主角度, 0, 副角度](弧度),tra = [0, SOD, 0] ← GeoReg 用 DICOM 角度初始化的写法 R = Rz(rot[0])·Ry(rot[1])·Rx(rot[2]) ← euler_angles_to_matrix(…, "ZYX") E = [R | R·tra] ← convert():camera_center = R·tra pose = E · reorient ← Detector.forward:reorient.compose(extrinsic) 源 = pose·(0, 0, 0)  像素 (row, col) = pose·(−delx·(col − (W−1)⁄2), −dely·(row − (H−1)⁄2), SDD)

最后一行的两个负号来自 _initialize_carm:行下标 t 取反;列下标 s 在 reverse_x_axis=True(DRR 的默认值,注释写着"obey radiologic convention")时也取反。

黑球 = 源;贴图的方框 = 探测器(贴的是这个视角的 DRR);橙线 = 中心射线。源旁边的浅色箭头是 R 的三列。
要点
  • tra = [0, SOD, 0] 是"沿相机自己的 y 轴退 SOD",不是世界里的固定位置:源 = R·tra = SOD × R 的第 2 列。LAT 和 AP 的 R 不同,同一句 tra 把源放到了完全不同的地方。
  • 不管角度怎么调:源到原点的距离恒为 SOD,中心射线恒穿过原点 → 世界原点就是 C 臂的旋转中心,头钉在那里不动。
  • 图像方向:零位时源在 +y(前方)、探测器在后方;col 增大 = 世界 −R·x̂,row 增大 = 世界 −R·ẑ。所以零位图像的左边是病人右侧、上边是头顶(放射学看片习惯)。
  • 两台 C 臂的相对位姿由 DICOM 定死(下面"两个视角"里的夹角和源间距),头相对整套系统只有 6 个自由度。
第 10 章

R2-Gaussian 的相机与投影

自检…

R2-Gaussian(用 3D 高斯做 CT 重建)用的是另一套写法:C 臂只绕世界 z 轴转,一个角度 θ 就定下相机。下面用一个简单的 R2-Gaussian 设置举例,坐标链是:

xyz_norm ∈ [−0.5, 0.5]³ × s_voxel → 世界坐标(体积中心在原点) θ → ang = 90° − θ → c2w = angle2pose(DSO, ang) ← 数据集的角度定义和 angle2pose 差 90° 且方向相反时,先换算 w2c = inv(c2w);view = w2cᵀ;full_proj = view · Pᵀ ← 行向量写法,给 CUDA 用 ndc → ndc2Pix → 渲染图 → flip(-1) ← 渲染图和投影图左右相反时,翻转一下 另一条路:手写投影公式"z 取反、绕 z 转 θ、除以 (DSO − y)" ← 需要按 3D 点去 2D 图上取值时常这样写

其中 c2w 的建法和第 6 章完全一样:从世界原点放一个局部坐标系,先转(R1、R2 得到基准朝向,R3 转到扫描角),再沿视线往后退 DSO。点下面的"▶ 分解 angle2pose"能看到同样的几步,标题里标着对应第 6 章的第几步。

虚线圆 = 源的轨道(半径 DSO,在 xy 平面);方框 = s_voxel 大小的体积;探测器上贴的是光栅化器"翻转之前"的图(物理朝向)。
要点
  • angle2pose 就是第 6 章那六步去掉第 ④ 步:R1、R2 是第 ②③ 步(得到基准朝向 R₀),R3 是第 ⑤ 步(方位角 = ang),trans 是第 ⑥ 步(沿视线往后退 d = DSO);没有仰角,所以光源只在 xy 平面的圆上转,画面上方永远是 +z。全是绕世界固定轴转,是外旋。
  • 代码里的 Camera.R / Camera.T 容易看错:dataset_readers 存的是 R = w2c[:3,:3].T,它其实就是 c2w 的旋转(列 = 相机三根轴);T = w2c[:3,3] 却是 w2c 的平移 −Rᵀ·C,不是光源位置。getWorld2View2(R, T) 再把两者拼回 w2c。
  • "R stored transposed due to glm":CUDA 里用 glm,矩阵按列存,所以 Python 这边把 w2c 转置后传进去;平移就跑到了最后一行,camera_center = view.inverse()[3, :3] 取的也是最后一行。
  • 两条投影路径逐位一致(本章自检用 500 个随机点验证)。三处"修正"缺一不可:渲染路径的 90° − θ、flip(-1),和手写投影的 z 取反。下面的开关可以一个个关掉看后果。
  • R2-Gaussian 的数据读取会把整个场景缩放到 [−1, 1]³(scene_scale = 2 ⁄ max(sVoxel),DSO、DSD、探测器尺寸一起乘,投影值也乘);像素位置不变。也可以不缩放、直接用物理尺寸,本章的例子就是这样。
第 11 章

对照表与常见坑

自检…

三套代码逐项对照

DiffDRR / GeoRegR2-Gaussian(数据读取默认)R2-Gaussian(第 10 章的例子)
世界原点体积中心(read() 的 canonicalize)体积中心(offOrigin = 0 时)体积中心(voxelize 的 center = (0, 0, 0))
长度单位mm缩放到体积落在 [−1, 1]³(scene_scale)配置里的物理尺寸(xyz_norm × s_voxel)
位姿怎么给rot(ZYX 欧拉角,弧度)+ tra,6 个数一个角度 θ,只绕 z一个角度,先换成 90° − θ
源的位置R·tra = SOD × R 的第 2 列DSO·(cos θ, sin θ, 0)DSO·(sin θ, cos θ, 0)
视线方向−R·ŷ(canonical z 经 reorient 变成 −y)相机 z_c,指向原点同左
矩阵写法列向量:E·pw2c 转置后传给 CUDA,行向量同左
像素方向row + = −R·ẑ;col + = −R·x̂(reverse_x_axis=True)col + = x_c,row + = y_c(ndc2Pix)渲染完再 flip(-1),col 反向
体数据下标torchio / nibabel:(i, j, k)按 nVoxel、sVoxel 查询网格volume_shape = (Z, Y, X)
按投影点在 2D 图上取值——grid_sample,坐标 [−1, 1],align_corners=False

四个最容易踩的坑(每个都能拖)

① 度 vs 弧度

② 行向量 vs 列向量

③ grid_sample 的 align_corners

④ 数组下标顺序

最后四道题

几何约定对照的源码:DiffDRR 0.6 detector.py / pose.py / data.py / drr.py;R2-Gaussian dataset_readers.py / graphics_utils.py / cuda_rasterizer/auxiliary.h。模拟头和所有数值都是示意,不是真实数据。