在Windows平台上为Matlab编译libSVM

2014-02-25 • 技术文章

在下载完libSVM后,会发现其中有多个目录,分别对应不同的平台和环境,其中Matlab目录是为Matlab准备的源文件,将该目录切换为当前目录。执行mex -setup根据提示可选择合适的编译器。因为源代码中含有C++代码并非全由C编写,所以需要使用C++编译器,如果系统仅给出了Lcc C而没有支持C++的编译器是无法进行编译的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
>> mex -setup
Please choose your compiler for building external interface (MEX) files: 
 
Would you like mex to locate installed compilers [y]/n? y
 
Select a compiler: 
[1] Digital Visual Fortran version 6.0 in C:\Program Files\Microsoft Visual Studio 
[2] Lcc C version 2.4 in C:\MATLAB7\sys\lcc 
[3] Microsoft Visual C/C++ version 6.0 in C:\Program Files\Microsoft Visual Studio 
 
[0] None 
 
Compiler: 3
 
Please verify your choices: 
 
Compiler: Microsoft Visual C/C++ 6.0 
Location: C:\Program Files\Microsoft Visual Studio

当执行make进行编译时,可能遇到错误。我们不妨打开make.m进行分析,其中虽然说该文件适用于各平台的Matlab,但其实在Windows平台上还是有问题的。其中各行编译命令基本上是这样的结构:

1
mex CFLAGS="\$CFLAGS -std=c99" -largeArrayDims <srcFile>

这里的编译参数-largeArrayDims是仅适用于64位操作系统的,如果你是32位机,则需要删掉此参数。若遇到CFLAGS="\$CFLAGS -std=c99"不能支持的错误,则需要更改该参数。我们执行help mex可看到编译方法的详细指导信息,其中Options File Details部分对于不同系统环境给出了不同的编译参数写法。对Windows平台应将make.m中的参数更改为Windows适用的方式才可顺利识别。

1
2
CFLAGS="\$CFLAGS -std=c99" // 适用于 UNIX 
COMPFLAGS="$COMPFLAGS -std=c99" // 适用于 Windows

在完成这些步骤后,如果执行make后提示有未声明的标识符(Undeclared identifier)mwIndex等等,这是因为在各源文件中都有这样的几行

1
2
3
#if MX_API_VER < 0x07030000
typedef int mwIndex;
#endif

可能是MX_API_VER不满足条件造成的问题,此时我们需要在宏外再加一行typedef int mwIndex;来完成类型声明。由于各源代码文件中均含有这一行,所以需要逐一修改。完成后,编译即可顺利进行。

在我所使用的Matlab 7(R14)中,编译后生成的是.dll,但据说在高版本并且中是生成.mexw32或.mexw64文件,具体扩展名根据软件版本和操作系统位数而定。

在编译器选择中,如果编译器版本太高,Matlab可能无法识别。如Matlab R2012a就无法识别Visual Studio 2013,这是需要我们到Matlab安装目录寻找mexopts路径和mbuildopts路径修改其中从注册表中读取有关VS路径等信息的行,改成可获取到新版本的有效途径即可,当然这并不保证完全兼容。最后还是建议“不折腾”,给Matlab装个低版本的编译器更好。

为了验证编译是否成功并且命令已经可用,在Matlab中执行libsvmread('../heart_scale'),如果返回读取到的数据说明配置成功。

双面打印的文档设置装订线的方法

2014-02-22 • 技术文章

在需要双面打印的文档中,设置左侧装订线后流出的空白默认情况下全在左侧,背面也就变成了右侧,为了防止内容被遮盖,这是需要在页边距选项中设置页边距为“对称页边距”或“镜像”(具体名称因软件和版本而异)。

C语言编译错误:Variably modified array at file scope

2014-02-09 • 技术文章

今天在编译一段C源程序时,遇到编译错误提示:Variably modified array at file scope。原因在于代码头部有这样几行:

1
2
const int length = 256;
int a[length];

在C语言中,const不是一个真真正正的常量,其代表的含义仅仅是只读。使用const声明的对象是一个运行时对象,无法使用其作为某个量的初值、数组的长度、case的值或在类型的情形中使用。以上是全局的情况,那么仅在一个块中定义的情况呢?下例中const所限定的值超出其生命周期后可被修改这很容易理解不必多作解释。

1
2
3
4
5
int i;
for(i = 0; i < 6; ++i) {
	const int j = i; // 试试 const int j = rand();
	printf("%d", j); // Output: 012345
}

如果要在C中定义编译时常量,也就是一个真正的常量,可以使用#define宏定义,但是#define的问题在于它是直接替换源代码中的所有匹配字符串,容易造成误替换,因此对于像int型这样的情况可以耍点小把戏,就像这样:

1
2
enum {length = 256};  // Or: #define length 256
int a[length];

那么我们经常说const也并非不可修改,利用某些小技巧可以绕过只读约束,下面的例子中被const限定的t就仍然可被修改:

1
2
3
const int t = 1;
*(int*)&t = 0;
printf("%d",t); // Output: 0

参考资料

ISO Sec. 6.4; H&S Secs. 7.11.2,7.11.3 pp. 226-7

ASP.NET ViewState(视图状态)浅析

2014-01-27 • 技术文章

状态管理的必要性

状态的有无在计算机领域通常被用来描述一个计算机程序是否可以在与用户的交互过程中的后续环节记住之前的处理事件。HTTP是一个无状态协议。服务器端对用户的页面请求返回一个相应的页面,但是却没有记录下这一请求,将连接关闭了。也就是说,每一次通信是独立的与之后在发生的请求无关,服务器无法获知用户当前的状态(State)。

HTTP是Web的基石,对于复杂的Web应用而言,这样的无状态服务是无法满足需求的。假如我们刚刚登录到某一站点,但是由于没有记录下你的登录状态,你基于登录权限才能请求的数据在接下来的环节就无法获取。所以必须要通过某种技术来实现状态的记录。

几种状态管理的技术

我们所熟悉的Cookie和Session就是典型的状态管理技术实现。状态管理技术分为用户端和服务器端两种。在用户端,通常有视图状态(View State)、隐藏域、Cookie、控制状态(Control State);在服务器端,通常有Session、Application Object、缓存、数据库。这两种不同的管理技术分别在用户端和服务器端存储数据,我们在选择要使用的技术时要根据具体的需求来确定,毕竟这牵扯到服务器端压力的问题。

ASP.NET的ViewState

ASP.NET提供了视图状态、Cookie、Session、Application Object等技术来进行页面的状态管理。这里我们介绍视图状态,ViewState是ASP.NET的开发中经常用到的一种状态管理机制。在使用ASP.NET服务器端控件的页面中,我们通常会发现__VIEWSTATE的隐藏域,这些就是根据视图状态的管理需求所生成的。ViewState可以在postback时保存页面的值,它是ASP.NET中一种内建的特性,可以自动的在同一页面的多次请求过程中存储页面状态。在程序中,如果我们需要人工的利用ViewState机制来保存某个值,是典型的键值式存取,可通过以下样例代码实现:

1
2
3
4
// 保存,值可为多种类型
ViewState["VarName"]=Value;
// 取出
string Test=ViewState["VarName"];

下图以一个Label控件的Text为例具体说明的ViewState的工作过程:

增强ViewState的安全性

ViewState是默认以非加密的base64形式来存储数据,这很容易处理为真实数据,易被劫持,因此安全性不高,为了提高安全性,我们可以启用加密特性。第一种选择是在页面中设置EnableViewStateMAC="true",这是一种校验和机制,在下一次的postback过程中校验和会被用来进行验证,如果发现了不匹配的情况则拒绝此次postback。另一种选择是在头部设置ViewStateEncryptionMode="Always",设置之后状态数据会得到加密保护。当然,这两种设置都可以在web.config中进行统一设定。

ViewState的开销

虽然ViewState是用户端的,但是同样要注意其开销。ViewState在保存一个状态时需要将其所辖的控件的状态都统一序列化为一个base64的字符串,在postback过程中加载状态时还要再进行反序列化。__VIEWSTATE的隐藏域如果存储的状态信息过多,base64的字符串就会非常长,在发送数据时会被放在HTTP POST的头部,额外增加了请求的响应时间。

禁用ViewState

笔者长期以来都采用PHP进行Web开发,基本上都是应用Cookie和Session来进行状态管理,对于ASP.NET中突然杀出来的ViewState不是太感冒。无论是基于习惯还是为了节约那么一点开销,我们有必要研究一下如何禁用该特性。

在页面中或对某一控件设置EnableViewState="False"可以禁用ViewState。禁用后我们仍会在页面中看到__VIEWSTATE隐藏域,这是正常的,因为仍然要通过某种手段判断postback。如果更彻底一点清除__VIEWSTATE,可以重写下面的方法:

1
2
3
4
5
protected override void SavePageStateToPersistenceMedium(object state){
}
protected override object LoadPageStateFromPersistenceMedium(){
	return null;
}

即使重写了以上方法,__VIEWSTATE值为空,但其仍然会出现在页面中。网上有文章称,可以通过生成页面时进行字符串过滤或者在用户端基于浏览器用js来彻底删除这些隐藏元素。这简直就是自己骗自己嘛!要真正降低开销来禁用ViewState才是有意义的,倘若像某些文章中的方法一样来实现彻底删除真没意思,更何况这个留下的__VIEWSTATE也还是有其作用的。

深入剖析,上点代码

如果您想深入一点了解ViewState的机制和高级使用方式,请果断点击参考资料中的第一个和最后一个超链接。毕竟笔者不喜欢这种机制,所以仅作浅析了解一下,不再继续深入。

参考资料与扩展阅读

http://msdn.microsoft.com/en-us/library/ms972976.aspx

http://www.codeproject.com/Articles/31344/Beginner-s-Guide-To-View-State

http://weblogs.asp.net/infinitiesloop/archive/2006/08/03/Truly-Understanding-Viewstate.aspx

几种常见计算机图像处理操作的原理及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