关于JPEG的那点事儿

将此文献给自己掉进兔子洞里消失的几小时光阴(笑)

今天有友人随便问了个问题:为什么Leanify这款软件能无损在已经压缩率很高的JPEG再压缩个5%-10%。当然直接的答案很简单:mozjpeg压缩算法。不过这并没有解决我们的疑问,具体的实现方式呢?尤其是无损优化是怎么做到的?

把Leanify的源代码拖下来看了下,首先一部分优化是来自于删掉metadata——大概能节省下去十几到几十KB而已。剩下的就是来自Mozjpeg的部分了。如果你有像我蛋疼地研究过jpeg的specification或者压缩原理(提示:Wikipedia就是个好去处,没必要去啃ISO的标准……),应该知道就和所有的压缩程序一样,虽然JPEG算法核心是有损压缩,但是为了最大化压缩效果,最后肯定会来一步熵编码(在JPEG语境下,又分霍夫曼和步进[progressive]编码)。所以我的猜想就是mozjpeg用了什么黑科技来优化熵编码。在浪费了1个小时调试Leanify的源代码后,我失望地发现mozjpeg的熵编码本身并无特殊之处——事实上,mozjpeg的核心代码来自libjpeg-turbo,而我拿libjpeg-turbo的原始文件替换相应的熵编码部分后(其实这部分本来就没被Mozilla修改),mozjpeg的再压缩能力丝毫没有受到影响:我用XnView编码出的已经开启霍夫曼和步进编码的jpeg依然可以有不小的(~5%)大小缩减。

柳暗花明,随便浏览之前打开的相关网页时看到了libjpeg-turbo作者对mozjpeg的回应——里面其实已经有了正确答案。引用作者原话:

mozjpeg relies on three technologies (progressive JPEG encoding, jpgcrush, and trellis quantization) to reduce the size of JPEG images.

mozjpeg其实用了三个技术来优化JPEG——强制开启前面提到过的步进编码(光这最基本的一步就能优化不少了,很多家用的图像软件却没这选项)、jpgcrush和trellis quantization(网格量化?)。jpgcrush是由x264主要开发者Loren Merritt闲着没事儿(大误)写的perl脚本源代码),被mozjpeg在首发版本中集成到libjpeg-turbo中。其工作原理其实也很简单——在步进编码模式下不断尝试,直至找到最好的配置。至于我为啥看代码没发现这块?呃这算法的实现好像混在一堆叫scan的逻辑判断和循环之中,我以为是无关的代码就略过了……当然主要原因是我的三脚猫功夫啦。在mozjpeg 2.0中引入的Trellis quantization则是一种“更智能的来决定应该丢掉哪些信息的算法”(是不是耳熟?想想音频压缩),是从人的感知出发来在DCT部分做手脚,所以这个其实并不是无损压缩的范畴了,而是提供一种更优良的兼容于JPEG框架内的编码算法,在我们的问题中(jpeg->jpeg无损[逐像素]转换)不涉及。顺便一提,trellis quantization的实现又是最早出现在视频编码中——再想想webp也是来自于Google在VP系列视频codec的研究成果,现在图像编码这方面的开发已经完全是视频带着图像走了。

在该文中,libjpeg-turbo的作者同时对mozjpeg的压缩效率以及必要性产生了一些质疑。用作者的话说,mozjpeg虽然可以增加少许压缩率不过速度比libjpeg-turbo慢了几十倍。作者同时抱怨了随着新功能的引入,mozjpeg也破坏了兼容性。作为被fork的本体,libjpeg-turbo不会跟进这些更新。不过作者同时也对其开发目的表示了理解(极限压缩),只是两者用途不同,外加自己本来就是义务开发又被众多开源计划使用(Mozilla家的Firefox都依然在继续用libjpeg-turbo),稳定性第一,实验功能可以再观察观察。虽然话不好听,相信两者之间应该还是比较愉快的,毕竟在mozjpeg 3.0的更新中就专门加强了ABI(应用二进制接口)上对libjpeg-turbo的兼容性。

有人可能忍不住了,libjpeg-turbo到底是什么鬼?这说来又话长了。JPEG是“联合图像专家小组”的意思,其实就是这种编码方法的创始者。不过,JPEG编码的具体实现,libjpeg,则是由一个独立的开发小组——(IJG,Independent JPEG Group)在1991年发布的,这甚至比JPEG的初版标准出炉还早1年。其领衔开发者是Tom Lane,这家伙也是开源界神人一枚,光在图像界就至少参与了JPEG、PNG和TIFF的开发。随着时间的发展,libjpeg逐渐成为了JPEG的标准实现,与此同时JPEG也慢慢成为使用最多的图像格式。不过libjpeg的开发在1998年引入了上面提过多次的步进编码之后就停滞在了ver. 6b上。在整整11年之后,更换了领导的IJG终于发布了ver. 7——其中一个修改是引入了算术编码的新熵编码方式,相比霍夫曼它可以再提升百分之几的无损压缩率。其实这不是什么新东西,JPEG最初的标准里就有,但是这玩意曾经有大批专利掌握在IBM手中,所以直到专利过期才敢加入进来。IJG的脚步并没有就此停止,在最近几年其接连发布了版本8、版本9,增加了一些诸如无损JPEG(同算术编码,无损JPEG也是最初JPEG标准里就有的东西,但是一直没有被实现)、动态“智能”调整DCT block大小(原先是固定8×8)之类的功能。然而,社群对这些新功能并不怎么感冒——因为他们和已经用了十几年的6.x并不兼容,而且性能非常值得怀疑。连IJG的创始人,前面提过的Lane,都出来发声反对。更糟糕的是,关于将动态DCT block等功能加入标准的提案也被管理JPEG的标准化组织之一ITU-T拒绝——这意味着,libjpeg已经在自创一种JPEG之上的新标准了(虽然这种生米煮成熟饭的事儿在IT界屡见不鲜):说得好听叫更先进,说的难听叫(无人支持的)自主标准。libjpeg的新领头人Guido Vollbeding也相当高调,经常在社群里和别人打嘴仗,甚至在维基百科的libjpeg条目中和别人打编辑战

就在IJG重启开发的同一时期,一个名叫libjpeg-turbo的新fork出现,其最初目的在提升libjpeg的性能。对于libjpeg新版本的争议,libjpeg-turbo的作者则决定依然基于6b开发,同时提供了一些对7/8的ABI/API模拟。作者也写了文章表明,libjpeg这些新功能性能提升很细微,尤其是无损JPEG无论是质量还是速度都劣于webp,甚至在大部分时候都不如PNG。很快,开源社区就全面转向了libjpeg-turbo。不过这里顺便提一句,虽然turbo已经支持了算术编码,但是绝大部分开源社区默认都不使用甚至移除相关代码,为了规避可能的专利风险。在libjpeg v9出来之后,turbo的作者更明确表达对libejpeg开发思路的不满,决定不再去模拟9版本,从此和libjpeg划清界限。

故事到这里也算告一段落了。libjpeg-turbo继续作为新的事实标准,使用在各大OS发行版以及其他重量级软件(如Chrome、Firefox)中;IJG也继续在开发他们的libjpeg的新版本,今年年初才公布了9b;mozjpeg在去年更新3.1之后则暂时消停了,不过这种东西本来也没有快速更新的必要。说到底,作为快三十岁的老格式,JPEG还能散发活力就已经很了不起了。

于是又几个小时的光阴没有了

Part2:关于JPEG的那点事儿 Part 2:JPEG原理

一个有关“关于JPEG的那点事儿”的想法

留下评论