Commit 215cbdad for libheif
commit 215cbdadef517b43b30889eb277ca1ba14b09bfa
Author: 21Z121Z1 <89170834+21Z121Z1@users.noreply.github.com>
Date: Sat Oct 3 20:04:52 2026 +0800
Fix 4:2:0 bilinear chroma border indexing
The border loops use chroma-sample coordinates. An extra division by two selects the wrong Cb and Cr samples after the first chroma interval.
Use cx and cy directly. Extend the bilinear upsampling test to cover all four borders.
diff --git a/libheif/color-conversion/chroma_sampling.cc b/libheif/color-conversion/chroma_sampling.cc
index b93a8095..a38139e2 100644
--- a/libheif/color-conversion/chroma_sampling.cc
+++ b/libheif/color-conversion/chroma_sampling.cc
@@ -592,10 +592,10 @@ Op_YCbCr420_bilinear_to_YCbCr444<Pixel>::convert_colorspace(const std::shared_pt
// top border
for (uint32_t cx = 0; cx < (width - 1) / 2; cx++) {
- out_cb[0 * out_cb_stride + 2 * cx + 1] = (Pixel) ((3 * in_cb[cx / 2] + 1 * in_cb[cx / 2 + 1] + 2) / 4);
- out_cb[0 * out_cb_stride + 2 * cx + 2] = (Pixel) ((1 * in_cb[cx / 2] + 3 * in_cb[cx / 2 + 1] + 2) / 4);
- out_cr[0 * out_cr_stride + 2 * cx + 1] = (Pixel) ((3 * in_cr[cx / 2] + 1 * in_cr[cx / 2 + 1] + 2) / 4);
- out_cr[0 * out_cr_stride + 2 * cx + 2] = (Pixel) ((1 * in_cr[cx / 2] + 3 * in_cr[cx / 2 + 1] + 2) / 4);
+ out_cb[0 * out_cb_stride + 2 * cx + 1] = (Pixel) ((3 * in_cb[cx] + 1 * in_cb[cx + 1] + 2) / 4);
+ out_cb[0 * out_cb_stride + 2 * cx + 2] = (Pixel) ((1 * in_cb[cx] + 3 * in_cb[cx + 1] + 2) / 4);
+ out_cr[0 * out_cr_stride + 2 * cx + 1] = (Pixel) ((3 * in_cr[cx] + 1 * in_cr[cx + 1] + 2) / 4);
+ out_cr[0 * out_cr_stride + 2 * cx + 2] = (Pixel) ((1 * in_cr[cx] + 3 * in_cr[cx + 1] + 2) / 4);
}
// top right corner
@@ -606,10 +606,10 @@ Op_YCbCr420_bilinear_to_YCbCr444<Pixel>::convert_colorspace(const std::shared_pt
// left border
for (uint32_t cy = 0; cy < (height - 1) / 2; cy++) {
- out_cb[(2 * cy + 1) * out_cb_stride + 0] = (Pixel) ((3 * in_cb[cy / 2 * in_cb_stride] + 1 * in_cb[(cy / 2 + 1) * in_cb_stride] + 2) / 4);
- out_cb[(2 * cy + 2) * out_cb_stride + 0] = (Pixel) ((1 * in_cb[cy / 2 * in_cb_stride] + 3 * in_cb[(cy / 2 + 1) * in_cb_stride] + 2) / 4);
- out_cr[(2 * cy + 1) * out_cr_stride + 0] = (Pixel) ((3 * in_cr[cy / 2 * in_cr_stride] + 1 * in_cr[(cy / 2 + 1) * in_cr_stride] + 2) / 4);
- out_cr[(2 * cy + 2) * out_cr_stride + 0] = (Pixel) ((1 * in_cr[cy / 2 * in_cr_stride] + 3 * in_cr[(cy / 2 + 1) * in_cr_stride] + 2) / 4);
+ out_cb[(2 * cy + 1) * out_cb_stride + 0] = (Pixel) ((3 * in_cb[cy * in_cb_stride] + 1 * in_cb[(cy + 1) * in_cb_stride] + 2) / 4);
+ out_cb[(2 * cy + 2) * out_cb_stride + 0] = (Pixel) ((1 * in_cb[cy * in_cb_stride] + 3 * in_cb[(cy + 1) * in_cb_stride] + 2) / 4);
+ out_cr[(2 * cy + 1) * out_cr_stride + 0] = (Pixel) ((3 * in_cr[cy * in_cr_stride] + 1 * in_cr[(cy + 1) * in_cr_stride] + 2) / 4);
+ out_cr[(2 * cy + 2) * out_cr_stride + 0] = (Pixel) ((1 * in_cr[cy * in_cr_stride] + 3 * in_cr[(cy + 1) * in_cr_stride] + 2) / 4);
}
// bottom left corner
@@ -621,20 +621,20 @@ Op_YCbCr420_bilinear_to_YCbCr444<Pixel>::convert_colorspace(const std::shared_pt
// right border
if (width % 2 == 0) {
for (uint32_t cy = 0; cy < (height - 1) / 2; cy++) {
- out_cb[(2 * cy + 1) * out_cb_stride + width - 1] = (Pixel) ((3 * in_cb[cy / 2 * in_cb_stride + width / 2 - 1] + 1 * in_cb[(cy / 2 + 1) * in_cb_stride + width / 2 - 1] + 2) / 4);
- out_cb[(2 * cy + 2) * out_cb_stride + width - 1] = (Pixel) ((1 * in_cb[cy / 2 * in_cb_stride + width / 2 - 1] + 3 * in_cb[(cy / 2 + 1) * in_cb_stride + width / 2 - 1] + 2) / 4);
- out_cr[(2 * cy + 1) * out_cr_stride + width - 1] = (Pixel) ((3 * in_cr[cy / 2 * in_cr_stride + width / 2 - 1] + 1 * in_cr[(cy / 2 + 1) * in_cr_stride + width / 2 - 1] + 2) / 4);
- out_cr[(2 * cy + 2) * out_cr_stride + width - 1] = (Pixel) ((1 * in_cr[cy / 2 * in_cr_stride + width / 2 - 1] + 3 * in_cr[(cy / 2 + 1) * in_cr_stride + width / 2 - 1] + 2) / 4);
+ out_cb[(2 * cy + 1) * out_cb_stride + width - 1] = (Pixel) ((3 * in_cb[cy * in_cb_stride + width / 2 - 1] + 1 * in_cb[(cy + 1) * in_cb_stride + width / 2 - 1] + 2) / 4);
+ out_cb[(2 * cy + 2) * out_cb_stride + width - 1] = (Pixel) ((1 * in_cb[cy * in_cb_stride + width / 2 - 1] + 3 * in_cb[(cy + 1) * in_cb_stride + width / 2 - 1] + 2) / 4);
+ out_cr[(2 * cy + 1) * out_cr_stride + width - 1] = (Pixel) ((3 * in_cr[cy * in_cr_stride + width / 2 - 1] + 1 * in_cr[(cy + 1) * in_cr_stride + width / 2 - 1] + 2) / 4);
+ out_cr[(2 * cy + 2) * out_cr_stride + width - 1] = (Pixel) ((1 * in_cr[cy * in_cr_stride + width / 2 - 1] + 3 * in_cr[(cy + 1) * in_cr_stride + width / 2 - 1] + 2) / 4);
}
}
// bottom border
if (height % 2 == 0) {
for (uint32_t cx = 0; cx < (width - 1) / 2; cx++) {
- out_cb[(height - 1) * out_cb_stride + 2 * cx + 1] = (Pixel) ((3 * in_cb[(height / 2 - 1) * in_cb_stride + cx / 2] + 1 * in_cb[(height / 2 - 1) * in_cb_stride + cx / 2 + 1] + 2) / 4);
- out_cb[(height - 1) * out_cb_stride + 2 * cx + 2] = (Pixel) ((1 * in_cb[(height / 2 - 1) * in_cb_stride + cx / 2] + 3 * in_cb[(height / 2 - 1) * in_cb_stride + cx / 2 + 1] + 2) / 4);
- out_cr[(height - 1) * out_cr_stride + 2 * cx + 1] = (Pixel) ((3 * in_cr[(height / 2 - 1) * in_cr_stride + cx / 2] + 1 * in_cr[(height / 2 - 1) * in_cr_stride + cx / 2 + 1] + 2) / 4);
- out_cr[(height - 1) * out_cr_stride + 2 * cx + 2] = (Pixel) ((1 * in_cr[(height / 2 - 1) * in_cr_stride + cx / 2] + 3 * in_cr[(height / 2 - 1) * in_cr_stride + cx / 2 + 1] + 2) / 4);
+ out_cb[(height - 1) * out_cb_stride + 2 * cx + 1] = (Pixel) ((3 * in_cb[(height / 2 - 1) * in_cb_stride + cx] + 1 * in_cb[(height / 2 - 1) * in_cb_stride + cx + 1] + 2) / 4);
+ out_cb[(height - 1) * out_cb_stride + 2 * cx + 2] = (Pixel) ((1 * in_cb[(height / 2 - 1) * in_cb_stride + cx] + 3 * in_cb[(height / 2 - 1) * in_cb_stride + cx + 1] + 2) / 4);
+ out_cr[(height - 1) * out_cr_stride + 2 * cx + 1] = (Pixel) ((3 * in_cr[(height / 2 - 1) * in_cr_stride + cx] + 1 * in_cr[(height / 2 - 1) * in_cr_stride + cx + 1] + 2) / 4);
+ out_cr[(height - 1) * out_cr_stride + 2 * cx + 2] = (Pixel) ((1 * in_cr[(height / 2 - 1) * in_cr_stride + cx] + 3 * in_cr[(height / 2 - 1) * in_cr_stride + cx + 1] + 2) / 4);
}
}
diff --git a/tests/conversion.cc b/tests/conversion.cc
index 8d6a95f1..7456151f 100644
--- a/tests/conversion.cc
+++ b/tests/conversion.cc
@@ -693,17 +693,19 @@ TEST_CASE("Bilinear upsampling", "[heif_image]")
.only_use_preferred_chroma_algorithm = true};
std::shared_ptr<HeifPixelImage> img = std::make_shared<HeifPixelImage>();
- img->create(4, 4, heif_colorspace_YCbCr, heif_chroma_420);
+ img->create(6, 6, heif_colorspace_YCbCr, heif_chroma_420);
- auto error = img->fill_new_channel(heif_channel_Y, 128, 4,4, 8, nullptr);
+ auto error = img->fill_new_channel(heif_channel_Y, 128, 6, 6, 8, nullptr);
REQUIRE(!error);
- fill_plane(img, heif_channel_Cb, 2,2,
- {10, 40,
- 100, 240});
- fill_plane(img, heif_channel_Cr, 2, 2,
- {255, 200,
- 50, 0});
+ fill_plane(img, heif_channel_Cb, 3, 3,
+ {10, 40, 80,
+ 100, 240, 160,
+ 180, 200, 220});
+ fill_plane(img, heif_channel_Cr, 3, 3,
+ {255, 200, 160,
+ 50, 0, 80,
+ 100, 40, 20});
auto conversionResult = convert_colorspace(img, heif_colorspace_YCbCr, heif_chroma_444,
nclx_profile::defaults(), 8, options, nullptr, heif_get_disabled_security_limits());
@@ -712,19 +714,22 @@ TEST_CASE("Bilinear upsampling", "[heif_image]")
assert_plane(out, heif_channel_Cb,
{
- 10, 18, 33, 40,
- 33, 47, 76, 90,
- 78, 106, 162, 190,
- 100, 135, 205, 240
+ 10, 18, 33, 50, 70, 80,
+ 33, 47, 76, 93, 98, 100,
+ 78, 106, 162, 178, 153, 140,
+ 120, 148, 203, 216, 189, 175,
+ 160, 173, 198, 209, 206, 205,
+ 180, 185, 195, 205, 215, 220
});
-
assert_plane(out, heif_channel_Cr,
{
- 255, 241, 214, 200,
- 204, 190, 163, 150,
- 101, 88, 63, 50,
- 50, 38, 13, 0
+ 255, 241, 214, 190, 170, 160,
+ 204, 190, 163, 148, 143, 140,
+ 101, 88, 63, 63, 88, 100,
+ 63, 49, 23, 24, 51, 65,
+ 88, 73, 44, 31, 34, 35,
+ 100, 85, 55, 35, 25, 20
});
}