您的位置:首页 > 编程语言 > C#

用C#模版匹配识别新教务系统登录验证码

2016-05-25 20:17 706 查看

用C#模版匹配识别新教务系统登录验证码

缘起

  学校教务网的系统终于换了,原来那个100W从清华还是哪里买来的放到现在简直就是本科毕设水平,前端丑爆不说,后端用的Java1.5,登录一点验证都没有,稍有点常识的人都能看出来,如果hack的铁蹄继续前进,这个螳臂当车的歹徒……

  不过前几天学校上线了新的教务系统,应该是找公司外包的,现在看起来还不错,没有深入分析,只简单的看了下登录的逻辑。

密码更复杂

使用了验证码

登录逻辑更加复杂



  如果要做模拟登录的话,首先要摸清后台的登录逻辑,然后前台这里就是验证码的识别了。前几天正好看了一篇关于使用Aforge.NET进行验证码识别的例子,这里正好拿来练练手。

理论研究

  首先分析这个验证码

,四位纯数字,线条干扰,数字本身无变形无粘连,只有简单的切变,而且每个数字的位置大体是固定的,数字颜色和底色相差较大。这个是比较典型的好识别的验证码,按照基本的图像处理步骤走一遍就差不多啦。

图片预处理

  第一步是去色,灰度化。

灰度化,在RGB模型中,如果R=G=B时,则彩色表示一种灰度颜色,其中R=G=B的值叫灰度值,因此,灰度图像每个像素只需一个字节存放灰度值(又称强度值、亮度值),灰度范围为0-255。

  这里我们使用Aforge.NET框架的提供的函数来处理



  Aforge.NET提供了三种算法,这里我选用的RMY算法(Aforge.NET二值化函数默认的算法),这种算法的原理就是将RGB中的R分量转化成YUV中的Y分量。

  代码如下

Bitmap grayg = Grayscale.CommonAlgorithms.RMY.Apply(srcBitmap);


  效果

  原始图片:

|灰度化处理后:


  至这一步,彩色图片被映射成0~255的灰度图片,为下一步的二值化做准备。

  

  第二步是灰度图片的二值化。

图像的二值化,就是将图像上的像素点的灰度值设置为0或255,也就是将整个图像呈现出明显的只有黑和白的视觉效果。

  简而言之,二值化后,图片只有黑和白两种颜色,对应0和1两种状态,这一步的目的就是去除背景的噪点,把数字主体留下,方便后面的匹配。

  这里我们使用Aforge.NET框架的提供的函数来处理

  


  具体代码如下

Bitmap desBitmap = new Threshold().Apply(grayg);


  效果

  原图片:

|灰度化处理后:

| 二值化处理后:


  

  可以说效果拔群,原图片背景干扰线被干净的剔除了,只留下前景的数字主体,这也是这种验证码好识别原因。到这一步,图片只剩0和1两种颜色,为了方便进行水平方向的单字符切割,我们需要对验证码进行切变矫正。

  

  以上过程都比较顺利,第三步是切变矫正。

  

切变也叫错切,错切是在某方向上,按照一定的比例对图形的每个点到某条平行于该方向的直线的有向距离做放缩得到的平面图形。



  错切是一种线性变换,其变换矩阵为



  其实一开始在这里我是打算用仿射变换(affine)来矫正切变,因为在matlab里用过,效果很好。因为是线性变换,所以能保证图片不发生畸变。但是遇到一个问题是,仿射变换过程比较复杂,牵扯到矩阵的运算,然而C#的矩阵运算能力不如matlab(其实还是因为懒不想写),所以放弃了。

  然后我就打算用C#自己提供的Matrix对Graphics进行Shear变换,这一个设置倒是比较简单。

Graphics g = Graphics.FromImage(srcBitmap);
Matrix mx = new Matrix();
mx.Shear((float)0.175, 0);
g.Transform = mx;


  Shear的参数是两个float,分别代表水平错切因子和竖直错切因子,很容易观察到验证码是水平方向的错切,大概有3~4个像素,设置水平错切因子在0.175左右比较合适。

  效果

  二值化处理后:

| 切变矫正后:


  按理说到这里就该结束了进入下一步了,但是我在研究的时候发现,Graphics有个神奇的方法DrawImage,光重载形式就有30种!!这里我用了这种,简单来说就是直接将原图映射到目标图形里规定的平行四边形里,一步到位。

  


  

  MSDN里的示例代码是这样的

private void DrawImageParaRect(PaintEventArgs e)
{
// Create image.
Image newImage = Image.FromFile("SampImag.jpg");

// Create parallelogram for drawing image.
Point ulCorner = new Point(100, 100);
Point urCorner = new Point(325, 100);
Point llCorner = new Point(150, 250);
Point[] destPara = {ulCorner, urCorner, llCorner};

// Create rectangle for source image.
Rectangle srcRect = new Rectangle(50, 50, 150, 150);
GraphicsUnit units = GraphicsUnit.Pixel;

// Draw image to screen.
e.Graphics.DrawImage(newImage, destPara, srcRect, units);
}


实际代码是

Bitmap curBitmap = new Bitmap(pictureBox1.Image.Width,      pictureBox1.Image.Height);

Graphics g = Graphics.FromImage(curBitmap);
g.InterpolationMode = InterpolationMode.HighQualityBicubic;

Rectangle cloneRect = new Rectangle(0, 0, pictureBox1.Width, pictureBox1.Height);

g.FillRectangle(Brushes.White, cloneRect);
Bitmap tmpBmp = ((Bitmap)pictureBox1.Image).Clone(cloneRect, pictureBox1.Image.PixelFormat);

System.Drawing.Point[] p = { new System.Drawing.Point(4,4),new System.Drawing.Point(63,4),new System.Drawing.Point(8,24)};

g.DrawImage(tmpBmp, p, new Rectangle(8,4,61,20),GraphicsUnit.Pixel);
tran = Grayscale.CommonAlgorithms.RMY.Apply(curBitmap);
tran = new Threshold().Apply(tran);


  最终处理后的效果是这样的

,到这里就可以说成功了一半了,下一步就是进行单字的分割,也是坑点所在。

  

  第四步是分割。

  这里的算法比较简单(其实是懒),由于是水平方向的切割,只要统计好每一列的黑色像素点数量,然后对数字边界做一些判断就好,这里字符间没有重叠,情况还算简单(并不!)

  处理用的代码

public List<Bitmap> Crop_Y(Bitmap b)
{
var list = new List<Bitmap>();
int[] cols = new int[b.Width];
for (int x = 0; x < b.Width; x++)//统计每一列的的黑色像素
{
for (int y = 0; y < b.Height; y++)
{
var pixel = b.GetPixel(x, y);
if (pixel.R == 0)
{
cols[x]++;
}
}
}

int[] left = new int[4];
int[] right = new int[4];
int last = 0;
for (int i = 0; i < cols.Length; i++)
{
if(cols[i] <2 )
{
continue;
}
//这里判断字符左边界
if (i - 1 >= 0 && cols[i - 1] < 2 && cols[i] > 1 || cols[i] > 1 && cols[i-1] ==0 || (cols[i] < 3 && (i-8)%13 < 5 && i < 55 && cols[i + 1] >1 &&cols[i+2] > 1 && i - last > 5))
{
for (int j = 0; j < 4; j++)
{
if (left[j] == 0)
{
left[j] = i;
last = i;
break;
}
}
}else if (i + 1 < cols.Length && cols[i + 1] < 2 && cols[i] > 1 || cols[i]>1 && cols[i+1] == 0|| (cols[ i + 1 ] < 3 && (i - 7) % 13 < 5 && i < 55 && cols[i + 2]>1 && cols[i + 3] > 1))//这里判断字符右边界
{
for (int j = 0; j < 4; j++)
{
if (right[j] == 0)
{
right[j] = i;
break;
}
}
}
}
for(int i = 0; i < 4; i++)
{
Crop crop = new Crop(new Rectangle(left[i],4,right[i] - left[i],20));//根据边界进行切割
var temp = crop.Apply(b);
list.Add(temp);//加入到字符集
}
return list;
}


切割完后就需要制作模板了,这里统一为20*20pixel的图片,字符居中放置,处理用的代码

public List<Bitmap> ToResizeAndCenterIt(List<Bitmap> list, int w = 20, int h = 20)
{
List<Bitmap> resizeList = new List<Bitmap>();

for (int i = 0; i < list.Count; i++)
{
//反转一下图片
list[i] = new Invert().Apply(list[i]);

int sw = list[i].Width;
int sh = list[i].Height;

Crop corpFilter = new Crop(new Rectangle(0, 0, w, h));

list[i] = corpFilter.Apply(list[i]);

//再反转回去
list[i] = new Invert().Apply(list[i]);

//计算中心位置
int centerX = (w - sw) / 2;
int centerY = (h - sh) / 2;

list[i] = new CanvasMove(new IntPoint(centerX, centerY), Color.White).Apply(list[i]);

resizeList.Add(list[i]);
}

return resizeList;
}


经过处理后效果

,哈哈哈是不是看起来很简单,为了实现100%的正确分割,这背后有多坑爹大概只有做的时候才知道。到了这里,基本就是万事俱备只欠东风了,下一步就是模板匹配。

  第五步,模板匹配。

  

模板匹配是在图像中寻找目标的方法之一。

模板匹配的工作方式跟直方图的反向投影基本一样,大致过程是这样的:通过在输入图像上滑动图像块对实际的图像块和输入图像进行匹配。

假设我们有一张100x100的输入图像,有一张10x10的模板图像,查找的过程是这样的:

(1)从输入图像的左上角(0,0)开始,切割一块(0,0)至(10,10)的临时图像;

(2)用临时图像和模板图像进行对比,对比结果记为c;

(3)对比结果c,就是结果图像(0,0)处的像素值;

(4)切割输入图像从(0,1)至(10,11)的临时图像,对比,并记录到结果图像;

(5)重复(1)~(4)步直到输入图像的右下角。

大家可以看到,直方图反向投影对比的是直方图,而模板匹配对比的是图像的像素值;模板匹配比直方图反向投影速度要快一些,但是我个人认为直方图反向投影的鲁棒性会更好。



这么复杂我是没耐心看,直接调用Aforge.NET提供的函数



具体的代码为

ExhaustiveTemplateMatching templateMatching = new ExhaustiveTemplateMatching(0.9f);
//0.9f为判定阈值,只有匹配度大于0.9,才认为匹配成功


完整的代码为

var files = Directory.GetFiles(@"I:\sample\");
var templateList = files.Select(i => { return new Bitmap(i); }).ToList();
var templateListFileName = files.Select(i => { return i.Substring(10, 1); }).ToList();
var result = new List<string>();
float[] confidence = new float[4];
for (int i = 0; i < list.Count; i++)
{
float max = 0;
int index = 0;
for (int j = 0; j < templateList.Count; j++)
{
var compare = templateMatching.ProcessImage(list[i], templateList[j]);
if (compare.Length > 0 && compare[0].Similarity > max)
{
max = compare[0].Similarity;
index = j;
confidence[i] = max;
}
}
result.Add(templateListFileName[index]);
}


识别结果



尾声

软件的最终效果



测试了近三十个不同字符的验证码,正确率基本100%。C#大法好,如果要是用Java,估计得写个半个星期。下一步就是做模拟登录了,等做好了我再来写文。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  验证码