Kornia LAF 边界校验修复解析:`laf_is_inside_image` 像素范围约定与 `ScaleSpaceDetector` 检测行为修正

原创2026-09-23 17:30:02162 阅读
文章标签:计算机视觉人工智能深度学习图像处理

Kornia LAF 边界校验修复解析:laf_is_inside_image 像素范围约定与 ScaleSpaceDetector 检测行为修正

导读

本文围绕 Kornia 本地特征(Local Affine Frame,LAF)模块中的一次关键 bug 修复展开:laf_is_inside_image 原先以 (w, h) 而非 (w - 1, h - 1) 作为图像有效范围的上界,导致边界判断上下界不对称——下界拒绝 x = 0 左侧,上界却放行 x = w(超出最后一个有效列 w - 1 整整一个像素)。本次修复(issue #4064)统一了 Kornia 的像素坐标约定,并连带修正了 ScaleSpaceDetector._process_octave 中内联检查的角间距常数错误。读完本文,你将掌握 Kornia 中 LAF 边界校验的准确语义、0 .. w-1 像素约定的来龙去脉,以及这类底层修复对检测器输出的实际影响,并可直接定位到 kornia/feature/laf.py 与 kornia/feature/scale_space_detector.py 的对应实现。

问题背景:边界校验的“一个像素之遥”

laf_is_inside_image 是 Kornia 本地特征模块中判断“LAF 是否完全位于图像内部”的核心函数,其作用是返回一个掩码,标记哪些 LAF(局部仿射框)完全在图像内、因而有效。它被广泛用于特征检测的后处理、特征可视化以及测试中,例如 tests/feature/test_laf.py 中对该函数的形状、边界阈值与 JIT 导出均有覆盖。

该函数的基本思路是:把每个 LAF 采样为一组边界点(默认 12 个点,含中心点),然后检查这些点的 x/y 坐标是否全部落在图像有效范围内。问题恰恰出在“有效范围”的定义上。

修复前:(w, h) 造成的非对称边界

修复前的实现将边界上限设为图像的宽高 w 和 h 本身:

  • 下界:拒绝任何 x < 0 的点;
  • 上界:却接受 x = w 的点。

而一张宽度为 w 的图像的像素列索引是 0 .. w-1。也就是说,上界放行了“最后一个有效列再往右一个像素”的位置,而下界却严格执行从 0 开始。这种不对称意味着:同样越界一个像素,靠左的被判为“外部”,靠右的却被判为“内部”,边界行为取决于方向。

修复后:统一采用 0 .. w-1 像素约定

本次修复(changelog 条目 +migration-129.fixed.md)将上界修正为 w - 1 - border 与 h - 1 - border,从而与 Kornia 全库的像素坐标约定保持一致。这个约定在源码中有多处明确证据:

  • get_laf_center 的文档注释明确记录了这一约定;
  • normalize_laf / denormalize_laf 早已使用 W - 1 与 H - 1 作为归一化因子。见 kornia/feature/laf.py 中 denormalize_laf 的说明:“The convention is that center of 5-pixel image (coordinates from 0 to 4) is 2, and not 2.5”,即一个 5 像素宽的图像(坐标 0..4)的中心是 2 而非 2.5——这正是“有效坐标只到 w-1”这一约定的直观体现。

修复后的实现位于 kornia/feature/laf.py:

def laf_is_inside_image(laf: torch.Tensor, images: torch.Tensor, border: int = 0) -> torch.Tensor:
    """Check if the LAF is touching or partly outside the image boundary.

    Returns the mask of LAFs, which are fully inside the image, i.e. valid.
    """
    KORNIA_CHECK_LAF(laf)
    _, _, h, w = images.size()
    pts = laf_to_boundary_points(laf, 12)
    # Valid pixel coordinates run 0 .. w-1 and 0 .. h-1, matching the convention documented on
    # `get_laf_center` and the `w - 1` / `h - 1` extent used by `normalize_laf` / `denormalize_laf`.
    x_max = float(w - 1) - border
    y_max = float(h - 1) - border
    good_lafs_mask = (pts[..., 0] >= border) * (pts[..., 0] <= x_max) * (pts[..., 1] >= border) * (pts[..., 1] <= y_max)
    # `.all` rather than `.min` on the bool mask: ONNX ReduceMin has no bool overload.
    return good_lafs_mask.all(dim=2)

关键细节解读:

  1. 边界点采样:laf_to_boundary_points(laf, 12) 将每个 LAF 转换为 12 个边界点(含中心点),形状为 (B, N, 12, 2)。采样实现见 kornia/feature/laf.py:在单位圆上以 linspace(0, 2π, n_pts - 1) 取角度采样 (sin, cos),先放入原点(中心)作为方向指示,再通过 torch.bmm 与 LAF 矩阵相乘得到实际边界点坐标。
  2. border 参数:border 表示额外收缩的边界宽度,默认 0。校验时下界为 >= border、上界为 <= (size - 1) - border,即让边界检查整体向内收缩 border 个像素,常用于给检测留出安全边距。
  3. 聚合方式:所有边界点必须全部满足条件(.all(dim=2)),任一越界即判为外部。注释还说明使用 .all 而非 .min 是为了兼容 ONNX(ReduceMin 没有 bool 重载),体现了对导出友好性的考量。

行为变化:边界点恰好到达最后一个有效像素坐标(w-1、h-1)的 LAF 仍然判定为内部;只有越过最后一个有效像素的 LAF 才会被新逻辑判为外部。严格位于内部的 LAF 行为完全不变。

连带修复:ScaleSpaceDetector 内联检查的两处偏差

ScaleSpaceDetector 是 Kornia 的尺度空间特征检测器(实现于 kornia/feature/scale_space_detector.py)。为了性能,它在 _process_octave 中对各向同性 LAF(rotmat = eye(2))使用了 laf_is_inside_image 的“内联等价检查”,以规避 scale_laf(涉及 torch.cat)和 laf_to_boundary_points(涉及 linspace/sin/cos 分配与启动开销以及一次小的 host→device 拷贝)带来的每调用开销。见 kornia/feature/scale_space_detector.py:

# Inline equivalent of laf_is_inside_image(scale_laf(current_lafs, 0.5), octave[:, 0], 5)
# for isotropic LAFs (rotmat = eye(2)). ...
# For the axis-aligned isotropic case the 12-pt boundary check reduces to:
#   max x-extent = max|sin| * half_s;  max y-extent = max|cos| * half_s = half_s
half_s = current_lafs[:, :, 0, 0] * 0.5
cx = current_lafs[:, :, 0, 2]
cy = current_lafs[:, :, 1, 2]
h, w = octave.shape[3], octave.shape[4]
# Valid pixel coordinates run 0 .. w-1 / 0 .. h-1, so the upper bound is (size - 1) - border.
x_max = float(w - 1) - 5
y_max = float(h - 1) - 5
good_mask = (
    (cx - half_s * _MAX_ABS_SIN_12 >= 5)
    & (cx + half_s * _MAX_ABS_SIN_12 <= x_max)
    & (cy - half_s >= 5)
    & (cy + half_s <= y_max)
)

偏差一:上界同样用了错误的 w / h

内联检查原来也以上界 w、h 判断,因此与 laf_is_inside_image 存在同样的不对称问题。修复后上界同步改为 w - 1 - border、h - 1 - border(此处 border=5)。这一变化会产生真实的检测行为差异:以前距离右边界或下边界 1 个像素以内的检测结果,曾经能通过 border=5 的过滤保留下来,现在会被丢弃。

偏差二:max|sin| 常数的角间距错误

内联检查还需要一个“12 点边界采样中 |sin| 的最大值”常数,用于把圆的半径投影到 x 方向。原实现用 2π/11 作为角间距计算该常数,而 laf_to_boundary_points(n_pts=12) 实际采样的角间距是 linspace(0, 2π, 11) 即 k * 2π/10(11 个点等分 2π,对应 10 个间隔)。正确的最大值出现在 k=2 与 k=3 处:sin(2π/5) ≈ 0.9511。修复后的定义见 kornia/feature/scale_space_detector.py:

# Max |sin| among the 11 boundary points sampled by laf_to_boundary_points(n_pts=12):
#   angles = linspace(0, 2π, n_pts - 1) = linspace(0, 2π, 11) → k * 2π/10 for k=0..10
#   max|sin| at k=2 and k=3: sin(2π/5) ≈ 0.9511;  max|cos| at k=0: cos(0) = 1.0
_MAX_ABS_SIN_12: float = math.sin(2 * 2 * math.pi / 10)  # ≈ 0.9511

原来的 2π/11 常数会高估 x 方向的检验范围约 4%(inflating the tested x-extent by 4%),使内联检查比它所声称复现的参考实现(laf_is_inside_image)更严格。修正后两者在数值上精确一致。

x 方向放宽带来的输出变化:由于这一修正在 x 方向是“放宽”,它可能单独改变 ScaleSpaceDetector 的输出——某个 x 方向范围落在新旧两个常数之间的检测,以前被丢弃、现在被保留。changelog 明确指出:这种情况罕见(检测必须落在宽度仅 0.039 * half_s 的条带内并紧贴 x 边界),但一旦触发,它可能把图像中最强的响应提升为被选中的特征,因此并非输出无关的纯数值修正。

修复的工程价值

  1. 语义统一:laf_is_inside_image 与 normalize_laf/denormalize_laf/get_laf_center 现在共享同一套 0 .. w-1 像素坐标约定,消除了“归一化往返后边界判定不一致”的隐患。
  2. 内联与参考实现严格等价:ScaleSpaceDetector 的性能优化路径(内联检查)与其语义参考(laf_is_inside_image)在修复后完全一致,避免了两者长期漂移带来的隐性行为分歧。
  3. 测试印证:tests/feature/test_laf.py 对边界阈值做了精确的“一个像素内/外”验证(如半径 29.0 判内、29.5 判外),并验证了 border 参数的收缩效果;同时 tests/feature/test_scale_space_detector.py 覆盖检测器的整体输出行为,是回归测试的直接保障。
  4. 导出兼容性:.all 的选用兼顾了 ONNX 导出(ReduceMin 无 bool 重载),体现了 Kornia 在 docs/export_support 维度上对算子可导出性的持续要求。

小结

本次修复表面上只是把边界上界从 w/h 改为 w-1/h-1 的“一个像素”调整,但它统一了 Kornia 的像素坐标约定、修正了内联检查的采样常数误差,并真实改变了 ScaleSpaceDetector 在图像边缘处的特征筛选结果。对于在自己的管线中复现、迁移或调优 Kornia 特征检测的用户,理解这一约定(有效坐标 0 .. w-1、0 .. h-1)是正确解读检测结果与边界掩码的前提;相关实现与测试均可直接在上述源码路径中继续深入研读。

登录后查看全文
kornia