在HTML5中,canvas节点所提供的2d上下文(context),具备一组对像素进行操作的方法:getImageData/putImageData/createImageData。通过这三个方法,我们可以进行像素级别的图像处理。比如可以移植传统的各种滤镜算法,在web页面中开发出一套类似于PS的图片处理应用,等等。
一般来讲,我们首先会通过getImageData方法,获取canvas上一个区域的像素数据。例如:
context.getImageData(10, 10, 100, 80);
这样我们会得到坐标为{10, 10},宽100、高80的一个区域的像素。返回对象ImageData有三个属性:width/height/data:
这里需要注意的是data。从上图中我们可以看到,它的数据类型是Uint8ClampedArray,是一个8位的无符号数组。请移步至http://www.khronos.org/registry/typedarray/specs/latest/,了解一下ES5中有关Typed Array的规范。
data是个一维的数组,每4个数字对应一个点的RGBA色值。所以可以知道,这个数组长度应该是总像素数*4。对像素的描述,是从左到右、从上到下逐行扫描的过程。这种存储方式,或者说一维的描述图像的方式,与我们所习惯的二维坐标系方式不同。所以我希望能创建一个对象,实现二维到一维的转换,把自己从苦逼算点的工作中解脱出来;同时也希望通过本文,让各位初学者能够了解对像素如何进行基本的操作。
首先,我设计一个接口,它应该具备以下几个功能:
// interface ISmartImageData{ long getPointCount(); //获取像素总数 Color getValue(long index); //获取索引为index的[像素]的色值 long getPointIndex(x, y); //转换坐标为像素索引 Color getPointValue(x, y); //获取坐标点的色值 }
这里我们还需要一个辅助对象Color。Color使用了数组作为基础结构,为了后续一些运算上的方便,绑定了一些额外的方法。
// function _extend(a, b){ //a -> b for(var p in a) b[p] = a[p]; } var _ColorMethods = { //两个色值相加 add : function(v){ this[0] += v[0]; this[1] += v[1]; this[2] += v[2]; this[3] += v[3]; }, //色值平均到c个点 avg : function(c){ this[0] = Math.round(this[0] / c); this[1] = Math.round(this[1] / c); this[2] = Math.round(this[2] / c); this[3] = Math.round(this[3] / c); }, //色值是否相等(Alpha值暂时忽略) eq : function(v){ return this[0] === v[0] && this[1] === v[1] && this[2] === v[2] //&& this[3] === v[3] ; }, //与色值v比较,各个色值差允许在sub以内。可以作为降噪的一种方式。 fuzzy : function(v, sub){ return Math.abs(this[0] - v[0]) <= sub && Math.abs(this[1] - v[1]) <= sub && Math.abs(this[2] - v[2]) <= sub //&& Math.abs(this[3] - v[3]) <= sub ; } } function Color(c){ _extend(_ColorMethods, c); return c; }
如果需要让Color对象具备更多的方法,可以对_ColorMethods进行扩展。额外说一点:Color使用数组还是{R:0,G:0,B:0,A:0}这种方式,在实际中性能上几乎没有的区别。
现在我们开始构建SmartImageData对象。首先我们使用一个ImageData来初始化。
// function SmartImageData(imageData){ this.data = imageData.data; this.width = imageData.width; this.height = imageData.height; }
在实际操作中发现一个问题。如果你频繁调用imageData原始对象的width/height属性时,会造成性能上的较大损耗。原因不明。所以我将三个属性分别独立出来。下面实现ISmartImageData接口。
// SmartImageData.prototype = { getPointCount : function(){ return this.width * this.height; }, getValue : function(index){ var i = index * 4; var d = this.data; return Color([d[i+0],d[i+1],d[i+2],d[i+3]]); }, getPointIndex : function(x, y){ return (y * this.width + x) * 4; }, getPointValue : function(x, y){ var index = this.getPointIndex(x, y); return this.getValue(index); } }
剩下的工作就没什么悬念了,可以借助这套系统,扩展出一系列的set方法,将处理过的数据,通过putImageData再放回到canvas中。
比如最近在做的马赛克的处理,至于是降低采样,还是算区域的平均值来得到区块的颜色,都可以通过坐标系来实现。
PS1:在刚结束的HTML5开发者嘉年华上,腾讯的工程师们实现了通过摄像头捕捉用户动作、进行体感操作。相信各位在今晚好好学习、通宵的努力,一定也能解决这个难题。
PS2:祝各位七夕快乐!