ICAnalyzer 开发小记

发布时间:

最后更新:

关键词: AVIF butteraugli ICAnalyzer SSIM WebP

ICAnalyzer 是我开发的一款网页应用,用来对比各种图片编码的优化效果,IC 不是集成电路,而是 Image Compression (or Convertor, Codec, Compare) 的缩写。

在线使用:https://ic-analyzer.kaciras.com

中文说明:https://github.com/Kaciras/ICAnalyzer/wiki/Tutorial-(Chinese)

开源地址:https://github.com/Kaciras/ICAnalyzer

ICAnalyzer 的目标的是提供一个方便(在线使用)、直观(多种视图并支持缩放和移动等交互)、定量(支持相似度指标)的图片压缩效果分析工具。

本来计划三个月写完,结果咕咕咕了 9 个月才搞定……不过最终功能还是达到了我的预期,特写本文记录下这期间的心得。

截图展示

首页 首页

编码选项 编码选项

结果视图 结果视图

差分、亮度和取色 差分、亮度和取色

HeatMap HeatMap

也可以上传图片做对比 也可以上传图片做对比

操作演示

开发原因

图片优化的复杂性

我对程序的性能有很高的要求,在 Web 开发中,过图片相关的优化是重中之重,首当其冲的就是图片的压缩。

使用更新的编码比如 WebP 和 AVIF 能降低图片的体积,这是众所周知的事情,但这些编码能优化到什么程度?图片质量下降了多少?默认的转换参数是最优的吗?各种参数对结果有怎样的影响?新一代编码一定比早些的更好吗?

据我所见很少有人关心这些问题,大部分人仅是简单地用新一代编码器转换下图片,然后就完成了优化。但在实际使用中,我发现它们没有那么简单。在我的另一篇文章 Web图片优化(一):压缩方案简介 里就提到了用 WebP 默认参数转换某些图片反而体积会变大。

就拿那篇文章里的图再举个例子:如何压缩该图效果最好?

就是这张图 就是这张图

这里简单的测试了一下,使用 Google 推出的在线图片转换应用 Squoosh 和 pngquant (版本 2.11.7),前四个都是默认参数,质量指标使用 Butteraugli Source(越小越好),结果如下:

编码 体积 Butteraugli
原图 68.7 KB 0
MozJPEG 109 KB 15.00
pngquant 29.6 KB 0.621
WebP 84.9 KB 7.60
AVIF 19.5KB 13.55
WebP无损 24.2 KB 0
AVIF无损 44.2KB 0
AVIF + YUV444 19.7KB 4.45

可以看到用了默认参数的全军覆没,均不如 WebP 无损,其中 MozJPEG 和 WebP 默认参压出来反而更大,AVIF 的默认参虽然压出来体积最小,但质量损失严重。后三个是调整了参数的,AVIF 的无损模式比 WebP 大了不少,然而使用 YUV4:4:4 采样后的 AVIF 有损压缩不仅质量大幅上升,肉眼很难分辨,而且体积仍然与默认的 YUV4:2:0 相差无几。

在以上的测试中优胜者既不是有损压缩,也不是最新的 AVIF,而是 WebP 无损,虽然它比最后一个大了 4.5KB(6.6%),但考虑到没有质量损失,以及 AVIF 算法的开销后,这点体积是可以接受的。当然这个测试比较简单,并没有覆盖所有情况,到底怎么压才是最佳方案,还需要更复杂的分析。

这个例子表明无脑相信新一代编码,以及仅使用默认参数来优化图片是不行的,做优化本来就不是拿个新技术一套就完事了, 对结果的测量以及定量分析都必不可少。

分析工具的缺乏

通常来说,如果不想自己费劲去测试的话可以查找相关的文章,事实上对编码效果分析的文章很多,但它们基本都使用自己构建或收集的工具,你很难自己重现。

最开始的时候,我写了个 Python 脚本,调用 OpenCV 来做运算,生成的图片保存到文件然后一个个打开,通过窗口切换来反复对比,当要换图或者改变参数时,都要修改源码再次运行——简直麻烦死了。

我理想中的分析工具应该有个图形界面,把图片拖上去,点几下就能设置好参数范围,结果通过一个滑块之类控件来切换,想怎么滑就怎么滑,还要有放大、移动等功能以便查看图片的细节……可惜搜遍 Google 我也没找到这样的工具,唯一接近的 Squoosh 也不支持质量指标。

总而言之,目前缺乏一个通用的工具,能够对图片进行转码并作质量分析,本着我行我上的原则,便想着开发这么一款应用。

该应用应当具有以下特点:

  1. 在线使用,纯静态网站,所有算法均运行在浏览器里,不需要下载、不需要安装、更不需要任何服务端交互。

  2. 交互丰富,支持移动、放大、取色等操作,能够观察到图片的每一个细节。

  3. 数据说话,使用可以量化的指标,比如结构相似度峰值信噪比等来评定图片的质量,比起肉眼更具说服力。

  4. 批量分析,每个编码器参数都支持采样,并将结果绘制成图表,增减趋势一目了然。图表支持导出,可以把它贴到任何地方。

设计思路

提到对比差异,最基本的方式就是初中就教过的:控制变量法。对同一张图片,使用不同的编码器或编码参数来转换,观察结果的差别,这也就是本项目的核心思想。为了实现这一目标,有以下几个关键点需要考虑:

  • 如何设计界面才能让用户方便地观察结果?

  • 怎样把编码器和分析算法集成到应用中?

  • 怎么实现动态交互,比如让质量参数逐渐增大,可以看到生成的图片越来越清晰?

得益于前端技术的发展,以及一些现代化应用的出现,让这些以往很难做到的需求变得可能。

技术选型

ICAnalyzer 的界面模仿了 Squoosh,它的界面正好也能满足图片展示 + 很多控件,我也没想出更好的设计。不过本项目采用了更主流的 React Hooks + 我熟悉的 Webpack,而 Squoosh 是 Preact + Rollup,所以代码没法直接抄,都要自己重头写。

Squoosh Squoosh

Squoosh 另一个很好的地方在于它自带了编译好的编码器,都是 WASM,而且覆盖了主流的格式,这意味着本项目只要拿过来用即可,无需自己折腾,这大大节省开发成本,因此说本项目是基于 Squoosh 的也不为过,这要特别感谢 Squoosh 的开发者们。

从复杂程度上看,本项目要比 Squoosh 要难,主要体现在动态交互这块。Squoosh 是单个图片,而本项目要批量转换,然后将结果缓存下来并与控件对应,这就需要设计下数据结构。另外本项目要支持参数采样,比如一个整数类型的参数,既可以指定一个值,也可以指定一个范围,从 0 到 100 每隔 5 一个,这导致选项的复杂度倍增,为了可维护性就做了单生成功能,而 Squoosh 的选项控件是直接写在 JSX 里的。

部署方面,因为是静态站所以可以直接用托管服务,什么 GitHub Pages 、Vercel 都安排上,反正不要钱。

兼容性

平台方面,考虑到本项目对性能有一定的要求,而且界面上面板较多,决定不支持移动端平台,毕竟不像 Squoosh,分析这种事情没必要在手机上做。

代码方面使用了最新的 JS 特性,比如 logical assignment 和 class fields,并且没有使用 babel。照我所想使用本项目的人应该不会去用旧版浏览器。

局限性

由于交互的需要,转换后的图片都放在内存里以便平滑切换,这也意味着能存放的结果是有限的,另外在浏览器上运行的效率比不上本地代码,即便使用 WebAssembly。所以为了性能考虑,在设计时限制了一次只能选一张图,不能批量转换。

如果一次转换要花几小时,它就不应该跑在浏览器里。

但真正的分析都需要一定量的样本,个别结果不一定适用于全部,这也是本项目的局限性,如果对压缩效果有极致的追求,最好还是自己用实际的数据做大规模的测试。

实现要点

数据模型

本项目的第一个难点是选项控件与背后的数据结构如何设计,为了实现控制变量法,在编码器选项表单里的每个控件都需要两种状态:常量和范围,拿数字类型的举例,定值是一个滑块,而范围则是三个输入框分别表示min,max,step,最后还要在前面加一个切换按钮以便决定是哪一种,底层的数据也得分为两份。

左边是常量模式,右边是范围 左边是常量模式,右边是范围

为了实现动态交互,每个变量模式的选项都对应结果视图右下角的一个变量控件。显然,范围模式的控件的值(如min,max,step),可以作为常量模式下控件的属性(如滑块就有这仨属性),这样一来可以直接复用常量模式的组件。

选项与变量控件 选项与变量控件

在用户点击 Start 按钮后开始转换图片,转换的第一步就是生成编码配置,对于每个编码,在遍历其选项控件时都会:

  • 先复制默认的配置对象作为起始,当遇到范围模式时会进行一对多映射,比如开关选项会将配置复制两份,并把对应的参数分别设置为falsetrue;常量类型直接赋值即可。这样就生成了一系列编码器配置 options

  • 再创建 options 的同时,每个范围模式的选项还会将自身的值保存到一个 key 对象中,该对象与 options 是一一对应的,它们共同构成OptionsList

  • 同时,对每个范围模式的选项都创建对应的变量控件,构成变量控件列表controls

这样就完成了配置的生成,生成的配置作为编码器的参数转码图片,说起来挺累,但实际代码很简单

核心流程 核心流程

将每个配置对应的key作为键,转码的结果作为值存入一个 Map 对象OutputMap,这样就把结果跟key对应了起来。在另一边,变量控件会显示在结果视图的右下角,所有变量控件当前的值组合成的对象就等于一个key,用它去OutputMap里取得结果并显示在界面上。

整个过程还是比较简单的。

其中有一个点要提一下,key是一个对象,因为对象实际上相当于指针,不能用于 Map 的键,所以必须转换一下,有点类似 Stock Keeping Unit 算法,本项目里选择直接 JSON 序列化成字符串。

JSON 序列化的对象中,属性的顺序是不确定的,跟浏览器的实现有关,但一个好消息是它是稳定的,以相同顺序向对象中设置属性,JSON 序列化后顺序也相同,而且修改已经存在的属性不改变顺序。这样一来通过调整代码,保证添加顺序即可得到相同的 JSON 字符串。

差分视图

对两张图做差分用不着挨个减像素,更不需要什么 OpenCV,浏览器自带这功能,直接两个 canvas 叠起来,上层的设置mix-blend-mode: difference即可。

mix-blend-mode 兼容性 mix-blend-mode 兼容性

还需要注意的是浏览器默认的图片渲染方式各不相同,Firefox 会对图片做平滑,导致放大以后看不到每个像素,这一点可以通过 CSS 的 image-rendering解决,详见 https://stackoverflow.com/a/14068216

左边是 Firefox 默认,右边是 pixelated 左边是 Firefox 默认,右边是 pixelated

TODOs

在第一版发布时一些功能仍在开发中,未来可能会加入:

  • 本地模式,允许在 Node 里编写转换代码,然后将结果用 ICAnalyzer 展示。这使得 ICAnalyzer 能够扩展,比如通过child_process调用外部的编码器而无须将其编译成 WASM,或是将本项目集成到其它应用中。

  • 更多的指标,目前本项目仅支持3个有参考的相似度:SSIM、PSNR、Butteraugli,然而与图片相关的指标还多得是,未来会有更多的算法集成到本项目中。

吐槽

WASM 模块

首先是各种编码和算法的实现,Squoosh 自带了不少编码器可以直接用,但是 butteraugli 我搜了一圈没有发现能在浏览器里运行的版本,于是只能自己上,使用 Emscripten 编译到 WebAssembly。

这也是我第一次做 WebAssembly,为此我捡起了荒废了多年的C艹,好在 butteraugli 代码不复杂无需用到模板等反人类的东西,照着官方的示例改改就行。

写着写着我就觉得 butteraugli 的代码质量不高,而且最后一次提交都是两年前了,它跟 squoosh 一样都是 Google 家的,而且 squoosh 里也用到了它,但就是没人接手去维护……说实话,有时间的话我都想用 Rust 把它重写一遍。

图表库的选择

比较火的有 D3、ECharts、Highcharts、Chart.js,网页上做图表也是第一次,于是对比了一下:

  • Chart.js 感觉太简单了,因为项目里的图表需要很多交互功能,所以还是想选个功能更多的。

  • D3 偏底层,它更像是一个 DOM 操作工具,虽然扩展性很强大,但是本项目要用的图表类型也就最基本的折线图,不需要这么强大的自定义能力。

  • Highcharts 的 GitHub 上星星没有 ECharts 多,而且是个商业产品,我还是倾向于选择开源的,而且还有 Apache 背书。

综上所述选择了 ECharts,当看到它前身是百度的项目时我心里就一凉,但毕竟没用过也不能妄下定论,虽然百度搜索很垃圾但不一定技术也垃圾啊,于是抱着这样的心态用了下去。

没过多久就遇到了坑:本项目需要把多种不同范围的数据画在一张图上,也就意味着很多 Y 轴,但给图表的空间却不大,所以选择了同一时刻只显示其中一个,在鼠标经过图例时切换要显示的 Y 轴。

然而这个功能我查了半天文档也没发现怎么做,ECharts 的图例似乎不支持鼠标事件,这里有一个相关的 Issue,2016 年的问题过了 5 年还没解决,然后莫名其妙地把 Issue 给关闭了。很明显 ECharts 就是个 KPI 产物,乍一看很炫酷,深入之后就会发现它有多垃圾。

说点题外话,部分大厂的开源项目有一种歪风邪气,就是当 Issue 长时间没有活动就关掉,好像时间一长问题自动就消失了一样。这种掩耳盗铃行为是对开源的嘲讽,你不如就把 Issues 板块给关了,眼不见心不烦岂不更好?

因为有无法解决的问题,所以我删除了 ECharts 换了 Highcharts 看看,这一看让我发现了新天地。

Highcharts 的网站和 StackOverflow 里都有专人回答问题,几乎每个回答都附有演示,文档比 ECharts 更详细,开发者的活跃程度也比 EChart 高——可以说完完全全碾压了 ECharts,当然它也有缺点就是商业使用收费以及文档没中文,但这跟我又有什么关系呢。

对 React 的理解

ICAnalyzer 是我第一个 React 项目,我接触 React 较晚都是在 16.8 之后了,所以顺理成章地使用了新一代 Hooks API,它让我感受到了简单粗暴的美:输入 Props + 状态 State = 输出 VDOM 就是渲染函数,超出纯函数范围的就用 Hooks 实现。

渲染函数的设计直击 MVVM 的本质,没有丝毫的多余,与传统的类组件相比优势明显,Hooks 未来必定会成为 React 的主流写法。

再对比一下我用过的 Vue 就会发现 Vue 的组件繁琐了许多,在使用 Vue 的时候我经常要看它的文档,里面有太多细枝末节和边界情况,完全记不住;而 React 只有开始学的那会看看,几个核心 API 了解之后就能干活,其它复杂的功能都可在这些 API 之上实现。

换句话说,Vue 适合初学者而 React 适合有一定水平的人,我是后者,React 用起来比 Vue 舒服太多了。

功能的取舍

独立开发项目也是做产品,一开始就要想好定位,要哪些功能以及不要哪些功能,不能陷入只满足眼前和什么都想要的极端。

!(我全都要)[我全都要.jpg]

在做本项目时,我也想过要不要加入更多的功能,分析了图片之后还能不能分析视频、音频?它们也有各种指标,也可以分析逐帧质量和声谱图,要不要搞成通用的平台?在最后我还是忍住了这些冲动。

视频和动图实际上就是一串图片合在一起,处理它们也就相当于前面提到的批量转换。

音频的话……算出声谱图,然后显示出来总觉得怪怪的,明显没有图片这么直观,而且想对比音频首先得买个好耳机,不像图片放大了随便什么屏幕都能看到细节。

总而言之本项目专注图片,没有计划支持更多类型的对象,如果有人从本项目获得了灵感,打算去做这些功能,那也算抛砖引玉了。

评论加载中