JPEG 编码知识
JPEG 是 Joint Photographic Exports Group 的英文缩写,中文称之为联合图像专家小组。该小组隶属于 ISO 国际标准化组织,主要负责定制静态数字图像的编码方法,即所谓的 JPEG 算法。
JPEG 专家组开发了两种基本的压缩算法、两种熵编码方法、四种编码模式。如下所示:
- 压缩算法:
-
有损的离散余弦变换 DCT(Discrete Cosine Transform)
-
无损的预测压缩技术;
- 熵编码方法:
-
Huffman 编码;
-
算术编码;
- 编码模式:
-
基于 DCT 的顺序模式:编码、解码通过一次扫描完成;
-
基于 DCT 的渐进模式:编码、解码需要多次扫描完成,扫描效果由粗到精,逐级递增;
-
无损模式:基于 DPCM,保证解码后完全精确恢复到原图像采样值;
-
层次模式:图像在多个空间分辨率中进行编码,可以根据需要只对低分辨率数据做解码,放弃高分辨率信息;
在实际应用中,JPEG 图像编码算法使用的大多是
离散余弦变换 DCT
、Huffman 编码
、顺序编码模式
。
这被称为 JPEG 的基本系统。这里介绍的 JPEG 编码算法的流程,也是针对基本系统而言。基本系统的 JPEG 压缩编码算法一共分为 11 个步骤:
颜色模式转换
采样
分块
离散余弦变换(DCT)
量化
Zigzag 扫描排序
DC 系数的差分脉冲调制编码
DC 系数的中间格式计算
AC 系数的游程长度编码
AC 系数的中间格式计算
熵编码
JPEG 编码步骤
JPEG 编码涉及到的知识还是很复杂的,每一个步骤如果展开的话都是非常大的篇幅。我们这里简单了解每一步是干什么的、怎么做,达到每一步的目标就可以了。
本文使用了 ffjpeg library a simple jpeg codec,几乎所有的编码算法都是使用了这个库里的。
1. 颜色模式转换
JPEG 采用的是 YCrCb 颜色空间,YCrCb 颜色空间中,Y 代表亮度,Cr,Cb 则代表色度和饱和度(也有人将 Cb,Cr 两者统称为色度),三者通常以 Y,U,V 来表示,即用 U 代表 Cb,用 V 代表 Cr。RGB 和 YCrCb 之间的转换关系如下所示:
Y = 0.299*R + 0.587*G + 0.114*B
Cb = -0.169*R - 0.331*G + 0.500*B + 128
Cr = 0.500*R - 0.419*G - 0.081*B + 128
取值范围:
R/G/B [0 ~ 255]
Y/Cb/Cr[0 ~ 255]
示例代码:
// Y = 0.299*R + 0.587*G + 0.114*B
// U = Cb = -0.169*R - 0.331*G + 0.500*B + 128
// V = Cr = 0.500*R - 0.419*G - 0.081*B + 128
// R/G/B [0 ~ 255]
// Y/Cb/Cr[0 ~ 255]
void rgb_to_yuv(uint8_t R, uint8_t G, uint8_t B, uint8_t *Y, uint8_t *U, uint8_t *V) {
int y_val = (int)round(0.299*R + 0.587*G + 0.114*B);
int u_val = (int)round(-0.169*R - 0.331*G + 0.500*B + 128);
int v_val = (int)round(0.500*R - 0.419*G - 0.081*B + 128);
*Y = bound(0, y_val, 255);
*U = bound(0, u_val, 255);
*V = bound(0, v_val, 255);
}
相关阅读:
YUV - RGB Color Format Conversion
2. 采样
为节省带宽起见,大多数 YUV 格式平均使用的每像素位数都少于 24 位。主要的采样(subsample)格式有 YCbCr4:2:0、YCbCr4:2:2、YCbCr4:1:1 和 YCbCr4:4:4。YUV 的表示法称为 A:B:C 表示法:
- 4:4:4 表示完全取样。
- 4:2:2 表示 2:1 的水平取样,垂直完全采样。
- 4:2:0 表示 2:1 的水平取样,垂直 2:1 采样。
- 4:1:1 表示 4:1 的水平取样,垂直完全采样。
表格中,每一格代表一个像素
未采样前
- | 1 | 2 | 3 | 4 |
---|---|---|---|---|
1 | Y0 U0 V0 | Y1 U1 V1 | Y2 U2 V2 | Y3 U3 V3 |
2 | Y4 U4 V4 | Y5 U5 V5 | Y6 U6 V6 | Y7 U7 V7 |
3 | Y8 U8 V8 | Y9 U9 V9 | Y10 U10 V10 | Y11 U11 V11 |
4 | Y12 U12 V12 | Y13 U13 V13 | Y14 U14 V14 | Y15 U15 V15 |
4:4:4 采样
4:4:4 表示完全取样
- | 1 | 2 | 3 | 4 |
---|---|---|---|---|
1 | Y0 U0 V0 | Y1 U1 V1 | Y2 U2 V2 | Y3 U3 V3 |
2 | Y4 U4 V4 | Y5 U5 V5 | Y6 U6 V6 | Y7 U7 V7 |
3 | Y8 U8 V8 | Y9 U9 V9 | Y10 U10 V10 | Y11 U11 V11 |
4 | Y12 U12 V12 | Y13 U13 V13 | Y14 U14 V14 | Y15 U15 V15 |
映射的像素:
- | 1 | 2 | 3 | 4 |
---|---|---|---|---|
1 | Y0 U0 V0 | Y1 U1 V1 | Y2 U2 V2 | Y3 U3 V3 |
2 | Y4 U4 V4 | Y5 U5 V5 | Y6 U6 V6 | Y7 U7 V7 |
3 | Y8 U8 V8 | Y9 U9 V9 | Y10 U10 V10 | Y11 U11 V11 |
4 | Y12 U12 V12 | Y13 U13 V13 | Y14 U14 V14 | Y15 U15 V15 |
4:2:2 采样
4:2:2 表示 2:1 的水平取样,垂直完全采样
每采样过一个像素点,都会采样其 Y 分量,而 U、V 分量就会间隔一个采集一个。
- | 1 | 2 | 3 | 4 |
---|---|---|---|---|
1 | Y0 U0 - | Y1 - V1 | Y2 U2 - | Y3 - V3 |
2 | Y4 U4 - | Y5 - V5 | Y6 U6 - | Y7 - V7 |
3 | Y8 U8 - | Y9 - V9 | Y10 U10 - | Y11 - V11 |
4 | Y12 U12 - | Y13 - V13 | Y14 U14 - | Y15 - V15 |
映射的像素:
- | 1 | 2 | 3 | 4 |
---|---|---|---|---|
1 | Y0 U0 V1 | Y1 U0 V1 | Y2 U2 V3 | Y3 U2 V3 |
2 | Y4 U4 V5 | Y5 U4 V5 | Y6 U6 V7 | Y7 U6 V7 |
3 | Y8 U8 V9 | Y9 U8 V9 | Y10 U10 V11 | Y11 U10 V11 |
4 | Y12 U12 V13 | Y13 U12 V13 | Y14 U14 V15 | Y15 U14 V15 |
4:2:0 采样
4:2:0 表示 2:1 的水平取样,垂直 2:1 采样
每采样过一个像素点,都会采样其 Y 分量,而 U、V 分量就会间隔一行按照 2 : 1 进行采样。
- | 1 | 2 | 3 | 4 |
---|---|---|---|---|
1 | Y0 U0 - | Y1 - - | Y2 U2 - | Y3 - - |
2 | Y4 - V4 | Y5 - - | Y6 - V6 | Y7 - - |
3 | Y8 U8 - | Y9 - - | Y10 U10 - | Y11 - - |
4 | Y12 - V12 | Y13 - - | Y14 - V14 | Y15 - - |
映射的像素:
- | 1 | 2 | 3 | 4 |
---|---|---|---|---|
1 | Y0 U0 V4 | Y1 U0 V4 | Y2 U2 V6 | Y3 U2 V6 |
2 | Y4 U0 V4 | Y5 U0 V4 | Y6 U2 V6 | Y7 U2 V6 |
3 | Y8 U8 V12 | Y9 U8 V12 | Y10 U10 V14 | Y11 U10 V14 |
4 | Y12 U8 V12 | Y13 U8 V12 | Y14 U10 V14 | Y15 U10 V14 |
4:1:1 采样
4:1:1 表示 4:1 的水平取样,垂直完全采样
每采样过一个像素点,都会采样其 Y 分量,而 U、V 分量就会间隔一行按照 2 : 1 进行采样。
- | 1 | 2 | 3 | 4 |
---|---|---|---|---|
1 | Y0 U0 - | Y1 - - | Y2 - V2 | Y3 - - |
2 | Y4 U4 - | Y5 - - | Y6 - V6 | Y7 - - |
3 | Y8 U8 - | Y9 - - | Y10 - V10 | Y11 - - |
4 | Y12 U12 - | Y13 - - | Y14 - V14 | Y15 - - |
映射的像素:
- | 1 | 2 | 3 | 4 |
---|---|---|---|---|
1 | Y0 U0 V2 | Y1 U0 V2 | Y2 U0 V2 | Y3 U0 V2 |
2 | Y4 U4 V6 | Y5 U4 V6 | Y6 U4 V6 | Y7 U4 V6 |
3 | Y8 U8 V10 | Y9 U8 V10 | Y10 U8 V10 | Y11 U8 V10 |
4 | Y12 U12 V14 | Y13 U12 V14 | Y14 U12 V14 | Y15 U12 V14 |
示例代码:
void yuv444_to_yuv420(uint8_t *inbuf, uint8_t *outbuf, int w, int h) {
uint8_t *srcY = NULL, *srcU = NULL, *srcV = NULL;
uint8_t *desY = NULL, *desU = NULL, *desV = NULL;
srcY = inbuf;//Y
srcU = srcY + w * h;//U
srcV = srcU + w * h;//V
desY = outbuf;
desU = desY + w * h;
desV = desU + w * h / 4;
int half_width = w / 2;
int half_height = h / 2;
memcpy(desY, srcY, w * h * sizeof(uint8_t));//Y分量直接拷贝即可
//UV
for (int i = 0; i < half_height; i++) {
for (int j = 0; j < half_width; j++) {
*desU = *srcU;
*desV = *srcV;
desU++;
desV++;
srcU += 2;
srcV += 2;
}
srcU = srcU + w;
srcV = srcV + w;
}
}
相关阅读:
3. 分块
后面的 DCT 变换是是对 8x8 的子块进行处理的,因此,在进行 DCT 变换之前必须把源图象数据进行分块。源图象经过上面的颜色模式转换
、采样
变成了 YUV 数据,所以需要分别对 Y
U
V
三个分量进行分块。由左及右,由上到下依次读取 8x8 的子块,存放在长度为 64 的数组中,即可以进行 DCT 变换。
图像 YUV444 数据进行分块:
示例代码:
// 计算分块数量
int calc_block_size(int width, int height) {
int h_block_size = width/8 + (width%8==0?0:1);
int v_block_size = height/8 + (height%8==0?0:1);
return h_block_size*v_block_size;
}
// 获取一个子块
void get_8x8_block(const uint8_t *data, uint8_t *block_data, int h_block_pos, int v_block_pos, int width, int height) {
for(int i = 0; i < 8; i++) {
for(int j = 0; j < 8; j++) {
int h_pos = h_block_pos*8 + i;
int v_pos = v_block_pos*8 + j;
if(h_pos >= width || v_pos >= height) {
block_data[8*i+j] = 0;
} else {
block_data[8*i+j] = data[h_pos*width + v_pos];
}
}
}
}
// 对数据进行分块
void block_data_8x8(uint8_t *data, uint8_t blocks[][64], int width, int height) {
int h_block_size = width/8 + (width%8==0?0:1);
int v_block_size = height/8 + (height%8==0?0:1);
for(int i = 0; i < h_block_size; i++) {
for(int j = 0; j < v_block_size; j++) {
uint8_t *tmp = blocks[h_block_size*i+j];
get_8x8_block(data, tmp, i, j, width, height);
}
}
}
相关阅读:
JPEG压缩原理与DCT离散余弦变换-以8x8的图象块为基本单位进行编码
4. 离散余弦变换(DCT)
DCT 变换的公式为:
将原始图像数据分为 8x8 的数据单元矩阵之后,还必须将每个数值减去 128,然后一一带入 DCT 变换公式,即可达到 DCT 变换的目的。图像的数据值必须减去 128,是因为 DCT 公式所接受的数字范围是 -128 到 127 之间。
举例来说明一下,例子来自【JPEG 原理详细实例分析及其在嵌入式 Linux 中的应用】
8x8 的原始图像:
减去 128 后,使其范围变为 -128~127:
使用离散余弦变换,并四舍五入取最接近的整数:
矩阵最左上角的是直流系数(
DC
)
矩阵其余 63 个是交流系数(AC
)
DCT 输出的频率系数矩阵最左上角的直流(DC
)系数幅度最大,图中为-415
;以 DC
系数为出发点向下、向右的其它 DCT 系数,离 DC 分量越远,频率越高,幅度值越小,图中最右下角为2,即图像信息的大部分集中于直流系数及其附近的低频频谱上,离 DC 系数越来越远的高频频谱几乎不含图像信息,甚至于只含杂波。
示例代码:
void fdct2d8x8(int *data) {
int u, v;
int x, y, i;
float buf[64];
float temp;
for (u=0; u<8; u++) {
for (v=0; v<8; v++) {
temp = 0;
for (x=0; x<8; x++) {
for (y=0; y<8; y++) {
temp += data[y * 8 + x]
* (float)cos((2.0f * x + 1.0f) / 16.0f * u * M_PI)
* (float)cos((2.0f * y + 1.0f) / 16.0f * v * M_PI);
}
}
buf[v * 8 + u] = alpha(u) * alpha(v) * temp;
}
}
for (i=0; i<64; i++) data[i] = buf[i];
}
for(int y_index = 0; y_index < block_size; y_index++) {
uint8_t *y_block = y_blocks[y_index];
// DCT 之前减去 128
for(int i = 0; i < 64; i++) {y_blocks_dct[y_index][i] = y_block[i]-128;}
fdct2d8x8(y_blocks_dct[y_index]);
}
for(int u_index = 0; u_index < block_size; u_index++) {
uint8_t *u_block = u_blocks[u_index];
// DCT 之前减去 128
for(int i = 0; i < 64; i++) {u_blocks_dct[u_index][i] = u_block[i] - 128;}
fdct2d8x8(u_blocks_dct[u_index]);
}
for(int v_index = 0; v_index < block_size; v_index++) {
uint8_t *v_block = v_blocks[v_index];
// DCT 之前减去 128
for(int i = 0; i < 64; i++) {v_blocks_dct[v_index][i] = v_block[i] - 128;}
fdct2d8x8(v_blocks_dct[v_index]);
}
相关阅读:
JPEG 原理详细实例分析及其在嵌入式 Linux 中的应用-3、离散余弦变换 DCT
5. 量化
量化过程实际上就是对 DCT 系数的一个优化过程。它是利用了人眼对高频部分不敏感的特性来实现数据的大幅简化。
量化过程实际上是简单地把频率领域上每个成份,除以一个对于该成份的常数,且接着四舍五入取最接近的整数。
这是整个过程中的主要有损运算。
以这个结果来说,经常会把很多高频率的成份四舍五入而接近0,且剩下很多会变成小的正或负数。
整个量化的目的是减小非“0”系数的幅度以及增加“0”值系数的数目。
量化是图像质量下降的最主要原因。
因为人眼对亮度信号比对色差信号更敏感,因此使用了两种量化表:亮度量化值和色差量化值。
使用这个量化矩阵与前面所得到的 DCT 系数矩阵:
如,使用−415(DC系数)且四舍五入得到最接近的整数
总体上来说,DCT 变换实际是空间域的低通滤波器。对 Y 分量采用细量化,对 UV 采用粗量化。
量化表是控制 JPEG 压缩比的关键,这个步骤除掉了一些高频量;另一个重要原因是所有图片的点与点之间会有一个色彩过渡的过程,大量的图像信息被包含在低频率中,经过量化处理后,在高频率段,将出现大量连续的零。
示例代码:
/* 全局变量定义 */
const int STD_QUANT_TAB_LUMIN[64] = {
16, 11, 10, 16, 24, 40, 51, 61,
12, 12, 14, 19, 26, 58, 60, 55,
14, 13, 16, 24, 40, 57, 69, 56,
14, 17, 22, 29, 51, 87, 80, 62,
18, 22, 37, 56, 68, 109,103,77,
24, 35, 55, 64, 81, 104,113,92,
49, 64, 78, 87, 103,121,120,101,
72, 92, 95, 98, 112,100,103,99,
};
const int STD_QUANT_TAB_CHROM[64] = {
17, 18, 24, 47, 99, 99, 99, 99,
18, 21, 26, 66, 99, 99, 99, 99,
24, 26, 56, 99, 99, 99, 99, 99,
47, 66, 99, 99, 99, 99, 99, 99,
99, 99, 99, 99, 99, 99, 99, 99,
99, 99, 99, 99, 99, 99, 99, 99,
99, 99, 99, 99, 99, 99, 99, 99,
99, 99, 99, 99, 99, 99, 99, 99,
};
/* 函数实现 */
void quant_encode(int du[64], int qtab[64]) {
int i; for (i=0; i<64; i++) du[i] /= qtab[i];
}
int *pqtab[2];
pqtab[0] = malloc(64*sizeof(int));
pqtab[1] = malloc(64*sizeof(int));
memcpy(pqtab[0], STD_QUANT_TAB_LUMIN, 64*sizeof(int));
memcpy(pqtab[1], STD_QUANT_TAB_CHROM, 64*sizeof(int));
for(int y_index = 0; y_index < block_size; y_index++) {
quant_encode(y_blocks_dct[y_index], pqtab[0]);
}
for(int u_index = 0; u_index < block_size; u_index++) {
quant_encode(u_blocks_dct[u_index], pqtab[1]);
}
for(int v_index = 0; v_index < block_size; v_index++) {
quant_encode(v_blocks_dct[v_index], pqtab[1]);
}
相关阅读:
JPEG 原理详细实例分析及其在嵌入式 Linux 中的应用-4. 量化
6. Zigzag 扫描排序
量化后的数据,有一个很大的特点,就是直流分量相对于交流分量来说要大,而且交流分量中含有大量的 0。这样,对这个量化后的数据如何来进行简化,从而再更大程度地进行压缩呢。
这就出现了“Z”字形编排,如图:
对于前面量化的系数所作的 “Z”字形编排结果就是:
底部 −26,−3,0,−3,−3,−6,2,−4,1 −4,1,1,5,1,2,−1,1,−1,2,0,0,0,0,0,−1,−1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 顶部
这样做的特点就是会连续出现多个0,这样很有利于使用简单而直观的游程编码(RLE:Run Length Coding)对它们进行编码。
示例代码:
/* 内部全局变量定义 */
const int ZIGZAG[64] =
{
0, 1, 8, 16, 9, 2, 3, 10,
17, 24, 32, 25, 18, 11, 4, 5,
12, 19, 26, 33, 40, 48, 41, 34,
27, 20, 13, 6, 7, 14, 21, 28,
35, 42, 49, 56, 57, 50, 43, 36,
29, 22, 15, 23, 30, 37, 44, 51,
58, 59, 52, 45, 38, 31, 39, 46,
53, 60, 61, 54, 47, 55, 62, 63,
};
/* 函数实现 */
void zigzag_encode(int data[64]) {
int buf[64], i;
for (i=0; i<64; i++) buf [i] = data[ZIGZAG[i]];
for (i=0; i<64; i++) data[i] = buf[i];
}
for(int y_index = 0; y_index < block_size; y_index++) {
zigzag_encode(y_blocks_dct[y_index]);
}
for(int u_index = 0; u_index < block_size; u_index++) {
zigzag_encode(u_blocks_dct[u_index]);
}
for(int v_index = 0; v_index < block_size; v_index++) {
zigzag_encode(v_blocks_dct[v_index]);
}
相关阅读:
7. DC 系数的差分脉冲调制编码
8x8 图像块经过 DCT 变换之后得到的 DC 直流系数有两个特点,一是系数的数值比较大,二是相邻 8x8 图像块的 DC 系数值变化不大。根据这个特点,JPEG 算法使用了差分脉冲调制编码 (DPCM) 技术,对相邻图像块之间量化DC系数的差值 (Delta) 进行编码。
DC(0)=0
Delta = DC(i) - DC(i-1)
示例代码:
void encode_du(HUFCODEC *hfcac, HUFCODEC *hfcdc, int du[64], int *dc) {
...
// 7. DC 系数的差分脉冲调制编码
// dc
diff = du[0] - *dc;
*dc = du[0];
...
}
int dc[3]= {0};
for(int index = 0; index < block_size; index++) {
encode_du(phcac[0], phcdc[0], y_blocks_dct[index], &(dc[0]));
encode_du(phcac[1], phcdc[1], u_blocks_dct[index], &(dc[1]));
encode_du(phcac[1], phcdc[1], v_blocks_dct[index], &(dc[2]));
}
相关阅读:
JPEG图像压缩算法流程详解-(7)DC系数的差分脉冲调制编码
8. DC 系数的中间格式计算
JPEG 中为了更进一步节约空间,并不直接保存数据的具体数值,而是将数据按照位数分为 16 组,保存在表里面。这也就是所谓的变长整数编码 VLI。即,第 0 组中保存的编码位数为 0,其编码所代表的数字为 0;第 1 组中保存的编码位数为 1,编码所代表的数字为 -1 或者 1 ......,如下面的表格所示,这里,暂且称其为 VLI 编码表:
如果 DC 系数差值为 3,通过查找 VLI 可以发现,整数 3 位于 VLI 表格的第 2 组,因此,可以写成(2)(3)的形式,该形式,称之为 DC 系数的中间格式。
示例代码:
void category_encode(int *code, int *size) {
unsigned absc = abs(*code);
unsigned mask = (1 << 15);
int i = 15;
if (absc == 0) { *size = 0; return; }
while (i && !(absc & mask)) { mask >>= 1; i--; }
*size = i + 1;
if (*code < 0) *code = (1 << *size) - absc - 1;
}
// 7. DC 系数的差分脉冲调制编码
diff = du[0] - *dc;
// 8. DC 系数的中间格式计算
code = diff;
category_encode(&code, &size);
相关阅读:
9. AC 系数的游程长度编码
游程编码 RLC(Run Length Coding)来更进一步降低数据的传输量。利用该编码方式,可以将一个字符串中重复出现的连续字符用两个字节来代替,其中,第一个字节代表重复的次数,第二个字节代表被重复的字符串。
57, 45, 0, 0, 0, 0, 23, 0, -30, -8, 0, 0, 1, 000…
经过 0 RLC 之后,将呈现出以下的形式:
(0,57) ; (0,45) ; (4,23) ; (1,-30) ; (0,-8) ; (2,1) ; (0,0)
注意,如果 AC 系数之间连续 0 的个数超过 16,则用一个扩展字节 (15,0) 来表示 16 连续的 0。
示例代码:
typedef struct {
unsigned runlen : 4;
unsigned codesize : 4;
unsigned codedata : 16;
} RLEITEM;
// 9. AC 系数的游程长度编码
// 10. AC 系数的中间格式计算
// rle encode for ac
for (i=1, j=0, n=0, eob=0; i<64 && j<63; i++) {
if (du[i] == 0 && n < 15) {
n++;
} else {
code = du[i]; size = 0;
// 10. AC 系数的中间格式计算
category_encode(&code, &size);
rlelist[j].runlen = n;
rlelist[j].codesize = size;
rlelist[j].codedata = code;
n = 0;
j++;
if (size != 0) eob = j;
}
}
// set eob
if (du[63] == 0) {
rlelist[eob].runlen = 0;
rlelist[eob].codesize = 0;
rlelist[eob].codedata = 0;
j = eob + 1;
}
相关阅读:
JPEG图像压缩算法流程详解-(9)AC系数的行程长度编码(RLC)
JPEG格式与压缩流程分析-9. AC系数的行程长度编码(RLC)
10. AC 系数的中间格式计算
根据前面提到的 VLI 表格,对于前面的字符串:
(0,57) ; (0,45) ; (4,23) ; (1,-30) ; (0,-8) ; (2,1) ; (0,0)
只处理每对数右边的那个数据,对其进行 VLI 编码: 查找上面的 VLI 编码表格,可以发现,57 在第 6 组当中,因此,可以将其写成 (0,6),57 的形式,该形式,称之为 AC 系数的中间格式。
同样的 (0,45) 的中间格式为:(0,6),45;
(1,-30) 的中间格式为:(1,5),-30;
示例代码:
void category_encode(int *code, int *size) {
unsigned absc = abs(*code);
unsigned mask = (1 << 15);
int i = 15;
if (absc == 0) { *size = 0; return; }
while (i && !(absc & mask)) { mask >>= 1; i--; }
*size = i + 1;
if (*code < 0) *code = (1 << *size) - absc - 1;
}
// 9. AC 系数的游程长度编码
// 10. AC 系数的中间格式计算
// rle encode for ac
for (i=1, j=0, n=0, eob=0; i<64 && j<63; i++) {
if (du[i] == 0 && n < 15) {
n++;
} else {
code = du[i]; size = 0;
// 10. AC 系数的中间格式计算
category_encode(&code, &size);
rlelist[j].runlen = n;
rlelist[j].codesize = size;
rlelist[j].codedata = code;
n = 0;
j++;
if (size != 0) eob = j;
}
}
// set eob
if (du[63] == 0) {
rlelist[eob].runlen = 0;
rlelist[eob].codesize = 0;
rlelist[eob].codedata = 0;
j = eob + 1;
}
相关阅读:
11. 熵编码
在得到 DC 系数的中间格式和 AC 系数的中间格式之后,为进一步压缩图像数据,有必要对两者进行熵编码,通过对出现概率较高的字符采用较小的 bit 数编码达到压缩的目的。JPEG 标准具体规定了两种熵编码方式:Huffman 编码和算术编码。JPEG 基本系统规定采用 Huffman 编码。
Huffman 编码:对出现概率大的字符分配字符长度较短的二进制编码,对出现概率小的字符分配字符长度较长的二进制编码,从而使得字符的平均编码长度最短。Huffman 编码的原理请参考数据结构中的 Huffman 树或者最优二叉树。
Huffman 编码时 DC 系数与 AC 系数分别采用不同的 Huffman 编码表,对于亮度和色度也采用不同的 Huffman 编码表。因此,需要 4 张 Huffman 编码表才能完成熵编码的工作。具体的 Huffman 编码采用查表的方式来高效地完成。然而,在 JPEG 标准中没有定义缺省的 Huffman 表,用户可以根据实际应用自由选择,也可以使用J PEG 标准推荐的 Huffman 表。或者预先定义一个通用的 Huffman 表,也可以针对一副特定的图像,在压缩编码前通过搜集其统计特征来计算 Huffman 表的值。
示例代码:
// huffman codec ac
HUFCODEC *phcac[2];
phcac[0]= calloc(1, sizeof(HUFCODEC));
phcac[1]= calloc(1, sizeof(HUFCODEC));
// huffman codec dc
HUFCODEC *phcdc[2];
phcdc[0] = calloc(1, sizeof(HUFCODEC));
phcdc[1] = calloc(1, sizeof(HUFCODEC));
// init huffman codec
memcpy(phcac[0]->huftab, STD_HUFTAB_LUMIN_AC, MAX_HUFFMAN_CODE_LEN + 256);
memcpy(phcac[1]->huftab, STD_HUFTAB_CHROM_AC, MAX_HUFFMAN_CODE_LEN + 256);
memcpy(phcdc[0]->huftab, STD_HUFTAB_LUMIN_DC, MAX_HUFFMAN_CODE_LEN + 256);
memcpy(phcdc[1]->huftab, STD_HUFTAB_CHROM_DC, MAX_HUFFMAN_CODE_LEN + 256);
phcac[0]->output = bs; huffman_encode_init(phcac[0], 1);
phcac[1]->output = bs; huffman_encode_init(phcac[1], 1);
phcdc[0]->output = bs; huffman_encode_init(phcdc[0], 1);
phcdc[1]->output = bs; huffman_encode_init(phcdc[1], 1);
// 7. DC 系数的差分脉冲调制编码
// 8. DC 系数的中间格式计算
// 9. AC 系数的游程长度编码
// 10. AC 系数的中间格式计算
// 11. 熵编码
// 7、8、9、10、11 在 encode_du 里完成
for(int index = 0; index < block_size; index++) {
encode_du(phcac[0], phcdc[0], y_blocks_dct[index], &(dc[0]));
encode_du(phcac[1], phcdc[1], u_blocks_dct[index], &(dc[1]));
encode_du(phcac[1], phcdc[1], v_blocks_dct[index], &(dc[2]));
}
void encode_du(HUFCODEC *hfcac, HUFCODEC *hfcdc, int du[64], int *dc) {
...
// 7. DC 系数的差分脉冲调制编码
diff = du[0] - *dc;
*dc = du[0];
// 8. DC 系数的中间格式计算
code = diff;
category_encode(&code, &size);
// 11. 熵编码 DC
huffman_encode_step(hfcdc, size);
bitstr_put_bits(bs, code, size);
// 9. AC 系数的游程长度编码
// 10. AC 系数的中间格式计算
// rle encode for ac
for (i=1, j=0, n=0, eob=0; i<64 && j<63; i++) {
...
}
...
// 11. 熵编码 AC
for (i=0; i<j; i++) {
huffman_encode_step(hfcac, (rlelist[i].runlen << 4) | (rlelist[i].codesize << 0));
bitstr_put_bits(bs, rlelist[i].codedata, rlelist[i].codesize);
}
}
相关阅读:
JPEG 文件写入
根据 音视频入门-14-JPEG文件格式详解, JPEG 文件大体上可以分成两个部分:标记码(Tag)和压缩数据。
常用的标记有 SOI、APP0、APPn、DQT、SOF0、DHT、DRI、SOS、EOI:
标记 | 标记代码 | 描述 |
---|---|---|
SOI | 0xD8 | 图像开始 |
APP0 | 0xE0 | JFIF应用数据块 |
APPn | 0xE1 - 0xEF | 其他的应用数据块(n, 1~15) |
DQT | 0xDB | 量化表 |
SOF0 | 0xC0 | 帧开始 |
DHT | 0xC4 | 霍夫曼(Huffman)表 |
DRI | 0xDD | 差分编码累计复位的间隔 |
SOS | 0xDA | 扫描线开始 |
EOI | 0xD9 | 图像结束 |
通过上面的 11 个编码步骤,我们也得到了需要的数据,下一步只需将数据写入文件即可。
// 下面开始将数据写入文件
//FILE *fp = fopen("C:\Users\Administrator\Desktop\rainbow-rgb-to-jpeg.jpg", "wb+");
FILE *fp = fopen("/Users/staff/Desktop/rainbow-rgb-to-jpeg.jpg", "wb");
// output SOI
fputc(0xff, fp);
fputc(0xd8, fp);
// output DQT
for (int i = 0; i < 2; i++) {
int len = 2 + 1 + 64;
fputc(0xff, fp);
fputc(0xdb, fp);
fputc(len >> 8, fp);
fputc(len >> 0, fp);
fputc(i , fp);
for (int j = 0; j < 64; j++) {
fputc(pqtab[i][ZIGZAG[j]], fp);
}
}
// output SOF0
int SOF0Len = 2 + 1 + 2 + 2 + 1 + 3 * 3;
fputc(0xff, fp);
fputc(0xc0, fp);
fputc(SOF0Len >> 8, fp);
fputc(SOF0Len >> 0, fp);
fputc(8 , fp); // precision 8bit
fputc(height >> 8, fp); // height
fputc(height >> 0, fp); // height
fputc(width >> 8, fp); // width
fputc(width >> 0, fp); // width
fputc(3, fp);
fputc(0x01, fp); fputc(0x11, fp); fputc(0x00, fp);
fputc(0x02, fp); fputc(0x11, fp); fputc(0x01, fp);
fputc(0x03, fp); fputc(0x11, fp); fputc(0x01, fp);
// output DHT AC
for (int i = 0; i < 2; i++) {
fputc(0xff, fp);
fputc(0xc4, fp);
int len = 2 + 1 + 16;
for (int j = 0; j < 16; j++) len += phcac[i]->huftab[j];
fputc(len >> 8, fp);
fputc(len >> 0, fp);
fputc(i + 0x10, fp);
fwrite(phcac[i]->huftab, len - 3, 1, fp);
}
// output DHT DC
for (int i = 0; i < 2; i++) {
fputc(0xff, fp);
fputc(0xc4, fp);
int len = 2 + 1 + 16;
for (int j = 0; j < 16; j++) len += phcdc[i]->huftab[j];
fputc(len >> 8, fp);
fputc(len >> 0, fp);
fputc(i + 0x00, fp);
fwrite(phcdc[i]->huftab, len - 3, 1, fp);
}
// output SOS
int SOSLen = 2 + 1 + 2 * 3 + 3;
fputc(0xff, fp);
fputc(0xda, fp);
fputc(SOSLen >> 8, fp);
fputc(SOSLen >> 0, fp);
fputc(3, fp);
fputc(0x01, fp); fputc(0x00, fp);
fputc(0x02, fp); fputc(0x11, fp);
fputc(0x03, fp); fputc(0x11, fp);
fputc(0x00, fp);
fputc(0x3F, fp);
fputc(0x00, fp);
// output data
if (buffer) {
fwrite(buffer, dataLength, 1, fp);
}
// output EOI
fputc(0xff, fp);
fputc(0xd9, fp);
// 释放各种资源
......
JPEG 结果验收
以上完整代码在 binglingziyu/audio-video-blog-demos 可以获取。
运行代码,生成 JPEG 图片:
代码:
15-rgb-to-jpeg
参考资料:
YUV - RGB Color Format Conversion
内容有误?联系作者: