news 2026/9/23 19:02:04

Kornia 深度平面方程 `depth_from_plane_equation` 掠射光线数值稳定性修复解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Kornia 深度平面方程 `depth_from_plane_equation` 掠射光线数值稳定性修复解析
  • 计算机视觉
  • 人工智能
  • 深度学习
  • 图像处理

【免费下载链接】kornia

🐍 Geometric Computer Vision Library for Spatial AI

项目地址:https://gitcode.com/gh_mirrors/ko/kornia
点击查看免费下载

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

关键缺陷链条如下:

  1. torch.sign(denom)denom == 0时返回0,这是torch.sign的语义——零的符号是零;
  2. 因此在掩码命中(denom == 0)时,保护值变成eps * 0 = 0,等于没有保护;
  3. 除法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 defaulteps=1e-8is below float16 resolution, so half-precision callers must pass a representableeps.

原因可以从两处测试的注释中得到印证:

  1. 默认eps=1e-8在 float16 下会舍入为零,导致denom_abs < eps永远不成立,掩码形同虚设;
  2. 即使掩码生效,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-3float(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)相机内参矩阵
epsfloat = 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)是一个典型的"数值奇点 + 导出兼容性"双重约束下的工程决策:

  1. 根因eps * torch.sign(denom)denom == 0时被 sign 的零值抵消,保护失效返回inf
  2. 修复:改用torch.where(denom < 0, -eps, eps)比较形式,堵住零点空洞并保留非零分母的符号;
  3. 约束:因torch.copysign在 legacy 与 dynamo 两条 ONNX 导出路径上均不可用,比较形式被写入文档化导出面并固定在 tests/onnx/test_export_coverage.py 中;
  4. 边界:默认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

项目地址:https://gitcode.com/gh_mirrors/ko/kornia
点击查看免费下载

相关推荐

上一篇:突破性能瓶颈:egui与WebGPU集成的高性能渲染方案
下一篇:在 Android 应用中集成 Lightweight Charts™:Android Wrapper 安装、配置与数据填充实战指南

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/23 19:00:19

SCMA瑞利信道下的PM-MPA检测器MATLAB仿真与避坑指南

简介&#xff1a;面向5G非正交多址技术研究者的MATLAB实现包&#xff0c;聚焦SCMA系统下的PM-MPA检测算法。资源基于消息传递与最大后验概率思想&#xff0c;提供瑞利信道环境中的完整仿真链路&#xff0c;适合通信工程高年级学生、算法工程师及科研人员参考复现。包内共5个文件…

作者头像 李华
网站建设 2026/9/23 18:55:57

DeepSeek企业知识库微调实战:从文档清洗到LoRA部署

简介&#xff1a;本资源是一份面向企业AI工程师与知识系统架构师的实战指南&#xff0c;聚焦DeepSeek大模型在跨行业知识库建设中的落地路径与微调方法论&#xff0c;解决传统知识管理系统语义理解弱、数据孤岛难打通、个性化服务缺失等共性难题。文档共24页PDF&#xff0c;结构…

作者头像 李华
网站建设 2026/9/23 18:55:55

DeepSeek+MIDI实现AI作曲:从乐谱生成到工程落地的完整指南

简介&#xff1a;面向AI音乐创作开发者的实战指南&#xff0c;聚焦DeepSeek与MIDI技术的融合应用&#xff0c;系统讲解从MIDI数据采集、清洗、特征提取&#xff0c;到模型架构设计、训练调优&#xff0c;再到音乐参数生成与MIDI文件输出的完整链路。文档共26页&#xff0c;以“…

作者头像 李华
网站建设 2026/9/23 18:54:03

微信小程序农产品销售平台毕设:SSM+MySQL源码部署与二次开发指南

简介&#xff1a;这是一套面向高校计算机相关专业毕业设计的微信小程序农产品销售平台完整项目源码&#xff0c;采用微信开发者工具配合Java、SSM框架与MySQL数据库实现&#xff0c;适合正在准备毕设或需要小程序全栈练手的同学参考。压缩包共1195个文件&#xff0c;约14.86MB&…

作者头像 李华