-
-
Notifications
You must be signed in to change notification settings - Fork 365
Open
Description
The RGB-to-YCbCr conversion code in libheif/heif_colorconversion.cc performs chroma subsampling by discarding samples only, without first lowpass filtering (taking a weighted average of) the samples. A representative example is the second nested for loop in
Op_RGB24_32_to_YCbCr::convert_colorspace():
std::shared_ptr<HeifPixelImage>
Op_RGB24_32_to_YCbCr::convert_colorspace(const std::shared_ptr<const HeifPixelImage>& input,
ColorState target_state,
ColorConversionOptions options)
{
...
for (int y = 0; y < height; y += chromaSubV) {
const uint8_t* p = &in_p[y * in_stride];
for (int x = 0; x < width; x += chromaSubH) {
uint8_t r = p[0];
uint8_t g = p[1];
uint8_t b = p[2];
p += bytes_per_pixel * chromaSubH;
float cb = r * coeffs.c[1][0] + g * coeffs.c[1][1] + b * coeffs.c[1][2];
float cr = r * coeffs.c[2][0] + g * coeffs.c[2][1] + b * coeffs.c[2][2];
if (full_range_flag) {
out_cb[(y / chromaSubV) * out_cb_stride + (x / chromaSubH)] = clip_f_u8(cb + 128);
out_cr[(y / chromaSubV) * out_cr_stride + (x / chromaSubH)] = clip_f_u8(cr + 128);
}
else {
out_cb[(y / chromaSubV) * out_cb_stride + (x / chromaSubH)] = (uint8_t) clip_f_u8(cb * 0.875f + 128.0f);
out_cr[(y / chromaSubV) * out_cr_stride + (x / chromaSubH)] = (uint8_t) clip_f_u8(cr * 0.875f + 128.0f);
}
}
}
...
}
Consider 4:2:0. This nested for loop subsamples each 2x2 block of chroma samples by taking the top-left sample and discarding the rest. This has two problems:
- The lack of lowpass filtering introduces distortion due to aliasing.
- The subsampled chroma is positioned at the top-left corner of the 2x2 block. But most YCbCr-to-RGB conversion functions in image decoders assume the chroma sample is positioned at the center of the 2x2 block. So the chroma samples are misaligned.
I will upload a pull request as a proof of concept for a fix.
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
No labels