Skip to content

Chroma subsampling does not lowpass filter (take average) before discarding samples #521

@wantehchang

Description

@wantehchang

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:

  1. The lack of lowpass filtering introduces distortion due to aliasing.
  2. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions