首页 > 代码库 > ComicEnhancerPro 系列教程十八:JPG文件长度与质量

ComicEnhancerPro 系列教程十八:JPG文件长度与质量

作者:马健
邮箱:stronghorse_mj@hotmail.com
主页:http://www.comicer.com/stronghorse/
发布:2017.07.23

教程十八:JPG文件长度与质量

众所周知,JPG是一种“有损”压缩格式,与PNG等无损压缩格式相比,最大的问题是:如果反复压缩,会造成图像质量逐渐退化。所以在对JPG文件进行处理,并且输出仍然选择JPG格式的情况下,很多人都会问同样的一个问题:如何才能在尽情享受有损压缩带来的较小文件长度的便利前提下,尽量避免图像质量退化?

为了解决这个问题,从v4.13开始,除传统的JPG质量系数外,在CEP中还允许对JPEG质量进行更精细的控制,界面如下图所示:

技术分享
图1

总体来说,允许对三个方面的参数进行控制:

  1. 是否使用原JPG文件的质量系数,其实就是是否要复制原JPG文件的量化表(Quantization Tables)。如果您觉得转存后的JPG文件长度与原JPG文件长度相差太多,请勾选此项。
  2. 颜色采样,就是指定是否要进行子采样(sub-sampling),或通俗点说压缩的时候是否要把颜色信息丢掉一半或3/4,解码的时候再用插值法找补回来。如果丢,毫无疑问文件长度会减小,但颜色退化就是不可避免的;如果不丢,颜色是保住了,但文件长度就要大一些。“自动”的解释见CEP使用说明,其实就是学Photoshop,根据用户指定的质量系数判断在用户的心目中是质量更重要还是文件长度更重要,然后自动决定是否 对颜色缩水。
  3. 是否对JPG进行优化编码。这个对JPG文件质量无影响,但对文件长度有影响,参见教程二十的量化测试。

如果三个“优先”都被勾选,那么当您用CEP打开一个JPG文件,然后什么也不做就另存为另外一个JPG文件,则两个文件的长度和质量应该差别都不大——注意我说的是“不大”,不是“没有”。长度差异一个DIR命令就知道,图像差异可以用眼睛看,也可以用CEP v4.13开始提供的“图像比较”功能进行量化比较,详见教程二十。

如果想深入了解这些参数究竟如何影响JPG文件长度与质量,可以阅读著名小众软件JPEGsnoop的作者Calvin Hass写的系列文章,以下是其中关键的几篇:
JPEG Compression, Quality and File Size
JPEG Color Space Conversion Error
JPEG Chroma Subsampling
JPEG Compression Quality from Quantization Tables
JPEG Huffman Coding Tutorial
What is an Optimized JPEG?

为便于后续理解,我先从上面第一篇文章中摘取对JPG压缩过程的说明,做无责任翻译(“无责任”的意思就是胡言乱语,各路专家不必较真,也谢绝较真):

  1. 色彩空间转换(Color Space Conversion)。彩色图像首先要经历色彩空间转换,从RGB(红/绿/蓝)转换成YCbCr(亮度/蓝色度/红色度)。这种色彩空间转换促使后续过程使用不同的量化表(quantization tables),其中一个量化表用于亮度(luminance),另一个量化表用于色度(chrominance)。译注:除RGB转YCbCr外,JPEG中还有一种常见转换是CMYK色彩空间转YCCK色彩空间。灰度图像没有色彩信息,也就不存在色彩空间转换。
  2. 分段切块(Segmentation into Blocks)。原始图像数据被按照8×8的像素尺寸切分成小块,称为最小编码单元(Minimum Coded Unit,MCU)。这意味着JPEG压缩算法严重依赖于这些块边界的位置和排列。
  3. 离散余弦变换(Discrete Cosine Transformation,DCT)。图像从空间域(空域)转换成频率域(频域)表示。这可能是整个过程中最乱和最难以理解的一步。简单而言,图像内容被转换成用一系列谐波(正弦波)合成的数学表示。例如,二进制串 101010 可以表示为每两个像素重复一次的谐波,串 1100110011可以表示为每4个像素重复一次,串 1001101011可以表示为几个谐波之和。现在请想象沿着X和Y方向做这种波动方程(也称为DCT基函数)的映射。
  4. 量化(Quantization)。在DCT步骤中生成的波动方程,按照从低频成分(在图像块中要跨越很长距离才发生变化的部分)到高频成分(可能每个像素都会有变化的成分) 的顺序存储。众所周知人眼对低频信息的误差要比对高频信息的更敏感。JPEG压缩算法抛弃了很多这种高频(多半是噪声)细节,同时保持变化缓慢的图像信息。这个过程是通过把所有方程系数按照量化表中的对应值进行分类,然后再圆整到最近的整数值。具有较小的系数或在量化表中有较大除数的成分将被圆整成零。质量设置得越低,量化表中的除数越大,导致被圆整成零的可能性也越大。反之,最高的质量设置将产生全部值都是1的量化表,意味着所有DCT数据都可以保留。
    这里需要注意的重要一点是此步骤所使用的量化表,对几乎所有数码相机和软件包而言都是不同的(译注:在IJG代码中,质量系数100%将产生全1的量化表。PS软件即使最高质量12也产生不了全1的量化表,相机固件就更不用说,要追求无损不如用RAW,可以参阅教程十九中给出的数据,或主要相机厂家、IrfanView的量化表)。由于这是对压缩或重新压缩所产生的“误差”贡献最大的因素,因此在不同的压缩器/源之间转存时都要忍受 (量化表不一致所带来的)图像退化的痛苦。相机厂家独立地选择一个任性的“图像质量”名称(或级别)来代表他们所设计的64值量化矩阵,因此在不同的厂家甚至相同的厂家之间都不能 按名字进行比较(如佳能的“Fine”与尼康的“Fine”)。
  5. “之”字扫描(Zigzag Scan)。量化产生的矩阵会有很多零。质量设置越低,矩阵中的零就越多。把矩阵从左上角开始按照“之”字扫描,重新排列成64元素的向量,其顺序将是从低频成分到高频成分。由于高频成分最有可能被圆整成零, 因此这个64元素的向量通常以一串零结尾。这对下一步非常重要。
  6. 直流分量上的差分脉冲调制编码(DPCM on DC component)。在前述分块基础上,均值(整个8×8块的均值,直流分量)的变化被按照与前一个块的均值之差进行编码。这被称为差分脉冲调制编码(Differential Pulse Code Modulation,DPCM)。
  7. 交流分量上的行程编码(RLE on AC components)。对于64值向量中的每一个值(交流分量),采用行程编码(Run Length Encoding,RLE)来存储。由于1×64向量中含有大量的零,高效的存储方法是存非零值及非零值之间零的数量。RLE存储的是“跳过”(skip)与“值”(value),其中“跳过”是 在分量之前零的数量,“值”是下一个非零分量。
  8. 熵编码/霍夫曼编码(Entropy Coding / Huffman Coding)。创建一个字典,用来对经常用到的值串进行缩位表示。常用串/模板使用较短的编码(只有几个bit的编码),而不常使用的串则采用较长的编码。所用的字典(霍夫曼表,Huffman table)存储在文件里,因此很容易查找字典把编码后的bit串恢复成原始值,参见JPEG Huffman Coding tutorial。

如果上面的翻译还是看得眼晕,可以再去看看hesays写的中文文章《JPEG图像编码解码》,或《常用多媒体文件格式与压缩标准解析》(姜楠,王健编著,电子工业出版社,2005.05,SSID=11417553)的第5章。

对于JPEG有损压缩的误差来源,Calvin Hass认为最大的来源是量化过程。用户可以通过选择压缩系数或质量系数对量化表进行控制,从而影响最终JPEG文件的文件长度和质量。其他来源,如色彩空间转换的舍入误差,其重要性都不如量化误差。因此对JPEG进行无损转存的唯一方法是不要经过解压过程,直接对MCU进行操作以避开量化过程。在IJG提供的命令行程序jpegtran中,就是通过直接操作MCU实现JPEG文件的无损90度旋转、 镜像翻转、裁剪(左上角必须是MCU的边界)等,我怀疑收费软件cPicture中的JPEG无损旋转、裁剪就是学它的,只不过加了图形界面。

我个人的看法:

  1. JPG文件的质量退化源自其“有损”压缩过程,其中的关键环节包括:色彩空间转换(从RGB色彩空间转换至YCbCr色彩空间, 从CMYK色彩空间转换至CYYK色彩空间)、色彩信息缩水(我一直觉得sub-sampling翻译成“子采样”实在太过文绉绉,听起来蛋疼得紧,既然现在不是在写学术论文,以后还是意译成更通俗的“缩水”吧)、离散余弦变换(DCT)结果通过量化表进行量化
  2. 色彩空间转换的误差来源是浮点运算后的取整圆整(转换公式请百度“RGB转YCbCr”), 即计算过程中用到了浮点数,但最终结算结果要圆整成不超过255的整数。这是人无法控制的事情,即使您把JPG质量系数设置为100%或Photoshop中的12,也管不到这一步。所以指望常规JPG可以无损压缩的人,看到这里 就可以死心了。 当然这种误差可能会小到肉眼看不出来的程度,参见教程二十里的测试。
  3. 色彩信息缩水是基于这样一个心理学模型:与色彩的变化相比,人类对明暗的对比或变化更敏感。所以JPG压缩的第一步就是色彩空间转换,即把色彩与明暗混为一体的RGB色彩空间转换成明暗、色彩分离的YCbCr色彩空间,其中Y是明暗 (亮度)信息,Cb、Cr是颜色(色度)信息,然后在编码时就可以把Cb、Cr信息沿纵、横方向缩减以减少需编码的信息量从而减小文件长度,解码时再用插值算法恢复成原大小,反正插值误差肉眼也不大看得出来。当然这种缩减也是讲档次的,比较有追求的就干脆不缩减,比如Photoshop里质量7以上 或CEP里质量系数90%以上;稍微差一点的就只在一个方向上进行缩减,另外一个方向不变,即颜色信息量缩减为1/2,比如尼康、佳能相机直出的JPEG文件 或CEP里质量系数70%~89%;比较不讲究的就在纵、横方向上同时缩减为原来的1/2,即信息量缩减为原来的1/4以获得最小文件长度,比如手机,Photoshop里质量1~6,或早期版本的CEP就是这么玩的。
    当然,以上两点只有彩色图像才会有,灰度图像没有色彩信息,自然不需要转换或缩减色彩。
    Photoshop、相机、手机的色度缩水系数参见《教程十九:用JpegQuality看JPG文件的压缩参数》。
    在Calvin Hass的其它文章中都提到了色度缩水的影响,但不知道为什么在JPEG Compression, Quality and File Size中没有提。
  4. 常规JPG质量系数控制的其实是DCT量化表,即把质量系数按公式转换成一个倍率系数(factor),与JPG标准里推荐的标准量化表相乘,作为量化时的除数。所以如果 能解码出JPG文件里存放的量化表,并且知道从质量系数到量化表的计算过程,其实是可以反算出压缩JPG文件时所选的质量系数的。但实际上只有开源的代码库 (如IJG)才真正知道其计算过程,商业的Photoshop等反算出来的质量系数只能说是“参考”。但即使是仅供参考的质量系数,也马马虎虎可以用来比较两个文件最后一次压缩的时候谁压得更狠一点。
  5. DCT量化之后的编码过程对图像质量无影响(参见教程二十的证明),但优化编码、渐进(progressive)编码在一定程度上可以减小文件长度,只不过编码时要先额外对图像扫描一遍甚至多遍才能找准优化方向,所以耗时略长。

JPEGsnoop的作者Calvin Hass更进一步认为:

  1. 如果转存JPG文件时继续使用原JPG文件的量化表、色彩缩减倍数,则可以认为转存过程是无损(Recompress Losslessly)。这就是在CEP的JPG选项里“优先使用原JPG质量系数”、“优先使用原JPG采样参数”的含义。当然从我个人的理解,这个“无损”其实也不是真正的无损,比如原JPG的色彩信息是纵、横各缩水一半,转存时仍然要再次缩水,您说是有损还是无损?所以按照我的理解,应该是这样说:如果继续沿用原JPG文件的量化表、色彩缩减倍数,可以在转存时获得与原JPG文件差不多的文件长度和质量。 具体证明过程请参见教程二十。
  2. 因为很多软件、相机所使用的量化表和色彩缩水倍数都各有其特色,所以Calvin Hass认为这代表了JPG生成器的指纹(fingerprint), 在JPEGsnoop中也用这两个参数来识别生成JPG文件的软件、相机,其他人则用这个功能来识别图像是原生的还是被P过的。不过我个人觉得JPEGsnoop的输出实在是太令人眼花缭乱了,所以做了一个简化的软件JpegQuality,并且与CEP做了集成,可以直接从CEP启动以查看当前JPG文件的压缩参数 ,包括质量系数、颜色缩水倍数等,详见《教程十九:用JpegQuality看JPG文件的压缩参数》。

针对上面的第1点,除CEP外,在我的其它一些软件中也采用了类似的技术进行JPG文件转存,包括:

  • Pdg2Pic:在“JPG文件修复”中,以前要手工指定重新压缩的JPG质量系数,从v4.09开始改成从原JPG文件中读取量化表、颜色缩水倍数,不再需要手工指定JPG质量系数。如果从源JPG文件中读取不到这些参数,只能说JPG文件损坏得很彻底,谁来了也修不好了。
  • FreePic2Pdf:Acrobat(我测试的版本是Adobe Acrobat XI Pro)对PDF中嵌入的JPG图像支持是有限的,某些用IJG能正常解码的JPG文件如果直接嵌入PDF,在Acrobat中解码时会出现错位。在将图像转成PDF时,如果检测到这种文件,就需要重新编码,然后再嵌入PDF。重新编码后自然希望文件长度、质量基本不变。
  • PdfToy:在导出PDF中的JPG图像时,有时希望实现自动反相(反白),否则直接导出的图像可能是黑漆漆一片。这个完全无损是不可能的,只能是老老实实解码、反白、重新编码,仍然要求重新编码后文件长度、质量基本不变。

在上面的叙述中,JPG质量系数即量化表对JPG质量的影响可能大家还比较好理解,但颜色缩水对质量的影响可能就有人要较真了。这里提供一个例子,先是原图(出自MyReader使用说明书的插图):

技术分享
图2

在CEP中打开后JPG质量系数点到95,颜色采样方式选“自动”,其实就是不缩水,另存为JPG后的结果:

技术分享
图3

在JPG质量系数不变的情况下,只是把颜色采样方式从“自动”改成“手动”,然后勾选“水平方向缩一半”和“垂直方向缩一半”,则效果变成:

技术分享
图4

把图片从网页里另存出来,用看图软件来回切换着看,可以看出颜色缩水后文件长度确实比不缩水的要小,但即使质量系数95%,红色仍然显得黯淡了许多(图4)。而颜色不缩水的JPG图像与原始PNG图像相比则肉眼基本看不出差距 (图3)。但不论是否缩水,JPG文件长度都比原PNG文件(图2)更大,这是因为我故意选择了一张不适合用JPG文件格式存储的人工(artificial)图像,一方面其中的渐变部分更容易看出差距,另一方面是想说明JPG格式并不是包打天下的,总有其适用范围(如颜色数较多的自然图像,包括风光、人物照片等),也有其不适用的范围(如颜色数较少的人工图像),算是给狂热的 坚持JPG格式可以包打天下的JPG偏执狂们浇点冷水。

杂七杂八写到这里,可能还是有人拎不清在CEP里究竟应该怎么设置JPG质量,以下是一些建议:

  • CEP的设计目标本来就不是给专家用的,所以如果实在拎不清,就还是用缺省设置吧,在大多数情况下可以获得相对均衡的结果:高质量系数时确保颜色无损失,低质量系数时则文件长度优先。
  • 如果处理从PDG转出来的JPG后感觉文件长度膨胀得太厉害,有点难以接受,就把三个“优先”选项全部都勾选。CX的JPG质量系数不是一般人敢选的,还是直接copy它吧。
  • 如果您真的对JPG质量很在意,就保持其它选项为缺省,尤其是“颜色采样方式”一定要选“自动”,然后增加JPG质量系数,直到100%。
  • 如果想尝试不同的颜色缩减比例的效果,可以把“颜色采样方式”改成“手动”,然后改变“水平方向缩一半”、“垂直方向缩一半”选项试试看。

附录 IJG中从质量系数到量化表的计算过程的源代码

在使用IJG把图像压缩成JPG时,通常调用下面的代码设置质量系数(出自IJG的示例源代码cjpeg.c)

 /* Set quantization tables for selected quality. */
/* Some or all may be overridden if -qtables is present. */
jpeg_set_quality(cinfo, quality, force_baseline);

而jpeg_set_quality函数及其所调用函数的实现如下:

GLOBAL(void)
jpeg_set_quality (j_compress_ptr cinfo, int quality, boolean force_baseline)
/* Set or change the ‘quality‘ (quantization) setting, using default tables.
* This is the standard quality-adjusting entry point for typical user
* interfaces; only those who want detailed control over quantization tables
* would use the preceding three routines directly.
*/
{
/* Convert user 0-100 rating to percentage scaling */
quality = jpeg_quality_scaling(quality);

/* Set up standard quality tables */
jpeg_set_linear_quality(cinfo, quality, force_baseline);
}
GLOBAL(int)
jpeg_quality_scaling (int quality)
/* Convert a user-specified quality rating to a percentage scaling factor
* for an underlying quantization table, using our recommended scaling curve.
* The input ‘quality‘ factor should be 0 (terrible) to 100 (very good).
*/
{
/* Safety limit on quality factor. Convert 0 to 1 to avoid zero divide. */
if (quality <= 0) quality = 1;
if (quality > 100) quality = 100;

/* The basic table is used as-is (scaling 100) for a quality of 50.
* Qualities 50..100 are converted to scaling percentage 200 - 2*Q;
* note that at Q=100 the scaling is 0, which will cause jpeg_add_quant_table
* to make all the table entries 1 (hence, minimum quantization loss).
* Qualities 1..50 are converted to scaling percentage 5000/Q.
*/
if (quality < 50)
quality = 5000 / quality;
else
quality = 200 - quality*2;

return quality;
}
GLOBAL(void)
jpeg_set_linear_quality (j_compress_ptr cinfo, int scale_factor,
boolean force_baseline)
/* Set or change the ‘quality‘ (quantization) setting, using default tables
* and a straight percentage-scaling quality scale. In most cases it‘s better
* to use jpeg_set_quality (below); this entry point is provided for
* applications that insist on a linear percentage scaling.
*/
{
/* These are the sample quantization tables given in JPEG spec section K.1.
* The spec says that the values given produce "good" quality, and
* when divided by 2, "very good" quality.
*/
static const unsigned int std_luminance_quant_tbl[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
};
static const unsigned int std_chrominance_quant_tbl[DCTSIZE2] = {
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
};

/* Set up two quantization tables using the specified scaling */
jpeg_add_quant_table(cinfo, 0, std_luminance_quant_tbl,
scale_factor, force_baseline);
jpeg_add_quant_table(cinfo, 1, std_chrominance_quant_tbl,
scale_factor, force_baseline);
}
GLOBAL(void)
jpeg_add_quant_table (j_compress_ptr cinfo, int which_tbl,
const unsigned int *basic_table,
int scale_factor, boolean force_baseline)
/* Define a quantization table equal to the basic_table times
* a scale factor (given as a percentage).
* If force_baseline is TRUE, the computed quantization table entries
* are limited to 1..255 for JPEG baseline compatibility.
*/
{
JQUANT_TBL ** qtblptr;
int i;
long temp;

/* Safety check to ensure start_compress not called yet. */
if (cinfo->global_state != CSTATE_START)
ERREXIT1(cinfo, JERR_BAD_STATE, cinfo->global_state);

if (which_tbl < 0 || which_tbl >= NUM_QUANT_TBLS)
ERREXIT1(cinfo, JERR_DQT_INDEX, which_tbl);

qtblptr = & cinfo->quant_tbl_ptrs[which_tbl];

if (*qtblptr == NULL)
*qtblptr = jpeg_alloc_quant_table((j_common_ptr) cinfo);

for (i = 0; i < DCTSIZE2; i++) {
temp = ((long) basic_table[i] * scale_factor + 50L) / 100L;
/* limit the values to the valid range */
if (temp <= 0L) temp = 1L;
if (temp > 32767L) temp = 32767L; /* max quantizer needed for 12 bits */
if (force_baseline && temp > 255L)
temp = 255L; /* limit to baseline range if requested */
(*qtblptr)->quantval[i] = (UINT16) temp;
}

/* Initialize sent_table FALSE so table will be written to JPEG file. */
(*qtblptr)->sent_table = FALSE;
} 

从jpeg_quality_scaling函数可以看出,用户指定的质量(quality)系数[1, 100]被分段线性映射成了[0, 1000),然后在jpeg_set_linear_quality函数中作为乘数与预设(推荐)的量化表相乘。

ComicEnhancerPro 系列教程十八:JPG文件长度与质量