【问题标题】:Chroma subsampling algorithm for jpegjpeg的色度子采样算法
【发布时间】:2017-04-03 19:39:55
【问题描述】:

我正在尝试编写一个 jpeg 编码器,并且在创建收集适当 Y、Cb 和 Cr 颜色分量以传递给执行转换的方法的算法时遇到了困难。

据我了解,四种最常见的子采样变体设置如下(我可能会离开这里):

  • 4:4:4 - 一个 8x8 像素的 MCU 块,每个像素表示 Y、Cb 和 Cr。
  • 4:2:2 - 一个 16x8 像素的 MCU 块,每个像素为 Y,每两个像素为 Cb、Cr
  • 4:2:0 - 一个 16x16 像素的 MCU 块,每两个像素为 Y,每四个像素为 Cb、Cr

到目前为止,我发现的 laout 最明确的描述是 here

我不明白的是如何以正确的顺序收集这些组件以作为 8x8 块传递以进行转换和量化。

有人能写一个例子,(我敢肯定,伪代码会很好,C# 更好),如何对字节进行分组以进行转换?

我将包含我正在运行的当前不正确的代码。

/// <summary>
/// Writes the Scan header structure
/// </summary>
/// <param name="image">The image to encode from.</param>
/// <param name="writer">The writer to write to the stream.</param>
private void WriteStartOfScan(ImageBase image, EndianBinaryWriter writer)
{
    // Marker
    writer.Write(new[] { JpegConstants.Markers.XFF, JpegConstants.Markers.SOS });

    // Length (high byte, low byte), must be 6 + 2 * (number of components in scan)
    writer.Write((short)0xc); // 12

    byte[] sos = {
        3, // Number of components in a scan, usually 1 or 3
        1, // Component Id Y
        0, // DC/AC Huffman table 
        2, // Component Id Cb
        0x11, // DC/AC Huffman table 
        3, // Component Id Cr
        0x11, // DC/AC Huffman table 
        0, // Ss - Start of spectral selection.
        0x3f, // Se - End of spectral selection.
        0 // Ah + Ah (Successive approximation bit position high + low)
    };

    writer.Write(sos);

    // Compress and write the pixels
    // Buffers for each Y'Cb Cr component
    float[] yU = new float[64];
    float[] cbU = new float[64];
    float[] crU = new float[64];

    // The descrete cosine values for each componant.
    int[] dcValues = new int[3];

    // TODO: Why null?
    this.huffmanTable = new HuffmanTable(null);

    // TODO: Color output is incorrect after this point. 
    // I think I've got my looping all wrong.
    // For each row
    for (int y = 0; y < image.Height; y += 8)
    {
        // For each column
        for (int x = 0; x < image.Width; x += 8)
        {
            // Convert the 8x8 array to YCbCr
            this.RgbToYcbCr(image, yU, cbU, crU, x, y);

            // For each component
            this.CompressPixels(yU, 0, writer, dcValues);
            this.CompressPixels(cbU, 1, writer, dcValues);
            this.CompressPixels(crU, 2, writer, dcValues);
        }
    }

    this.huffmanTable.FlushBuffer(writer);
}

/// <summary>
/// Converts the pixel block from the RGBA colorspace to YCbCr.
/// </summary>
/// <param name="image"></param>
/// <param name="yComponant">The container to house the Y' luma componant within the block.</param>
/// <param name="cbComponant">The container to house the Cb chroma componant within the block.</param>
/// <param name="crComponant">The container to house the Cr chroma componant within the block.</param>
/// <param name="x">The x-position within the image.</param>
/// <param name="y">The y-position within the image.</param>
private void RgbToYcbCr(ImageBase image, float[] yComponant, float[] cbComponant, float[] crComponant, int x, int y)
{
    int height = image.Height;
    int width = image.Width;

    for (int a = 0; a < 8; a++)
    {
        // Complete with the remaining right and bottom edge pixels.
        int py = y + a;
        if (py >= height)
        {
            py = height - 1;
        }

        for (int b = 0; b < 8; b++)
        {
            int px = x + b;
            if (px >= width)
            {
                px = width - 1;
            }

            YCbCr color = image[px, py];
            int index = a * 8 + b;
            yComponant[index] = color.Y;
            cbComponant[index] = color.Cb;
            crComponant[index] = color.Cr;
        }
    }
}

/// <summary>
/// Compress and encodes the pixels. 
/// </summary>
/// <param name="componantValues">The current color component values within the image block.</param>
/// <param name="componantIndex">The componant index.</param>
/// <param name="writer">The writer.</param>
/// <param name="dcValues">The descrete cosine values for each componant</param>
private void CompressPixels(float[] componantValues, int componantIndex, EndianBinaryWriter writer, int[] dcValues)
{
    // TODO: This should be an option.
    byte[] horizontalFactors = JpegConstants.ChromaFourTwoZeroHorizontal;
    byte[] verticalFactors = JpegConstants.ChromaFourTwoZeroVertical;
    byte[] quantizationTableNumber = { 0, 1, 1 };
    int[] dcTableNumber = { 0, 1, 1 };
    int[] acTableNumber = { 0, 1, 1 };

    for (int y = 0; y < verticalFactors[componantIndex]; y++)
    {
        for (int x = 0; x < horizontalFactors[componantIndex]; x++)
        {
            // TODO: This can probably be combined reducing the array allocation.
            float[] dct = this.fdct.FastFDCT(componantValues);
            int[] quantizedDct = this.fdct.QuantizeBlock(dct, quantizationTableNumber[componantIndex]);
            this.huffmanTable.HuffmanBlockEncoder(writer, quantizedDct, dcValues[componantIndex], dcTableNumber[componantIndex], acTableNumber[componantIndex]);
            dcValues[componantIndex] = quantizedDct[0];
        }
    }
}

这段代码是我在Github上写的一个开源库的一部分

【问题讨论】:

  • 您引用的链接有很好的信息,但您误解了它。当有颜色子采样时,MCU 像素大小会发生变化(例如 8x8、16x8、8x16、16x16)。在该 MCU 中,您需要对颜色数据进行适当的二次采样,然后按照文章中显示的顺序将其排列成 8x8 DCT 块(例如 Y0、Y1、Y2、Y3、Cb、Cr)
  • 谢谢,我猜了很多。我似乎无法理解如何执行该子采样。 IE。从整个像素阵列中抓取哪些像素以及在我的各个组件阵列中以什么顺序排列它们。

标签: c# algorithm jpeg


【解决方案1】:

JPEG 颜色子采样可以以简单但实用的方式实现,无需太多代码。基本思想是,您的眼睛对颜色变化相对于亮度变化不太敏感,因此通过丢弃一些颜色信息,JPEG 文件可以变得更小。有很多方法可以对颜色信息进行二次采样,但 JPEG 图像倾向于使用 4 种变体:无、1/2 水平、1/2 垂直和 1/2 水平+垂直。还有其他 TIFF/EXIF 选项,例如二次采样颜色的“中心点”,但为简单起见,我们将使用求和技术的平均值。

在最简单的情况下(无二次采样),每个 MCU(最小编码单元)是一个 8x8 像素块,由 3 个分量(Y、Cb、Cr)组成。图像以 8x8 像素块进行处理,其中 3 个颜色分量被分离,通过 DCT 变换并按顺序(Y、Cb、Cr)写入文件。在所有子采样情况下,DCT 块总是由 8x8 系数或 64 个值组成,但这些值的含义会​​因颜色子采样而异。

下一个最简单的情况是在一维(水平或垂直)中进行二次采样。让我们在这个例子中使用 1/2 水平二次采样。 MCU 现在是 16 像素宽 x 8 像素高。每个 MCU 的压缩输出现在将是 4 个 8x8 DCT 块(Y0、Y1、Cb、Cr)。 Y0 表示左侧 8x8 像素块的亮度值,Y1 表示右侧 8x8 像素块的亮度值。 Cb 和 Cr 值分别是基于水平像素对的平均值的 8x8 块。我找不到任何好的图片可以在这里插入,但是一些伪代码可以派上用场。

(更新:可能代表二次采样的图像:)

这是一个简单的循环,它对我们的 1/2 水平情况进行颜色二次采样:

unsigned char ucCb[8][8], ucCr[8][8];
int x, y;

for (y=0; y<8; y++)
{
   for (x=0; x<8; x++)
   {
      ucCb[y][x] = (srcCb[y][x*2] + srcCb[y][(x*2)+1] + 1)/2; // average each horiz pair
      ucCr[y][x] = (srcCr[y][x*2] + srcCr[y][(x*2)+1] + 1)/2;
   } // for x
} // for y

如您所见,没有太多内容。来自源图像的每一对 Cb 和 Cr 像素被水平平均以形成一个新的 Cb/Cr 像素。然后将它们进行 DCT 转换、曲折和编码,格式与往常一样。

最后,对于 2x2 子样本情况,MCU 现在是 16x16 像素,写入的 DCT 块将是 Y0、Y1、Y2、Y3、Cb、Cr。其中 Y0 代表左上角 8x8 亮度像素,Y1 代表右上角,Y2 代表左下角,Y3 代表右下角。在这种情况下,Cb 和 Cr 值代表 4 个源像素 (2x2),它们已被一起平均。以防万一您想知道,颜色值在 YCbCr 颜色空间中一起平均。如果在 RGB 颜色空间中将像素平均在一起,它将无法正常工作。

仅供参考 - Adob​​e 支持 RGB 颜色空间中的 JPEG 图像(而不是 YCbCr)。这些图像不能使用颜色子采样,因为 R、G 和 B 具有同等重要性,并且在此颜色空间中对它们进行子采样会导致更糟糕的视觉伪影。

【讨论】:

  • 啊...我想我现在有了。我读了几遍才发现示例代码中沿 x 的水平采样是 [0-1] [2-3] [4-5] [6-7] [8-9] [10-11] [12-13] [14-15] 重复。我会尝试实施它。
  • @BitBank 如果您的图像大小为 8 像素 x 8 像素并且您的 MCU 为 16x16 或 16x8。这将如何运作?如果你的图像不是 8 的倍数怎么办?
  • @juFo - 在这种情况下,MCU 的未使用部分将被编码器和解码器忽略。编码 8x8 图像的 16x16 MCU 是完全有效的。不幸的是,在这种情况下,您仍然需要对由 6 个 8x8 DCT 块 (y0,y1,y2,y3,Cb,Cr) 组成的完整 MCU 进行编码。在这种情况下,可能不值得对颜色进行二次采样。
  • 嗯,谢谢...您如何填充 MCU,使用什么值?这是写在某处吗,因为我没有在那个案子上找到任何东西。
  • 用于 MCU 未使用部分的最佳值是 0 - 这将使用最少的位数进行编码。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-07-21
  • 1970-01-01
  • 2013-05-11
  • 1970-01-01
  • 2011-09-02
相关资源
最近更新 更多