JPEG(Joint Photographic Experts Group)是联合图像专家组的英文缩写。 该组织从1986年正式开始制订静止数字图像的压缩编码标准,该标准于1992年正式通过,称为JPEG标准。
JPEG是第一个数字图像压缩的国际标准,它不仅适于静止图像的压缩,对 于电视图像序列的帧内压缩也常采用JPEG算法,因此JPEG是一个适用范围广泛的通用标准。
为了减少各分量之间的相关性,减少数据的冗余,通常会把RGB颜色空间转换成YUV来进行各分量的编码。
该步骤的作用是,图像内容平均亮度较高,将0电平移到中间,平均亮度降低, 便于DCT变换量化后直流的系数大大降低,也就降低了数据量。
将灰度级2 n 2^n2n的像素值,全部减去2 n − 1 2^{n-1}2n−1,数据形式由无符号数变为有符号数(补码),单极性数据变为双极性数据。
该步骤主要是用于去除图像数据之间的相关性,便于量化过程去除图像数据的空间冗余。
将图像分为8×8的像块;对于宽(高)不是8的整数倍的图像,使用图像边缘像素填充,以不改变频谱分布。然后对每一个子块进行DCT(Discrete Cosine Transform,离散余弦变换)。
DCT变换:
其中,C是8x8的DCT变换二维核矩阵,f ( x , y ) f(x,y)f(x,y)是原始的数据。由于DCT变换是一个正交变换,故CT = C−1。
变换核矩阵如下所示:
需要特别强调的是,DCT是一种无损变换,也无法对图像进行压缩,这样做的目的是在为下一步的量化做准备。
量化器主要是利用人眼视觉特性设计而成的矩阵量化DCT系数,减少视觉冗余。
将DCT变换后的临时结果,除以各自量化步长并四舍五入后取整,得到量化系数。
JPEG系统分别规定了亮度分量和色度分量的量化表,色度分量相应的量化步长比亮度分量大。
在量化步骤中,JPEG采用了中平型(Midtread)的均匀量化器。
量化是编码流程中唯一会引入误差也是唯一会带来压缩的步骤。Y、UV各一张表,共两张表。
8×8图像块经过DCT变换之后得到的DC直流系数有两个特点:
根据这个特点,JPEG算法使用了差分脉冲调制编码(DPCM)技术,对相邻图像块之间量化DC系数的差值DIFF进行编码:
DPCM是对于DC系数处理时所需要用到的预测编码方法。此方法在之前的实验中已经有详细说明。
对DPCM后算出的DIFF差值使用Huffman编码。
将其分成类别,类似于指数的Golomb编码(只不过Golomb是一元码+定长码),也就是类别ID使用规范哈夫曼编码,类内索引使用定长码(自然码)。
所以,DC系数会产生一张长度为16的Huffman码表。
对于量化后的数据,我们将其分为两路进行处理。一路是AC通路,一路是DC通路。ZigZag Scan+RLE是用于AC通路的,这是因为AC分量出现较多的0。JPEG采用对0系数的游程长度编码。而对非0值,则要保存所需数和实际值。
在编码之前,需要把二维的变换系数矩阵转换为一维序列,由于量化之后右下角高频系数大部分为零,采用ZigZag Scan读取可以制造较长的零游程,提高编码效率。在扫描中,如果后续的系数全部为零,则用“EOB”表示块结束。
在扫描后,采用RLE进行编码。
例:例如,现有一个字符串,如下所示:
57,45,0,0,0,0,23,0,-30,-8,0,0,1,000…
经过RLE之后,将呈现出以下的形式:
(0,57) ; (0,45) ; (4,23) ; (1,-30) ; (0,-8) ; (2,1) ; (0,0)
注意,如果AC系数之间连续0的个数超过16,则用一个扩展字节(15,0)来表示16连续的0。
所以,最后总共有4张Huffman码表(亮度DC,亮度AC,色度DC,色度AC)。
那么,这些码表如何存储?源数据又放在哪里?针对这些未解之谜,接下来我们就分析JPEG的存储结构。
某个图象的一个8*8方块的亮度值:
Level Offset 后:
DCT变换后:
量化后:
其中,参照的量化表是:
随后,对于这个8*8方块的亮度量化后的数据分别进行AC和DC两路的编码。
由于后面需要导出码表等操作,势必需要掌握jpeg的存储格式。因此,我们针对实验所用的testrgb-2x2.jpg,作一个完整解析。
JPEG以segment组成。每个segment都有一个名字,为其segment marker.我们逐一对每个marker进行分析。
整个文件以SOI开始,EOI结束。中间包含了APP0字段,两个DQT字段,一个SOF0字段,四个DHT字段,然后包含所有的ImageData数据。每个字段的作用都会在下面详细解释。
图像以SOI(Start of Image)标志图像开始,内容为固定值FFD8。
接下来是APP0字段。APP0字段是应用程序保留标记0。
该字段以FFE0开启,后面包含信息:
DQT就是DCT后的两张量化表,一张AC,一张DC。每张量化表都由FFDB字段开始。随后的4个字节说明了该字段的长度,然后存放的就是量化表。
量化表中的第一个字节被分成了高四位和低四位来用。高四位表示了该量化表的精度,0:8位;1:16位;低四位表示了量化表ID,取值范围为0~3;接下来是所有的表项,数量为(64×(精度+1))字节,里面都是量化的系数。量化表中的数据按照Z字形保存量化表内8x8的数据
。
该jpeg文件存放了0和1两张码表。
SOF0[0]为帧图像开始marker.以FFC0为开始标记
后两个字节标注数据长度;然后一个字节标注了每个颜色分量每个像素的位数(8),然后表明了行数和每行的采样点数,然后附上了三个components体信息。每个component都是一个颜色分量,内含颜色索引ID、Sample factor(高四位水平因子、低四位垂直因子)、和采用的量化表号。
DHT是存放Huffman码表的地方。该Marker以FFC4作为开始标记。然后是字段长度,类型(AC/DC),索引(Index),位表(bit table),值表(value table)。
表的内容如课件所示。
一共有4张DHT,对应AC/DC的Y/UV。
在大量的图像数据开始前,还有一个SOS字段。该字段表明了扫描开始,说明了数据是如何组织的。该字段以FFDA开始,然后表明了字段的长度,然后说明了颜色分量数,该与SOF字段中的数据应该是保持一致的。然后针对于每一个颜色分量信息,给出了每个分量的DC/AC使用的哈夫曼表编号。
然后是)谱选择开始、谱选择结束和谱选择固定值003F00。然后就是正式的图像数据了。
从main入口开始,我们观察到程序设定了两个模式,一个跑分模式(benchmark_mode)和一个转换图像模式(convert_one_image)。由于我们这次不涉及到跑分模式,我们只对转换图像模式(convert_one_image)进行分析。
我们跳入函数convert_one_image,查看里面干了什么。首先,将程序加载到内存中然后就关闭;然后解码jpeg(这是最主要的工作);然后获取图像大小;然后获取每个通道的内存地址;拥有了获取的这些信息后,才可以以想要的输出方式存储。然后系统对于解码后的文件进行了写出,以用户选择的模式进行写出。
在该函数中,设置了这些变量:
FILE *fp; //打开的文件
unsigned int length_of_file; //文件长度
unsigned int width, height; //宽、高
unsigned char *buf; //存储的buffer
struct jdec_private *jdec; //一个结构体指针
unsigned char *components[3]; //三个通道的指针
可以看到,后面的处理都是针对于jdec这个指针指向的jdec_private结构体进行处理的:
jdec = tinyjpeg_init(); //该函数用于初始化jdec结构体
tinyjpeg_parse_header(jdec, buf, length_of_file)//解析JPEG文件头
tinyjpeg_get_size(jdec, &width, &height); // 计算图像宽高
tinyjpeg_decode(jdec, output_format);// 解码实际数据
tinyjpeg_get_components(jdec, components); //获得每个通道的数据
所以,我们有必要对于jdec_private这个结构体进行一个分析。
jdec_private是每一个JPEG的码流中的一小块的结构体,包含了所有完整的内容的定义。
在jedc_private 中,定义了指向三个components的指针和三个components结构体(下面详述其意义);定义了图像的宽高;码流长度、始末指针;还有三张量化表(最终只用到两张,Y一张,UV一张);以及DC\AC各四张哈夫曼表(实际各用两张)。
components主要用于单个颜色通道的DCT变换后的值的存储;以及指明使用了哪个huffman_table和量化table。
huffman_table这个结构体主要用于存储所有的Huffman表。huffman表分为快速的查找表和慢速表。
在tinyjpeg_parse_header中,解析了JPEG的文件头。在读完SOI后,调用parse_JFIF对于每个Marker进行解析。这个解析过程持续到遇到了sos Marker,也就是扫描行开始。
while (!sos_marker_found)
我们输入的JPEG文件需要先解码DQT,也就是量化表。在这一步里,构建起了对应的编号的量化表。通过信息的剥离,在parse_DQT()首先找到了对应的需要创建的是哪张量化表,然后就开始调用build_quantization_table开始创建了。
static int parse_DQT(struct jdec_private *priv, const unsigned char *stream)
{
int qi; //该参数记录了采用的量化表号码
float *table; //指针,指向量化表开始
const unsigned char *dqt_block_end;//指针,指向量化表结束
#if TRACE
fprintf(p_trace,"> DQT marker\n");
fflush(p_trace);
#endif
dqt_block_end = stream + be16_to_cpu(stream);
stream += 2; /* Skip length */
while (stream < dqt_block_end)
{
qi = *stream++; //读入stream中的一个字节,该字节是流中的量化表系数,并赋值给qi
#if SANITY_CHECK
if (qi>>4)
snprintf(error_string, sizeof(error_string),"16 bits quantization table is not supported\n");
if (qi>4)
snprintf(error_string, sizeof(error_string),"No more 4 quantization table is supported (got %d)\n", qi);
#endif
table = priv->Q_tables[qi];
build_quantization_table(table, stream); //开始构建量化表
stream += 64;
}
#if TRACE
fprintf(p_trace,"< DQT marker\n");
fflush(p_trace);
#endif
return 0;
}
我们进入build_quantization_table进行创建量化表的查看:
static void build_quantization_table(float *qtable, const unsigned char *ref_table)
{
/* Taken from libjpeg. Copyright Independent JPEG Group's LLM idct.
* For float AA&N IDCT method, divisors are equal to quantization
* coefficients scaled by scalefactor[row]*scalefactor[col], where
* scalefactor[0] = 1
* scalefactor[k] = cos(k*PI/16) * sqrt(2) for k=1..7
* We apply a further scale factor of 8.
* What's actually stored is 1/divisor so that the inner loop can
* use a multiplication rather than a division.
*/
int i, j;
static const double aanscalefactor[8] = {
1.0, 1.387039845, 1.306562965, 1.175875602,
1.0, 0.785694958, 0.541196100, 0.275899379
};
const unsigned char *zz = zigzag;
for (i=0; i<8; i++) {
for (j=0; j<8; j++) {
*qtable++ = ref_table[*zz++] * aanscalefactor[i] * aanscalefactor[j];
}
}
}
从中我们可以看出,量化表的建立传入了两个参数,一个是正式的要写入的量化表qtable,另一个是参考表(reftable),是从流中读取的数据。
对于这个数据,首先程序设定了一个比例因子, scalefactor[0] = 1
;scalefactor[k] = cos(k*PI/16) * sqrt(2) for k=1…7,且存的是倒数,这是便于进行乘法运算的。
这里主要是因为,在编码的时候,进行了人眼视觉特性设计而成的矩阵量化DCT系数,解码的时候需要进行反操作。
且采用了zigzag扫描:
static const unsigned char zigzag[64] =
{
0, 1, 5, 6, 14, 15, 27, 28,
2, 4, 7, 13, 16, 26, 29, 42,
3, 8, 12, 17, 25, 30, 41, 43,
9, 11, 18, 24, 31, 40, 44, 53,
10, 19, 23, 32, 39, 45, 52, 54,
20, 22, 33, 38, 46, 51, 55, 60,
21, 34, 37, 47, 50, 56, 59, 61,
35, 36, 48, 49, 57, 58, 62, 63
};
因此,我们如果想要输出量化表,其实在这一步后面把ref_table表输出就可以了。码流里直接读出来的量化表是真正的量化表。后来计算出来的量化表,乘了比例因子,是为了辅助浮点dct ,idct 运算用的。如果是浮点运算,在编码端要乘比例因子,所以在解码端也要乘。具体的代码添加见下面。
接下来该JPEG头中是SOF,故解析了SOF,帧图像开始marker。这段解析较为简单,主要读出了图像的通道数量、图像的宽和高、每个通道的ID、水平垂直采样因子和使用的量化表等。
接下来就到了Huffman码表的解析。
length = be16_to_cpu(stream) - 2; // 表长(可能包含多张表) stream += 2; /* Skip length */
while (length>0) { // 是否还有表
index = *stream++;
/* We need to calculate the number of bytes 'vals' will takes */
huff_bits[0] = 0; count = 0;
for (i=1; i<17; i++) {
huff_bits[i] = *stream++; count += huff_bits[i];
}
if(index&0xf0)//AC 表
// (index&0xf), Huffman 表序号
build_huffman_table(huff_bits, stream, &priv->HTAC[index&0xf]); else // DC 表
build_huffman_table(huff_bits, stream, &priv->HTDC[index&0xf]);
length -= 1; length -= 16; length -= count; stream += count;
}
想要修改程序的输出,我们需要先对程序进行一个逻辑解读。从main入口开始,我们观察到程序设定了两个模式,一个跑分模式(benchmark_mode)和一个转换图像模式(convert_one_image)。由于我们要做的事情是修改程序输出为YUV,在这里我们忽略benchmark_mode。
我们跳入函数convert_one_image,查看里面干了什么。首先,将程序加载到内存中然后就关闭;然后解码jpeg(这是最主要的工作);然后获取图像大小;然后获取每个通道的内存地址;拥有了获取的这些信息后,才可以以想要的输出方式存储。
我们想要的存储方式是TINYJPEG_FMT_YUV420P,对应的处理函数是write_yuv。因此我们跳入这个函数进行查看:
static void write_yuv(const char *filename, int width, int height, unsigned char **components)
{
FILE *F;
char temp[1024];
snprintf(temp, 1024, "%s.Y", filename);
F = fopen(temp, "wb");
fwrite(components[0], width, height, F);
fclose(F);
snprintf(temp, 1024, "%s.U", filename);
F = fopen(temp, "wb");
fwrite(components[1], width*height/4, 1, F);
fclose(F);
snprintf(temp, 1024, "%s.V", filename);
F = fopen(temp, "wb");
fwrite(components[2], width*height/4, 1, F);
fclose(F);
}
因此,想要完成这个任务非常容易,只需要将所有内容写入到一个文件中,输出一个.yuv文件即可。
static void write_yuv(const char *filename, int width, int height, unsigned char **components)
{
FILE *F;
char temp[1024];
snprintf(temp, 1024, "%s.YUV", filename);
F = fopen(temp, "wb");
fwrite(components[0], width, height, F);
fwrite(components[1], width*height/4, 1, F);
fwrite(components[2], width*height/4, 1, F);
fclose(F);
}
将所有内容全部写入一个文件中,再次运行程序,已成功生成了我们所需的文件,打开进行查看:
至此,修改程序输出为YUV已完成。
我们想要在程序执行的过程中获得一些调试信息时候,可以使用之前打开的TRACE文件进行输出。TRACE可以在代码中的任意地方进行条件编译。比如下面这段:
#if TRACE
fprintf(p_trace,"< DQT marker\n");
fflush(p_trace);
#endif
也就是说,当TRACE被设定为1,程序的if条件成立,就会执行这段宏内的条件编译内容,完成对于trace文件的写入。
至于TRACE和输出文件的设定,是在cpp头宏定义的:
#define TRACE 1
#define TRACEFILE "trace_jpeg.txt"//add by nxn
所以,我们想关闭TraceFile的输出,设置TRACE=0即可。
在4.1小节我们已经说明,量化矩阵会在哪里产生,也就是build_quantization_table里。现在我们在该函数末尾加上一个TRACE,写上输出它的代码:
#if TRACE
const unsigned char* anotherzz = zigzag;
for (int i = 0; i < 8; i++) {
for (int j = 0; j < 8; j++) {
fprintf(p_trace, "%-6d", ref_table[*anotherzz++]);
if (j == 7) {
fprintf(p_trace, "\n");
}
}
}
#endif
输出结果:
默认的代码中已经输出了所有的Huffman码表。在3.3中我们可以看出,AC和DC的Huffman码表都是在build_huffman_table函数中完成建立的。因此我们进入这两个函数,然后对他们进行查看,并输出码表。函数中已经写好:
#if TRACE
fprintf(p_trace,"val=%2.2x code=%8.8x codesize=%2.2d\n", val, code, code_size);
fflush(p_trace);
#endif
我们可以在TRACE文件里看到所有码表内容。完整的输出可以在附录中查询到。
首先采用类似TRACEFILE文件的方式添加宏定义:
#define snprintf _snprintf//add by nxn
#define TRACE 1 1//add by nxn
#define TRACEFILE "trace_jpeg.txt"//add by nxn
#define OUTPUTACDC 1
#define OUTPUTACFILE "output_ac.yuv"
#define OUTPUTDCFILE "output_dc.yuv"
然后添加文件读写:
FILE *p_trace;//add by nxn
FILE* output_ac;
FILE* output_dc;
# if OUTPUTACDC
output_ac = fopen(OUTPUTACFILE, "wb");
output_dc = fopen(OUTPUTDCFILE, "wb");
# endif
图像的解码在解出jpeg头后的tinyjpeg_decode函数中完成。因此,我们跳转进该函数,首先添加需要开的buffer:
unsigned char* dcImgBuff;
unsigned char* acImgBuff;
unsigned char* uvBuff = 128;
int count = 0;
在循环里:
for (x=0; x < priv->width; x+=xstride_by_mcu)
{
decode_MCU(priv);
# if OUTPUTACDC
dcImgBuff = (unsigned char)((priv->component_infos->DCT[0] + 512.0) / 4 + 0.5); // DCT[0]为DC系数;DC系数范围-512~512;变换到0~255
acImgBuff = (unsigned char)(priv->component_infos->DCT[1] + 128); // 选取DCT[1]作为AC的observation;+128便于观察
fwrite(&dcImgBuff, 1, 1, output_dc);
fwrite(&acImgBuff, 1, 1, output_ac);
count++;
# endif
convert_to_pixfmt(priv);
priv->plane[0] += bytes_per_mcu[0];
priv->plane[1] += bytes_per_mcu[1];
priv->plane[2] += bytes_per_mcu[2];
if (priv->restarts_to_go>0)
{
priv->restarts_to_go--;
if (priv->restarts_to_go == 0)
{
priv->stream -= (priv->nbits_in_reservoir/8);
resync(priv);
if (find_next_rst_marker(priv) < 0)
return -1;
}
}
}
}
#if TRACE
fprintf(p_trace,"Input file size: %d\n", priv->stream_length+2);
fprintf(p_trace,"Input bytes actually read: %d\n", priv->stream - priv->stream_begin + 2);
fflush(p_trace);
#endif
return 0;
}
我们通过计数器,可以看到最后计数下来是4096个像素:
因此,在我们的打开图像时候应该选择64*64的4:2:0的图像。
可以看到效果:
使用python对图像进行统计
import numpy as np
import matplotlib.pyplot as plt
import collections
import pandas as pd
from collections import Counter
fraw = open("output_ac.yuv", "rb")
frec = open("output_dc.yuv", "rb")
raw = []
rec = []
i = 0
while i < 64 * 64:
i += 1
buf1 = fraw.read(1)
buf2 = frec.read(1)
if buf1:
buf1 = int.from_bytes(buf1, byteorder='big')
buf2 = int.from_bytes(buf2, byteorder='big')
raw.append(buf1)
rec.append(buf2)
a = Counter(raw)
b = Counter(rec)
x=[]
y=[]
x_dc=[]
y_dc=[]
for i in a:
x.append(i);
y.append(a[i])
x_dc.append(i)
y_dc.append(b[i])
# x是值,y是计数
plt.rcParams['font.sans-serif'] = ['Songti SC'] # 指定默认字体
fig,rgb = plt.subplots()
rgb.bar(x,y)
rgb.bar(x_dc,y_dc)
rgb.set_xlabel('值')
rgb.set_ylabel('出现频率')
rgb.set_title('AC/DC系数统计图')
rgb.legend()
plt.show()
trace_jpeg.txt
> Unknown marker e0
> DQT marker
2 1 1 2 2 4 5 6
1 1 1 2 3 6 6 6
1 1 2 2 4 6 7 6
1 2 2 3 5 9 8 6
2 2 4 6 7 11 10 8
2 4 6 6 8 10 11 9
5 6 8 9 10 12 12 10
7 9 10 10 11 10 10 10
< DQT marker
> DQT marker
2 2 2 5 10 10 10 10
2 2 3 7 10 10 10 10
2 3 6 10 10 10 10 10
5 7 10 10 10 10 10 10
10 10 10 10 10 10 10 10
10 10 10 10 10 10 10 10
10 10 10 10 10 10 10 10
10 10 10 10 10 10 10 10
< DQT marker
> SOF marker
> SOF marker
Size:1024x1024 nr_components:3 (????) precision:8
Component:1 factor:2x2 Quantization table:0
Component:2 factor:1x1 Quantization table:1
Component:3 factor:1x1 Quantization table:1
< SOF marker
> DHT marker (length=27)
Huffman table DC[0] length=10
val=04 code=00000000 codesize=02
val=05 code=00000001 codesize=02
val=06 code=00000002 codesize=02
val=03 code=00000006 codesize=03
val=07 code=0000000e codesize=04
val=02 code=0000001e codesize=05
val=01 code=0000003e codesize=06
val=00 code=0000007e codesize=07
val=09 code=000000fe codesize=08
val=08 code=000001fe codesize=09
< DHT marker
> DHT marker (length=60)
Huffman table AC[0] length=43
val=00 code=00000000 codesize=02
val=01 code=00000002 codesize=03
val=03 code=00000003 codesize=03
val=02 code=00000008 codesize=04
val=04 code=00000009 codesize=04
val=05 code=0000000a codesize=04
val=11 code=0000000b codesize=04
val=21 code=0000000c codesize=04
val=22 code=0000001a codesize=05
val=31 code=0000001b codesize=05
val=61 code=0000001c codesize=05
val=06 code=0000003a codesize=06
val=12 code=0000003b codesize=06
val=a1 code=0000003c codesize=06
val=32 code=0000007a codesize=07
val=41 code=0000007b codesize=07
val=62 code=0000007c codesize=07
val=13 code=000000fa codesize=08
val=51 code=000000fb codesize=08
val=23 code=000001f8 codesize=09
val=42 code=000001f9 codesize=09
val=71 code=000001fa codesize=09
val=81 code=000001fb codesize=09
val=91 code=000001fc codesize=09
val=15 code=000003fa codesize=10
val=52 code=000003fb codesize=10
val=63 code=000003fc codesize=10
val=07 code=000007fa codesize=11
val=14 code=000007fb codesize=11
val=33 code=000007fc codesize=11
val=53 code=000007fd codesize=11
val=16 code=00000ffc codesize=12
val=43 code=00000ffd codesize=12
val=08 code=00001ffc codesize=13
val=b1 code=00001ffd codesize=13
val=34 code=00003ffc codesize=14
val=c1 code=00003ffd codesize=14
val=24 code=00007ffc codesize=15
val=d1 code=0000fffa codesize=16
val=09 code=0000fffb codesize=16
val=72 code=0000fffc codesize=16
val=f0 code=0000fffd codesize=16
val=a2 code=0000fffe codesize=16
< DHT marker
> DHT marker (length=28)
Huffman table DC[1] length=11
val=05 code=00000000 codesize=02
val=06 code=00000001 codesize=02
val=07 code=00000002 codesize=02
val=04 code=00000006 codesize=03
val=03 code=0000000e codesize=04
val=02 code=0000001e codesize=05
val=00 code=0000007c codesize=07
val=01 code=0000007d codesize=07
val=0a code=0000007e codesize=07
val=08 code=000000fe codesize=08
val=09 code=000001fe codesize=09
< DHT marker
> DHT marker (length=43)
Huffman table AC[1] length=26
val=00 code=00000000 codesize=02
val=01 code=00000002 codesize=03
val=03 code=00000003 codesize=03
val=04 code=00000004 codesize=03
val=05 code=00000005 codesize=03
val=02 code=0000000c codesize=04
val=21 code=0000000d codesize=04
val=31 code=0000000e codesize=04
val=61 code=0000001e codesize=05
val=11 code=0000003e codesize=06
val=41 code=000000fc codesize=08
val=06 code=000001fa codesize=09
val=12 code=000001fb codesize=09
val=13 code=000001fc codesize=09
val=15 code=000001fd codesize=09
val=14 code=000003fc codesize=10
val=22 code=000003fd codesize=10
val=51 code=000003fe codesize=10
val=07 code=000007fe codesize=11
val=32 code=00000ffe codesize=12
val=23 code=00003ffc codesize=14
val=71 code=00003ffd codesize=14
val=08 code=00007ffc codesize=15
val=16 code=00007ffd codesize=15
val=33 code=00007ffe codesize=15
val=81 code=0000fffe codesize=16
< DHT marker
> SOS marker
ComponentId:1 tableAC:0 tableDC:0
ComponentId:2 tableAC:1 tableDC:1
ComponentId:3 tableAC:1 tableDC:1
< SOS marker
Use decode 2x2 sampling
Input file size: 111269
Input bytes actually read: 111268