背景

最近在做一个需求是对页面某一块区域截图,本身没什么,但是发现需要截图的区域非常大,然后里面的内容通常只有一小点,这样我把整张图片拿出去就会发现只有中间一点点有东西,其他地方都是空白的,用户体验就会比较差了,记得PhotoShop里面有一个功能是能截取整张图片的最小有效像素区域,本来以为canvas自带有这种功能,没想到还是要自己实现的,虽然功能很简单,但是网上也没找着相应类库,可能是我搜索的姿势不对,或者这个需求实在太简单了,没人封装成类库了,虽然完全没做过图像处理,也开始找一些资料。

文字描述比较抽象,放上一张图片。

原始图片

处理后图片

canvas

网页内容是用Html2Canvas把DOM截图成了canvas,原理不是很清楚,大概是读取DOM样式然后在canvas里面重新画一遍,这不是重点,这个时候我们已经拿到画成了的canvas了,改怎么处理呢?

1
2
3
var canvas  = document.getElementById( 'canvas' );
var ctx = canvas.getContext( '2d' );
var imgData = ctx.getImageData( 0, 0, canvas.width, canvas.height );

通过getImageData方法就能够拿到canvas的原始像素点信息了,四个参数描述了一个矩形,返回的像素点信息就是这个矩形内的信息。

返回的是一个Uint8ClampedArrayUint8ClampedArrayJavaScript中的类型数组,具体的可以参看这里,简单来看就是这个数组中的元素都是unsigned int并且都是8位的,所以取值范围是0~255,非常适合用来存放ImageData,而且类型数组因为类型都是确定的,使用起来比普通数组速度上会快很多。

ImageData

拿到了原始的像素点数据,来看看数据长什么样子,首先看数组长度,假设是一个100X100的图片,那么理论上像素点的数量是100100,也就是10000个,拿到ImageData的数组看长度其实有40000,简单猜测一下大概是10000 rgba,看具体的数组内容很容易就能够验证我们的猜想。

一个典型的ImageData大概长这个样子,这是一个从左往右,从上到下按顺序排列的一个数组:

1
2
3
4
5
6
imageData = [ 
0, 0, 0, 0,
255, 255, 255, 1,
122, 122, 122, 0.5,
....
];

如何获取最小像素区域?

要实现这个功能的本质,其实就是如何寻找最上、最右、最下以及最左四个点(P1,P2,P3,P4),有了这四个点,我们就能画出一个最小矩形,把这个图像框在里面。

一维组数映射到二维空间

根据数组下标计算当前点在二维空间的坐标,方法其实很简单。

看下面这张图,可以认为这是一张 5X4 的一张图片,那么总共就有20个像素点,每个格子里的0-19数字就意味着这是在一维数组里面的下标N,由此可得

X = N % width;
Y = Math.floor( N / width );

pixel

全数组扫描

首先能想到的最简单的办法,就是循环整个数组,当alpha值大于0的时候,即可判断这个点是一个有效像素点,然后整个数组循环完成之后,即可知道得到P1,P2,P3,P4四个点。

以下是实现代码:

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
46
47
function getEffectiveRect( imageData, width, height ) {
// 上点, Y值最小
var p1 = [ 0, height ];
// 右点, X值最大
var p2 = [ 0, 0 ];
// 下点, Y值最大
var p3 = [ 0, 0 ];
// 左点, X值最小
var p4 = [ width, 0 ];

var data = imageData.data;
var number = 0;
console.time( 'image1' );
for ( var i = 0; i < data.length; i ) {
number++;
var r = data[ i++ ];
var g = data[ i++ ];
var b = data[ i++ ];
var a = data[ i++ ];
if ( a === 0 ) {
continue;
}
var x = number % width;
var y = ( number - x ) / width;
if ( p1[ 1 ] > y ) {
p1[ 0 ] = x;
p1[ 1 ] = y;
}

if ( p2[ 0 ] < x ) {
p2[ 0 ] = x;
p2[ 1 ] = y;
}

if ( p3[ 1 ] < y ) {
p3[ 0 ] = x;
p3[ 1 ] = y;
}

if ( p4[ 0 ] > x ) {
p4[ 0 ] = x;
p4[ 1 ] = y;
}
}

return { p1, p2, p3, p4 };
}

从矩形的四个角分别去寻找

具体的思路如下图:

from-rect

理论上只要有效像素区域越大,那么需要计算的量就越少,速度也就越快,以下是我的实现代码:

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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
function getEffectiveRect2( imageData, width, height ) {
var data = imageData.data;

console.time( 'image2' );
var p1 = [];
var p2 = [];
var p3 = [];
var p4 = [];

// 寻找P1
var len = 0
for ( var i = 0; i < data.length / 4; i ++ ) {
len += 7;
var a = data[ len ];
if ( a !== 0 ) {
var x = i % width;
var y = ( i - x ) / width;
p1 = [ x, y ];
break;
}
};

//寻找P3
var len = 0;
for ( var i = data.length / 4 - 1; i >= 0; i -- ) {
len += 7;
var a = data[ len ];
if ( a !== 0 ) {
var x = i % width;
var y = ( i - x ) / width;
p3 = [ x, y ];
break;
}
}

// 寻找P2
for ( var i = width - 1; i >= 0; i -- ) {
if ( p2.length ) {
break;
}
var a = p1[ 1 ];
var b = p3[ 1 ];
var len = i + a * width;
for ( var j = a; j < b; j ++ ) {
len += width;
// var point = i + j * width;
var point = len;
var a = data[ point * 4 + 3 ];
if ( a !== 0 ) {
var x = point % width;
var y = ( point - x ) / height;
p2 = [ x, y ];
break;
}
}
};

// 寻找P4
for ( var i = 0; i < width; i ++ ) {
if ( p4.length ) {
break;
}
for ( var j = p1[ 1 ]; j < p3[ 1 ]; j ++ ) {
var point = i + j * width;
var a = data[ point * 4 + 3 ];
if ( a !== 0 ) {
var x = point % width;
var y = ( point - x ) / height;
p4 = [ x, y ];
break;
}
}
}

return { p1, p2, p3, p4 };
}

将两种方法分别去计算最开头的图片,两种方式的计算时间分别是:

1
2
image1: 10.83ms
image2: 15.32ms

结论

方法2引入的复杂的逻辑反而降低了计算时间,逻辑最简单的全数组扫描比第二种方式快了30%,因为涉及到的东西比较多,目前还不清楚到底是什么原因导致第二种方式速度如此慢,测试了一下,只有当一整张图片几乎全部有像素点的时候,两种方法的速度才差不多。后续我会继续寻找原因,猜测可能是方法2的计算量比方法1多,而方法1更多的是比较和赋值。