调用canvas.toDataURL 之后发生了什么

FE @ 字节跳动

在一个惠风和畅适合写码的下午,技术群里的一位小伙伴抛出了这样一个问题

有同学了解为什么canvas.toDataURL 在处理图片后,有的图片体积比原图还大吗?

这个问题一出,立刻涌现出了四种意见

A. base64 字符肯定要比原来长,谁base64它都长

B. chrome的处理算法不一样,chromium出来背锅啦

C. toDataURL用的姿势不对,还是要多学习一下

D. 图处理了信息自然多了,体积大了是没办法的事情

(你站哪一队?在评论区留下你的第一反应吧)

大家经常会使用 toDataURL + blob的方式,来把canvas中的图像变成可下载的文件, 一个图片文件经过了怎样的旅程才能到我们面前? 上面小伙伴的问题,到底原因如何?

一张图是怎么来的

带着这些问题就让我们来看看一张图片是怎么来的?

怎么描述一张图

图片是颜色的容器,如果要描述颜色 ,可以用rgb三种颜色,通过rgb三种颜色的混合就可以模拟出所需要的颜色,就像我们写css的color一样,给出rgb。

image.png

描述的方法找到了,接下来就来聊聊怎么存储这些数据呢?

方案1: 逐个编码

“一个一个记录就好了,很容易想到的方法。” —— BMP

在以前的windows系统中,可以经常见到这种Bmp图像格式。这种类型可以逐个记录RGB数据,在逻辑编写上非常简单。

image.png

方案2: 颜色表

“不要每个像素都存,做映射表,岂不美哉” —— Gif

同样的问题Gif格式给出了不同的方案,作为动图格式,往往多个画面内容类似,与其一个一个进行记录,不如构造一个字典,一张图片里面所用到的颜色终究是可以穷举出来的,那么在字典中完全可以存储下所有用到的颜色,这样可以在对应的像素点中只记录index。

例如下图,假设有一张5个像素的图片,如果记录rgb的话,那么需要记录三个数字 3*5 = 15;可是如果记录index的话只需要记录1个数字 + 3个颜色表就可以了。1 * 5 + 3 = 8;也就是说只要图片中的颜色相同的越多,那么这种方法的效果就会越来越好。

image.png

颜色表是一种无损压缩方案,无损压缩就是经过压缩后,信息还可以恢复为原样。上面的压缩表,如果我们把每个像素中存储的index 与颜色表进行匹配,的确可以恢复出来每个像素点的RGB值。

这种方案的效果也是很不错的,在一般情况下压缩比可以达到10:1。

那非一般的情况是什么呢?

假设我们有一段字符,可以认为这就是一张图片,一个字母就是一个像素,字母的颜色就是像素的颜色,现在用颜色表的思路来处理这段字符信息

image.png

构建索引表

image.png

经过我们的编码

image.png

效果不错!非常高效的帮助我们压缩了数据

却说又来了一个字符串:

image.png

老样子我们继续构建索引表然后编码:

image.png

image.png

结果我们发现经过编码后的数据,反而比编码前更多了,还不如逐个编码呢!

图片事小,小钱钱事大,有开过站点的兄弟应该明白,这些图片流量可都是钱的,每多1%的数据,少的都是兜里的小钱钱。

从结果上来看无损压缩在一些特殊情况反而会增加信息的长度,从表象上看,这种当变化很快的信息似乎和结果有些关系

方案3:有损压缩

“不需要的就不要记了,看我斩尽芜杂” —— PNG

有损压缩对应无损压缩,压缩后的信息是不可以恢复原样的。

就像小时候做过的古文翻译一样:

逐字翻译就像无损压缩,还可以恢复成文言文;

意译就像有损压缩,意思没错但是想恢复回文言文就不容易了。

比如朋友邀请出仕,古文与白话文的拒绝辞令:

无损压缩 vs 有损压缩

那么上面辞令的有损压缩“干不了 谢谢”,都丢弃了什么信息呢?比如 详细的拒绝的原因(才疏学浅)、个人感受(不堪从命)。这些不是那么重要的信息,就是有损压缩要“损”的地方。

有损压缩,损的到底是什么

在图片中,很重要一个部分就是:视觉冗余

当然还有同样重要的编码冗余等等,就不展开了

这里的3个像素块一眼能区分出来差异吗?

是有差异的,这三个颜色块的红色通道分量都增加了一些。

可能有的设计师同学可以区分的出来,但我是区分不出来哈哈~

image.png

image.png

那么,再加大难度,如果放在1080p 的画布上 200w像素中我们能区分出来吗?

所以这些信息有那么重要吗? 似乎也不是那么重要,毕竟我们看起来都一样,用上面的3个颜色的粒子来说,本来颜色表需要记录3个颜色,但是如果我们觉得这三种颜色看着都一样,不如记一个吧,反正看不出来。在图形领域有句话说:“看起来是对的,它就是对的”

接下来回到我们刚才的颜色表问题,有损压缩派会怎么解决这个问题?

原始数据:

image.png

经过图形噪声抑制等技术手段,把不重要的信息平滑化后,让数据变化不要那么快:

image.png

最后编码:

image.png

相对于无损压缩的方案:

image.png 解决问题✿✿ヽ(°▽°)ノ✿

RGB vs YUV

Png 8 vs Png32

png图片是上述算法的落实者,在衍生过程中出现了 Png8\24\32 多种格式

后面的数字意思就是这个格式的颜色表有多大:例如8bit-256色

有损压缩的确能解决问题,但是人们在使用过程中发现,png8和32到底还是能肉眼看出来的:

rgb的能力是有极限的,越是压榨颜色的极限,就会越为其所困,除非超越rgb的桎梏.

如果有种颜色表示的方法能让颜色变化很小,而且大部分都一样就好了。

于是人们尝试不用rgb来表示颜色了,人们希望有一种格式在颜色表达上能够更加“平滑”一些。

YUV

后来人们发现一种方案:YUV。这种通过 Y 明度、 U 色度、 V 浓度 三个颜色通道来表示颜色,可以解决上面的问题,为什么呢?

  1. 能力通用:通过YUV同样可以表示出rgb覆盖的色域效果

在y=0.5 的情况下,通过uv的变化亦可以表示出rgb的色域

image.png

  1. 频率特性

人们发现yuv表示的颜色中,uv分量变化的幅度非常小,非常适合用来做有损压缩。

比如下面的图片,第一张是原图,剩下的图片依次是yuv分量,这里面的y分量的图片似乎和原图非常相似了,看起来就像是原图的灰度图一样了。

而uv的图片的质量似乎就差一些了,一些山的细节都没有了,大部分的颜色似乎是一样的?

image.png

大部分一样这是好事呀~ 这就是意思差距很小,也就说如果我们微调一下,肉眼可能会看不出来。我们上面就提到过期望的颜色是怎么样的呢 —— 变化小,大部分一样。

这样的话就非常适合我们来做有损压缩了。

余弦变换与量化

yuv颜色帮助我们解决了信号表示的问题。

还有一个问题是:如何识别出不重要的信息,从而进行丢弃达到不会严重损害图像质量的目的

期望能够找到对人眼不敏感像素信息,这样即使进行了平滑化等操作,也不会被人所感知。

同种物体的多种表示方法

这里要引入一种新的表示方法,就像同样颜色可以用rgb也可以用yuv 两种不同的表示方法来表示一样,像素的坐标表示也有类似的方法:

笛卡尔坐标vs极坐标

一种是常用的笛卡尔坐标,使用 x,y 两个坐标轴来表示。

一种是极坐标,使用半径与旋转角表示。

11.png

坐标.png

所以同样的,在图像领域一张图像也有多种标识方式,通过颜色分量值表示是一种方法,另一种方式就是通过 频域 的方式

二维信号 -> 频率信号

为什么 频域 可以帮我们解决 “识别出不重要的信息” 这个问题呢? 因为我们需要识别出来某些颜色差距很小,差距小了,才能平滑化成一个颜色,这样就能让人发现不了。

而频域非常擅长表示信息之间的变化情况,比如多个像素之间的变化是快还是慢,变化慢就是颜色之间差距小。

(这里是整个文章中最难懂的部分,大家打起精神!我们本次主要说明DCT的目的和作用,具体的实现会在下一篇文章中详细说明,如果大家希望催更务必多点赞评论,你的支持就是最大的鼓励!)

在频域的表示方式中,我们可以通过余弦变换(DCT)的方式来找到,具体是 哪里的像素变化比较慢,可以被我们平滑化,我们用下面的矩阵类比yuv中的u分量,经过DCT(实际是经过DCT+量化)之后,变成了一些频域数据。

原来的u分量 3*3 需要在颜色表中记录9个数,经过DCT之后,由于有多个0的存在,现在只需要在颜色表记录8个数字了。

如果给一个更加“强力”的DCT处理,那么数字0就会越来越多,从而让我们记录的数据越来越少

image.png

DCT作用类似于一个函数,输入一个颜色分量的矩阵和处理力度 返回一个处理后的结果:

const dct = (inputMatrix,strength) => outputMatrix
复制代码

最后,经过DCT“挑选”后的像素,就是适合平滑化的数据,因为有很多0,再用无损压缩就可以达到非常好的效果了。

DCT是非常重要的算法,在我们日常看的视频中也会存在这种算法来进行帧内预测,是视频编码中使用非常广泛的一种。

编码成为文件

一个图像能成为jpeg就要走完我们上述的所有流程,才能变成一张jpeg

image.png

一张图是怎么渲染的

如果说编码是压缩过程,渲染就是解压缩过程,大部分是对编码操作逐步进行逆运算。

由于chrome底层是采用OpenGL 中的GLSL进行渲染的,所以图片最后在解码变成RGB了之后,被opengl所渲染,就呈现在我们面前。

回到开始

终于搞明白了一张图片的诞生和渲染,就来回到最开始的问题吧

为什么toDataURL之后图片体积变大了呢?

A. base64 字符肯定要比原来长,谁base64它都长

正确。不仅base64会长,通过base64存下的文件也是变大的

B. chrome的处理算法不一样,chromium出来背锅啦

正确。每个厂家算法都是不同的,比如phototshop支持10级的压缩处理

C. toDataURL用的姿势不对,还是要多学习一下

正确。chrome默认压缩质量是0.92,给小一点可以解决一些问题

D. 图处理了信息自然多了,体积大了是没办法的事情

正确。就像一张全黑的图片,体积一定是很小的

总结

这次主要和大家一起探讨了

  1. 图片编码中的常用压缩方式:有损、无损的使用场景
  2. 各种颜色格式的使用场景,解决了什么问题
  3. 了解相同图片下体积背后有所不同的原因

你对哪部分最感兴趣呢?快来评论告诉笔者吧!

分类:
前端
标签:
分类:
前端
标签:
收藏成功!
已添加到「」, 点击更改