汉字验证码识别
一、概述
1.1 问题描述
验证码类型如图1所示,需要从图中六个汉字中识别出两个相同的汉字,汉字中有繁体字,字体经过扭曲和旋转处理,汉字周围存在粗细不同,颜色各异的干扰线。
![](https://i-blog.csdnimg.cn/blog_migrate/08f17530a2c9b822755262ecfda1fe64.png)
![](https://i-blog.csdnimg.cn/blog_migrate/9969cf93d7e955793de529f34b1c6c0b.png)
![](https://i-blog.csdnimg.cn/blog_migrate/dae9f00fc12cc458785368f50dc95f22.png)
![](https://i-blog.csdnimg.cn/blog_migrate/bf7b9b0e0f21e6e5c5ea4c12dd0f1ea1.png)
1.2 背景概述
识别常见的验证码一般需要经过以下步骤:
- 预处理,包括去灰度、调节亮度以及对比度、腐蚀膨胀、滤波去噪点、去干扰、二值化等,其中去干扰的方式有很多种,例如干扰线比较细而验证码比较粗,可以检测单个像素点相邻像素与该像素间是否有颜色突变,若有则认为是干扰线,或者干扰线与验证码之间有明显的颜色差异,可以根据颜色加以区分;
- 分割,位置固定的可以按确定尺寸分割,不固定位置的可以利用轮廓检测技术确定大致位置,再进行分割,分割效果受验证码图片中字符间的紧密程度以及预处理效果的影响;
- 训练,将分割得到的样本利用相应算法进行训练,提高识别率;
- 识别
验证码形式各异,基本没有统一的方式解决所有类型的验证码识别问题,需要根据具体情况具体分析。
1.3 解决思路
对于图1所示的验证码类型,背景颜色为白色,汉字颜色鲜明,噪点也非常少,可以把重点放在如何去除干扰线的问题上,仔细观察可以发现干扰线的颜色基本与汉字的颜色不同,且图片基本可以划分为6个区域,不同验证码图片在相同区域内,汉字颜色是相同的。而最终的目的也只是找出相同的汉字,所以可以把问题简化为从六张图片中找出两个最相似的图片。具体的解决思路如下:
- 将一张验证码图片等比例分割成6张含有单个汉字的图片;
- 对每张图片进行预处理,拾取汉字颜色作为目标颜色,去除图片中其他颜色以达到去干扰的目的,由于图片颜色通常都会进行渐变处理,所以按照RGB方式处理将不太方便,而本文采用HSV方式来表征颜色——HSV对肉眼可见的颜色可以通过具体的范围值加以区分。最后将去除干扰后的图片进行二值化处理;
- 由于图片中的汉字进行了扭曲和旋转处理,汉字在图片中的相对位置也有所变化,所以无法通过比较对应位置像素点颜色的方式加以比较。本文采用图像识别中的SIFT算法——SIFT算法具有尺度不变性和旋转不变性,分别提取图片中的特征值并两两比较,找出并统计两张图片中相似的特征点个数,个数最多的就认为是相似度最高的两张图片,即两个相同的汉字。
二、具体过程
2.1 配置opencv(JAVA环境)
- 下载opencv 2.4.13.6并配置环境变量https://sourceforge.net/projects/opencvlibrary/files/opencv-win/2.4.13/opencv-2.4.13.6-vc14.exe/download
由于需要用到SIFT算法,而opencv 3.0以上版本将SIFT算法移至第三方库中,需要重新编译才能使用,为了方便,本文使用opencv 2.4.13.6 这一版本。
——path配置到opencv的bin目录下。 - 添加opencv的jar包,并静态加载dll文件
String x64 = " .../opencv/build/java/x64/";
System.load(x64 + "opencv_java2413.dll");
或者在添加jar包后再在IDEA中配置Edit Configuration–VM options:-Djava.library.path= …\opencv\build\java\x64,再静态加载library
System.loadLibrary(Core.NATIVE_LIBRARY_NAME);
2.2 分割
//读取图片
Mat src = imread(file.getAbsolutePath());
int eachHeight=src.rows()/2;
int eachWidth=src.cols()/3;
Rect rect;
Mat dst;
for (int i = 0; i < 6; i++) {
//按起始坐标及宽高大小进行分割
rect = new Rect((i % 3) * eachWidth, (i / 3) *eachHeight,eachWidth,eachHeight);
dst= new Mat(src,rect);
}
分割效果如下:
![](https://i-blog.csdnimg.cn/blog_migrate/932885ef75981eb453d9b15c3409ab0c.png)
![](https://i-blog.csdnimg.cn/blog_migrate/df215955fdd72d1140c3037dd02972a7.png)
![](https://i-blog.csdnimg.cn/blog_migrate/72110f900476602d755db47fa5d62424.png)
![](https://i-blog.csdnimg.cn/blog_migrate/e2acfa3358485815f7f5366f3d051c10.png)
![](https://i-blog.csdnimg.cn/blog_migrate/51533ae1cfcff390f2680856ad9ea3a5.png)
![](https://i-blog.csdnimg.cn/blog_migrate/2f6067d8bbbabdedf016c981acbc91af.png)
2.3 去干扰
//将RGB图片转化为HSV格式
Imgproc.cvtColor(image, dst, Imgproc.COLOR_BGR2HSV);
Scalar lowerScalar=null;
Scalar upperScalar=null;
//根据字体颜色设置目标颜色的数值范围
switch (index){
//yellow
case 0:
case 4:
lowerScalar=new Scalar(26,60,60);
upperScalar=new Scalar(33,255,220);
break;
//purple
case 1:
case 5:
lowerScalar=new Scalar(125,10,10);
upperScalar=new Scalar(155,255,220);
break;
//green
case 2:
lowerScalar=new Scalar(35,43,46);
upperScalar=new Scalar(77,255,220);
break;
//blue
case 3:
lowerScalar=new Scalar(100,43,46);
upperScalar=new Scalar(124,255,220);
}
//只留下目标颜色的内容
Core.inRange(dst,lowerScalar,upperScalar,dst);
inRange之后的效果如下,在一定程度上将干扰线去除:
![](https://i-blog.csdnimg.cn/blog_migrate/198185e0b8fd781b9feef2a7ad8d8729.png)
![](https://i-blog.csdnimg.cn/blog_migrate/823d724f50673450e5b0863a7f592eaf.png)
![](https://i-blog.csdnimg.cn/blog_migrate/c424eb5636b0aea2448f517b14558e1c.png)
![](https://i-blog.csdnimg.cn/blog_migrate/17f5f120a33f36aa3cf3ab99c4cfa6e4.png)
![](https://i-blog.csdnimg.cn/blog_migrate/02f41e66bb41e919ad4067d266a2fa35.png)
![](https://i-blog.csdnimg.cn/blog_migrate/b16738d57ce666b089b088edb64e391a.png)
2.4 识别
2.4.1 提取特征点
//灰度图转换
Imgproc.cvtColor(image, image, Imgproc.COLOR_RGB2GRAY);
//提取特征点
MatOfKeyPoint keyPoint =new MatOfKeyPoint();
FeatureDetector FeatureDetector=FeatureDetector.create(FeatureDetector.SIFT);
DescriptorExtractor descriptorExtractor=DescriptorExtractor.create(DescriptorExtractor.SIFT);
FeatureDetector.detect(image,keyPoint);
//特征点描述,为下边的特征点匹配做准备
Mat imageDesc=new Mat();
descriptorExtractor.compute(image,keyPoint,imageDesc);
2.4.2 特征点匹配
DescriptorMatcher descriptorMatcher= descriptorMatcher=DescriptorMatcher.create(DescriptorMatcher.FLANNBASED);
//匹配器训练
List<Mat> matList=new ArrayList<>();
matList.add(imageDesc1);
descriptorMatcher.add(matList);
descriptorMatcher.train();
//得到最相近和次相近的匹配点
List<MatOfDMatch> matches=new ArrayList<>();
descriptorMatcher.knnMatch(imageDesc2,matches,2);
DMatch[] dMatches=null;
List<DMatch> goodMatch = new LinkedList<>();
// Lowe's algorithm,获取优秀匹配点
for (int i = 0; i < matches.size(); i++) {
dMatches=matches.get(i).toArray();
//比较最接近和次接近匹配点的距离差,若比例系数小于0.8则认为是优秀匹配点
if(dMatches[0].distance<0.8*dMatches[1].distance) {
goodMatch.add(dMatches[0]);
}
}
MatOfDMatch goodMatOfDMatch=new MatOfDMatch();
goodMatOfDMatch.fromList(goodMatch);
Mat OutImage = new Mat();
//得到最终匹配图
Features2d.drawMatches(img2, keyPoint2, img1, keyPoint1, goodMatOfDMatch, OutImage);
最终的匹配点数为:
0 | 1 | 2 | 3 | 4 | 5 | |
---|---|---|---|---|---|---|
1 | 6 | – | – | – | – | – |
2 | 9 | 3 | – | – | – | – |
3 | 3 | 3 | 7 | – | – | – |
4 | 9 | 5 | 4 | 1 | – | – |
5 | 5 | 3 | 13 | 28 | 3 | – |
匹配点数最多的前两组图片分别为 Pic.2和Pic.5、Pic.3和Pic.5,具体情况如下所示:
![](https://i-blog.csdnimg.cn/blog_migrate/ddec3b505a4a10391859d0a1ea67c819.png)
![](https://i-blog.csdnimg.cn/blog_migrate/e70f4e34963131f50017960c59110a1f.png)
三、总结
- 如果干扰线穿插在汉字周围,且条数比较多,那么根据颜色进行筛选很容易将汉字笔画打断成一个个零散的点,识别率降低;
- 不同汉字之间也能找到对应的特征匹配点;
- 笔画越多,结构相近的汉字可能找到的特征匹配点越多;
- 目标汉字如果笔画少,而干扰汉字笔画多,且结构相近,则很可能得不到想要的结果;
- 初步测试该方法的验证码识别率在 60% 以上。