Kornia 修复空批次几何变换的 ZeroDivisionError:`transform_points` 与 `PinholeCamera.project` 的 empty-batch 语义
Kornia 修复空批次几何变换的 ZeroDivisionErrortransform_points与PinholeCamera.project的 empty-batch 语义【免费下载链接】kornia Geometric Computer Vision Library for Spatial AI项目地址: https://gitcode.com/gh_mirrors/ko/kornia导读本文围绕 Kornia 变更记录 changelog.d/4487.fixed.md对应上游 issue #4466展开剖析transform_points在“空变换批次 非空点轴”这一组合输入下引发的ZeroDivisionError崩溃及其修复方案。文章从底层0 // 0的成因出发结合 transform_points 源码、PinholeCamera.project 实现 与回归测试讲清 Kornia 对空批次empty batch输入的统一约定空输入应返回同形状的空输出而不是崩溃。读完你将掌握这一修复的完整来龙去脉以及在实际几何/相机管线中如何规避同类问题。一、问题背景Kornia 的空批次empty batch约定在 Kornia 的几何管线中张量普遍以批处理batched方式组织。例如 3D 点云形状为(B, N, 3)对应的齐次变换矩阵形状为(B, 4, 4)。当某些输入批次恰好为空batch size 为 0即B 0时Kornia 的既定语义是空输入进空输出出empty-in / empty-out对空集执行变换结果仍为空集且形状与输入保持一致这一约定对批处理数据加载、滑动窗口推理、无标注样本的预处理等场景至关重要——例如一张图像裁剪块image chip内没有任何标注点时变换逻辑不应中断整个管线。本变更记录所修复的正是这一约定被破坏的边角场景变换批次为空(0, 4, 4)而点张量的点轴非空(0, 5, 3)。注意这里点张量的 batch 轴也为 0但由于点轴N5非空旧实现会在广播broadcast变换矩阵时触发崩溃。二、崩溃根因展开变换矩阵时的0 // 0在修复前transform_points的核心逻辑依赖torch.repeat_interleave将变换矩阵广播到与点张量相同的 batch 数量。相关实现见 kornia/geometry/linalg.py广播部分位于repeats points_1.shape[0] // trans_01.shape[0] trans_01 torch.repeat_interleave(trans_01, repeatsint(repeats), dim0)当输入为trans_01.shape (0, 4, 4)空变换批次且points_1.shape (0, 5, 3)时points_1.shape[0] // trans_01.shape[0]即0 // 0Python 中整数0 // 0直接抛出ZeroDivisionError于是整个transform_points在真正开始矩阵乘法之前就崩溃了。值得注意的是0 // 0只有在两个 batch 维度都为 0时才会触发。若只有变换批次为空、点张量 batch 也完全为空(0, 0, 3)除数与被除数同为 0同样崩溃而若变换批次非空如(1, 4, 4)0 // 1 0反而能正常返回空结果。这正是该 bug 表现“不一致”的原因——同样是“空输入”有的形状能通过、有的形状崩溃。三、修复方案提前短路与除零保护本次修复对transform_points做了两处关键改动见 kornia/geometry/linalg.py1. 空点轴提前返回# No points to transform (e.g. an image chip with no annotations): transforming an empty set # yields the same empty set. Return early — the reshape below cannot infer -1 from a # 0-element tensor, so this also avoids a spurious crash on valid empty inputs. if points_1.shape[-2] 0: return points_1当点张量的点轴N 0时直接原样返回。除了规避0 // 0这一早退还有另一层原因后续points_1.reshape(-1, ...)需要从 0 元素张量推断-1维度-1无法从空张量推断本身也会引发运行时错误。提前返回同时规避了这一隐患。2. 除法加保护repeats points_1.shape[0] // trans_01.shape[0] if trans_01.shape[0] 0 else 0当变换批次为空trans_01.shape[0] 0时不再执行除法而是直接将repeats置为 0。此时torch.repeat_interleave对空张量的 repeat 次数为 0结果仍为空张量随后的torch.bmm在空输入下自然产出空输出最终返回与输入形状一致的空结果。3. 修复后的完整行为修复后transform_points的输入输出对应关系为变换矩阵形状点张量形状修复前行为修复后行为(B, 4, 4)B0(B, N, 3)N0正常变换正常变换不变(B, 4, 4)(B, 0, 3)reshape 崩溃或0//0原样返回空点集(0, 4, 4)(0, 5, 3)ZeroDivisionError0//0返回(0, 5, 3)空结果(1, 4, 4)(0, 5, 3)正常返回空结果正常返回空结果不变另外需要说明transform_points的形状校验linalg.py#L203-L210要求两个输入 batch 大小相等或变换批次为 1且最后一维相差 1如 4x4 变换对应 3 维点这些校验在修复前后保持一致未做放宽。四、PinholeCamera.project的连带修复ZeroDivisionError不仅影响transform_points本身还通过调用链传染给了PinholeCamera.project。从 kornia/geometry/camera/pinhole.py#L382-L419 可以看到其投影实现def project(self, point_3d: torch.Tensor) - torch.Tensor: if len(point_3d.shape) 2: raise ValueError(fInput must be at least a 2D tensor. Got {point_3d.shape}) P self.intrinsics self.extrinsics return convert_points_from_homogeneous(transform_points(P, point_3d))project将内参矩阵与外参矩阵相乘得到投影矩阵P形状(B, 4, 4)后直接委托给transform_points完成齐次变换与透视除法。因此当相机批次为空内参/外参为(0, 4, 4)、点张量为(0, 3)时旧实现恰好能“侥幸”通过而当点张量为(0, N, 3)N0时transform_points内部的0 // 0崩溃被直接继承project也随之抛ZeroDivisionError。修复transform_points后PinholeCamera.project对空相机批次 任意点轴形状的空点集均能正确返回空结果无需单独改动project本身——这正是“在底层修复、在上层受益”的典型回归修复模式。从源码结构看kornia.geometry.pose中的同名transform_points入口以及convert_points_from_homogeneous的调用路径也遵循同样的 empty-in/empty-out 约定。五、回归测试如何验证修复本次修复配有专门的回归测试覆盖了此前崩溃的精确输入组合1.transform_points空批次测试见 tests/geometry/test_linalg.py#L56-L79pytest.mark.parametrize(num_dims, [2, 3]) pytest.mark.parametrize(points_shape, [(0, 1), (0, 5)]) def test_transform_points_empty_batch_with_points(self, num_dims, points_shape, device, dtype): # An empty transform batch with a non-empty point axis divided 0 // 0 while expanding the # transforms (kornia#4466); it must return the same empty shape as a B1 transform does. points torch.zeros(*points_shape, num_dims, devicedevice, dtypedtype) empty_trans torch.eye(num_dims 1, devicedevice, dtypedtype).expand(0, -1, -1) single_trans torch.eye(num_dims 1, devicedevice, dtypedtype)[None] out kgl.transform_points(empty_trans, points) assert out.shape points.shape assert out.dtype dtype assert out.shape kgl.transform_points(single_trans, points).shape该测试的断言设计值得注意空批次的结果形状必须与B1变换作用于同一点集的结果形状一致即空输入的结果语义应与“只有一个变换”时的广播结果对齐而不是凭空多出维度。2.PinholeCamera.project继承修复测试见 tests/geometry/camera/test_pinhole.py#L923-L931def test_project_an_empty_batch_with_a_point_axis_4466(self, device, dtype): # Regression for kornia#4466: an empty camera batch projected (0, 3) points but raised # ZeroDivisionError on (0, N, 3), inside transform_points. trans torch.eye(4, devicedevice, dtypedtype).expand(0, 4, 4) empty torch.zeros(0, devicedevice, dtypedtype) camera kornia.geometry.camera.PinholeCamera(trans, trans, empty, empty) assert camera.project(torch.zeros(0, 3, devicedevice, dtypedtype)).shape (0, 2) assert camera.project(torch.zeros(0, 1, 3, devicedevice, dtypedtype)).shape (0, 1, 2)测试同时验证了(0, 3)与(0, 1, 3)两种点形状前者是修复前就能通过的路径后者是修复前崩溃的路径两者修复后均返回正确形状的空张量。此外 tests/geometry/camera/test_pinhole.py#L900-L921 还验证了空相机批次在构造、形状与 dtype 上的一致性以及kornia.geometry.camera.project_points自由函数遵循同样的空进空出约定。六、对开发者代码的影响与建议从本次修复可以提炼出几条对 Kornia 使用者有价值的实践升级后行为变化是“变好”而非“破坏”修复只影响此前抛异常的输入组合。如果你的代码曾依赖捕获ZeroDivisionError来识别空批次升级后需改为直接检查输出形状如out.shape[0] 0或提前判断输入是否为空因为异常不再抛出。空批次语义是 Kornia 的通用约定从 transform_points 到 PinholeCamera.project再到PinholeCamera构造器的空批次支持见 test_pinhole.py#L900-L931 中对kornia#4281的回归Kornia 正在系统性地统一“空输入 → 空输出”的约定。在自己封装几何工具时建议对 batch 维度为 0 的输入做短路返回避免在0 // 0、reshape(-1)等位置踩坑。形状校验仍然有效修复并未放宽输入合法性检查。transform_points仍要求 batch 相等或变换批次为 1linalg.py#L205-L208project仍要求点张量至少为 2 维pinhole.py#L416-L417非法形状依旧会以ValueError明确拒绝便于尽早发现调用方错误。小结变更 4487.fixed.md 修复的是一个小而典型的边界缺陷transform_points在空变换批次配合非空点轴时因0 // 0抛出ZeroDivisionError并通过调用链波及PinholeCamera.project。修复通过“空点轴提前返回 除零保护”双保险让空批次输入在任意形状组合下都稳定返回同形状空结果并配套了 test_linalg.py 与 test_pinhole.py 两组回归测试锁定行为。这一修复也再次印证了 Kornia 几何模块“空进空出”的设计约定为批处理数据管线的健壮性提供了保障。【免费下载链接】kornia Geometric Computer Vision Library for Spatial AI项目地址: https://gitcode.com/gh_mirrors/ko/kornia创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考