png的故事:隔行扫描算法

转载自AlloyTeam:http://www.alloyteam.com/2017/06/the-story-of-png-deinterlacing-algorithm/

前言

前文已经讲解过如何解析一张png图片,然而对于扫描算法里只是说明了逐行扫描的方式。其实png还支持一种隔行扫描技术,即Adam7隔行扫描算法。

优劣

使用隔行扫描有什么好处呢?如果大家有去仔细观察的话,会发现网络上有一些png图在加载时可以做到先显示出比较模糊的图片,然后逐渐越来越清晰,最后显示出完整的图片,类似如下效果:

这就是隔行扫描能带来的效果。隔行扫描一共会进行1到7次扫描,每一次都是跳着部分像素点进行扫描的,先扫描到像素点可以先渲染,每多一次扫描,图片就会更清晰,到最后一次扫描时就会扫描完所有像素点,进而渲染出完整的图片。

当然,也因为要进行跳像素扫描,整张图片会存储更多额外数据而导致图片大小会稍微变大,具体增加了什么额外数据下文会进行讲解。

生成

要导出一张基于Adam7隔行扫描的png图片是非常简单,我们可以借助Adobe的神器——PhotoShop(以下简称ps)。我们把一张普通的图片拖入到ps中,然后依次点选【文件】-【存储为Web所用的格式】,在弹出的框里选择存储为PNG-24,然后勾选交错,最后点击存储即可。

这里的交错就是只将扫描算法设为Adam7隔行扫描,如果不勾选交错,则是普通逐行扫描的png图片。

原理

Adam7隔行扫描算法的原理并不难,本质上是将一张png图片拆分成多张png小图,然后对这几张png小图进行普通的逐行扫描解析,最后将解析出来的像素数据按照一定的规则进行归位即可。

  • 分析

在解压缩完图像数据后就要马上进行拆图。拆图并不难,就是将原本存储图像数据的Buffer数组拆分成多个Buffer数组而已。关键的问题是怎么拆,这时我们先祭上wiki上这张图:

上面这张图就说明了每次扫描需要扫描到的像素,正常来说一张基于Adam7隔行扫描的png图片是要经历7次扫描的,不过有些比较小的图片的实际扫描次数不到7次,这是因为有些扫描因为没有实际像素点而落空的原因,所以下面的讲解还是以标准的7次扫描来讲解,本质上此算法的代码写出来后,是能兼容任何大小的png图片的,因为算法本身和图片大小无关。

7次扫描,其实就回答了上面拆图的问题:要拆成7张小图。每张小图就包含了每次扫描时要归位的像素点。

以第一次扫描为例:第一次扫描的规则是从左上角(我们设定此坐标为(0,0))开始,那么它扫描到的下一个点是同一行上一个点往右偏移8个像素,即(8,0)。以此类推,再下一个点就是(16,0)、(24,0)等。当当前行所有符合规则的点都扫描完时则跳到下一个扫描行的起点,即(8,0),也就是说第一次扫描的扫描行也是以8个像素为偏移单位的。直到所有扫描行都已经扫描完成,我们就可以认为这次扫描已经结束,可以考虑进入第二次扫描。

我们以一张10*10大小的png图片来举例,下面每个数字代表一个像素点,数字的值代表这个点在第几次扫描时被扫描到:

按照规则,在第一次扫描时我们会扫描到4个像素点,我们把这4个像素点单独抽离出来合在一起,就是我们要拆的第一张小图:

也就是说,我们的第一张小图就是2*2大小的png图片。后面的小图大小以此类推,这样我们就能得知拆图的依据了。

  • 拆图

上面有提到,拆图本质上就是把存放图片数据的Buffer数组进行切分,在nodejs里的Buffer对象有个很好用的方法——slice,它的用法和数组的同名方法一样。

直接用上面的例子,我们的第一张小图是2*2点png图片,在假设我们一个像素点所占的字节数是3个,那么我们要切出来的第一个Buffer子数组的长度就是2*(2*3+1)。也许就有人好奇了,为什么是乘以2*3+1而不是直接乘以2*3呢?之前我们提到过,拆成小图后要对小图进行普通的逐行扫描解析,这样解析的话每一行的第一个字节实际存放的不是图像数据,而是过滤类型,因此每一行所占用的字节需要在2*3的基础上加1。

  • 像素归位

其他的小图拆分的方法是一样,在最后一次扫描完毕后,我们就会拿到7张小图。然后我们按照上面的规则对这些小图的像素进行归位,也就是填回去的意思。下面简单演示下归位的流程:

待到7张小图的像素全部都归位后,最后我们就能拿到一张完整的png图片了。

  • 代码

整个流程的代码如下:

let width; // 完整图像宽度,解析IHDR数据块可得

let height; // 完整图像高度,解析IHDR数据块可得

let colors; // 通道数,解析IHDR数据块可得

let bitDepth; // 图像深度,解析IHDR数据块可得

let data; // 完整图像数据

let bytesPerPixel = Math.max(1, colors * bitDepth / 8); // 每像素字节数

let pixelsBuffer = Buffer.alloc(bytesPerPixel * width * height, 0xFF); // 用来存放最后解析出来的图像数据

// 7次扫描的规则

let startX = [0, 0, 4, 0, 2, 0, 1];

let incX = [8, 8, 8, 4, 4, 2, 2];

let startY = [0, 4, 0, 2, 0, 1, 0];

let incY = [8, 8, 4, 4, 2, 2, 1];

let offset = 0; // 记录小图开始位置

// 7次扫描

for(let i=0; i<7; i++) {

// 子图像信息

let subWidth = Math.ceil((width - startY[i]) / incY[i], 10); // 小图宽度

let subHeight = Math.ceil((height - startX[i]) / incX[i], 10); // 小图高度

let subBytesPerRow = bytesPerPixel * subWidth; // 小图每行字节数

let offsetEnd = offset + (subBytesPerRow + 1) * subHeight; // 小图结束位置

let subData = data.slice(offset, offsetEnd); // 小图像素数据

// 对小图进行普通的逐行扫描

let subPixelsBuffer = this.interlaceNone(subData, subWidth, subHeight, bytesPerPixel, subBytesPerRow);

let subOffset = 0;

// 像素归位

for(let x=startX[i]; x<height; x+=incX[i]) {

for(let y=startY[i]; y<width; y+=incY[i]) {

// 逐个像素拷贝回原本所在的位置

for(let z=0; z<bytesPerPixel; z++) {

pixelsBuffer[(x * width + y) * bytesPerPixel + z] = subPixelsBuffer[subOffset++] & 0xFF;

}

}

}

offset = offsetEnd; // 置为下一张小图的开始位置

}

return pixelsBuffer;

尾声

整个Adam7隔行扫描的流程大概就是这样:

前面提到基于此种扫描方式的png图片往往会更大些,这是因为图片存储了一些额外数据导致的。这里的额外数据就是指过滤类型。原本的png大图拆成小图后,扫描行的数目就会蹭蹭蹭往上涨,每个扫描行的第一个字节都是用来存储过滤类型的,所以行数增加的越多,额外数据就会越多。至于在用png图片等时候要选用哪种扫描方式等图片,就要视具体场景而定了。如果对完整代码有兴趣的同学可以戳这里

参考资料:

?

时间: 2024-10-12 08:42:09

png的故事:隔行扫描算法的相关文章

我有一个梦想,希望每一位提到算法的人,不再立即紧皱眉头

点击关注 异步图书,置顶公众号 每天与你分享 IT好书 技术干货 职场知识 参与文末话题讨论,每日赠送异步图书 --异步小编 多年来,我有一个梦想,希望每一位提到算法的人,不再立即紧皱眉头,脑海闪现枯燥的公式.冗长的代码:希望每一位阅读和使用算法的人,体会到算法之美,像躺在法国普罗旺斯小镇的长椅上,呷一口红酒,闭上眼睛,体会舌尖上的美味,感受鼻腔中满溢的薰衣草的芳香-- 打开算法之门 瑞士著名的科学家N.Wirth教授曾提出:数据结构+算法=程序. 数据结构是程序的骨架,算法是程序的灵魂. 在我

上周热点回顾(5.16-5.22)

热点随笔: · [无私分享:从入门到精通ASP.NET MVC]从0开始,一起搭框架.做项目(5.2) 登录功能的实现,接口注入.log4net的使用(果冻布丁喜之郎)· [无私分享:从入门到精通ASP.NET MVC]从0开始,一起搭框架.做项目(5.3) 登录功能的实现,丰富数据表.建立关联(果冻布丁喜之郎)· 微软.NET Core RC2正式发布,横跨所有平台(张善友)· [无私分享:从入门到精通ASP.NET MVC]从0开始,一起搭框架.做项目(5.4) 登录功能的实现,创建与登录用

《大道至简》第一章伪代码观后感

●愚公移山的故事~ Begin(算法开始) while(!山平)do { For(愚公:=移山之时 To 愚公死时) {率子孙荷弹者三夫及邻人京城氏之孀妻,叩石垦壤,箕畚运于渤海之尾./} if(愚公死)Then { 子孙存焉: 毕力平险,指通豫南,达于汉阴: /} End(算法结束) ●你会不会写程序的问题~ Begin(算法开始) if<你知道天冷先穿衣后洗漱&&天热反之&&日复一日直至死亡>Then { 你就可以编程啦: /} if<你知道生病||不

一份程序员必备的经典算法书单

点击关注 异步图书,置顶公众号 每天与你分享 IT好书 技术干货 职场知识 参与文末话题讨论,每日赠送异步图书 --异步小编 当我们在工作中使用算法时,通常都是希望能更有效地解决问题.使程序运行得更快,并且让解决方案变得更为简短.但实际情况如何呢?我们获得所需要的效率.速度和简洁性了吗?为什么人们在使用Python这种语言时依然要在乎这些事呢?选择这种语言对于追求高速度的人来说是一个好的开端吗?为什么不选择C或Java这样的语言呢? 首先,可能是因为Python语言本身很讨人喜欢,以至于人们不想

对比《网络是怎样连接的》PDF+《程序是怎样跑起来的》PDF+《计算机是怎样跑起来的》PDF

理解知识最好的办法是写一写汇编.从中感觉自己对浮点数在寄存器的存放机制了解还不够.加深了对基址寄存器和变址寄存器的了解.不足的地方在于有些例子陈旧了.记住0.1循环100相加不是10. 从计算机的内部结构开始讲起,以图配文的形式详细讲解了二进制.内存.数据压缩.源文件和可执行文件.操作系统和应用程序的关系.汇编语言.硬件控制方法等内容,目的是了解从用户双击程序图标到程序开始运行之间到底发生了什么.<程序是怎样跑起来的>图文并茂,通俗易懂,非常适合计算机爱好者阅读. <计算机是怎样跑起来的

《深入理解ES6》 PDF中英文+《ES6标准入门第3版》PDF分析

ES6是ECMAScript标准十余年来变动最大的一个版本,其中添加了许多新的语法特性,既有大家耳熟能详的Promise,也有闻所未闻的Proxy代理和Reflection反射:既有可以通过转译器(Transpiler)等方式在旧版本浏览器中实现兼容的let.const.不定参数.展开运算符等功能,亦有无论如何都无法实现向前兼容的尾调用优化.深入理解ES6的特性对于所有JavaScript开发者而言至关重要,在可预见的未来,ES6中引入的语言特性会成为JavaScript应用程序的主流特性,这也

HDU 4513 吉哥系列故事――完美队形II (manacher算法)

吉哥系列故事――完美队形II hdu-4513 Description 吉哥又想出了一个新的完美队形游戏!  假设有n个人按顺序站在他的面前,他们的身高分别是h[1], h[2] ... h[n],吉哥希望从中挑出一些人,让这些人形成一个新的队形,新的队形若满足以下三点要求,则就是新的完美队形: 1.挑出的人保持原队形的相对顺序不变,且必须都是在原队形中连续的:  2.左右对称,假设有m个人形成新的队形,则第1个人和第m个人身高相同,第2个人和第m-1个人身高相同,依此类推,当然如果m是奇数,中

hdu 4513 吉哥系列故事——完美队形II (manachar算法)

吉哥系列故事——完美队形II Time Limit: 3000/1000 MS (Java/Others)    Memory Limit: 65535/32768 K (Java/Others) Problem Description 吉哥又想出了一个新的完美队形游戏! 假设有n个人按顺序站在他的面前,他们的身高分别是h[1], h[2] ... h[n],吉哥希望从中挑出一些人,让这些人形成一个新的队形,新的队形若满足以下三点要求,则就是新的完美队形: 1.挑出的人保持原队形的相对顺序不变,

hdu----(4513)吉哥系列故事——完美队形II(manacher(最长回文串算法))

吉哥系列故事——完美队形II Time Limit: 3000/1000 MS (Java/Others)    Memory Limit: 65535/32768 K (Java/Others)Total Submission(s): 1012    Accepted Submission(s): 358 Problem Description 吉哥又想出了一个新的完美队形游戏! 假设有n个人按顺序站在他的面前,他们的身高分别是h[1], h[2] ... h[n],吉哥希望从中挑出一些人,让