四角变换后的最小包围视口,为什么不能只把宽高取整?

问题与范围

四角透视变换、文档矫正或 corner pin 都会先把源图四个角投到新的位置,再为结果分配一个足以容纳它的输出纹理。常见做法是求浮点包围盒、把宽和高取整,然后继续渲染。

这个做法在对称、偶数尺寸的例子里经常看似正确。但当源图是奇数尺寸,或者某一角只向外移动了一小段小数距离时,它可能只把"尺寸"变成整数,却没有保证原始的最小边界和最大边界仍落在输出内。后果通常不是明显的崩溃,而是某一边被裁掉一行像素、单像素细节消失,或不同输入尺寸下出现难解释的半像素偏移。

本文讨论单帧 GPU 图像几何变换中的输出视口合同;不讨论相机、视频编解码、设备级性能或完整照片编辑产品。

先给结论

  • 最小包围视口的目标是覆盖四个变换后的边界,不是让宽高看起来是整数。
  • 在左上为原点的像素坐标中,最小边界应向下取整、最大边界应向上取整;这等价于对完整矩形向外积分。
  • 若内部使用中心坐标系,应先平移到左上像素坐标取整,再平移回来,不能只改宽高。
  • 输出纹理的 resize 与 shader 的视口 uniform 必须来自同一份解析结果;否则尺寸和采样范围会各自正确、组合后却不一致。

为什么只取整宽高不够

设四角变换得到的水平范围是:

text 复制代码
minX = -9.2, maxX = 11.0

范围的长度是 20.2。把宽度取为 21 并不能单独说明新矩形是否仍覆盖 -9.2 ... 11.0:还要知道它的原点如何确定。若代码在中心坐标中根据新宽度重建视口,浮点误差、半像素原点或非对称外扩都可能把某一侧向内推。

因此应先保护边界。把范围转换到源图左上角为原点的像素坐标后,分别执行:

text 复制代码
pixelMinX = floor(pixelMinX)
pixelMinY = floor(pixelMinY)
pixelMaxX = ceil(pixelMaxX)
pixelMaxY = ceil(pixelMaxY)

这样得到的整数矩形一定覆盖原始浮点矩形。宽和高只是这四个已确定边界的差值,而不是先被取整、再反过来猜边界的输入。

中心坐标系中的处理顺序

很多渲染路径把图像中心当作原点:一个宽为 W 的图像在水平方向上位于 [-W/2, W/2]。这种表示适合变换矩阵,但不适合直接做像素边界取整。

可靠的处理顺序是:

text 复制代码
变换后的四个角
  → 浮点包围盒
  → 平移到左上像素坐标
  → min 用 floor、max 用 ceil
  → 得到整数像素视口
  → 平移回中心坐标

这里的关键不是使用哪一个 API,而是取整发生在哪个坐标系。对一个 5 × 3 的 identity 变换,正确结果仍应是 5 × 3;不能因为中心原点带来半像素表示,就悄悄扩成 6 × 4 或移动输出范围。

一个视口,只解析一次

输出尺寸与实际采样范围必须说同一种语言。假如 resize 根据向外取整后的范围分配纹理,而 fragment shader 仍使用未取整的浮点视口作为 uniform,那么目标纹理的像素网格与变换后的可见区域就失去了一致性,边缘问题仍会出现。

工程上更稳妥的方式是先解析一份 resolvedViewport:resize 用它决定输出大小,fragment uniform 也用它提供 viewport origin 和 size。之后的采样、透明边缘处理和输出尺寸都围绕同一份范围工作。旋转、裁剪、镜像、非均匀缩放等需要整数输出纹理的路径,也能沿用同样的判断方法。

如何测试这类问题

只看一张正常尺寸截图很难发现视口边界错误。至少应覆盖:

  • 奇数尺寸的 identity 变换,防止中心坐标转换引入额外像素;
  • 某一角向外移动小数距离,确认最小包围范围向外扩展;
  • 单像素输入,确认几何路径不会让唯一的可见 texel 消失;
  • 输出尺寸与 shader 所用视口来自同一份解析结果。

这些测试把"没有裁边"从肉眼判断变成可复现的合同。它们同样适用于需要根据几何变换重新分配纹理的其他 GPU 管线。

本机验证与参考实现

Harbeth 当前的 RenderQuadTransform 提供 alignsViewportToPixels 配置,默认关闭。启用后,仅在 minimumEnclosing 模式下,它会先把浮点包围盒平移到源图左上像素坐标,再对整个矩形向外取整,最后转换回中心坐标。resize 与 fragment uniform 都消费同一个解析后的视口。

在 Harbeth 358e1de6、macOS arm64e 上,本轮执行:

text 复制代码
swift test --filter Transform3DTests

共执行 17 项、0 失败,耗时 0.208 秒。定向覆盖包括 5 × 3 奇数尺寸 identity、小数外扩的 20 × 10 → 21 × 11、单像素 GPU 可见性,以及 3D、四角变换和矫正的相关路径。这是当前本机源码与定向测试证据,不代表全设备性能、完整照片校正流程或公开发版已经完成。

Kakapos 在媒体链路中可以把 Harbeth 用作逐帧处理后端;不过时间线、预览、导出与媒体生命周期仍由 Kakapos 自己负责。视口边界的像素合同属于逐帧图像处理层,不应被媒体编排层吞掉。

小结

最小包围视口的关键不是"宽高取整",而是"整数化之后仍完整覆盖原始边界"。当坐标系以中心为原点时,先回到左上像素坐标,分别向外取整最小和最大边界,再回到中心坐标,能让奇数尺寸、透视外扩和边缘 texel 都有明确的可测试行为。

如果你的图像管线同时包含透视变换、GPU 采样和动态输出尺寸,这个细节值得单独立约。许多看似随机的边缘裁切和单像素消失,最终都来自同一类边界取整错误。

源码与参考实现

本文基于本轮本机源码与定向测试,不代表已经公开发版或完成全设备验证。

相关推荐
茶底世界之下1 小时前
羽毛颜色已经很鲜艳,还要继续加色吗?用 Awaking 试一次减饱和
前端·ios·运营
天神哥哥啊3 小时前
cocos联调注意事项-IOS
ios·cocoa·xcode
终端安全笔记4 小时前
配置锁状态那一栏查的是什么:报告里几项各自指向什么风险
android·前端·安全·ios
Carson带你学Android5 小时前
OpenAI Dots 登陆手机端:手机上派任务,AI 在云端写代码
android·ios
老李IT笔记17 小时前
设备指纹由哪几个字段拼出来:换一块主板会变成新设备
ios·智能手机
andyweike17 小时前
iOS App 测试
ios·测试·ad hoc·分发
茶底世界之下1 天前
预乘 Alpha 图层合成为什么会越叠越暗?
架构·swift
茶底世界之下1 天前
花瓣要更柔和,还是更有反差?用 Awaking 做一次对比度取舍
ios
Anhty2 天前
2026最新免费手机音频处理工具 !!
android·功能测试·ios·智能手机·音视频