Kornia 颜色变换精度修复解析:YUV/XYZ 转换的整数输入与 float64 系数保真
Kornia 颜色变换精度修复解析:YUV/XYZ 转换的整数输入与 float64 系数保真
【免费下载链接】kornia🐍 Geometric Computer Vision Library for Spatial AI项目地址: https://gitcode.com/GitHub_Trending/ko/kornia
本篇文章围绕 Kornia 仓库中
changelog.d/+migration-108.fixed.md记录的修复展开,剖析rgb_to_yuv/yuv_to_rgb与rgb_to_xyz/xyz_to_rgb系列颜色空间转换在整数输入与float64系数处理上的行为变化。通过对照 kornia/color/yuv.py 与 kornia/color/xyz.py 的源码实现及 tests/color/test_yuv.py、tests/color/test_xyz.py 的回归测试,读者可以掌握:整数图像输入时结果 dtype 与数值尺度的正确预期、float64转换系数的保留方式,以及两个私有线性变换辅助函数合并后的 CPU 计算路径。适合在迁移 Kornia 版本时排查颜色转换输出差异的开发者阅读。
修复背景:颜色转换中的两类精度缺陷
+migration-108.fixed.md(对应 PR #4053)描述的修复共涉及四个行为变更,全部集中在颜色空间中三通道线性变换(3×3 矩阵乘)的实现细节上:
- 整数输入的截断问题:YUV 与 XYZ 变换在计算整数输入时,原来的实现会"截断(truncate)"变换核(kernel)的精度,导致结果与预期不符;
float64系数的丢失:当输入是直接构造的float64张量时,原本独立构造的变换核系数(字面量精度仅到float32)会被丢弃,转换精度达不到输入 dtype 应有的水平;- 有符号整数 YUV 结果错误:修复前对整数输入(例如
torch.int32/torch.int64)执行rgb_to_yuv会产生错误的有符号整型结果; - 实现冗余:此前存在两个"私有线性变换辅助函数",现已合并为单一实现。
核心变更一:整数输入在float32中计算,不再截断变换核
修复后的行为契约
修复后的语义可以用一句话概括:
对整数(非浮点)输入,先在内部提升为
float32参与矩阵运算,最终结果返回float32,并且输出的数值尺度与输入保持一致(即uint8输入在 0–255 尺度上运算,而不是先归一化到 0–1)。
以 kornia/color/yuv.py 中的rgb_to_yuv为例,源码的关键逻辑如下:
def rgb_to_yuv(image: torch.Tensor) -> torch.Tensor: KORNIA_CHECK_SHAPE(image, ["*", "3", "H", "W"]) dtype = image.dtype if image.is_floating_point() else torch.float32 kernel = torch.tensor( [ [0.299, 0.587, 0.114], [-0.147, -0.289, 0.436], [0.615, -0.515, -0.100], ], device=image.device, dtype=dtype, ) return _apply_linear_transformation(image, kernel)dtype的取值规则是:
- 输入为浮点类型(
float16/float32/float64/bfloat16)时,dtype与输入一致,变换核按该 dtype 构造; - 输入为整数类型(
uint8/int32/int64等)时,dtype固定为torch.float32,变换核按float32构造。
uint8输入的结果示例
修复后,rgb_to_yuv(uint8)会在输入原始的 0–255 尺度上计算并返回float32。测试 tests/color/test_yuv.py 对此有明确的断言:
def test_uint8_input_4053(self, device): rgb = (torch.eye(3) * 255).to(device=device, dtype=torch.uint8).unsqueeze(-2) expected = (torch.tensor(_RGB_TO_YUV_KERNEL, device=device, dtype=torch.float32) * 255).unsqueeze(-2) actual = kornia.color.rgb_to_yuv(rgb) assert actual.dtype == torch.float32 self.assert_close(actual, expected, atol=0.0 if device.type == "cpu" else 1e-3, rtol=0.0)即:对uint8输入的期望值是把float32的变换核整体乘以 255,而不是把输入先除以 255 再在 0–1 尺度上计算。因此修复后rgb_to_yuv(uint8)返回float32,并且数值保留在输入原始的 0–255 尺度上,这正是本文档所说的 "returnfloat32on the input's original 0--255 scale"。
有符号整数输入的修复
对int32/int64输入,测试 tests/color/test_yuv.py 同样验证了输出必须是float32,且与float32变换核直接作用的结果一致:
@pytest.mark.parametrize("integer_dtype", [torch.int32, torch.int64]) def test_integer_input_4053(self, device, integer_dtype): rgb = torch.eye(3).to(device=device, dtype=integer_dtype).unsqueeze(-2) expected = torch.tensor(_RGB_TO_YUV_KERNEL, device=device, dtype=torch.float32).unsqueeze(-2) actual = kornia.color.rgb_to_yuv(rgb) assert actual.dtype == torch.float32修复前,整数输入会被当作整型进行截断运算,导致 YUV 的 U/V 分量(包含负数系数)产生错误的有符号整型结果;修复后所有整数输入统一走float32路径,输出恒为float32。同样的模式也应用于 kornia/color/xyz.py 的rgb_to_xyz与xyz_to_rgb,对应测试见 tests/color/test_xyz.py 与 tests/color/test_xyz.py。
核心变更二:直接构造的float64系数被完整保留
修复前的问题
修复前,XYZ 转换中的 "float32 系数丢失" 问题表现如下:rgb_to_xyz/xyz_to_rgb的变换核字面量虽然带有完整的十进制精度(例如3.2404813432005266),但内部实现可能会把核强转为float32后再与float64输入运算,导致float64输入的转换精度被核的float32精度所限制,无法发挥float64应有的精度。
修复后的实现
现在的实现(kornia/color/xyz.py)按输入 dtype 构造核:
# CIE RGB to XYZ Matrix (D65 White Point) dtype = image.dtype if image.is_floating_point() else torch.float32 kernel = torch.tensor( [ [0.412453, 0.357580, 0.180423], [0.212671, 0.715160, 0.072169], [0.019334, 0.119193, 0.950227], ], device=image.device, dtype=dtype, ) # Apply Optimized Linear Transformation return _apply_linear_transformation(image, kernel)当输入为float64时,核以float64构造,字面量中的完整精度被保留。测试 tests/color/test_xyz.py 用atol=0.0, rtol=0.0的严格断言验证了这一点:
def test_float64_kernel_precision_4053(self): rgb = torch.eye(3, dtype=torch.float64).unsqueeze(-2) expected = torch.tensor(_RGB_TO_XYZ_KERNEL, dtype=torch.float64).unsqueeze(-2) self.assert_close(kornia.color.rgb_to_xyz(rgb), expected, atol=0.0, rtol=0.0)即:对单位矩阵输入,float64输入 +float64核必须逐位等于float64变换核本身,不允许任何精度损失。YUV 方向同样如此(tests/color/test_yuv.py)。
YUV 方向的特殊情况:精确逆矩阵
值得单独说明的是 kornia/color/yuv.py 中yuv_to_rgb的核。它的注释明确写道:该核是rgb_to_yuv舍入核的精确逆(而非另行舍入的公开逆变换关系),因此 RGB → YUV → RGB 的往返误差只受输入 dtype 精度限制。其在有理数域上的逆矩阵为:
[[1, -1/25344, 144439/126720], [1, -10001/25344, -73561/126720], [1, 51499/25344, -61/126720]]源码中的每个字面量都是上述精确分数正确舍入到float64的结果(如-3.945707070707071e-05、1.139827967171717),而不是调用torch.linalg.inv得到的近似值——注释指出 LU 分解连第一列的精确 1.0 都无法保证。相关回归测试见 tests/color/test_yuv.py:rgb = (1, 1, 0)(单位立方体上 B 通道往返误差最大的点)在float64下往返误差被压到1e-12以下,且 U → B 系数必须精确等于2.0319996843434343。
核心变更三:两个私有辅助函数合并为单一实现
合并后的单一实现
修复前存在两个功能重复的私有线性变换辅助函数;现在统一收敛到 kornia/color/utils.py 中的_apply_linear_transformation。rgb_to_yuv、yuv_to_rgb(kornia/color/yuv.py)、rgb_to_xyz、xyz_to_rgb(kornia/color/xyz.py)四个函数均通过它完成计算。
其核心逻辑分三步:
- 整数输入安全提升:
image.is_floating_point()为假时,将输入提升为float32(image.float()),避免整数参与矩阵乘时的截断问题; - 核与输入 dtype/device 对齐:通过
kernel.to(dtype=image_compute.dtype, device=image_compute.device)把核对齐到计算 dtype 与设备。这里的关键点在于——如果输入是float64,核的 dtype 会被对齐为float64,从而保留全部系数精度;可选偏置bias也执行同样的对齐; - 按设备分支计算:根据计算设备选择不同的矩阵运算实现。
CPU 路径:dtype/device 对齐的einsum计算
文档中提到的 "CPU path uses its dtype/device-aligned compute operands" 对应 kornia/color/utils.py 的第一分支:
# BRANCH 1: CPU and empty tensors (Einsum) # Einsum handles every empty shape and keeps image, kernel, and bias in the graph. if image_compute.device.type == "cpu" or image_compute.numel() == 0: out = torch.einsum("oi, ...ihw -> ...ohw", kernel_compute, image_compute) if bias_compute is not None: out = out + bias_compute.view(-1, 1, 1) return out.contiguous()在 CPU 上使用torch.einsum执行oi(输出通道 × 输入通道)与通道维的收缩;变换核与输入都已完成 dtype/device 对齐,因此浮点输入的精度被完整保留,空张量也能被正确支持。
GPU/加速器路径:conv2d分支
第二分支(kornia/color/utils.py)针对 GPU/CUDA 等加速器,将输入 reshape 为(-1, 3, H, W)后用F.conv2d实现 1×1 卷积式的通道线性变换,最后还原为原始形状:
# BRANCH 2: GPU/Accelerators (Conv2d) input_flat = image_compute.reshape(-1, 3, input_shape[-2], input_shape[-1]) weight = kernel_compute.view(3, 3, 1, 1) out_flat = F.conv2d(input_flat, weight, bias=bias_compute) return out_flat.reshape(input_shape)注释说明了分支选择的依据:经验基准表明该模式在 CPU 上einsum更快,而在 GPU/CUDA 上conv2d有显著加速,因此按设备分流以获得两端的较优性能。_apply_linear_transformation自身的单元测试位于 tests/color/test_utils.py,其中就包含"float64核 +float32输入 → 输出float32"以及"float32输入时核被对齐为float32"的对齐行为验证。
对使用者的实际影响与迁移建议
输出 dtype 与尺度对照表
修复后各输入类型的输出行为可总结如下:
| 输入 dtype | 计算/输出 dtype | 数值尺度 | 说明 |
|---|---|---|---|
float64 | float64 | 原始尺度 | 核按float64构造,完整精度保留 |
float32 | float32 | 原始尺度 | 常规路径,行为不变 |
float16/bfloat16 | float16/bfloat16 | 原始尺度 | 核按输入 dtype 构造 |
uint8 | float32 | 0–255 | 结果尺度与输入一致,不归一化 |
int32/int64 | float32 | 原始整数尺度 | 修复了错误的有符号整型结果 |
需要关注的差异点
uint8输入的输出尺度变化:如果此前代码依赖"整数输入被截断"的旧行为(例如期望输出落在 0–1 区间或整型结果),修复后需改为按float32处理;float64结果更精确:float64转换现在完整保留系数精度,往返误差显著下降,对需要高精度颜色计算的场景(如科学研究、色彩管理管线)是严格改进;- 往返误差显著下降:对 YUV 而言,由于
yuv_to_rgb精确求逆了rgb_to_yuv的舍入核,RGB → YUV → RGB 往返在float64下约为1.1e-16、float32下约为1.2e-7级别的纯浮点精度误差(tests/color/test_yuv.py 给出了各 dtype 的往返容差表)。
验证方式
仓库中的回归测试覆盖了上述全部行为:
- 整数输入与
uint8输入:test_integer_input_4053/test_uint8_input_4053(tests/color/test_yuv.py、tests/color/test_xyz.py); float64核精度:test_float64_kernel_precision_4053(tests/color/test_yuv.py、tests/color/test_xyz.py);- YUV 往返回归:
test_convention_yuv_to_rgb_inverts_rgb_to_yuv_4044(tests/color/test_yuv.py)。
这些测试以4053命名,直接对应文档引用的 PR 编号,便于在版本迁移时定位行为变更的出处。
小结
+migration-108.fixed.md(#4053)所记录的变化本质上是把颜色变换中"核精度"与"输入 dtype"解耦并统一:整数输入一律提升到float32计算且保持原始数值尺度,float64输入则完整保留变换核的float64精度,同时将两个重复的私有辅助函数合并为 kornia/color/utils.py 中 dtype/device 对齐的单一实现。对于依赖rgb_to_yuv、yuv_to_rgb、rgb_to_xyz、xyz_to_rgb的 Kornia 用户,升级后应重点核对整数图像输入的输出 dtype 与尺度,并可在精度敏感场景下受益于float64系数保真带来的更低往返误差。
【免费下载链接】kornia🐍 Geometric Computer Vision Library for Spatial AI项目地址: https://gitcode.com/GitHub_Trending/ko/kornia
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考