几种常见计算机图像处理操作的原理及canvas实现

2013-09-21 • 技术文章 • 评论

前言
即使没有计算机图形学基础知识的读者也完全不用担心您是否适合阅读此文,本文的性质属于科普文章,将为您揭开诸如Photoshop、Fireworks、GIMP等软件的图像处理操作的神秘面纱。之前您也许对这些处理技术感到惊奇和迷惑,但笔者相信您读完本文后会豁然开朗。本文主要介绍几种常见计算机图像处理操作的原理,为了操作简便和保证平台兼容性,采用HTML5的canvas作为代码实现样例,当然您也可以使用Qt、VisualStudio系列、Java等进行实现且可以利用多线程和GPU编程技术提高大像素文件的处理效率。本文的原理部分适合所有层面的读者,代码实现部分需要读者对小学数学的加减乘除运算有一定了解(其实写一些基础性代码不就是小学数学这种层次的事吗?非专业读者完全不用怕!笔者就是在作为计算机白痴的小学生时期就开始写程序的)。
预备知识1:图像色点在计算机中的表示
对于一个图像,计算机单独处理组成该图像的每一个像素点。对于普通的位图(bitmap),每一个像素点的数据在计算机中是以红绿蓝(RGB)三色外加透明度(也就是Alpha通道,简记为A)进行存储的,RGBA四项分别由0-255的值表示,不同的RGB配比将显示为不同的颜色,A值从0-255代表了从完全透明到完全不透明。255,难道计算机不是用0和1来表示数值吗?当然,从0到255,恰好是256个数,也即2的8次方,也就是说本质是8位二进制数。如果我们进行位逻辑运算,当然应该把R/G/B都作为8位二进制值来进行计算。但是如果是做普通的算术计算,为什么不用我们熟悉的十进制呢?所以上面我说的是0-255,而不是00000000-11111111,由于都是很小的整数,我们也没有必要考虑有些十进制没法精确表示成二进制会带来浮点误差(举个浮点误差的例子:0.2+0.1=0.30000000000000004,原因是0.2没法表示成有限二进制数,只能产生误差,但一般而言256以内的小整数加减法计算机还是hold住的)。
举个简单的例子,当Windows用户熟练地用画图(mspaint)保存图像时,在保存格式(可通俗理解为扩展名)选项中可以看到24位位图(.bmp)这一项,其中的24位正是上面所讲的RGB的二进制共计8×3=24位,没有A值是完全不透明的。
此外,我们再扩展一点16进制(0到F)颜色表示的知识,那就是每4位二进制表示成一位十六进制,比如1111就等于F。所以我们经常可以看到不少网页的样式中有类似color:#FF6600这样的表示的颜色,其实就是11110110011000000000的24位RGB,不带A值。而CSS3中引入了RGBA表示,我们就可以设定一个color:rgba(255,0,0,0.5),也就是半透明的红色,和上面位图存储的A值的区别是它使用了0-1来表示透明度而不是0-255。在部分图形处理代码中你可能会看到位运算中有0xFFFFFF之类的表示,0x就是告诉计算机后面这是16进制数。
预备知识2:卷积核
在计算机图形处理中,不了解卷积矩阵(Convolution Matrix)的计算是万万不行的。大多数滤镜都用到了卷积矩阵计算,所以这是必备知识。数学对于计算机科学是极为重要的,微积分、离散数学、线性代数、概率论与数理统计、数值方法都是基础性支撑。3x3矩阵和5x5矩阵的卷积计算是最基本的,学习过信号处理的同学一定对利用卷积计算进行滤波有深入的认识,没学习过的请继续向下阅读本节。
卷积是图像处理常用的方法,给定输入图像,在输出图像中每一个像素是输入图像中一个小区域中像素的加权平均,其中权值由一个函数定义,这个函数称为卷积核(kernel)。这里所介绍的卷积运算,就是这样一个过程,图像区域中的每个像素分别与权矩阵的每个元素对应相乘,所有乘积之和作为区域中心像素的新值。形象一点来讲,对于下图左侧所示的一个图像中的一块3x3区域和一个权矩阵W=[0 1 0; 0 0 0; 0 0 0]进行卷积核运算:中心像素值=40×0+42×1+46×0+46×0+50×0+55×0+52×0+56×0+58×0=42,卷积核运算相对于卷积运算要简单得多。假如我们将除了边界像素的其余像素点一一作为中心像素和W矩阵进行卷积核运算,那么将会实现图像向下位移一个像素。你看,最左绿框中间居上的42是不是向下移动了一个格子成为了红框中的值呢?是的,它发生了一个像素的位移。如果W矩阵中的1位置不同则位移方向不同,这非常易于理解。
W矩阵的不同将带来各种不同的炫酷效果,接下来几个部分中我们将举几个典型的例子进行说明。
使用Matlab可以很容易地进行各类卷积计算,但是我们下面是用JavaScript实现的计算函数,它的通用性很高,除了卷积核计算外还包含了颜色偏移量和除数这两个参数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function ConvolutionMatrix(input, m, divisor, offset){
	var output = document.createElement("canvas").getContext('2d').createImageData(input);
	var w = input.width, h = input.height;
	var iD = input.data, oD = output.data;
	// 对除了边缘的点之外的内部点的 RGB 进行操作,透明度在最后都设为 255
	for (var y = 1; y < h-1; y += 1) {
		for (var x = 1; x < w-1; x += 1) {
			for (var c = 0; c < 3; c += 1) {
				var i = (y*w + x)*4 + c;
				oD[i] = offset
					+(m[0]*iD[i-w*4-4] + m[1]*iD[i-w*4] + m[2]*iD[i-w*4+4]
					+ m[3]*iD[i-4]     + m[4]*iD[i]     + m[5]*iD[i+4]
					+ m[6]*iD[i+w*4-4] + m[7]*iD[i+w*4] + m[8]*iD[i+w*4+4])
					/ divisor;
			}
			oD[(y*w + x)*4 + 3] = 255; // 设置透明度为不透明
		}
	}
	return output;
}
预备知识3:使用canvas对像素点实现基本的处理操作
1
2
3
4
// 获取像素点数据
var canvas = document.getElementById('myCanvasElt');
var ctx = canvas.getContext('2d');
var canvasData = ctx.getImageData(0, 0, canvas.width, canvas.height);
获取到的canvasData对象包含下列成员,其中的data数组结构大概是这样的,一行一行存,然后一个列点一个列点存,每个点占4个下标,分别是RGBA呗,则对于坐标(x,y)(这里的y是下方正向),RGBA分别是data[(y*width+x)*4],data[(y*width+x)*4+1],data[(y*width+x)*4+2],data[(y*width+x)*4+3]。
1
2
3
4
5
canvasData {
    width: unsigned long,
    height: unsigned long,
    data: CanvasPixelArray
}
至于像素数据的刷新,直接对上面的data[i]赋值不就得了。下面是刷新图像,只需一行。
1
ctx.putImageData(canvasData, 0, 0);
下面是一个完整处理过程的样例代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var canvas = document.getElementById('myCanvasElt');
var ctx = canvas.getContext('2d');
var canvasData = ctx.getImageData(0, 0, canvas.width, canvas.height);
for (var x = 0; x < canvasData.width; x++) {
    for (var y = 0; y < canvasData.height; y++) {
        var idx = (x + y * canvas.width) * 4;
        var r = canvasData.data[idx + 0];
        var g = canvasData.data[idx + 1];
        var b = canvasData.data[idx + 2];
        var avg = (r + g + b) / 3;
        canvasData.data[idx + 0] = avg;
        canvasData.data[idx + 1] = avg;
        canvasData.data[idx + 2] = avg;
    }
}
ctx.putImageData(canvasData, 0, 0);
牛刀小试:亮度调整、透明化、灰化、反色、对比度增强、侵蚀和膨胀
亮度处理和透明化处理的过程非常简单,就是刷新一下RGBA四个值而已。亮度提高可以通过增大RGB值实现,比如我们给RGB三个值分别加100(请放心,如果结果超过255计算机会自动按255处理)就实现了亮度的提高。而我们把A值赋一个127,则实现了半透明。赋值过程使用下面的代码替代掉上面代码样例中的几层for循环即可。
1
2
3
4
5
6
7
var offset = 100; //自定义
for (var i=0; i< canvasData.data.length; i+=4) {
	d[i] += offset;
	d[i+1] += offset;
	d[i+2] += offset;
	d[i+3] = 127;
}
灰化的实现要分析人类视觉的特点,人眼弱于识别红和蓝,所以需要调低他们的亮度。科学家们整理出一个灰化公式,将RGB都赋值为 0.2126*r+0.7152*g+0.0722*b即可实现彩色图像灰度化。这很简单,不再给出代码样例。科学界值得一提的一项设计就是彩色电视信号无需任何其它处理即可被黑白电视机接受并输出为黑白显示结果,当然这与我们这里的灰化处理并不一样,只是顺便提一句。
反色只要用255减去各点RGB值。
对比度增强只要各点的RGB值乘以2再减掉255或者150(可以根据需要设定),下界为0。
侵蚀:中心像素取周边8个像素的最亮值,可用于去除小的噪点。
膨胀:中心像素取周边8个像素的最暗值,可用于加粗字体、制作氖灯效果。
利剑出鞘:图形中的字符识别
你没看错,就是利用canvas进行图像处理实现字符识别,本节以验证码识别为例来展开。一个普通的验证码(腾讯、迅雷、Google都有推出连人都很难识别出来的验证码,复旦大学选课系统还推出了微积分计算验证码,这一类我们就先不让计算机做尝试了,这太残酷了),通常由浅色的噪音干扰和深色字符组成。我们需要将验证码的图形做二值化处理,也就是通过计算,把浅色的统一处理成白色,深色的统一处理成黑色,然后提取出黑白的二进制RGB值,刷新足够多的次数,把0-9的RGB码值特征都拿到手。然后对于一个新的验证码,我们通过对比这些特征码,就可以识别出是哪几个数字。
首先我们从某站点找到了一种无扭曲的0-9四位验证码,然后提取出特征码numbers=["111000111100000001100111001001111100001111100001111100001111100001111100001111100001111100100111001100000001111000111111111111111111111111111111","111000111100000111100000111111100111111100111111100111111100111111100111111100111111100111111100111100000000100000000111111111111111111111111111","100000111000000011011111001111111001111111001111110011111100111111001111110011111100111111001111111000000001000000001111111111111111111111111111","100000111000000001011111001111111001111110011100000111100000011111110001111111001111111001011110001000000011100000111111111111111111111111111111","111110011111100011111100011111000011110010011110010011100110011100110011000000000000000000111110011111110011111110011111111111111111111111111111","000000001000000001001111111001111111001111111000001111000000011111110001111111001111111001011110001000000011100000111111111111111111111111111111","111000011110000001100111101100111111001111111001000011000000001000111000001111100001111100100111000100000001111000011111111111111111111111111111","100000000100000000111111100111111101111111001111110011111110111111100111111101111111001111111001111110011111110011111111111111111111111111111111","110000011100000001100111001100111001100011011110000011110000011100110001001111100001111100000111000100000001110000011111111111111111111111111111","110000111100000001000111001001111100001111100000111000100000000110000100111111100111111001101111001100000011110000111111111111111111111111111111"]。通过以下方式处理即可得到其中的4个数字,我们就可以通过console看到识别结果了。如果把结果的值赋给验证码input元素的value,再模拟一个click()动作,那么就可以免输验证码直接登录了。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
var recResult = "";
var image = document.querySelector("#img1");
var canvas = document.createElement('canvas');
var ctx = canvas.getContext("2d");
canvas.width = image.width;
canvas.height = image.height;
ctx.drawImage(image, 0, 0);
for (var i = 0; i < 4; i++) {
	var ldString = "";
	var getDat = ctx.getImageData(13 * i + 7, 3, 9, 16);
	var pixels = getDat.data;
	for (var j = 0,length = pixels.length; j < length; j += 4) {
		ldString = ldString + (+(pixels[j]*0.3+pixels[j+1]*0.59+pixels[j+2]*0.11>=140));
	}
	var comms = numbers.map(function (value) {
		return ldString.split("").filter(function(v,index) {
			return value[index] === v;
		}).length
	});
	recResult += comms.indexOf(Math.max.apply(null,comms));
}
console.log(recResult);
数学魅力:卷积核的鬼斧神工
模糊:模糊矩阵可以设定为全1矩阵,除数为9,相当于值全为1/9的矩阵。这个矩阵把周边元素和中心元素做了一个平均数,从而使点间过渡更加光滑,也就实现了模糊。
锐化:锐化矩阵为[0 -1 0; -1 5 -1; 0 -1 0],本质是使中心点与上下左右几个点的过渡更加粗糙,也就实现了锐化。
根据计算公式我们可以很清楚地理解矩阵的含义,所以下面不再进行具体说明,仅给出矩阵。
浮雕:[-2 -1 0; -1 1 1; 0 1 2]。
边缘增强:[0 0 0; -1 1 0; 0 0 0]。
边缘检测:[0 1 0; 1 -4 1; 0 0 0]。
索贝尔边缘检测:横向[-1 0 1; -2 0 2; -1 0 1],纵向[1 2 1; 0 0 0; -1 -2 -1]。
将以上矩阵代入ConvolutionMatrix()函数,并对像素点进行计算即可实现这些效果。
另外,对视频图像和图片中的人物等对象进行识别、识图搜索也是目前计算机科学领域正在研究的方向,前景广阔,这其中也有很多卷积运算、微积分等数学知识的应用。
试试看
光说不练假把式,效果预览请访问 测试页面,笔者在里面给出了一些实现的样例供参考。
如果读者对canvas图形感兴趣,也可以访问这个 链接以饱眼福。
后记
以上我们介绍了一些图像处理的基础知识,但通常我们在处理图像时是对局部进行的,这种情况需要我们利用操作系统的API定位光标位置确定要对哪块图像进行处理。如果您是专业读者,建议您在理解这些原理后,自己尝试开发一款图像处理软件替代Photoshop,以规避高额的软件授权费和盗版带来的法律风险,当然完全替代还需要很多更复杂的理论知识,本文作为科普文章就不多加介绍了。
HTML5的canvas对于图形的处理非常方便,很受开发人员的欢迎,更多canvas的应用也有待我们去探索。
问与答
如何将canvas处理得到的图形保存为文件?答:canvas提供了toDataURL的接口,可以方便的将canvas画布转化成base64编码的图形。如果要直接把图片生成后下载到本地可以直接改图片的mimeType,强制改成steam流类型。
参考资料与扩展阅读
http://hacks.mozilla.org/2009/06/pushing-pixels-with-canvas/(文中有一个赋值错误)
http://docs.gimp.org/en/filters-generic.html

你可能感兴趣的:(图像处理)