当前位置:17727 > web前端信息 > 获取图片信息和像素内容,Canvas像素处理使用接

获取图片信息和像素内容,Canvas像素处理使用接

文章作者:web前端信息 上传时间:2019-11-29

代码

整个流程的代码如下:

JavaScript

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;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
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;

数据块类型

数据块类型有很多种,但是其中大部分我们都不需要用到,因为里面没有存储我们需要用到的数据。我们需要关注的数据块只有以下四种:

  • IHDR:存放图片信息。
  • PLTE:存放索引颜色。
  • IDAT:存放图片数据。
  • IEND:图片数据结束标志。

只要解析这四种数据块就可以获取图片本身的所有数据,因此我们也称这四种数据块为“关键数据块”

内容概要:本文通过简单的代码实例,以及略猥琐的图片demo,展示了canvas在图像像素数据操作方面的常用接口。至于如何利用这几个接口实现更复杂的效果,则会在后续章节里继续讲述。
一、canvas图片填充; 2、设置/获取canvas图片数据; 3、创建canvas图片数据;4、关于imageData.data的一点补充; 5、写在后面
一、canvas图片填充

概念理解

  • #### 分辨率(Resolution)

分辨率(Resolution,也称为“解析度”)是单位长度内包含的像素点的数量,它的单位通常为像素/英寸,表示为ppi。

由于屏幕上的点、线都是由像素组成的,因此显示器可显示的像素越多,画面就越精细,同样的屏幕区域内能显示的像素越多。

以分辨率720 * 576 的屏幕来说,即每一条水平线上,包含720px,共有576条线,即扫描列数为720列,行数为576行。

分辨率不仅与显示尺寸有关,还受显像管点距、视频带宽等因素的影响,另外,它还和刷新频率的关系比较密切。

当然,分辨率过大的图像在视频制作时会浪费更多的制作时间和计算资源,分辨率过小的图像则会使图像在播放时清晰度不够。

 

  • #### 隔行扫描和逐行扫描

通常显示器有逐行扫描与隔行扫描两种扫描方式。

逐行扫描

相对隔行扫描,逐行扫描是一种先进的扫描方式,它是指显示屏对显示图像进行扫描时,从屏幕左上角第一行开始,逐行扫描,整个图像扫描一次即完成扫描。因此图像显示画面闪烁小,效果好。目前先进的显示器都采用逐行扫描方式;

隔行扫描

隔行扫描是指每一帧被分割为两场,每一场包含了一帧中所有的奇数扫描行或者偶数扫描行,通常是先扫描奇数行得到第一场,然后扫描偶数行得到第二场。由于视觉暂留效应,人眼会看到平滑的运动而不是闪动的半帧半帧的图像。但是,这种方法导致两幅图像显示的时间间隔较大,从而使图像画面闪烁较大。因此,这种扫描方式较为落后,通常用在早期的显示产品中。

注意

至于选择哪一种扫描方式,主要取决于视频系统的用途。在电视的标准显示模式中,i表示隔行扫描,p表示逐行扫描。

 

  • #### 数字信号与模拟信号

视频记录方式一般有两种,一种是以数字信号(Digital)的方式记录,另一种是以模拟信号(Analog)的方式记录。

数字信号

数字信号以0和1记录数据内容,常用于一些新型的视频设备,如DC、Digits、Beta Cam和DV-Cam等。数字信号可以通过有线和无线的方式传播,传播质量不会随着传输距离的变化而变化,但必须使用特殊的传播设置,以保证在传输过程中不受外部因素的影响。

模拟信号

模拟信号以连续的波形记录数据,用于传统影音设备,如电视、VHS、S-VHS、V8、Hi8摄像机等。模拟信号也可以通过有线和无线的方式传播,其传输质量随着传输距离的增加而衰减。

 

JPEG

JPEG是最常见的一种图像格式,它的拓展名为 .jpg或.jpeg,其压缩技术十分先进。它用有损压缩方式去除冗余的图像和彩色数据,在取得极高的压缩率的同时能展示十分丰富生动的图像(换句话说,就是可以用最少的磁盘空间得到较好的图像质量)。

由于JPEG格式是采用平衡像素之间的亮度色彩的算法来压缩,因而更有利于表现带有渐变色彩且没有清晰轮廓的图像。

 

优劣

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

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

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

解析

代码如下:

像素归位

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

JavaScript

(1) ( ) ( ) ( ) ( ) ( ) ( ) ( ) (1) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) 1 1 ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) 1 1 ==> ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) (1) ( ) ( ) ( ) ( ) ( ) ( ) ( ) (1) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( )

1
2
3
4
5
6
7
8
9
10
                  (1) ( ) ( ) ( ) ( ) ( ) ( ) ( ) (1) ( )
                  ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( )
                  ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( )
1 1              ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( )
1 1     ==>      ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( )
                  ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( )
                  ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( )
                  ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( )
                  (1) ( ) ( ) ( ) ( ) ( ) ( ) ( ) (1) ( )
                  ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( )

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

前言

现在时富媒体时代,图片的重要性对于数十亿互联网用户来说不言而喻,图片本身就是像素点阵的合集,但是为了如何更快更好的存储图片而诞生了各种各样的图片格式:jpeg、png、gif、webp等,而这次我们要拿来开刀的,就是png。

代码如下:

png的故事:隔行扫描算法

2017/06/21 · 基础技术 · PNG

原文出处: AlloyTeam/june01   

扫描

上面说过,此次我们只考虑逐行扫描的方式:

JavaScript

// 读取8位无符号整型数 function readInt8(buffer, offset) {     offset = offset || 0;     return buffer[offset] << 0; }   let width; // 解析IHDR数据块时得到的图像宽度 let height; // 解析IHDR数据块时得到的图像高度 let colors; // 解析IHDR数据块时得到的通道数 let bitDepth; // 解析IHDR数据块时得到的图像深度   let bytesPerPixel = Math.max(1, colors * bitDepth / 8); // 每像素字节数 let bytesPerRow = bytesPerPixel * width; // 每行字节数   let pixelsBuffer = new Buffer(bytesPerPixel * width * height); // 存储过滤后的像素数据 let offset = 0; // 当前行的偏移位置   // 逐行扫描解析 for(let i=0, len=data.length; i<len; i+=bytesPerRow+1) {     let scanline = Array.prototype.slice.call(data, i+1, i+1+bytesPerRow); // 当前行     let args = [scanline, bytesPerPixel, bytesPerRow, offset];       // 第一个字节代表过滤类型     switch(readInt8(data, i)) {         case 0:             filterNone(args);             break;         case 1:             filterSub(args);             break;         case 2:             filterUp(args);             break;         case 3:             filterAverage(args);             break;         case 4:             filterPaeth(args);             break;         default:             throw new Error('未知过滤类型!');     }       offset += bytesPerRow; }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
// 读取8位无符号整型数
function readInt8(buffer, offset) {
    offset = offset || 0;
    return buffer[offset] << 0;
}
 
let width; // 解析IHDR数据块时得到的图像宽度
let height; // 解析IHDR数据块时得到的图像高度
let colors; // 解析IHDR数据块时得到的通道数
let bitDepth; // 解析IHDR数据块时得到的图像深度
 
let bytesPerPixel = Math.max(1, colors * bitDepth / 8); // 每像素字节数
let bytesPerRow = bytesPerPixel * width; // 每行字节数
 
let pixelsBuffer = new Buffer(bytesPerPixel * width * height); // 存储过滤后的像素数据
let offset = 0; // 当前行的偏移位置
 
// 逐行扫描解析
for(let i=0, len=data.length; i<len; i+=bytesPerRow+1) {
    let scanline = Array.prototype.slice.call(data, i+1, i+1+bytesPerRow); // 当前行
    let args = [scanline, bytesPerPixel, bytesPerRow, offset];
 
    // 第一个字节代表过滤类型
    switch(readInt8(data, i)) {
        case 0:
            filterNone(args);
            break;
        case 1:
            filterSub(args);
            break;
        case 2:
            filterUp(args);
            break;
        case 3:
            filterAverage(args);
            break;
        case 4:
            filterPaeth(args);
            break;
        default:
            throw new Error('未知过滤类型!');
    }
 
    offset += bytesPerRow;
}

上面代码前半部分不难理解,就是通过之前解析得到的图像宽高,再加上图像深度和通道数计算得出每个像素占用的字节数和每一行数据占用的字节数。因此我们就可以拆分出每一行的数据和每一个像素的数据。

在得到每一行数据后,就要进行这个png编码里最关键的1步——过滤。

代码如下:

尾声

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

图片 2

 

1 赞 2 收藏 评论

图片 3

文件头

png的文件头就是png图片的前8个字节,其值为[0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A],人们常常把这个头称之为“魔数”。玩过linux的同学估计知道,可以使用file命令类判断一个文件是属于格式类型,就算我们把这个文件类型的后缀改得乱七八糟也可以识别出来,用的就是判断“魔数”这个方法。有兴趣的同学还可以使用String.fromCharCode将这个“魔数”转成字符串看看,就知道为什么png会取这个值作为文件头了。

用代码来判断也很简单:

JavaScript

// 读取指定长度字节 function readBytes(buffer, begin, length) {     return Array.prototype.slice.call(buffer, begin, begin + length); }   let header = readBytes(pngBuffer, 0, 8); // [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]

1
2
3
4
5
6
// 读取指定长度字节
function readBytes(buffer, begin, length) {
    return Array.prototype.slice.call(buffer, begin, begin + length);
}
 
let header = readBytes(pngBuffer, 0, 8); // [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]

代码如下:

分析

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

图片 4

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

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

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

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

JavaScript

1 6 4 6 2 6 4 6 1 6 7 7 7 7 7 7 7 7 7 7 5 6 5 6 5 6 5 6 5 6 7 7 7 7 7 7 7 7 7 7 3 6 4 6 3 6 4 6 3 6 7 7 7 7 7 7 7 7 7 7 5 6 5 6 5 6 5 6 5 6 7 7 7 7 7 7 7 7 7 7 1 6 4 6 2 6 4 6 1 6 7 7 7 7 7 7 7 7 7 7

1
2
3
4
5
6
7
8
9
10
1 6 4 6 2 6 4 6 1 6
7 7 7 7 7 7 7 7 7 7
5 6 5 6 5 6 5 6 5 6
7 7 7 7 7 7 7 7 7 7
3 6 4 6 3 6 4 6 3 6
7 7 7 7 7 7 7 7 7 7
5 6 5 6 5 6 5 6 5 6
7 7 7 7 7 7 7 7 7 7
1 6 4 6 2 6 4 6 1 6
7 7 7 7 7 7 7 7 7 7

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

JavaScript

(1) 6 4 6 2 6 4 6 (1) 6 7 7 7 7 7 7 7 7 7 7 5 6 5 6 5 6 5 6 5 6 7 7 7 7 7 7 7 7 7 7 1 1 3 6 4 6 3 6 4 6 3 6 ==> 1 1 7 7 7 7 7 7 7 7 7 7 5 6 5 6 5 6 5 6 5 6 7 7 7 7 7 7 7 7 7 7 (1) 6 4 6 2 6 4 6 (1) 6 7 7 7 7 7 7 7 7 7 7

1
2
3
4
5
6
7
8
9
10
(1)  6   4   6   2   6   4   6  (1)  6
7   7   7   7   7   7   7   7   7   7
5   6   5   6   5   6   5   6   5   6
7   7   7   7   7   7   7   7   7   7                   1 1
3   6   4   6   3   6   4   6   3   6        ==>        1 1
7   7   7   7   7   7   7   7   7   7
5   6   5   6   5   6   5   6   5   6
7   7   7   7   7   7   7   7   7   7
(1)  6   4   6   2   6   4   6  (1)  6
7   7   7   7   7   7   7   7   7   7

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

过滤

早先我们说过过滤方法只有1种,其中包含5种过滤类型,图像每一行数据里的第一个字节就表示当前行数什么过滤类型。

png为什么要对图像数据进行过滤呢?

大多数情况下,图像的相邻像素点的色值时很相近的,而且很容易呈现线性变化(相邻数据的值是相似或有某种规律变化的),因此借由这个特性对图像的数据进行一定程度的压缩。针对这种情况我们常常使用一种叫差分编码的编码方式,即是记录当前数据和某个标准值的差距来存储当前数据。

比如说有这么一个数组[99, 100, 100, 102, 103],我们可以将其转存为[99, 1, 0, 2, 1]。转存的规则就是以数组第1位为标准值,标准值存储原始数据,后续均存储以前1位数据的差值。

当我们使用了差分编码后,再进行deflate压缩的话,效果会更好(deflate压缩是LZ77延伸出来的一种算法,压缩频繁重复出现的数据段的效果是相当不错的,有兴趣的同学可自行去了解)。

好,回到正题来讲png的5种过滤类型,首先我们要定义几个变量以便于说明:

JavaScript

C B A X

1
2
C B
A X

function getAndSetImageData(){
var url = 'xiangjishi.png';
getImage(url, function(img){
$('draw_image_canvas').width = img.width;
$('draw_image_canvas').height = img.height;
var context = $('draw_image_canvas').getContext('2d');
context.drawImage(img, 0, 0, img.width, img.height);
//获取像素信息
var offsetX = img.width/2;
var offsetY = img.height/2;
var getImgWidth = img.width/2;
var getImgHeight = img.height/2;
var imgageData = context.getImageData(offsetX, offsetY, getImgWidth, getImgHeight);
//设置像素信息,此处先忽略具体代码,知道是把上面获取的像素信息原封不动放到另一canvas里即可
var startX = 0;
var startY = 0;
var ct = $('get_image_canvas').getContext('2d');
$('get_image_canvas').width = img.width;
$('get_image_canvas').height = img.height;
ct.putImageData(imgageData, startX, startY);
});
}

拆图

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

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

数据块

去掉了png图片等前8个字节,剩下的就是存放png数据的数据块,我们通常称之为chunk

顾名思义,数据块就是一段数据,我们按照一定规则对png图片(这里指的是去掉了头的png图片数据,下同)进行切分,其中一段数据就是一个数据块。每个数据块的长度是不定的,我们需要通过一定的方法去提取出来,不过我们要先知道有哪些类型的数据块才好判断。

接口比较简单,创建的数据可以像用getImageData获取到的数据那样进行同样的处理,这里仅需要注意的是:这组图像数据不一定会反映canvas的当前状态。
四、关于imageData的一点补充
再《HTML5高级程序设计》以及很多文章里面,都把imageData.data当作一个数组来讲,但其实:

生成

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

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

png的故事:获取图片信息和像素内容

2017/03/25 · JavaScript · 1 评论 · PNG

原文出处: AlloyTeam   

该方法返回一个ImageData对象,该对象主要有三个属性:
imageData.width:每行有多少个元素
imageData.height:每列有多少个元素
imageData.data:一维数组,存储了从canvas中获取的每个像素的RGBA值。该数组为每个像素点保存了四个值——红、绿、蓝和alpha透明度。每个值都在0~255之间。因此,canvas上的每个像素在这个数组中就变成了四个整数值。数组的填充顺序从左到右,从上到下。

前言

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

PLTE

类型为PLTE的数据块用来存放索引颜色,我们又称之为“调色板”。

由IHDR数据块解析出来的图像信息可知,图像的数据可能是以索引值的方式进行存储。当图片数据采用索引值的时候,调色板就起作用了。调色板的长度和图像深度有关,假设图像深度的值是x,则其长度通常为2的x次幂 * 3。原因是图像深度保存的就是通道占用的位数,而在使用索引颜色的时候,通道里存放的就是索引值,2点x次幂就表示这个通道可能存放的索引值有多少个,即调色板里的颜色数。而每个索引颜色是RGB3色通道存放的,因此此处还需要乘以3。

通常使用索引颜色的情况下,图像深度的值即为8,因而调色板里存放的颜色就只有256种颜色,长度为256 * 3个字节。再加上1位布尔值表示透明像素,这就是我们常说的png8图片了。

demo_01如下:
<canvas id="draw_image_canvas" style="background:#ccc;"></canvas>

原理

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

数据块格式

数据块格式如下:

描述 长度
数据块内容长度 4字节
数据块类型 4字节
数据块内容 不定字节
crc冗余校验码 4字节

这样我们就可以轻易的指导当前数据块的长度了,即数据块内容长度 + 12字节,用代码实现如下:

JavaScript

// 读取32位无符号整型数 function readInt32(buffer, offset) {     offset = offset || 0;     return (buffer[offset] << 24) + (buffer[offset + 1] << 16) + (buffer[offset + 2] << 8) + (buffer[offset + 3] << 0); }   let length = readInt32(readBytes(4)); // 数据块内容长度 let type = readBytes(4); // 数据块类型 let chunkData = readBytes(length); // 数据块内容 let crc = readBytes(4); // crc冗余校验码

1
2
3
4
5
6
7
8
9
10
// 读取32位无符号整型数
function readInt32(buffer, offset) {
    offset = offset || 0;
    return (buffer[offset] << 24) + (buffer[offset + 1] << 16) + (buffer[offset + 2] << 8) + (buffer[offset + 3] << 0);
}
 
let length = readInt32(readBytes(4)); // 数据块内容长度
let type = readBytes(4); // 数据块类型
let chunkData = readBytes(length); // 数据块内容
let crc = readBytes(4); // crc冗余校验码

这里的crc冗余校验码在我们解码过程中用不到,所以这里不做详解。除此之外,数据块内容长度和数据块内容好解释,不过数据块类型有何作用呢,这里我们先将这个type转成字符串类型:

JavaScript

// 将buffer数组转为字符串 function bufferToString(buffer) {     let str = '';     for(let i=0, len=buffer.length; i<len; i++){         str += String.fromCharCode(buffer[i]);     }     return str; }   type = bufferToString(type);

1
2
3
4
5
6
7
8
9
10
// 将buffer数组转为字符串
function bufferToString(buffer) {
    let str = '';
    for(let i=0, len=buffer.length; i<len; i++){
        str += String.fromCharCode(buffer[i]);
    }
    return str;
}
 
type = bufferToString(type);

然后会发现type的值是四个大写英文字母,没错,这就是上面提到的数据块类型。上面还提到了我们只需要解析关键数据块,因此遇到type不等于IHDR、PLTE、IDAT、IEND中任意一个的数据块就直接舍弃好了。当我们拿到一个关键数据块,就直接解析其数据块内容就可以了,即上面代码中的chunkData字段。

图片 5 
到这里,基本能够清除getImageData方法四个参数对应的含义。putImageData参数的理解也不难,demo_2的代码略加修改后看下效果就知道了

解压缩

当我们收集完IDAT的所有数据块内容时,我们要先对其进行解压缩:

JavaScript

const zlib = require('zlib');   let data = new Buffer(length); let index = 0; dataChunks.forEach((chunkData) => {     chunkData.forEach((item) => {data[index++] = item}); });   // inflate解压缩 data = zlib.inflateSync(new Buffer(data));

1
2
3
4
5
6
7
8
9
10
const zlib = require('zlib');
 
let data = new Buffer(length);
let index = 0;
dataChunks.forEach((chunkData) => {
    chunkData.forEach((item) => {data[index++] = item});
});
 
// inflate解压缩
data = zlib.inflateSync(new Buffer(data));

二、获取/设置canvas图片数据

简介

首先,png是什么鬼?我们来看看wiki上的一句话简介:

Portable Network Graphics (PNG) is a raster graphics file format that supports lossless data compression.

也就是说,png是一种使用无损压缩的图片格式,而大家熟知的另外一种图片格式——jpeg则是采用有损压缩的方式。用通俗易懂的方式来讲,当原图片数据被编码成png格式后,是可以完全还原成原本的图片数据的,而编码成jpeg则会损耗一部分图片数据,这是因为两者的编码方式和定位不同。jpeg着重于人眼的观感,保留更多的亮度信息,去掉一些不影响观感的色度信息,因此是有损耗的压缩。png则保留原始所有的颜色信息,并且支持透明/alpha通道,然后采用无损压缩进行编码。因此对于jpeg来说,通常适合颜色更丰富、可以在人眼识别不了的情况下尽可能去掉冗余颜色数据的图片,比如照片之类的图片;而png适合需要保留原始图片信息、需要支持透明度的图片。

以下,我们来尝试获取png编码的图片数据:

function getAndSetImageData(){
var url = 'xiangjishi.png';
getImage(url, function(img){
$('draw_image_canvas').width = img.width;
$('draw_image_canvas').height = img.height;
var context = $('draw_image_canvas').getContext('2d');
context.drawImage(img, 0, 0, img.width, img.height);
//获取像素信息
var offsetX = img.width/2;
var offsetY = img.height/2;
var getImgWidth = img.width/2;
var getImgHeight = img.height/2;
var imgageData = context.getImageData(offsetX, offsetY, getImgWidth, getImgHeight);
//设置像素信息
var startX = img.width/2; //这里原先为0
var startY = img.width/2; //这里原先为0
var ct = $('get_image_canvas').getContext('2d');
$('get_image_canvas').width = img.width;
$('get_image_canvas').height = img.height;
ct.putImageData(imgageData, startX, startY);
});
}

IDAT

类型为IDAT的数据块用来存放图像数据,跟其他关键数据块不同的是,其数量可以是连续的复数个;其他关键数据块在1个png文件里有且只有1个。

这里的数据得按顺序把所有连续的IDAT数据块全部解析并将数据联合起来才能进行最终处理,这里先略过。

JavaScript

let dataChunks = []; let length = 0; // 总数据长度   // ...   while(/* 存在IDAT数据块 */) {     dataChunks.push(chunkData);     length += chunkData.length; }

1
2
3
4
5
6
7
8
9
let dataChunks = [];
let length = 0; // 总数据长度
 
// ...
 
while(/* 存在IDAT数据块 */) {
    dataChunks.push(chunkData);
    length += chunkData.length;
}

/**
* @description
* @param {Number} x 图像起始绘制点距离canvas最左侧的距离
* @param {Number} y 图像起始绘制点距离canvas最顶部的距离
* @param {Number} width 最终图像在canvas上绘制出来的宽度
* @param {Number} height 最终图像在canvas上绘制出来的高度
*/
context.drawImage(image, x, y, width, height)

结构

图片是属于2进制文件,因此在拿到png图片并想对其进行解析的话,就得以二进制的方式进行读取操作。png图片包含两部分:文件头和数据块。

var offsetX = 20;
var offsetY = 20;
var drawWidth = img.width/2;
var drawHeight = img.height/2;
context.drawImage(img, offsetX, offsetY, drawWidth, drawHeight);

IHDR

类型为IHDR的数据块用来存放图片信息,其长度为固定的13个字节:

描述 长度
图片宽度 4字节
图片高度 4字节
图像深度 1字节
颜色类型 1字节
压缩方法 1字节
过滤方式 1字节
扫描方式 1字节

其中宽高很好解释,直接转成32位整数,就是这张png图片等宽高(以像素为单位)。压缩方法目前只支持一种(deflate/inflate 压缩算法),其值为0;过滤方式也只有一种(包含标准的5种过滤类型),其值为0;扫描方式有两种,一种是逐行扫描,值为0,还有一种是Adam7隔行扫描,其值为1,此次只针对普通的逐行扫描方式进行解析,因此暂时不考虑Adam7隔行扫描。

图片深度是指每个像素点中的每个通道(channel)占用的位数,只有1、2、4、8和16这5个值;颜色类型用来判断每个像素点中有多少个通道,只有0、2、3、4和6这5个值:

颜色类型的值 占用通道数 描述
0 1 灰度图像,只有1个灰色通道
2 3 rgb真彩色图像,有RGB3色通道
3 1 索引颜色图像,只有索引值一个通道
4 2 灰度图像 + alpha通道

代码如下:

IEND

当解析到类型为IEND的数据块时,就表明所有的IDAT数据块已经解析完毕,我们就可以停止解析了。

IEND整个数据块的值时固定的:[0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4E, 0x44, 0xAE, 0x42, 0x60, 0x82],因为IEND数据块没有数据块内容,所以其数据块内容长度字段(数据块前4个字节)的值也是0。

修改后的demo效果如下,结合上面API的说明,应该不难理解四个参数所代表的含义
图片 6 

代码如下:

点评:本文通过简单的代码实例,以及略猥琐的图片demo,展示了canvas在图像像素数据操作方面的常用接口,有需求的朋友可以参考下

代码如下:

demo_2 展示效果如下

图片 7 
三、创建canvas图片数据

/**
* @description 预先创建一组图像数据,并绑定在canvas对象上
* @param {Number} width 创建的宽度
* @param {Number} height 创建的高度
*/
context.createImageData(width, height)

function $(id) { return document.getElementById(id); }
function getImage(url, callback){
var img = document.createElement('img');
img.onload = function(){
callback && callback(this);
};
img.src = url;
document.body.appendChild(img);
}
function drawImage(){
var url = 'xiangjishi.png';
var canvas = $('draw_image_canvas');
var context = canvas.getContext('2d');
getImage(url, function(img){
canvas.width = img.width;
canvas.height = img.height;
var offsetX = 20;
var offsetY = 20;
var drawWidth = img.width/4;
var drawHeight = img.height/4;
context.drawImage(img, offsetX, offsetY, drawWidth, drawHeight);
});
}
drawImage();

demo说明:加载xiangjishi.png,加载完成后,从相对于画布左上角坐标(0, 0)处开始,将xiangjishi.png绘制在画布上,效果如下:
图片 8
看到这里,可能对于 context.drawImage(image, x, y, width, height) 里四个参数的含义理解还不是特别清楚,可以简单把几个参数修改下看看效果:

context.drawImage(image, x, y, width, height)

代码如下:

<canvas id="draw_image_canvas" style="background:#ccc;"></canvas>
<canvas id="get_image_canvas" style="background:#ccc;"></canvas>

然后再将imageData.data的具体内容打印出来,内容较长,仅截取最前面以及最后面的一段,可以看出:
imageData.data其实是一个对象,其索引从0开始,一直到width*height*4-1。

图片 9
为什么不直接用数组存放?因为数组的长度有个上限,假设为limitLength,超过limitLength的元素,均以键值的方式存储,如 data[limitLength + 100] 其实是 data['limitLength + 100 + ''](limitLength具体值记不得了,有兴趣的童鞋可以查下)
至于最后面的byteLength、byteOffset、buffer属性,未深究,此处不展开以防误导读者。
五、写在后面
水平有限,如有疏误,敬请指出

/**
* @description 用特定的imageData设置canvas特定区域的像素信息
* @param {Number} x 从canvas的x点处开始设置
* @param {Number} y 从canvas的y点处开始设置
* @param {Number} width 获取的宽度
* @param {Number} height 最终的高度
*/
context.putImageData(imageData, x, y)

下面结合demo_2来说明getImageData()的用法以及各自参数的对应的含义
DEMO_02 源代码如下,在demo_01的基础上稍事修改:

/**
* @description 获取canvas特定区域的像素点信息
* @param {Number} x 获取信息的起始点距离canvas最左侧的距离
* @param {Number} y 获取信息的起始距离canvas最顶部的距离
* @param {Number} width 获取的宽度
* @param {Number} height 最终的高度
*/
context.getImageData(x, y, width, height)

代码如下:

demo_3展示效果如下,可是试着把几个参数自己改一下试下,可能会有更好的理解:

代码如下:

imageData.data返回的并不是真正的数组,而是一个类数组的对象,可以将imageData.data的类型打印出来
console.log(Object.prototype.toString.call(imgageData.data)); //输出:[object Uint8ClampedArray]

代码如下:

本文由17727发布于web前端信息,转载请注明出处:获取图片信息和像素内容,Canvas像素处理使用接

关键词: