jpeg 的色度子采样算法
Chroma subsampling algorithm for jpeg
我正在尝试编写一个 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
目前为止我找到的最明确的布局描述是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
上编写的开源库的一部分
JPEG 颜色子采样可以以一种简单而实用的方式实现,无需太多代码。基本思想是您的眼睛对颜色变化相对于亮度变化不太敏感,因此通过丢弃一些颜色信息,JPEG 文件可以小得多。对颜色信息进行子采样的方法有很多,但 JPEG 图像倾向于使用 4 种变体:none、1/2 水平、1/2 垂直和 1/2 水平+垂直。还有其他 TIFF/EXIF 选项,例如子采样颜色的 "center point",但为简单起见,我们将使用求和技术的平均值。
在最简单的情况下(无子采样),每个 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 颜色空间中将像素一起平均,它将无法正常工作。
仅供参考 - Adobe 支持 RGB 色彩空间(而不是 YCbCr)中的 JPEG 图像。这些图像不能使用颜色子采样,因为 R、G 和 B 具有同等重要性,并且在此颜色空间中对它们进行子采样会导致更糟糕的视觉伪影。
我正在尝试编写一个 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
目前为止我找到的最明确的布局描述是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
上编写的开源库的一部分JPEG 颜色子采样可以以一种简单而实用的方式实现,无需太多代码。基本思想是您的眼睛对颜色变化相对于亮度变化不太敏感,因此通过丢弃一些颜色信息,JPEG 文件可以小得多。对颜色信息进行子采样的方法有很多,但 JPEG 图像倾向于使用 4 种变体:none、1/2 水平、1/2 垂直和 1/2 水平+垂直。还有其他 TIFF/EXIF 选项,例如子采样颜色的 "center point",但为简单起见,我们将使用求和技术的平均值。
在最简单的情况下(无子采样),每个 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 颜色空间中将像素一起平均,它将无法正常工作。
仅供参考 - Adobe 支持 RGB 色彩空间(而不是 YCbCr)中的 JPEG 图像。这些图像不能使用颜色子采样,因为 R、G 和 B 具有同等重要性,并且在此颜色空间中对它们进行子采样会导致更糟糕的视觉伪影。