Skip to content

필사 모드: 塞进照片的二维码,代价是什么 —— 纠错是一笔预算

中文
0%
정확도 0%
💡 왼쪽 원문을 읽으면서 오른쪽에 따라 써보세요. Tab 키로 힌트를 받을 수 있습니다.

看到一张长得像照片的二维码时冒出的疑问

你大概见过那种看上去像黑白照片、扫一下却能打开链接的二维码。不是中间嵌了个 logo 那种程度,而是整张码就是一幅画。第一眼看会觉得挺厉害,第二个念头通常是:那玩意儿是怎么被读出来的。

答案比预想的简单,而且正因为简单才更重要。实务上真正该知道的,不是它为什么能被读出来,而是它拿什么当代价付掉了。本文沿着 Andrew T. 公开的误差扩散二维码生成器的说明文档,把那份代价说清楚。

二维码由两种格子组成

原始资料把二维码分成两部分。一部分是功能图形,也就是角上那种粗方块之类的图案,扫描器靠它找到码、定出方向和大小。另一部分是数据模块,也就是剩下的全部,真正的数据和头部信息装在这里。

关键在于读取顺序。扫描器先用功能图形确定码的坐标系,然后才去读数据模块。所以功能图形必须非常清晰,而数据模块相对留有可以折腾的余地。各家品牌做的那些有个性的二维码用的就是这份余地;按资料的说法,扫描的稳健性确实会掉一些,但折腾得挺多也大体还能读出来。

照片放在哪里 —— 把一个模块切成九格

这里出现的技法很有意思。原始资料介绍的做法是:把一个模块切成 3×3 的格子,只保留中间那一格为数据规定的颜色,剩下八格让给照片。

它能成立的原因就在前面那个顺序里。扫描器以功能图形给出的位置为基准,看的是每个模块的中心。只要中心对了,周围就是自由的。所以在一个格子里只守住中心那一格、把八格填成画面,码照样能读,画也照样看得见。

做出来的成品是一张低分辨率的 1 位黑白照片。原始资料举例的图像是 147×147 像素。这个数字除以 3 得到 49,正好对上一边为 49 个模块的二维码。也就是说,它是把 49 个模块各自切成 3 格之后的结果。

于是九格里有一格是随机的

这种做法带着无法回避的噪声。每个模块中心的那一格,不管照片长什么样,都必须是数据规定的颜色。数据和照片毫无关系,所以从画面的角度看,那一格的颜色事实上是随机的。

原始资料就是这样准确表述的:九个像素里有一个事实上是随机颜色。于是结果图像上会出现像撒了椒盐一样的斑点。147×147 的 1 位图像本来就粗糙,但粗糙里相当一部分正是来自这九分之一。

这时通常会冒出一个解法:把那些中心像素挑几个改成贴合画面的颜色。因为二维码有纠错,错几个也照样能读。中间嵌 logo 的二维码正是靠这个原理工作的。然而原始资料的结论不一样。后面会讲到。

抖动 —— 不做二值化,而是把误差流出去

得先把画面这一侧的事说清楚。把亮度压成黑白两个值时,如果用简单的二值化,中间调就会全部消失。所以通常会用网格图案去模拟中间调,但规则的网格本身也会显出一种花纹。

Floyd-Steinberg 误差扩散换了个思路。照搬原始资料的说明是这样的:从左上角开始,像平常一样对像素做二值化。亮度超过 50% 就变白,否则变黑。如果某个像素原本是 70%,把它变白就等于多给了 30% 的亮度。把这 30% 分摊给还没处理的周边像素,让它们相应变暗。等轮到那些像素做二值化时,这个值就被计入了。

整轮跑完之后,图像的各个部分在平均意义上更接近原来的亮度。而且出来的是不规则的图案而不是网格,看着没那么碍眼。

核心思路 —— 先把数据模块的误差流出去

现在两条线汇合了。原始资料真正的想法是把误差扩散跑两遍

第二遍是普通的 Floyd-Steinberg。不一样的是第一遍:它只针对数据模块的中心像素。这些像素的颜色已经定死了,那就按定死的颜色涂上,并把这一刻产生的误差流到周围的八格里去

这里的误差可能变得非常大。普通的抖动里颜色是我们自己挑的,所以误差不会超过 50%。可在这种情形下,有可能要把一个非常暗的区域里的像素涂成纯白,那就会产生 95% 的误差。原始资料说,这听上去像是坏事,但恰恰因为如此,把它流出去才重要。直接吞下就会在原地留下一个斑点,流出去则会让周围相应变暗,平均值就对上了。

于是前面搁置的那个问题有了答案。原始资料的结论是:把几个数据模块改成贴合画面的颜色,这种做法对画质帮助不大,却把扫描性能削得很厉害。因为只要把误差好好流出去,改颜色的必要性本身就几乎消失了。

余量是一笔预算

这是本文最想说的一点。提升画质的那根轴,和消耗纠错余量的那根轴,是两根不同的轴。第二遍误差扩散一粒余量都不花就改善了画面。而改动数据模块,则是在直接烧掉余量。

想一想这份余量是为什么存在的,就明白这个区分为何重要。二维码的纠错等级有四档,制定规格的电装波动(Denso Wave)说明其中使用最广的那一档能恢复 15%。等级提高,恢复能力会上升,但同样的数据就需要更大的码。而这份恢复能力,本来是留给印刷瑕疵、褶皱、污渍、倾斜角度和昏暗照明的。

也就是说,为了好看而花掉余量,等于把为现场恶劣条件准备的预算挪去做设计。原始资料也走到同一个结论:贴在大屏幕或海报上的码没问题,但如果是会被揉皱的纸质传单,那份余量就会派上用场。它还补了一句:你用自己的手机在笔记本屏幕上读得很顺,并不等于陌生人拿着旧手机能在昏暗处从印刷品上读出来。

真正上手时会绊住你的 —— 留白与放大

原始资料最后附上的两条实务注意事项,同样值得原样搬过来。

第一条是留白。生成器给出的是一张没有留白的小图,但二维码要被稳定读取,外面需要留白。而且这圈留白的颜色,必须和角上大方块内侧的颜色相反。通常是白色,但如果你做的是反色的码,那就需要黑色背景。

第二条是放大。浏览器把小图放大显示时,默认会做模糊插值。被糊掉的边界会让扫描器难以判定模块中心在哪里。如果不在 CSS 里把插值关掉,这个问题会悄悄发生。

这两条有个共同点:在制作者的屏幕上一切正常,只在使用者的环境里失败。漂亮二维码的失败,大多就是这个形状。

小结与出处

往二维码里放画面是你的自由。只是最好知道这份自由是从哪里借来的。扫描失败发生在用户手里而不是我们的屏幕上,而那时候东西已经印完了。

  • How to make error-diffused QR codes —— 功能图形与数据模块的区分、3×3 的切分、九分之一的噪点、两遍误差扩散、95% 的误差、留白与放大的注意事项,全部出自这份文档的讲解。
  • 生成器源代码 —— 可以自己动手做一个。
  • 电装波动关于纠错等级的文档 —— 我从这里确认了有四个等级、使用最广的那一档是 15%、以及提高等级会让码变大这三点。其余三个等级的准确数值在该页面上只以图片形式提供,所以我没有写进正文。
  • 147 除以 3 得 49 这个计算,以及它对应一边 49 个模块的说法,是我从原始资料的数字里推出来的。

현재 단락 (1/29)

你大概见过那种看上去像黑白照片、扫一下却能打开链接的二维码。不是中间嵌了个 logo 那种程度,而是整张码就是一幅画。第一眼看会觉得挺厉害,第二个念头通常是:那玩意儿是怎么被读出来的。

작성 글자: 0원문 글자: 2,926작성 단락: 0/29