Error[8]: Undefined offset: 2292, File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 121
File: /www/wwwroot/outofmemory.cn/tmp/plugin_ss_superseo_model_superseo.php, Line: 473, decode(

本文章的地址:https://blog.csdn.net/weixin_40279971/article/details/118523920
欢迎阅读、点赞、批评、指正或引用。 O(∩_∩)O

目录

写在前面 JPEG文件格式介绍

JPEG压缩算法定于1993年的CCITT T.81文件,我看完以后发现当年官方对JPEG的规范还是很复杂的(比如算数编码和子图),而且比较理论,并没有规定对应的计算机文件格式应该时怎样的。时光荏苒,目前最流行的JPEG文件格式仅仅是DCT+Huffman这一支了。

计算机中的JPEG文件后缀名一般是*.jpg,*.jpeg,而其中的文件头主要就是JFIF和Exif,其中计算机生成的图片多使用JFIF,拍摄的照片多使用Exif,而文件头后面的压缩数据都是一样的。

我这里的JPEG编解码专门针对JFIF,如果是Exif会直接略过其内容直接读取图像信息,如果实在需要查看相机的光圈快门等Exif信息可以在后面代码部分里判断Exif的地方自己添加哦。

上面是JPEG文件头的常用段,其中APP0是JFIF段,APP1是Exif段,JPEG规定文件开头是SOI,结尾是EOI,EOI前面必须是压缩数据,压缩数据前面必须是SOS,其他段的顺序可能会有变化,所以读取文件头最好用switch case语句,这里给出一个参考顺序

各个段的具体内容很容易就能搜到,我就不详细介绍了,下面给出我定义的结构体,我将会对每一个代码片段进行简单【批注】。
首先是定义三个可能用到的类型 u8,u16,u32,这仨对于玩过单片机的同学应该很熟悉 O(∩_∩)O

typedef unsigned char	u8;
typedef unsigned short	u16;
typedef unsigned int	u32;

JPEG文件头结构体

typedef struct s_DQT {
	u16		label = 0xFFDB;			//Define Quantization Table (after Zigzaged)
	u16		dqtlen = 67;			//0x43 = 2+1+64=67
	u8		dqtacc_id = 0x00;		//acc=[7:4] 0:8bit, 1:16bit, id=[3:0] #0-3 (up to 4 tables)
	u8		QTable[64] = { 0 };		//init QTable[128] if acc=1 e.g. 16bit
}DQT;


typedef struct s_DHT_DC {
	u16		label = 0xFFC4;			//Define DC Huffman Table
	u16		dhtlen = 31;			//0x1F = 2+1+16+12
	u8		hufftype_id = 0x00;		//type=[7:4] 0:DC, 1:AC, id=[3:0] #0-1 (DC/AC id increases respectively)
	u8		DC_NRcodes[16] = { 0 };	//how many codes in each code_length (1-16)
	u8		DC_Values[12] = { 0 };	//Huffman tree num of node (不一定固定为12字节)
}DHT_DC;
typedef struct s_DHT_AC {
	u16		label = 0xFFC4;			//Define AC Huffman Table
	u16		dhtlen = 181;			//0xB5 = 2+1+16+162
	u8		hufftype_id = 0x10;
	u8		AC_NRcodes[16] = { 0 };
	u8		AC_Values[162] = { 0 };	//不一定固定为162字节
}DHT_AC;



struct s_JPEG_header {
	u16		SOI = 0xFFD8;			//Start Of Image

	u16		APP0 = 0xFFE0;			//Application 0
	u16		app0len = 16;			//APP0's len, =16 for usual JPEG e.g. no thumbnail
	char	app0id[5] = "JFIF";		//"JFIF=" = 0x4A46494600
	u16		jfifver 0x0101 ;//JFIF's version: 0x0101(v1.1) or 0x0102(v1.2)		=
	u8		xyunit 0 ;//x/y density's unit, 0:no unit, 1:dot/inch, 2:dot/cm				=
	u16		xden 0x0001 ;//x density			=
	u16		yden 0x0001 ;//y density			=
	u8		thumbh 0 ;//thumbnail horizon(width)				=
	u8		thumbv 0 ;//thumbnail vertical(height)				;

	DQT		DQT0//DQT for Luminance					;
	DQT		DQT1//DQT for Chrominance					=

	u16		SOF0 0xFFC0 ;//Start Of Frame			=
	u16		sof0len 0x0011 ;//len = 17 for 24bit TrueColor		=
	u8		sof0acc 0x08 ;//acc = 0 (8 bit/sample), optional: 0x08(almost), 0x12, 0x16			=
	u16		imheight 0 ;//image height/pixel			=
	u16		imwidth 0 ;//image width/pixel			=
	u8		clrcomponent 3 ;//color components=3, e.g. JFIF只使用YCbCr(3),Exif这里也是3		=
	u8		clrY_id 0x01 ;//Y_component_id=1			=
	u8		clrY_sample 0x11 ;//Y_Hor_sam_factor=1, Y_Ver_sam_factor=1			=
	u8		clrY_QTable 0x00 ;=		//Y -> QTable #0
	u8		clrU_id 0x02 ;//U_component_id=2			=
	u8		clrU_sample 0x11 ;//			=
	u8		clrU_QTable 0x01 ;=		//U -> QTable #1
	u8		clrV_id 0x03 ;//V_component_id=3			=
	u8		clrV_sample 0x11 ;//			=
	u8		clrV_QTable 0x01 ;;		//V -> QTable #1

	DHT_DC	DHT_DC0//DHT for Luminance				;
	DHT_AC	DHT_AC0;
	DHT_DC	DHT_DC1//DHT for Chrominance				;
	DHT_AC	DHT_AC1=

	u16		SOS 0xFFDA ;//Start Of Scan			=
	u16		soslen 12 ;//0x0C			=
	u8		component 3 ;//Should equal to color component			=
	u16		Y_id_dht 0x0100 ;//Y: id=1, use #0 DC and #0 AC		=
	u16		U_id_dht 0x0211 ;//U: id=1, use #1 DC and #1 AC		=
	u16		V_id_dht 0x0311 ;//V: id=1, use #1 DC and #1 AC		//always fit
	=
	u8		SpectrumS 0x00 ;//spectrum start		=
	u8		SpectrumE 0x3F ;//spectrum end		=
	u8		SpectrumC 0x00 ;//spectrum choose		//Here is filled with Compressed-Image-Data

	=

	u16		EOI 0xFFD9 ;//End Of Image			//init
	s_JPEG_header
	()[ { app0id0]= 'J' ;[ app0id1]= 'F' ;[ app0id2]= 'I' ;[ app0id3]= 'F' ;[ app0id4]= 0 ;} }
;
  • DHT中的Values数组长度是不固定的,要满足Values[].size()=NRcodes[].sum(),即NRcodes数组的总和等于Values数组的长度。NRcodes[16]指示1bit-16bit码长的码字数量,而Values[]指示各个码字对应的值,Values[]的长度不能超过256Byte。
  • 以上只是最基本的JPEG结构体,值得注意的是:

    JPEG编码流程-Encode

    JPEG编码流程为:

    分块 => DCT变换 => 量化 => RLE&Huffman => BitString

    1.分块

    结构体中SOF0段的clr_sample代表采样率,YUV三个采样率都是0x11时代表YUV444采样,UV不变而Y为0x22时代表YUV411(也叫YUV420)采样。

    JPEG的YUV444采样准确来说是yuv444p,即 “ YYYY–UUUU–VVVV ” 的排列方式,Y/U/V三个分量的个数就是像素个数(实际上既然要分块,实际数量肯定是块的长宽的整数倍的,这里暂时按照普通YUV来讲),又因为YUV分量默认使用3个8bit来存(和RGB888一样哦),因此YUV文件的大小可以计算(估算)为=width×height×3Byte。

    JPEG的YUV420采样同样是指yuv420p,即 “ YYYY–U–V ” 的排列方式,田字格排布的四个Y共用一个U和一个V,(哈哈,这样你肯定又发现Y在某一个维度上的个数是奇数时有涉及补齐问题了,这都是后话了),此时YUV文件的大小可以计算(估算)为=width×height×3/2Byte。

    这里简单说一下为啥用YUV:Y代表明度Luminance,UV代表色度Chrominance。人眼对色度的细微差异不太敏感,所以如果对UV压缩4倍后都不咋看得出来差别,这是RGB模式达不到的压缩比;另外只保留Y分量时图片会变为黑白的,这时可以方便的兼容黑白电视。

    分块时yuv444p按照8×8分割成若干块或称为宏块(MCU,minimum coded unit),MCU中包含一个完整的Y,一个完整的U和一个完整的V,每个宏块编码后单独用YYYY-UUUU-VVVV的格式输出。

    yuv420p则按照16×16分割成MCU,内部划分为4个8×8的小宏块,因此每个MCU包含1个完整的Y,一个1/4的U和一个1/4的V,编码后按照YYYY YYYY YYYY YYYY-UUUU-VVVV输出。

    分块要注意补齐的问题,一般是复制补齐而不是补零。
    分块的代码融于for循环之中,不单独放出来了。

    2.DCT变换

    根据《数字图像处理》中的说法,以为边界存在更剧烈的不连续的情况,DFT比DCT更不适合用于图像时频转换,小波变换(WT,Wavelet Transform)比DCT优秀,WT用在了JPEG2000格式中了哈。
    DCT即离散余弦变换,它变换后只有实部,简单好用。实际代码中为了提速并不会真的用三角函数带进去猛算,而是用矩阵运算实现。因为两种MCU分块实际上都是8×8的小块,所以DCT退化为固定的矩阵运算,DCT正向运算为 Y=A·X·AT,逆运算为 Y=AT·X·A,下面是A矩阵以及Aconst矩阵:

    double [ DCT8][8]= 0.35355 {
    	{,0.35355	,0.35355	,0.35355	,0.35355	,0.35355	,0.35355	,0.35355	},0.49039
    	{,0.41573	,0.27779	,0.09756	,-	0.09756,-	0.27779,-	0.41573,-	0.49039},0.46194
    	{,0.19134	,-	0.19134,-	0.46194,-	0.46194,-	0.19134,0.19134	,0.46194	},0.41573
    	{,-	0.09755,-	0.49039,-	0.27779,0.27779	,0.49039	,0.09755	,-	0.41573},0.35355
    	{,-	0.35355,-	0.35355,0.35355	,0.35355	,-	0.35355,-	0.35355,0.35355	},0.27779
    	{,-	0.49039,0.09755	,0.41573	,-	0.41573,-	0.09755,0.49039	,-	0.27779},0.19134
    	{,-	0.46194,0.46194	,-	0.19134,-	0.19134,0.46194	,-	0.46194,0.19134	},0.09755
    	{,-	0.27779,0.41573	,-	0.49039,0.49039	,-	0.41573,0.27779	,-	0.09754}}
    ;static
    
    const double [ DCT_T8][8]= 0.35355 {
    	{,0.49039	,0.46194	,0.41573	,0.35355	,0.27779	,0.19134	,0.09755	},0.35355
    	{,0.41573	,0.19134	,-	0.09755,-	0.35355,-	0.49039,-	0.46194,-	0.27779},0.35355
    	{,0.27779	,-	0.19134,-	0.49039,-	0.35355,0.09755	,0.46194	,0.41573	},0.35355
    	{,0.09755	,-	0.46194,-	0.27779,0.35355	,0.41573	,-	0.19134,-	0.49039},0.35355
    	{,-	0.09755,-	0.46194,0.27779	,0.35355	,-	0.41573,-	0.19134,0.49039	},0.35355
    	{,-	0.27779,-	0.19134,0.49039	,-	0.35355,-	0.09755,0.46194	,-	0.41573},0.35355
    	{,-	0.41573,0.19134	,0.09755	,-	0.35355,0.49039	,-	0.46194,0.27779	},0.35355
    	{,-	0.49039,0.46194	,-	0.41573,0.35355	,-	0.27779,0.19134	,-	0.09754}}
    ;static
    

    DCT的物理意义其实还是时频转换,二维DCT之后得到的矩阵左上角为低频部分,右下角为高频分量,人眼对高频细节的敏感有限,因此可以减弱高频来进行有损压缩,这就是量化。

    3.量化

    JPEG的量化并不是PCM里的那个量化编码的量化,大白话讲出来就是乘数法,把DCT得到的8×8矩阵点除(对应元素间的运算)量化矩阵就完成量化 *** 作了…这里给个当年CCITT规范里的参考量化矩阵

    unsigned char [ Q_Y64]= 16 {
    	,11 ,12 ,14 ,12 ,10 ,16 ,14 ,13
    	,14 ,18 ,17 ,16 ,19 ,24 ,40 ,26
    	,24 ,22 ,22 ,24 ,49 ,35 ,37 ,29
    	,40 ,58 ,51 ,61 ,60 ,57 ,51 ,56
    	,55 ,64 ,72 ,92 ,78 ,64 ,68 ,87
    	,69 ,55 ,56 ,80 ,109 ,81,87 ,95
    	,98 ,103 ,104,103,62,77 ,113,121
    	,112,100,120,92,101 ,103,99}
    ;static
    unsigned char [ Q_UV64]= 17 {
    	,18 ,18 ,24 ,21 ,24 ,47 ,26 ,26
    	,47 ,99 ,66 ,56 ,66 ,99 ,99 ,99
    	,99 ,99 ,99 ,99 ,99 ,99 ,99 ,99
    	,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 }
    ;static
    

    规范里说是经过人眼视觉心理学得到的表,我反正觉得很悬,毕竟分母有点大了哈哈。量化表里的数字越小就会让JPEG压缩的图片越接近原图,换句话说就是数字越大压缩得越猛,图片越失真。再放一个我喜欢的量化表吧

    unsigned char [ Q_Y64]= 5 {
    	 ,3  ,4  ,4  ,4  ,3  ,5  ,4  ,4
    	 ,4  ,5  ,5  ,5  ,6  ,7  ,12 ,8
    	 ,7  ,7  ,7  ,7  ,15 ,12 ,12 ,9
    	 ,12 ,17 ,15 ,18 ,18 ,17 ,15 ,17
    	,17 ,19 ,22 ,28 ,23 ,19 ,20 ,26
    	,21 ,17 ,17 ,24 ,33 ,24 ,26 ,29
    	,29 ,29 ,31 ,31 ,31 ,19 ,23 ,34
    	,36 ,34 ,30 ,36 ,30 ,31 ,30 }
    ;static
    unsigned char [ Q_UV64]= 5 {
    	 ,5  ,5  ,7  ,6  ,7  ,14 ,8  ,8
    	 ,14 ,30 ,20 ,17 ,20 ,30 ,30 ,30
    	,30 ,30 ,30 ,30 ,30 ,30 ,30 ,30
    	,30 ,30 ,30 ,30 ,30 ,30 ,30 ,30
    	,30 ,30 ,30 ,30 ,30 ,30 ,30 ,30
    	,30 ,30 ,30 ,30 ,30 ,30 ,30 ,30
    	,30 ,30 ,30 ,30 ,30 ,30 ,30 ,30
    	,30 ,30 ,30 ,30 ,30 ,30 ,30 ,}
    ;:
    

    官方给的参考量化表压缩率还是蛮大的,不过2021年了,内存条和硬盘已经今非昔比了,JPEG的压缩率也不会像当年那么大了,我甚至见过几乎都是1的量化表,啧啧啧。

    量化完以后会有一步Zigzag的步骤,这是因为靠近右下角的高频分量本来就比较小,大概率会被量化到0,所以Zigzag之字形重新组织小宏块的矩阵数据可以让更多的0聚集在一起,方便RLE编码。
    上面给的量化表是已经Zigzag之后的表了,否则的话依然需要单独Zigzag *** 作下的。

    4.RLE&Huffman

    RLE,即游程编码(Run Length Coding)是最简单的图像压缩编码。
    简单来说就是把 [ 7, 6, 6, 0, 0, 0, 0, 2, 4, 4, 4, 4 ] 这串数字表示成 7(1), 6(2), 0(4), 2(1), 4(4)。
    上文说到量化和Zigzag后的矩阵数据将会出现大量的连0,这种情况使用RLE编码可以极大地节省码流。
    JPEG将RLE的含义转变,记录出现连0的个数,如果遇到16个0就合并标记为(15,0),比如:
    [ 5, 31, 0, 0, 23, 0, -30, -8, 0, 1 ] 将被编码为

    DC(5),: AC(0,31),(2,23),(1,-30),(0,-8),(2,1)(
    

    进一步地,(DC值因为是开头所以省略0)将每组第二个数的二进制码长添加到首位,得到:

    3,5),(0/5,31),(2/5,23),(1/5,-30),(0/4,-8),(2/1,1)// DC
    

    上述元组数据的前一个数据进行Huffman编码,后一个数据进行Magnitude编码。

    Huffman,即霍夫曼编码,属于VLC的一种,也叫最佳编码。
    简单来说就是用最短的码长表示出现最频繁的符号,这样得到的编码效率是最高的。但是因为构建二叉树的左右子树问题,导致Huffman树不是唯一的,为了便于解码,JPEG采用了范式Huffman构造树,它强制码字必须严格递增且码长可自推导,这样只需保留码字的字长分布就可以重构整个表。
    下面是codec工程里定义的标准码表:

    static
    const unsigned char [ Std_Lumi_DC_NRCodes16]= 0 { ,1 ,5 ,1 ,1 ,1 ,1 ,1 ,1 ,0 ,0 ,0 ,0 ,0 ,0 ,0 };static
    const unsigned char [ Std_Lumi_DC_Values12]= 0 { ,1 ,2 ,3 ,4 ,5 ,6 ,7 ,8 ,9 ,10 ,11 } ;static
    const unsigned char [ Std_Chromi_DC_NRCodes16]= 0 { ,3 ,1 ,1 ,1 ,1 ,1 ,1 ,1 ,1 ,1 ,0 ,0 ,0 ,0 ,0 } ;static
    const unsigned char [ Std_Chromi_DC_Values12]= 0 { ,1 ,2 ,3 ,4 ,5 ,6 ,7 ,8 ,9 ,10 ,11 } ;//AC
    
    static
    const unsigned char [ Std_Lumi_AC_NRCodes16]= 0 { ,2 ,1 ,3 ,3 ,2 ,4 ,3 ,5 ,5 ,4 ,4 ,0 ,0 ,1 ,0x7D } ;static
    const unsigned char [ Std_Lumi_AC_Values162]= 0x01 {
    	,0x02 ,0x03 ,0x00 ,0x04 ,0x11 ,0x05 ,0x12 ,0x21
    	,0x31 ,0x41 ,0x06 ,0x13 ,0x51 ,0x61 ,0x07 ,0x22
    	,0x71 ,0x14 ,0x32 ,0x81 ,0x91 ,0xa1 ,0x08 ,0x23
    	,0x42 ,0xb1 ,0xc1 ,0x15 ,0x52 ,0xd1 ,0xf0 ,0x24
    	,0x33 ,0x62 ,0x72 ,0x82 ,0x09 ,0x0a ,0x16 ,0x17
    	,0x18 ,0x19 ,0x1a ,0x25 ,0x26 ,0x27 ,0x28 ,0x29
    	,0x2a ,0x34 ,0x35 ,0x36 ,0x37 ,0x38 ,0x39 ,0x3a
    	,0x43 ,0x44 ,0x45 ,0x46 ,0x47 ,0x48 ,0x49 ,0x4a
    	,0x53 ,0x54 ,0x55 ,0x56 ,0x57 ,0x58 ,0x59 ,0x5a
    	,0x63 ,0x64 ,0x65 ,0x66 ,0x67 ,0x68 ,0x69 ,0x6a
    	,0x73 ,0x74 ,0x75 ,0x76 ,0x77 ,0x78 ,0x79 ,0x7a
    	,0x83 ,0x84 ,0x85 ,0x86 ,0x87 ,0x88 ,0x89 ,0x8a
    	,0x92 ,0x93 ,0x94 ,0x95 ,0x96 ,0x97 ,0x98 ,0x99
    	,0x9a ,0xa2 ,0xa3 ,0xa4 ,0xa5 ,0xa6 ,0xa7 ,0xa8
    	,0xa9 ,0xaa ,0xb2 ,0xb3 ,0xb4 ,0xb5 ,0xb6 ,0xb7
    	,0xb8 ,0xb9 ,0xba ,0xc2 ,0xc3 ,0xc4 ,0xc5 ,0xc6
    	,0xc7 ,0xc8 ,0xc9 ,0xca ,0xd2 ,0xd3 ,0xd4 ,0xd5
    	,0xd6 ,0xd7 ,0xd8 ,0xd9 ,0xda ,0xe1 ,0xe2 ,0xe3
    	,0xe4 ,0xe5 ,0xe6 ,0xe7 ,0xe8 ,0xe9 ,0xea ,0xf1
    	,0xf2 ,0xf3 ,0xf4 ,0xf5 ,0xf6 ,0xf7 ,0xf8 ,0xf9
    	,0xfa }
    ;static
    
    const unsigned char [ Std_Chromi_AC_NRCodes16]= 0 { ,2 ,1 ,2 ,4 ,4 ,3 ,4 ,7 ,5 ,4 ,4 ,0 ,1 ,2 ,0x77 } ;static
    const unsigned char [ Std_Chromi_AC_Values162]= 0x00 {
    	,0x01 ,0x02 ,0x03 ,0x11 ,0x04 ,0x05 ,0x21 ,0x31
    	,0x06 ,0x12 ,0x41 ,0x51 ,0x07 ,0x61 ,0x71 ,0x13
    	,0x22 ,0x32 ,0x81 ,0x08 ,0x14 ,0x42 ,0x91 ,0xa1
    	,0xb1 ,0xc1 ,0x09 ,0x23 ,0x33 ,0x52 ,0xf0 ,0x15
    	,0x62 ,0x72 ,0xd1 ,0x0a ,0x16 ,0x24 ,0x34 ,0xe1
    	,0x25 ,0xf1 ,0x17 ,0x18 ,0x19 ,0x1a ,0x26 ,0x27
    	,0x28 ,0x29 ,0x2a ,0x35 ,0x36 ,0x37 ,0x38 ,0x39
    	,0x3a ,0x43 ,0x44 ,0x45 ,0x46 ,0x47 ,0x48 ,0x49
    	,0x4a ,0x53 ,0x54 ,0x55 ,0x56 ,0x57 ,0x58 ,0x59
    	,0x5a ,0x63 ,0x64 ,0x65 ,0x66 ,0x67 ,0x68 ,0x69
    	,0x6a ,0x73 ,0x74 ,0x75 ,0x76 ,0x77 ,0x78 ,0x79
    	,0x7a ,0x82 ,0x83 ,0x84 ,0x85 ,0x86 ,0x87 ,0x88
    	,0x89 ,0x8a ,0x92 ,0x93 ,0x94 ,0x95 ,0x96 ,0x97
    	,0x98 ,0x99 ,0x9a ,0xa2 ,0xa3 ,0xa4 ,0xa5 ,0xa6
    	,0xa7 ,0xa8 ,0xa9 ,0xaa ,0xb2 ,0xb3 ,0xb4 ,0xb5
    	,0xb6 ,0xb7 ,0xb8 ,0xb9 ,0xba ,0xc2 ,0xc3 ,0xc4
    	,0xc5 ,0xc6 ,0xc7 ,0xc8 ,0xc9 ,0xca ,0xd2 ,0xd3
    	,0xd4 ,0xd5 ,0xd6 ,0xd7 ,0xd8 ,0xd9 ,0xda ,0xe2
    	,0xe3 ,0xe4 ,0xe5 ,0xe6 ,0xe7 ,0xe8 ,0xe9 ,0xea
    	,0xf2 ,0xf3 ,0xf4 ,0xf5 ,0xf6 ,0xf7 ,0xf8 ,0xf9
    	,0xfa }
    ;
  • 在编码结束后,将得到的二进制码进行拼接,得到二进制码流。码流(序列)可能不满足整除字节,这很正常,多出来的部分一般是补1’b0,代码里是用空Byte的位运算实现的,所以多出来bits的默认就是0。
  • 本部分参考了下面的文章,对于还不太理解的同学,强烈建议看下:
    a. https://www.cnblogs.com/buaaxhzh/p/9138307.html --> JPEG原理解释
    b. https://www.cnblogs.com/buaaxhzh/p/9119870.html --> RLE&Huffman详解
    c. void --> 标准Huffman表

    JPEG解码流程-Decode

    JPEG解码流程为:

    恢复Huffman&RLE => 反量化 => 反DCT变换 => 合并块

    1. 恢复编码数据

    在编码部分的学习之后,相信你已经有如何解码的思路了,我在这里之说几处注意点:

    Build_Huffman_Table (const* u8, nr_codesconst * u8, std_table* HuffType, huffman_table* \
    						u16, max_in_ThisLen[ u8 huff_len_table][130])unsigned
    {
    	char = pos_in_table 0 ;unsigned
    	short = code_value 0 ;for
    	( int= k 1 ;<= k 16 ;++ k)= {
    		u8 num 0 ;for
    		( int= j 1 ;<= j [ nr_codes-k 1 ];++ j)[ {
    			huff_len_table]k[]num= [ std_table]pos_in_table;++
    			num;[
    			huffman_table[std_table]pos_in_table].=code ; code_value[
    			huffman_table[std_table]pos_in_table].=len ; k++
    			pos_in_table;++
    			code_value;}
    		[
    		max_in_ThisLen]k= ; code_value<<=
    		code_value 1 ;}
    	}
    
  • Huffman解码时需要码字匹配,你可以对比特流进行逐bit匹配,也可以使用上述代码的方式,根据范式Huffman码字的特性,根据当前长度最大码字的查找表来进行更快的匹配,更可以用字典或哈希表来查找,总之里面优化的地方还是有的,但有点跑题了。
  • 本部分参考了下面的文章,对于还不太理解的同学,强烈建议看下:
    a. https://blog.csdn.net/yun_hen/article/details/78135122 --> JPEG格式信息

    2. 反量化

    反量化就是将解码得到的矩阵数据点乘上量化矩阵,这里的量化矩阵也必须用图片指定的。
    反量化完记得进行反Zigzag *** 作。

    3. DCT逆变换

    已在“DCT变换”那一节解释过,同理。

    4. 合并块

    已在“分块”那一节讲过,同理。

    写在后面

    我自认为不是一个能够梳理好结构并循循善诱地讲解知识的人,毕竟自己目前对于jpeg的算法只能说熟悉,还做不到“通透”。本文是我的回忆之作,难免有遗漏之处,但所写皆是基于成功运行的代码测试之上,我有责任保证文章的正确性。我再给大家推荐两个别人的实现,第一个是encoder,第二个是codec:
    1. https://github.com/thejinchao/jpeg_encoder
    2. https://github.com/rockcarry/ffjpeg

    工程资源

    本文的代码可见于: https://github.com/WhiteRadiance/jpeg_codec --> 项目源码地址

    不装了,Plexer 和 WhiteRadiance 是同一个人。
    欢迎访问我的GitHub主页,有帮助的话请点击“Star” ☆→★,十分感谢!

    )
    File: /www/wwwroot/outofmemory.cn/tmp/route_read.php, Line: 126, InsideLink()
    File: /www/wwwroot/outofmemory.cn/tmp/index.inc.php, Line: 166, include(/www/wwwroot/outofmemory.cn/tmp/route_read.php)
    File: /www/wwwroot/outofmemory.cn/index.php, Line: 30, include(/www/wwwroot/outofmemory.cn/tmp/index.inc.php)
    JPEG编解码流程_C_内存溢出

    JPEG编解码流程

    JPEG编解码流程,第1张

    本文章的地址:https://blog.csdn.net/weixin_40279971/article/details/118523920
    欢迎阅读、点赞、批评、指正或引用。 O(∩_∩)O

    目录
      • 写在前面
      • JPEG文件格式介绍
      • JPEG编码流程-Encode
        • 1.分块
        • 2.DCT变换
        • 3.量化
        • 4.RLE&Huffman
      • JPEG解码流程-Decode
        • 1. 恢复编码数据
        • 2. 反量化
        • 3. DCT逆变换
        • 4. 合并块
      • 写在后面
      • 工程资源

    写在前面
    • 由于本人的懒惰,本文写于完成 Jpeg-Codec 工程的一年多之后,完成撰写的过程就是”朝花夕拾“的过程,虽然算是又复习了一遍编解码流程,但是内容的细致程度肯定远不及一年前的自己,还望体谅。不过退一步说,对于学生来说,学习和理解算法的思想就可以,而代码的具体实现很容易随着时间不断遗忘。
    • Jpeg是图片压缩领域的敲门砖,是有损压缩算法的入门考试。我是在搞完bmp格式后开始学习jpeg的,调试通过程序的瞬间现在还记忆犹新,这极大地刺激了我的积极性。走进音视频编解码的大门,还有png,mp3,H.264,AAC等等,它们的算法梳理了思想,它们的格式规范了实现,在此感谢前辈们的研究成果。
    JPEG文件格式介绍

    JPEG压缩算法定于1993年的CCITT T.81文件,我看完以后发现当年官方对JPEG的规范还是很复杂的(比如算数编码和子图),而且比较理论,并没有规定对应的计算机文件格式应该时怎样的。时光荏苒,目前最流行的JPEG文件格式仅仅是DCT+Huffman这一支了。

    计算机中的JPEG文件后缀名一般是*.jpg,*.jpeg,而其中的文件头主要就是JFIF和Exif,其中计算机生成的图片多使用JFIF,拍摄的照片多使用Exif,而文件头后面的压缩数据都是一样的。

    我这里的JPEG编解码专门针对JFIF,如果是Exif会直接略过其内容直接读取图像信息,如果实在需要查看相机的光圈快门等Exif信息可以在后面代码部分里判断Exif的地方自己添加哦。

    上面是JPEG文件头的常用段,其中APP0是JFIF段,APP1是Exif段,JPEG规定文件开头是SOI,结尾是EOI,EOI前面必须是压缩数据,压缩数据前面必须是SOS,其他段的顺序可能会有变化,所以读取文件头最好用switch case语句,这里给出一个参考顺序

    各个段的具体内容很容易就能搜到,我就不详细介绍了,下面给出我定义的结构体,我将会对每一个代码片段进行简单【批注】。
    首先是定义三个可能用到的类型 u8,u16,u32,这仨对于玩过单片机的同学应该很熟悉 O(∩_∩)O

    typedef unsigned char	u8;
    typedef unsigned short	u16;
    typedef unsigned int	u32;
    

    JPEG文件头结构体

    typedef struct s_DQT {
    	u16		label = 0xFFDB;			//Define Quantization Table (after Zigzaged)
    	u16		dqtlen = 67;			//0x43 = 2+1+64=67
    	u8		dqtacc_id = 0x00;		//acc=[7:4] 0:8bit, 1:16bit, id=[3:0] #0-3 (up to 4 tables)
    	u8		QTable[64] = { 0 };		//init QTable[128] if acc=1 e.g. 16bit
    }DQT;
    
    
    typedef struct s_DHT_DC {
    	u16		label = 0xFFC4;			//Define DC Huffman Table
    	u16		dhtlen = 31;			//0x1F = 2+1+16+12
    	u8		hufftype_id = 0x00;		//type=[7:4] 0:DC, 1:AC, id=[3:0] #0-1 (DC/AC id increases respectively)
    	u8		DC_NRcodes[16] = { 0 };	//how many codes in each code_length (1-16)
    	u8		DC_Values[12] = { 0 };	//Huffman tree num of node (不一定固定为12字节)
    }DHT_DC;
    typedef struct s_DHT_AC {
    	u16		label = 0xFFC4;			//Define AC Huffman Table
    	u16		dhtlen = 181;			//0xB5 = 2+1+16+162
    	u8		hufftype_id = 0x10;
    	u8		AC_NRcodes[16] = { 0 };
    	u8		AC_Values[162] = { 0 };	//不一定固定为162字节
    }DHT_AC;
    
    
    
    struct s_JPEG_header {
    	u16		SOI = 0xFFD8;			//Start Of Image
    
    	u16		APP0 = 0xFFE0;			//Application 0
    	u16		app0len = 16;			//APP0's len, =16 for usual JPEG e.g. no thumbnail
    	char	app0id[5] = "JFIF";		//"JFIF=" = 0x4A46494600
    	u16		jfifver 0x0101 ;//JFIF's version: 0x0101(v1.1) or 0x0102(v1.2)		=
    	u8		xyunit 0 ;//x/y density's unit, 0:no unit, 1:dot/inch, 2:dot/cm				=
    	u16		xden 0x0001 ;//x density			=
    	u16		yden 0x0001 ;//y density			=
    	u8		thumbh 0 ;//thumbnail horizon(width)				=
    	u8		thumbv 0 ;//thumbnail vertical(height)				;
    
    	DQT		DQT0//DQT for Luminance					;
    	DQT		DQT1//DQT for Chrominance					=
    
    	u16		SOF0 0xFFC0 ;//Start Of Frame			=
    	u16		sof0len 0x0011 ;//len = 17 for 24bit TrueColor		=
    	u8		sof0acc 0x08 ;//acc = 0 (8 bit/sample), optional: 0x08(almost), 0x12, 0x16			=
    	u16		imheight 0 ;//image height/pixel			=
    	u16		imwidth 0 ;//image width/pixel			=
    	u8		clrcomponent 3 ;//color components=3, e.g. JFIF只使用YCbCr(3),Exif这里也是3		=
    	u8		clrY_id 0x01 ;//Y_component_id=1			=
    	u8		clrY_sample 0x11 ;//Y_Hor_sam_factor=1, Y_Ver_sam_factor=1			=
    	u8		clrY_QTable 0x00 ;=		//Y -> QTable #0
    	u8		clrU_id 0x02 ;//U_component_id=2			=
    	u8		clrU_sample 0x11 ;//			=
    	u8		clrU_QTable 0x01 ;=		//U -> QTable #1
    	u8		clrV_id 0x03 ;//V_component_id=3			=
    	u8		clrV_sample 0x11 ;//			=
    	u8		clrV_QTable 0x01 ;;		//V -> QTable #1
    
    	DHT_DC	DHT_DC0//DHT for Luminance				;
    	DHT_AC	DHT_AC0;
    	DHT_DC	DHT_DC1//DHT for Chrominance				;
    	DHT_AC	DHT_AC1=
    
    	u16		SOS 0xFFDA ;//Start Of Scan			=
    	u16		soslen 12 ;//0x0C			=
    	u8		component 3 ;//Should equal to color component			=
    	u16		Y_id_dht 0x0100 ;//Y: id=1, use #0 DC and #0 AC		=
    	u16		U_id_dht 0x0211 ;//U: id=1, use #1 DC and #1 AC		=
    	u16		V_id_dht 0x0311 ;//V: id=1, use #1 DC and #1 AC		//always fit
    	=
    	u8		SpectrumS 0x00 ;//spectrum start		=
    	u8		SpectrumE 0x3F ;//spectrum end		=
    	u8		SpectrumC 0x00 ;//spectrum choose		//Here is filled with Compressed-Image-Data
    
    	=
    
    	u16		EOI 0xFFD9 ;//End Of Image			//init
    	s_JPEG_header
    	()[ { app0id0]= 'J' ;[ app0id1]= 'F' ;[ app0id2]= 'I' ;[ app0id3]= 'F' ;[ app0id4]= 0 ;} }
    ;
  • DHT中的Values数组长度是不固定的,要满足Values[].size()=NRcodes[].sum(),即NRcodes数组的总和等于Values数组的长度。NRcodes[16]指示1bit-16bit码长的码字数量,而Values[]指示各个码字对应的值,Values[]的长度不能超过256Byte。
  • 以上只是最基本的JPEG结构体,值得注意的是:

    • JPEG的DHT段中要先后出现四个Huffman表,对应YCbCr(以下简称YUV)中Y分量的DC/AC俩表和UV分量共用的DC/AC俩表。
    • T
    JPEG编码流程-Encode

    JPEG编码流程为:

    分块 => DCT变换 => 量化 => RLE&Huffman => BitString

    1.分块

    结构体中SOF0段的clr_sample代表采样率,YUV三个采样率都是0x11时代表YUV444采样,UV不变而Y为0x22时代表YUV411(也叫YUV420)采样。

    JPEG的YUV444采样准确来说是yuv444p,即 “ YYYY–UUUU–VVVV ” 的排列方式,Y/U/V三个分量的个数就是像素个数(实际上既然要分块,实际数量肯定是块的长宽的整数倍的,这里暂时按照普通YUV来讲),又因为YUV分量默认使用3个8bit来存(和RGB888一样哦),因此YUV文件的大小可以计算(估算)为=width×height×3Byte。

    JPEG的YUV420采样同样是指yuv420p,即 “ YYYY–U–V ” 的排列方式,田字格排布的四个Y共用一个U和一个V,(哈哈,这样你肯定又发现Y在某一个维度上的个数是奇数时有涉及补齐问题了,这都是后话了),此时YUV文件的大小可以计算(估算)为=width×height×3/2Byte。

    这里简单说一下为啥用YUV:Y代表明度Luminance,UV代表色度Chrominance。人眼对色度的细微差异不太敏感,所以如果对UV压缩4倍后都不咋看得出来差别,这是RGB模式达不到的压缩比;另外只保留Y分量时图片会变为黑白的,这时可以方便的兼容黑白电视。

    分块时yuv444p按照8×8分割成若干块或称为宏块(MCU,minimum coded unit),MCU中包含一个完整的Y,一个完整的U和一个完整的V,每个宏块编码后单独用YYYY-UUUU-VVVV的格式输出。

    yuv420p则按照16×16分割成MCU,内部划分为4个8×8的小宏块,因此每个MCU包含1个完整的Y,一个1/4的U和一个1/4的V,编码后按照YYYY YYYY YYYY YYYY-UUUU-VVVV输出。

    分块要注意补齐的问题,一般是复制补齐而不是补零。
    分块的代码融于for循环之中,不单独放出来了。

    2.DCT变换

    根据《数字图像处理》中的说法,以为边界存在更剧烈的不连续的情况,DFT比DCT更不适合用于图像时频转换,小波变换(WT,Wavelet Transform)比DCT优秀,WT用在了JPEG2000格式中了哈。
    DCT即离散余弦变换,它变换后只有实部,简单好用。实际代码中为了提速并不会真的用三角函数带进去猛算,而是用矩阵运算实现。因为两种MCU分块实际上都是8×8的小块,所以DCT退化为固定的矩阵运算,DCT正向运算为 Y=A·X·AT,逆运算为 Y=AT·X·A,下面是A矩阵以及Aconst矩阵:

    double [ DCT8][8]= 0.35355 {
    	{,0.35355	,0.35355	,0.35355	,0.35355	,0.35355	,0.35355	,0.35355	},0.49039
    	{,0.41573	,0.27779	,0.09756	,-	0.09756,-	0.27779,-	0.41573,-	0.49039},0.46194
    	{,0.19134	,-	0.19134,-	0.46194,-	0.46194,-	0.19134,0.19134	,0.46194	},0.41573
    	{,-	0.09755,-	0.49039,-	0.27779,0.27779	,0.49039	,0.09755	,-	0.41573},0.35355
    	{,-	0.35355,-	0.35355,0.35355	,0.35355	,-	0.35355,-	0.35355,0.35355	},0.27779
    	{,-	0.49039,0.09755	,0.41573	,-	0.41573,-	0.09755,0.49039	,-	0.27779},0.19134
    	{,-	0.46194,0.46194	,-	0.19134,-	0.19134,0.46194	,-	0.46194,0.19134	},0.09755
    	{,-	0.27779,0.41573	,-	0.49039,0.49039	,-	0.41573,0.27779	,-	0.09754}}
    ;static
    
    const double [ DCT_T8][8]= 0.35355 {
    	{,0.49039	,0.46194	,0.41573	,0.35355	,0.27779	,0.19134	,0.09755	},0.35355
    	{,0.41573	,0.19134	,-	0.09755,-	0.35355,-	0.49039,-	0.46194,-	0.27779},0.35355
    	{,0.27779	,-	0.19134,-	0.49039,-	0.35355,0.09755	,0.46194	,0.41573	},0.35355
    	{,0.09755	,-	0.46194,-	0.27779,0.35355	,0.41573	,-	0.19134,-	0.49039},0.35355
    	{,-	0.09755,-	0.46194,0.27779	,0.35355	,-	0.41573,-	0.19134,0.49039	},0.35355
    	{,-	0.27779,-	0.19134,0.49039	,-	0.35355,-	0.09755,0.46194	,-	0.41573},0.35355
    	{,-	0.41573,0.19134	,0.09755	,-	0.35355,0.49039	,-	0.46194,0.27779	},0.35355
    	{,-	0.49039,0.46194	,-	0.41573,0.35355	,-	0.27779,0.19134	,-	0.09754}}
    ;static
    

    DCT的物理意义其实还是时频转换,二维DCT之后得到的矩阵左上角为低频部分,右下角为高频分量,人眼对高频细节的敏感有限,因此可以减弱高频来进行有损压缩,这就是量化。

    3.量化

    JPEG的量化并不是PCM里的那个量化编码的量化,大白话讲出来就是乘数法,把DCT得到的8×8矩阵点除(对应元素间的运算)量化矩阵就完成量化 *** 作了…这里给个当年CCITT规范里的参考量化矩阵

    unsigned char [ Q_Y64]= 16 {
    	,11 ,12 ,14 ,12 ,10 ,16 ,14 ,13
    	,14 ,18 ,17 ,16 ,19 ,24 ,40 ,26
    	,24 ,22 ,22 ,24 ,49 ,35 ,37 ,29
    	,40 ,58 ,51 ,61 ,60 ,57 ,51 ,56
    	,55 ,64 ,72 ,92 ,78 ,64 ,68 ,87
    	,69 ,55 ,56 ,80 ,109 ,81,87 ,95
    	,98 ,103 ,104,103,62,77 ,113,121
    	,112,100,120,92,101 ,103,99}
    ;static
    unsigned char [ Q_UV64]= 17 {
    	,18 ,18 ,24 ,21 ,24 ,47 ,26 ,26
    	,47 ,99 ,66 ,56 ,66 ,99 ,99 ,99
    	,99 ,99 ,99 ,99 ,99 ,99 ,99 ,99
    	,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 }
    ;static
    

    规范里说是经过人眼视觉心理学得到的表,我反正觉得很悬,毕竟分母有点大了哈哈。量化表里的数字越小就会让JPEG压缩的图片越接近原图,换句话说就是数字越大压缩得越猛,图片越失真。再放一个我喜欢的量化表吧

    unsigned char [ Q_Y64]= 5 {
    	 ,3  ,4  ,4  ,4  ,3  ,5  ,4  ,4
    	 ,4  ,5  ,5  ,5  ,6  ,7  ,12 ,8
    	 ,7  ,7  ,7  ,7  ,15 ,12 ,12 ,9
    	 ,12 ,17 ,15 ,18 ,18 ,17 ,15 ,17
    	,17 ,19 ,22 ,28 ,23 ,19 ,20 ,26
    	,21 ,17 ,17 ,24 ,33 ,24 ,26 ,29
    	,29 ,29 ,31 ,31 ,31 ,19 ,23 ,34
    	,36 ,34 ,30 ,36 ,30 ,31 ,30 }
    ;static
    unsigned char [ Q_UV64]= 5 {
    	 ,5  ,5  ,7  ,6  ,7  ,14 ,8  ,8
    	 ,14 ,30 ,20 ,17 ,20 ,30 ,30 ,30
    	,30 ,30 ,30 ,30 ,30 ,30 ,30 ,30
    	,30 ,30 ,30 ,30 ,30 ,30 ,30 ,30
    	,30 ,30 ,30 ,30 ,30 ,30 ,30 ,30
    	,30 ,30 ,30 ,30 ,30 ,30 ,30 ,30
    	,30 ,30 ,30 ,30 ,30 ,30 ,30 ,30
    	,30 ,30 ,30 ,30 ,30 ,30 ,30 ,}
    ;:
    

    官方给的参考量化表压缩率还是蛮大的,不过2021年了,内存条和硬盘已经今非昔比了,JPEG的压缩率也不会像当年那么大了,我甚至见过几乎都是1的量化表,啧啧啧。

    量化完以后会有一步Zigzag的步骤,这是因为靠近右下角的高频分量本来就比较小,大概率会被量化到0,所以Zigzag之字形重新组织小宏块的矩阵数据可以让更多的0聚集在一起,方便RLE编码。
    上面给的量化表是已经Zigzag之后的表了,否则的话依然需要单独Zigzag *** 作下的。

    4.RLE&Huffman

    RLE,即游程编码(Run Length Coding)是最简单的图像压缩编码。
    简单来说就是把 [ 7, 6, 6, 0, 0, 0, 0, 2, 4, 4, 4, 4 ] 这串数字表示成 7(1), 6(2), 0(4), 2(1), 4(4)。
    上文说到量化和Zigzag后的矩阵数据将会出现大量的连0,这种情况使用RLE编码可以极大地节省码流。
    JPEG将RLE的含义转变,记录出现连0的个数,如果遇到16个0就合并标记为(15,0),比如:
    [ 5, 31, 0, 0, 23, 0, -30, -8, 0, 1 ] 将被编码为

    DC(5),: AC(0,31),(2,23),(1,-30),(0,-8),(2,1)(
    

    进一步地,(DC值因为是开头所以省略0)将每组第二个数的二进制码长添加到首位,得到:

    3,5),(0/5,31),(2/5,23),(1/5,-30),(0/4,-8),(2/1,1)// DC
    

    上述元组数据的前一个数据进行Huffman编码,后一个数据进行Magnitude编码。

    Huffman,即霍夫曼编码,属于VLC的一种,也叫最佳编码。
    简单来说就是用最短的码长表示出现最频繁的符号,这样得到的编码效率是最高的。但是因为构建二叉树的左右子树问题,导致Huffman树不是唯一的,为了便于解码,JPEG采用了范式Huffman构造树,它强制码字必须严格递增且码长可自推导,这样只需保留码字的字长分布就可以重构整个表。
    下面是codec工程里定义的标准码表:

    static
    const unsigned char [ Std_Lumi_DC_NRCodes16]= 0 { ,1 ,5 ,1 ,1 ,1 ,1 ,1 ,1 ,0 ,0 ,0 ,0 ,0 ,0 ,0 };static
    const unsigned char [ Std_Lumi_DC_Values12]= 0 { ,1 ,2 ,3 ,4 ,5 ,6 ,7 ,8 ,9 ,10 ,11 } ;static
    const unsigned char [ Std_Chromi_DC_NRCodes16]= 0 { ,3 ,1 ,1 ,1 ,1 ,1 ,1 ,1 ,1 ,1 ,0 ,0 ,0 ,0 ,0 } ;static
    const unsigned char [ Std_Chromi_DC_Values12]= 0 { ,1 ,2 ,3 ,4 ,5 ,6 ,7 ,8 ,9 ,10 ,11 } ;//AC
    
    static
    const unsigned char [ Std_Lumi_AC_NRCodes16]= 0 { ,2 ,1 ,3 ,3 ,2 ,4 ,3 ,5 ,5 ,4 ,4 ,0 ,0 ,1 ,0x7D } ;static
    const unsigned char [ Std_Lumi_AC_Values162]= 0x01 {
    	,0x02 ,0x03 ,0x00 ,0x04 ,0x11 ,0x05 ,0x12 ,0x21
    	,0x31 ,0x41 ,0x06 ,0x13 ,0x51 ,0x61 ,0x07 ,0x22
    	,0x71 ,0x14 ,0x32 ,0x81 ,0x91 ,0xa1 ,0x08 ,0x23
    	,0x42 ,0xb1 ,0xc1 ,0x15 ,0x52 ,0xd1 ,0xf0 ,0x24
    	,0x33 ,0x62 ,0x72 ,0x82 ,0x09 ,0x0a ,0x16 ,0x17
    	,0x18 ,0x19 ,0x1a ,0x25 ,0x26 ,0x27 ,0x28 ,0x29
    	,0x2a ,0x34 ,0x35 ,0x36 ,0x37 ,0x38 ,0x39 ,0x3a
    	,0x43 ,0x44 ,0x45 ,0x46 ,0x47 ,0x48 ,0x49 ,0x4a
    	,0x53 ,0x54 ,0x55 ,0x56 ,0x57 ,0x58 ,0x59 ,0x5a
    	,0x63 ,0x64 ,0x65 ,0x66 ,0x67 ,0x68 ,0x69 ,0x6a
    	,0x73 ,0x74 ,0x75 ,0x76 ,0x77 ,0x78 ,0x79 ,0x7a
    	,0x83 ,0x84 ,0x85 ,0x86 ,0x87 ,0x88 ,0x89 ,0x8a
    	,0x92 ,0x93 ,0x94 ,0x95 ,0x96 ,0x97 ,0x98 ,0x99
    	,0x9a ,0xa2 ,0xa3 ,0xa4 ,0xa5 ,0xa6 ,0xa7 ,0xa8
    	,0xa9 ,0xaa ,0xb2 ,0xb3 ,0xb4 ,0xb5 ,0xb6 ,0xb7
    	,0xb8 ,0xb9 ,0xba ,0xc2 ,0xc3 ,0xc4 ,0xc5 ,0xc6
    	,0xc7 ,0xc8 ,0xc9 ,0xca ,0xd2 ,0xd3 ,0xd4 ,0xd5
    	,0xd6 ,0xd7 ,0xd8 ,0xd9 ,0xda ,0xe1 ,0xe2 ,0xe3
    	,0xe4 ,0xe5 ,0xe6 ,0xe7 ,0xe8 ,0xe9 ,0xea ,0xf1
    	,0xf2 ,0xf3 ,0xf4 ,0xf5 ,0xf6 ,0xf7 ,0xf8 ,0xf9
    	,0xfa }
    ;static
    
    const unsigned char [ Std_Chromi_AC_NRCodes16]= 0 { ,2 ,1 ,2 ,4 ,4 ,3 ,4 ,7 ,5 ,4 ,4 ,0 ,1 ,2 ,0x77 } ;static
    const unsigned char [ Std_Chromi_AC_Values162]= 0x00 {
    	,0x01 ,0x02 ,0x03 ,0x11 ,0x04 ,0x05 ,0x21 ,0x31
    	,0x06 ,0x12 ,0x41 ,0x51 ,0x07 ,0x61 ,0x71 ,0x13
    	,0x22 ,0x32 ,0x81 ,0x08 ,0x14 ,0x42 ,0x91 ,0xa1
    	,0xb1 ,0xc1 ,0x09 ,0x23 ,0x33 ,0x52 ,0xf0 ,0x15
    	,0x62 ,0x72 ,0xd1 ,0x0a ,0x16 ,0x24 ,0x34 ,0xe1
    	,0x25 ,0xf1 ,0x17 ,0x18 ,0x19 ,0x1a ,0x26 ,0x27
    	,0x28 ,0x29 ,0x2a ,0x35 ,0x36 ,0x37 ,0x38 ,0x39
    	,0x3a ,0x43 ,0x44 ,0x45 ,0x46 ,0x47 ,0x48 ,0x49
    	,0x4a ,0x53 ,0x54 ,0x55 ,0x56 ,0x57 ,0x58 ,0x59
    	,0x5a ,0x63 ,0x64 ,0x65 ,0x66 ,0x67 ,0x68 ,0x69
    	,0x6a ,0x73 ,0x74 ,0x75 ,0x76 ,0x77 ,0x78 ,0x79
    	,0x7a ,0x82 ,0x83 ,0x84 ,0x85 ,0x86 ,0x87 ,0x88
    	,0x89 ,0x8a ,0x92 ,0x93 ,0x94 ,0x95 ,0x96 ,0x97
    	,0x98 ,0x99 ,0x9a ,0xa2 ,0xa3 ,0xa4 ,0xa5 ,0xa6
    	,0xa7 ,0xa8 ,0xa9 ,0xaa ,0xb2 ,0xb3 ,0xb4 ,0xb5
    	,0xb6 ,0xb7 ,0xb8 ,0xb9 ,0xba ,0xc2 ,0xc3 ,0xc4
    	,0xc5 ,0xc6 ,0xc7 ,0xc8 ,0xc9 ,0xca ,0xd2 ,0xd3
    	,0xd4 ,0xd5 ,0xd6 ,0xd7 ,0xd8 ,0xd9 ,0xda ,0xe2
    	,0xe3 ,0xe4 ,0xe5 ,0xe6 ,0xe7 ,0xe8 ,0xe9 ,0xea
    	,0xf2 ,0xf3 ,0xf4 ,0xf5 ,0xf6 ,0xf7 ,0xf8 ,0xf9
    	,0xfa }
    ;
  • 在编码结束后,将得到的二进制码进行拼接,得到二进制码流。码流(序列)可能不满足整除字节,这很正常,多出来的部分一般是补1’b0,代码里是用空Byte的位运算实现的,所以多出来bits的默认就是0。
    • “标准码表是咋来的,是否是固定的?”
    • 这里多一嘴,我猜会有小伙伴询问 https://www.cnblogs.com/Arvin-JIN/p/9133745.html,首先标准码表是ISO组织通过统计方法推荐的,可以应对大部分编码情况,不过显然此码表把几乎所有的大数值都编成了十几bits的码值,只意味着在极端情况下,此表的效率可能蛮低的哈哈;其次为了追求最优编码,应该对图片本身进行统计,生成适合该图的码表,笔者看过一些不同来源的图片的hex数据,发现PhotoShop生成的jpeg喜欢用自家的码表,而大多数jpeg都用的标准码表,考虑到现在带宽够大了,编码时图个方便的做法就更常见吧。

    本部分参考了下面的文章,对于还不太理解的同学,强烈建议看下:
    a. https://www.cnblogs.com/buaaxhzh/p/9138307.html --> JPEG原理解释
    b. https://www.cnblogs.com/buaaxhzh/p/9119870.html --> RLE&Huffman详解
    c. void --> 标准Huffman表

    JPEG解码流程-Decode

    JPEG解码流程为:

    恢复Huffman&RLE => 反量化 => 反DCT变换 => 合并块

    1. 恢复编码数据

    在编码部分的学习之后,相信你已经有如何解码的思路了,我在这里之说几处注意点:

    • 解码时必须使用图片指定的码表,用错表了就别指望解码成功了(苦笑)
      文件在hex模式下可以看到它指定的码表(见参考文章),用下面的代码来生成:
    Build_Huffman_Table (const* u8, nr_codesconst * u8, std_table* HuffType, huffman_table* \
    						u16, max_in_ThisLen[ u8 huff_len_table][130])unsigned
    {
    	char = pos_in_table 0 ;unsigned
    	short = code_value 0 ;for
    	( int= k 1 ;<= k 16 ;++ k)= {
    		u8 num 0 ;for
    		( int= j 1 ;<= j [ nr_codes-k 1 ];++ j)[ {
    			huff_len_table]k[]num= [ std_table]pos_in_table;++
    			num;[
    			huffman_table[std_table]pos_in_table].=code ; code_value[
    			huffman_table[std_table]pos_in_table].=len ; k++
    			pos_in_table;++
    			code_value;}
    		[
    		max_in_ThisLen]k= ; code_value<<=
    		code_value 1 ;}
    	}
    
  • Huffman解码时需要码字匹配,你可以对比特流进行逐bit匹配,也可以使用上述代码的方式,根据范式Huffman码字的特性,根据当前长度最大码字的查找表来进行更快的匹配,更可以用字典或哈希表来查找,总之里面优化的地方还是有的,但有点跑题了。
    • RLE解码后要对矩阵左上角的DC数据进行还原,因为编码时DC是逐次作差,还原时就应逐次求和。

    本部分参考了下面的文章,对于还不太理解的同学,强烈建议看下:
    a. https://blog.csdn.net/yun_hen/article/details/78135122 --> JPEG格式信息

    2. 反量化

    反量化就是将解码得到的矩阵数据点乘上量化矩阵,这里的量化矩阵也必须用图片指定的。
    反量化完记得进行反Zigzag *** 作。

    3. DCT逆变换

    已在“DCT变换”那一节解释过,同理。

    4. 合并块

    已在“分块”那一节讲过,同理。

    写在后面

    我自认为不是一个能够梳理好结构并循循善诱地讲解知识的人,毕竟自己目前对于jpeg的算法只能说熟悉,还做不到“通透”。本文是我的回忆之作,难免有遗漏之处,但所写皆是基于成功运行的代码测试之上,我有责任保证文章的正确性。我再给大家推荐两个别人的实现,第一个是encoder,第二个是codec:
    1. https://github.com/thejinchao/jpeg_encoder
    2. https://github.com/rockcarry/ffjpeg

    工程资源

    本文的代码可见于: https://github.com/WhiteRadiance/jpeg_codec --> 项目源码地址

    不装了,Plexer 和 WhiteRadiance 是同一个人。
    欢迎访问我的GitHub主页,有帮助的话请点击“Star” ☆→★,十分感谢!

    欢迎分享,转载请注明来源:内存溢出

    原文地址: http://outofmemory.cn/langs/713804.html

    (0)
    打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
    上一篇 2022-04-24
    下一篇 2022-04-24

    发表评论

    登录后才能评论

    评论列表(0条)

    保存