深入解析JPEG图片解码技术

发表于2016-06-20
评论0 6.3k浏览

简介

 

JPEG是一种广泛适用的压缩图像标准方式。JPEG就是「联合图像专家组」(JointPhotographicExpertsGroup)的首字母缩写。采用这种压缩格式的文件一般就称为JPEG;此类文件的一般扩展名有:.jpeg.jfif.jpg.jpe,其中在主流平台最常见的是.jpg

 

JPEG/JFIF是互联网上最常见的图像存储和传送格式。但此格式不适合用来绘制线条、文字或图标,因为它的压缩方式对这几种图片损坏严重。PNGGIF文件更适合以上几种图片。不过GIF每像素只支持8bits色深,不适合色彩丰富的照片,但PNG格式就能提供JPEG同等甚至更多的图像细节。

 

Photoshop软件中以JPEG格式储存时,提供11级压缩级别,以0—10级表示。其中0级压缩比最高,图像品质最差。即使采用细节几乎无损的10 级质量保存时,压缩比也可达 51。以BMP格式保存时得到4.28MB图像文件,在采用JPG格式保存时,其文件仅为178KB,压缩比达到241。经过多次比较,采用第8级压缩为存储空间与图像质量兼得的最佳比例。

 

JPEG本身只有描述如何将一个视频转换为字节的数据流(streaming),但并没有说明这些字节如何在任何特定的存储媒体上被封存起来。JPEG的压缩方式通常是破坏性数据压缩(lossy compression),意即在压缩过程中图像的质量会遭受到可见的破坏,有一种以JPEG为基础的标准Lossless JPEG是采用无损的压缩方式,但Lossless JPEG并没有受到广泛的支持。

 

JPEG的压缩编码流程:

 

* 色彩模型(色彩空间转换)

 

   JPEG 的图片使用的是 YCrCb 颜色模型(YUV颜色编码,Y”表示明亮度(LuminanceLuma),“U”“V”则是色度、浓度(ChrominanceChroma)), 而不是计算机上最常用的 RGB。以每个点保存一个8bit的亮度值,每2x2个点保存一个Cr Cb值,而图象在肉眼中的感觉不会起太大的变化。所以,原来用 RGB 模型,4个点需要 4x3=12字节。而现在仅需要4+2=6字节,平均每个点占12bit。当然JPEG格式里允许每个点的C值都记录下来。不过 MPEG 里都是按12bit一个点来存放的, 我们简写为 YUV12

 

   YUV分量可以由PAL制系统中归一化(经过伽马校正)的R',G',B'经过下面的计算得到:

 

       

?
1
2
3
4
5
Y=0.299R'+0.587G'+0.114B'
 
U=-0.147R'-0.289G'+0.436B'
 
V=0.615R'-0.515G'-0.100B'

 

   ITU-R版本的公式:

 

        

?
1
2
3
4
5
6
7
8
9
10
11
12
13
Y=0.299*R+0.587*G+0.114*B (亮度)
 
Cb=-0.1687*R-0.3313*G+0.5*B+128
 
Cr=0.5*R-0.4187*G-0.0813*B+128
 
  
 
R=Y+1.402*(Cr-128)
 
G=Y-0.34414*(Cb-128)-0.71414*(Cr-128)
 
B=Y+1.772*(Cb-128)

 

* 缩减取样(Downsampling

 

    上面所作的转换使下一步骤变为可能,也就是减少UV的成份(称为"缩减取样""色度抽样"chroma subsampling)。在JPEG上这种缩减取样的比例可以是4:4:4(无缩减取样),4:2:2(在水平方向2的倍数中取一个),以及最普遍的4:2:0(在水平和垂直方向2的倍数中取一个)。对于压缩过程的剩余部份,YU、和V都是以非常类似的方式来个别地处理。

 

* 离散余弦变换(Discrete cosine transform

    将视频中的每个成份(Y, U, V)生成三个区域,每一个区域再划分成如瓷砖般排列的一个个的8×8子区域,每一子区域使用二维的离散余弦变换(DCT)转换到频率空间。经过这个变换,就把图片里点和点间的规律呈现出来了,更方便压缩。JPEG 里是对每8x8个点为一个单位处理的。所以如果原始图片的长宽不是8的倍数,都需要先补成8的倍数,好一块块的处理。另外,记得CrCb都是2x2记录一次吗? 所以大多数情况。是要补成16x16的整数块。按从左到右,从上到下的次序排列 (和我们写字的次序一样)JPEG 里是对YCrCb分别做DCT变换的。

 

   JPEG 编码时使用的是 Forward DCT (FDCT) 解码时使用的 Inverse DCT(IDCT),这个步骤很花时间,另外有种 AA&N 优化算法,在Intel主页上可以找到AA&N IDCT MMX优化代码。

 

    左上角之相当大的数值称为DC系数(直流系数);其他63个值称为AC系数(交流系数)。下面将对所有8×8表格中的DC系数使用差分编码,对AC系数使用进程编码。

 

* 重排列 DCT 结果

 

   DCT将一个8x8的数组变换成另一个8x8的数组。但是内存里所有数据都是线形存放的,如果我们一行行的存放这 64 个数字,每行的结尾的点和下行开始的点就没有什么关系,所以JPEG规定按如下次序整理64个数字。

 

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
UINT16 requant[DCTSIZE2] =
 
{
 
    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
 
}

            

    这样数列里的相邻点在图片上也是相邻的了。

 

* 量化(Quantization

 

    对于前面得到的64个空间频率振幅值,我们将对它们作幅度分层量化操作。方法就是分别除以量化表里对应值并四舍五入。

 

            

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
UINT16 requant[DCTSIZE2] =
 
{
 
    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,
 
}
 
  
 
    for (i = 0 ; i<=63; i++ )
 
        vector[i] = (int) (vector[i] / quantval[i] + 0.5)

 

    这张表依据心理视觉阀制作,对 8bit 的亮度和色度的图象的处理效果不错。当然我们可以使用任意的量化表。量化表是定义在jpegDQT标记后。一般为Y值定义一个,为 C 值定义一个。量化表是控制JPEG压缩比的关键。这个步骤除掉了一些高频量,损失了很高细节。但事实上人眼对高空间频率远没有低频敏感。所以处理后的视觉损失很小。另一个重要原因是所有的图片的点与点之间会有一个色彩过渡的过程。大量的图象信息被包含在低空间频率中。经过量化处理后,在高空间频率段,将出现大量连续的零。

 

* 编码(Coding

 

    颜色转换完成到编码之前,图像并没有得到进一步的压缩,DCT变换和量化可以说是为编码阶段做准备。

 

    编码采用两种机制:一是0值的行程长度编码;二是熵编码(EntropyCoding

 

   * 之字形排序(Zig-zag ordering

   * 使用RLE对交流系数(AC)进行编码

   * 使用DPCM对直流系数(DC)进行编码

   * 熵编码

 

    编码实际上是一种基于统计特性的编码方法。在JPEG中允许采用HUFFMAN编码或者算术编码。而基线JPEG算法(baselinesequential)采用的是前者。

 

   JPEG的压缩模式有以下几种:

 

   * 顺序式编码(Sequential Encoding):(基于DCT)一次将图像由左到右、由上到下顺序处理;

   * 递增式编码(Progressive Encoding):(基于DCT)当图像传输的时间较长时,可将图像分数次处理,以从模糊到清晰的方式来传送图像(效果类似GIF在网络上的传输);

   * 无有损编码(Lossless Encoding):(基于DPCM)保证解码后完全精确恢复到原图像采样值;

   * 阶梯式编码(Hierarchical Encoding):图像以数种分辨率来压缩,其目的是为了让具有高分辨率的图像也可以在较低分辨率的设备上显示。

 

文件结构

 

JPEG文件大体上可以分成两个部分:标记码(Tag)和压缩数据,JPEG文件格式中,一个字(16位)的存储使用的是Motorola格式,而不是Intel格式。也就是说,一个字的高字节(高8位)在数据流的前面,低字节(低8位)在数据流的后面,与平时习惯的Intel格式不一样。

 

标记码由两个字节构成,其前一个字节是固定值0xFF,后一个字节则根据不同意义有不同数值。在每个标记码之前还可以添加数目不限的无意义的0xFF填充,也就说连续的多个0xFF可以被理解为一个0xFF,并表示一个标记码的开始。而在一个完整的两字节的标记码后,就是该标记码对应的压缩数据流,记录了关于文件的诸种信息。常用的标记有SOIAPP0DQTSOF0DHTDRISOSEOI

 

完成的JPEG标记表内包含很多的标记码,有需要的可以去查阅相关的资料,这里简单地介绍下几个常用的标记。

 

JFIF格式的JPEG文件(*.jpg)的一般顺序为:

 

标记

标记码

作用

SOI

0xFFD8

图像开始

APP0

0xFFE0

JFIF应用数据块

APPn

0xFFEn

其他的应用数据块(n, 115)

DQT

0xFFDB

量化表

SOF0

0xFFC0

帧图像开始

DHT

0xFFC4

霍夫曼(Huffman)

SOS

0xFFDA

扫描线开始

压缩图像数据

EOI

0xFFD9

图像结束

 

 

所以我们可以看到-x里面jpg格式的判断函数:

 

            

?
1
2
3
4
5
6
7
8
9
10
11
bool Image::isJpg(const unsigned char * data, ssize_t dataLen)
{
    if (dataLen <= 4)
    {
        return false;
    }
 
    static const unsigned char JPG_SOI[] = {0xFF, 0xD8};
 
    return memcmp(data, JPG_SOI, 2) == 0;
}

 

关于jpeg标记码的详细信息这里先不介绍,后面手动压缩jpeg图片的时候再详细分析下。

 

    

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
//cocos2dx libjpg的解码
bool Image::initWithJpgData(const unsigned char * data, ssize_t dataLen)
{
#if CC_USE_JPEG
    /* these are standard libjpeg structures for reading(decompression) */
    struct jpeg_decompress_struct cinfo;
    /* We use our private extension JPEG error handler.
     * Note that this struct must live as long as the main JPEG parameter
     * struct, to avoid dangling-pointer problems.
     */
    struct MyErrorMgr jerr;
    /* libjpeg data structure for storing one row, that is, scanline of an image */
    JSAMPROW row_pointer[1] = {0};
    unsigned long location = 0;
 
    bool ret = false;
    do
    {
        /* We set up the normal JPEG error routines, then override error_exit. */
        //设置异常处理
        cinfo.err = jpeg_std_error(&jerr.pub);
        jerr.pub.error_exit = myErrorExit;
        /* Establish the setjmp return context for MyErrorExit to use. */
        if (setjmp(jerr.setjmp_buffer))
        {
            /* If we get here, the JPEG code has signaled an error.
             * We need to clean up the JPEG object, close the input file, and return.
             */
            jpeg_destroy_decompress(&cinfo);
            break;
        }
 
        /* setup decompression process and source, then read JPEG header */
        //初始化jpeg_decompress_struct类型结构体,libjpeg内部使用
        jpeg_create_decompress( &cinfo );
 
#ifndef CC_TARGET_QT5
        jpeg_mem_src(&cinfo, const_castchar*="">(data), dataLen);
#endif /* CC_TARGET_QT5 */
 
        /* reading the image header which contains image information */
#if (JPEG_LIB_VERSION >= 90)
        // libjpeg 0.9 adds stricter types.
        jpeg_read_header(&cinfo, TRUE);
#else
        jpeg_read_header(&cinfo, TRUE);
#endif
 
        // we only support RGB or grayscale
        //设置输出的颜色格式,cocos2dx 3.2的版本暂时支持RGB和I8的格式
        if (cinfo.jpeg_color_space == JCS_GRAYSCALE)
        {
            _renderFormat = Texture2D::PixelFormat::I8;
        }else
        {
            cinfo.out_color_space = JCS_RGB;
            _renderFormat = Texture2D::PixelFormat::RGB888;
        }
 
        /* Start decompression jpeg here */
        //开始解码
        jpeg_start_decompress( &cinfo );
 
        /* init image info */
        //初始化成员变量
        _width  = cinfo.output_width;
        _height = cinfo.output_height;
        _hasPremultipliedAlpha = false;
 
        //申请内存
        _dataLen = cinfo.output_width*cinfo.output_height*cinfo.output_components;
        _data = static_castchar*="">(malloc(_dataLen * sizeof(unsigned char)));
        CC_BREAK_IF(! _data);
 
        /* now actually read the jpeg into the raw buffer */
        /* read one scan line at a time */
        //逐行扫描像素信息,存入指定的内存当中
        while (cinfo.output_scanline < cinfo.output_height)
        {
            row_pointer[0] = _data + location;
            location += cinfo.output_width*cinfo.output_components;
            jpeg_read_scanlines(&cinfo, row_pointer, 1);
        }
 
    /* When read image file with broken data, jpeg_finish_decompress() may cause error.
     * Besides, jpeg_destroy_decompress() shall deallocate and release all memory associated
     * with the decompression object.
     * So it doesn't need to call jpeg_finish_decompress().
     */
    //jpeg_finish_decompress( &cinfo );
        //释放内存
        jpeg_destroy_decompress( &cinfo );
        /* wrap up decompression, destroy objects, free pointers and close open files */
        ret = true;
    } while (0);
 
    return ret;
#else
    return false;
#endif // CC_USE_JPEG
}

 

libjpeg-turbo

 

libjpeg-turbo是一种使用了SIMD指令(MMXSSE2NEON)来加快图片在x86x86-64以及其他ARM系统上的基线压缩和解压的JPEG解码器,是libjpeg的一个扩展。在上述系统中,在其他条件相同的情况下,libjpeg-turbolibjpeg2-4倍速度。在其他类型的系统中,libjpeg-turbo同样可以通过高度优化的哈夫曼编码程序比libjpeg速度更快。在很多情况下,libjpeg-turbo都是非常好用的jpeg解码器。

 

详细的介绍可以去libjpeg-turbo主页

 

可以自己编译源码,也可以下载已经编译好的链接库。导入到cocos2dx中非常简单,添加libjpeg.alibjpeg-turbo.a两个链接库,包含相应的include文件夹即可。

 

BPG

BPG(全称 Better Portable Graphics),它由著名的法国程序员 Fabrice BellardFFmpegQEMU的作者)设计并提出。其优势在于具有更高的压缩率,在相同图像质量下,BPG文件的大小只有JPEG的一半。此外它还原生支持8位和16位通道。

 

详细的介绍可以去Bellard主页查看。

 

我下载的源码进行了编译,在文件大小和图像质量的表现上都很不错,不过目前0.9.5版本不支持x86-64架构,期待作者以后的更新。

 

扩展链接

 

1YUV颜色编码

 

Y'UV的发明是由于彩色电视与黑白电视的过渡时期[1]。黑白视讯只有YLumaLuminance)视讯,也就是灰阶值。到了彩色电视规格的制定,是以YUV/YIQ的格式来处理彩色电视图像,把UV视作表示彩度的CChrominanceChroma),如果忽略C信号,那么剩下的YLuma)信号就跟之前的黑白电视信号相同,这样一来便解决彩色电视机与黑白电视机的相容问题。Y'UV最大的优点在于只需占用极少的带宽。

 

彩色图像记录的格式,常见的有RGBYUVCMYK等。彩色电视最早的构想是使用RGB三原色来同时传输。这种设计方式是原来黑白带宽的3倍,在当时并不是很好的设计。RGB诉求于人眼对色彩的感应,YUV则着重于视觉对于亮度的敏感程度,Y代表的是亮度,UV代表的是彩度(因此黑白电影可省略UV,相近于RGB),分别用CrCb来表示,因此YUV的记录通常以Y:UV的格式呈现。如下图所示:

 


  腾讯GAD游戏程序交流群:484290331Gad游戏开发核心用户群

如社区发表内容存在侵权行为,您可以点击这里查看侵权投诉指引