- 计算机视觉
- 人工智能
- 深度学习
- 图像处理
【免费下载链接】kornia
🐍 Geometric Computer Vision Library for Spatial AI
depth_from_plane_equation是 Kornia 几何模块中通过平面方程(Hessian 形式n · X = d)逐像素计算相机坐标系深度z的核心函数。本篇文章围绕该函数的一处关键缺陷修复展开:当光线与平面精确平行(掠射,grazing ray)时,旧的近奇点保护逻辑会因torch.sign(0) == 0而失效,导致深度返回inf。读完本文,你将理解该 Bug 的数学成因、比较形式(comparison form)修复的取舍、为何不能改用torch.copysign、半精度(float16)下的eps约束,以及该修复如何在 ONNX 导出覆盖测试中被固定下来。
一、问题背景:掠射光线是真实场景而非边缘输入
在基于平面假设的深度估计、地面平面深度推理与自动驾驶感知中,经常需要把某个平面(例如地面)的方程与相机光心发出的光线求交,从而为每个像素解出深度。depth_from_plane_equation做的正是这件事,其核心公式为:
denom = ray · n depth = d / denom当denom趋于零(即光线与平面趋于平行)时,深度会趋于无穷大。在地面平面场景中,地平线上的每一个像素都对应一条与地面近似平行、甚至严格平行的光线——这是非常普遍的输入,而不是数值测试才会构造的极端用例。
原 changelog 条目(changelog.d/+migration-045.fixed.md)明确指出:
depth_from_plane_equationreturns a finite depth for a ray exactly parallel to the plane.
修复前,near-singular 保护逻辑为eps * torch.sign(denom)。问题在于torch.sign在输入恰好为零时返回 0,因此在精确奇点处,epsilon 被乘法抵消(eps * 0 = 0),分母仍然以零参与除法,最终返回inf。
二、修复前的缺陷根因:sign 在零处的空洞
从源码实现可以复现这条缺陷路径。函数位于 kornia/geometry/depth.py,旧版保护逻辑大致是:
# 修复前的形式(有缺陷) denom = torch.sum(rays * plane_normals_exp, dim=-1) zero_mask = torch.abs(denom) < eps denom = torch.where(zero_mask, eps * torch.sign(denom), denom) depth = plane_offsets / denom关键缺陷链条如下:
torch.sign(denom)在denom == 0时返回0,这是torch.sign的语义——零的符号是零;- 因此在掩码命中(
denom == 0)时,保护值变成eps * 0 = 0,等于没有保护; - 除法
plane_offsets / 0直接返回inf,破坏了"对精确平行光线返回有限深度"的契约。
这正是 issue #4280 报告并最终由 PR #4348 修复的问题(见测试中的回归标注,tests/geometry/test_depth.py)。
三、修复方案:用比较取符号,堵住零点的空洞
修复后的逻辑改用比较来为 epsilon 选择符号(kornia/geometry/depth.py):
denom = torch.sum(rays * plane_normals_exp, dim=-1) # (B, N) denom_abs = torch.abs(denom) zero_mask = denom_abs < eps # 用比较选择符号:denom < 0 取 -eps,否则取 +eps signed_eps = torch.where(denom < 0, torch.full_like(denom, -eps), torch.full_like(denom, eps)) denom = torch.where(zero_mask, signed_eps, denom) depth = plane_offsets / denom # (B, N)该方案有两个关键性质:
- 在零处无空洞:
denom < 0是布尔比较,对denom == 0的结果是False,因此精确为零的奇点会被替换为正的eps。测试明确断言了这一点:对(0, ±1, 0)两种符号的法向量,返回的都是+2 / 1e-8 = +2e8——零处没有符号可保留,因此固定为正号(tests/geometry/test_depth.py)。 - 保持已有小非零分母的符号:对于绝对值小于
eps但非零的denom,比较形式保留了denom原有的符号(denom < 0取负、否则取正),与旧逻辑中eps * sign(denom)对非零输入的行为一致。测试test_small_denominators_keep_their_sign用±eps/4构造一对符号相反的小分母,断言两个深度幅值相等、符号相反(tests/geometry/test_depth.py)。
此外,掩码分支保证了掩码之外的元素完全不受影响("Nothing outside the mask is touched"),因此正视平面(fronto-parallel,n = (0, 0, 1), d = 2)上每个像素深度精确等于 2 的约定不受扰动,convention 测试以atol=0.0, rtol=0.0严格验证(tests/geometry/test_depth.py)。
四、为何不用torch.copysign:ONNX 导出约束决定了实现形式
从工程实现上看,torch.copysign(eps, denom)也能读出同样的语义,甚至代码更简洁。但函数注释(kornia/geometry/depth.py)与 ONNX 导出测试(tests/onnx/test_export_coverage.py)共同给出了明确的否决理由:
- legacy ONNX exporter没有
aten::copysign算子; - dynamo ONNX exporter在把
torch.copysign分解为prims.signbit后,同样缺少对应的 ONNX 函数。
也就是说,torch.copysign在两条导出路径上都不可导出。而depth_from_plane_equation是 Kornia文档化的 ONNX 导出面的一部分:
- 它出现在 docs/source/geometry.depth.rst 的
autofunction列表中; - 它被收录在导出支持探测用例集 docs/export_support/cases_geomB.py 中;
tests/onnx/test_export_coverage.py中专门构造了"与平面精确平行的光线"作为输入(plane_normals = [[0, 1, 0]],主点光线(0, 0, 1)与之点积精确为零),确保被导出的正是掠射分支本身,并用onnx.checker+ ONNX Runtime 推理与 eager 模式逐位比对(_assert_same,rtol/atol 均为 1e-4)。
因此,比较形式torch.where(denom < 0, -eps, eps)被固定下来,成为该函数的正式实现——它既是数值上正确的,也是导出友好的。
五、半精度陷阱:默认eps=1e-8低于 float16 分辨率
changelog 条目末尾还强调了一个 dtype 层面的边界:
The finite-depth promise is bounded by the dtype: the default
eps=1e-8is below float16 resolution, so half-precision callers must pass a representableeps.
原因可以从两处测试的注释中得到印证:
- 默认
eps=1e-8在 float16 下会舍入为零,导致denom_abs < eps永远不成立,掩码形同虚设; - 即使掩码生效,
2 / 1e-8 = 2e8也超出 float16 的有限表示范围(float16 最大约 65504),结果依然是inf。
因此测试中统一采用eps = max(1e-8, float(torch.finfo(dtype).eps))来为 float16 提供可表示的 epsilon(tests/geometry/test_depth.py),并在若干 convention 测试中对 float16 显式pytest.skip(例如正常分量2.38e-9在 float16 下下溢为零,输入不再是"小但非零"的分母,见 tests/geometry/test_depth.py)。
实践建议:当调用方使用 float16/bfloat16 张量时,应显式传入eps(例如eps=1e-3或float(torch.finfo(dtype).eps)),并注意被钳制后的深度量级d / eps是否落在该 dtype 的有限范围内。float32/float64 下默认值1e-8可直接使用。
六、函数完整契约与使用方式
结合源码 docstring(kornia/geometry/depth.py)与测试类TestDepthFromPlaneEquation(tests/geometry/test_depth.py),该函数的使用契约如下:
6.1 参数与形状
| 参数 | 形状 | 说明 |
|---|---|---|
plane_normals | (B, 3) | 相机坐标系下的平面法向量,Hessian 形式n · X = d |
plane_offsets | (B, 1) | 平面偏移量d |
points_uv | (B, N, 2) | 整数像素中心的像素坐标;函数内部用camera_matrix自行归一化,输入的是像素而非归一化坐标 |
camera_matrix | (B, 3, 3) | 相机内参矩阵 |
eps | float = 1e-8 | 数值稳定性 epsilon |
返回(B, N)的深度张量——每个像素一个深度值,是深度列表而非深度图。
6.2 约定(Convention)
- 平面以 Hessian 形式给出:
n = (0, 0, 1)且d = 2表示平面z = 2,其上每个像素深度均为 2; - 结果是被测像素的相机坐标系
z值; - 射线-平面点积在
(-eps, eps)内被掩码钳制到±eps(保留分母符号),精确为零的分母替换为正eps;掩码之外的值完全不动。
6.3 使用示例
import torch import kornia B, N = 1, 4 plane_normals = torch.tensor([[0.0, 0.0, 1.0]]) # (B, 3):正视平面 z = 2 plane_offsets = torch.tensor([[2.0]]) # (B, 1) points_uv = torch.tensor([[[0., 0.], [1., 0.], [0., 1.], [1., 1.]]]) # (B, N, 2) camera_matrix = torch.eye(3).unsqueeze(0) # (B, 3, 3) depth = kornia.geometry.depth.depth_from_plane_equation( plane_normals, plane_offsets, points_uv, camera_matrix ) # depth == [[2., 2., 2., 2.]],与 test_simple 的期望一致float16 场景请显式传入可表示的eps:
eps = max(1e-8, float(torch.finfo(torch.float16).eps)) depth = kornia.geometry.depth.depth_from_plane_equation( plane_normals.half(), plane_offsets.half(), points_uv.half(), camera_matrix.half(), eps=eps )6.4 测试覆盖总览
TestDepthFromPlaneEquation(tests/geometry/test_depth.py)从多个维度锁定行为:
test_smoke/test_shapes/test_shapes_broadcast:形状与广播((B, N)输出、(1, 3)法向量与(B, N, 2)像素的广播);test_simple:正视平面逐像素深度精确为 2;test_grazing_ray_is_finite:精确平行光线返回有限深度;test_small_denominators_keep_their_sign:小非零分母保留符号;test_gradcheck:float64 下对法向量、偏移、像素坐标与内参矩阵求梯度,验证可微性;test_convention_*:Hessian 约定、钳制行为与奇点回归(#4280)。
七、小结
depth_from_plane_equation的这次修复(#4280 / #4348)是一个典型的"数值奇点 + 导出兼容性"双重约束下的工程决策:
- 根因:
eps * torch.sign(denom)在denom == 0时被 sign 的零值抵消,保护失效返回inf; - 修复:改用
torch.where(denom < 0, -eps, eps)比较形式,堵住零点空洞并保留非零分母的符号; - 约束:因
torch.copysign在 legacy 与 dynamo 两条 ONNX 导出路径上均不可用,比较形式被写入文档化导出面并固定在 tests/onnx/test_export_coverage.py 中; - 边界:默认
eps=1e-8低于 float16 分辨率,半精度调用方必须传入可表示的eps,并注意钳制深度d/eps的量级是否超出该 dtype 的有限范围。
对于在地面平面感知、深度补全或平面拟合类任务中使用kornia.geometry.depth.depth_from_plane_equation的开发者,理解这条修复路径既能帮助你规避半精度下的inf陷阱,也能解释为何该函数在导出为 ONNX 后依然保持相同的数值行为。
- 计算机视觉
- 人工智能
- 深度学习
- 图像处理
【免费下载链接】kornia
🐍 Geometric Computer Vision Library for Spatial AI
相关推荐
Kornia 修复 `depth_from_plane_equation` 掠射射线数值稳定性:从 `inf` 到有限深度
Kornia 修复 depth_from_plane_equation 掠射射线数值稳定性:从 inf 到有限深度 本文聚焦 Kornia 中 kornia.g
计算机视觉深度学习人工智能图像处理Kornia 相机去畸变数值稳定性修复解析:`undistort_points_kannala_brandt` 的 float16 精度与梯度修复
Kornia 相机去畸变数值稳定性修复解析: undistort_points_kannala_brandt 的 float16 精度与梯度修复 本篇技术指南围
计算机视觉人工智能深度学习图像处理Kornia 修复 `distance_transform` 全零输入下 NaN 梯度:级联卷积距离变换的数值稳定性解析
Kornia 修复 distance_transform 全零输入下 NaN 梯度:级联卷积距离变换的数值稳定性解析 kornia.contrib.distan
计算机视觉深度学习人工智能图像处理
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考