文档章节

图像验证码识别(七)——字符分割

moki_oschina
 moki_oschina
发布于 2016/11/29 11:38
字数 2120
阅读 600
收藏 1
点赞 0
评论 0

前面经过各种去除噪点、干扰线,验证码图片现在已经只有两个部分,如果pixel为白就是背景,如果pixel为黑就为字符。正如前面流畅所提到的一样,为了字符的识别,这里需要将图片上的字符一个一个“扣”下来,得到单个的字符,接下来再进行OCR识别。

字符分割可以说是图像验证码识别最关键的一步,因为分割的正确与否直接关系到最后的结果,如果4个字符分割成了3个,即便后面的识别算法识别率达到100%,结果也是错的。当然,前面预处理如果做得够好,干扰因素能够有效的去除,而没有影响到字符的pixel,那么分割来讲要容易得多。反过来,如果前面的干扰因素都没有去除掉,那么分割出来的可能就不是字符了。

字符的粘连是分割的难点,这一点也可以作为验证码安全系数的标准,如果验证码上的几个字符完全是分开的,那么可以保证字符分割成功率百分之百,这样验证码破解的难度就降低了很多,比如下面的字符:

这个就是CSDN的验证码,经过二值化和降噪得到的图片,可以看到这里图片已经非常干净,没有一点多余的信息,字符之间没有重叠的部分,分割起来毫无难度。

当然,大多数IT巨头的网页验证码里地字符都是粘连在一起的,比如谷歌的验证码:

谷歌的验证码不仅粘连成都很大,而且字符扭曲地也特别厉害,所以破解起来那是难度非常大了

至于图片分割,我再这里介绍两种简单地方法。

一、 泛水填充法

泛水填充法在前面降噪的地方就提到过,主要思路还是连通域的思想。对于相互之间没有粘连的字符验证码,直接对图片进行扫描,遇到一个黑的pixel就对其进行泛水填充,所有与其连通的字符都被标记出来,因此一个独立的字符就能够找到了。这个方法优点是效率高,时间复杂度是O(N),N为像素的个数;而且不用考虑图片的大小、相邻字符间隔以及字符在图片中得位置等其他任何因素,任何验证码图片只要字符相互是独立的,不需要对其他任何阀值做预处理,直接就操作;用这种方法分割正确率非常高,几乎不会出现分割错误的情况。但是缺点也很致命:那就是字符之间必须完全隔离,没有粘连的部分,否则会将两个字符误认为一个字符。

代码如下:

 

[cpp]  view plain  copy

  1. for (i = 0; i < nWidth; ++i)  
  2.         for (j = 0; j < nHeight; ++j)  
  3.         {  
  4.             if ( !getPixel(i,j) )  
  5.             {  
  6.                 //FloodFill each point in connect area using different color  
  7.                 floodFill(m_Mat,cvPoint(i,j),cvScalar(color));  
  8.                 color++;  
  9.             }  
  10.         }  
  11.   
  12.     int ColorCount[256] = { 0 };  
  13.     for (i = 0; i < nWidth; ++i)  
  14.     {  
  15.         for (j = 0; j < nHeight; ++j)  
  16.         {  
  17.             //caculate the area of each area  
  18.             if (getPixel(i,j) != 255)  
  19.             {  
  20.                 ColorCount[getPixel(i,j)]++;  
  21.             }  
  22.         }  
  23.     }  
  24.     //get rid of noise point  
  25.     for (i = 0; i < nWidth; ++i)  
  26.     {  
  27.         for (j = 0; j < nHeight; ++j)  
  28.         {  
  29.             if (ColorCount[getPixel(i,j)] <= nMin_area)  
  30.             {  
  31.                 setPixel(i,j,WHITE);  
  32.             }  
  33.         }  
  34.     }  
  35.   
  36.     int k = 1;  
  37.     int minX,minY,maxX,maxY;  
  38.     vector<Image> vImage;  
  39.     while( ColorCount[k] )  
  40.     {  
  41.         if (ColorCount[k] > nMin_area)  
  42.         {  
  43.             minX = minY = 100;  
  44.             maxX = maxY = -1;  
  45.             //get the rect of each charactor  
  46.             for (i = 0; i < nWidth; ++i)  
  47.             {  
  48.                 for (j = 0; j < nHeight; ++j)  
  49.                 {  
  50.                     if(getPixel(i,j) == k)  
  51.                     {  
  52.                         if(i < minX)  
  53.                             minX = i;  
  54.                         else if(i > maxX)  
  55.                             maxX = i;  
  56.                         if(j < minY)  
  57.                             minY = j;  
  58.                         else if(j > maxY)  
  59.                             maxY = j;  
  60.                     }  
  61.                 }  
  62.             }  
  63.             //copy to each standard mat  
  64.             Mat *ch = new Mat(HEIGHT,WIDTH,CV_8U,WHITE);  
  65.             int m,n;  
  66.             m = (WIDTH - (maxX-minX))/2;  
  67.             n = (HEIGHT - (maxY-minY))/2;  
  68.             for (i = minX; i <= maxX; ++i)  
  69.             {  
  70.                 for (j = minY; j <= maxY; ++j)  
  71.                 {  
  72.                     if(getPixel(i,j) == k)  
  73.                     {  
  74.                         *(ch->data+ch->step[0]*(n+j-minY)+m+(i-minX)) = BLACK;  
  75.                     }  
  76.                 }  
  77. <span style="white-space:pre">    </span>}  


这段代码就是使用泛水填充法,每次扫到一个连通域就把连通域所有的pixel的灰度值改为0-255之间的一个值,比如第一个是254,下一个是253...接下来再对每一个灰度值(即每一个连通域)的pixel出现的X,Y坐标的最大、最小的值记录下来,这样就得到了每个字符的最小外包矩形,最后将这个最小外包矩形全部复制到固定大小的一个单独的Mat对象中,这个对象存储的就是一个固定分辨率大小的表现为单独字符的图片。

 

分割的效果可以见下面的图:

可以看到,分割效果非常好。

二、X像素投影法

对于粘连的字符,也并非没有方法分割。一个方法就是将两个粘连的验证码一刀切开,从哪里切?当然是从粘连的薄弱的地方切。前面提到过图片的像素就像一个二维的矩阵,对每一个x值,统计所有x值为这个值的pixel中黑色的数目,直观来讲就是统计每一条竖线上黑色点的数目。显而易见的是,如果这一条线为背景,那么这一条线肯定都是白色的,那么黑色点的数目为0,如果一条竖线经过字符,那么这条竖线上的黑色点数目肯定不少。

对于完全独立的两个字符之间,肯定有黑色点数目为0的竖线,但是如果粘连,那么不会有黑色点数为0的竖线存在,但是字符粘连最薄弱的地方一定是黑色点数目最少的那条竖线,因此切就要从这个地方切。

在代码的实现的过程中,可以先从左到右扫描一遍,统计投影到每个X值的黑色点的数目,然后设定一个阀值范围,这个阀值大概就是一个字符的宽度。从左到右,先找到第一个x黑色点投影不为0的x值,然后在这个x值加上大概一个字符宽度的大小找到x投影数目最小的x值,这两个x值分割出来就是一个字符了。

这个方法的特点就是能够分割粘连的字符,但是缺点就是容易分割不干净,可能会出现分割错误的情况,另外就是需要提供相应的阀值。

代码如下:

 

[cpp]  view plain  copy

  1. void Image::xProjectDivide(int nMin_thsd,int nMax_thsd)  
  2. {  
  3.     int i,j;  
  4.     int nWidth = getWidth();  
  5.     int nHeight = getHeight();  
  6.     int *xNum = new int[nWidth];  
  7.   
  8.     //inital the x-projection-num  
  9.     memset(xNum,0,nWidth*sizeof(int));  
  10.   
  11.     //compute the black pixel num in X coordinate  
  12.     for (j = 0; j < nHeight; ++j)  
  13.         for (i = 0; i < nWidth; ++i)  
  14.         {  
  15.             if ( getPixel(i,j) == BLACK ) xNum[i]++;  
  16.         }  
  17.     /*-----------------show x project map-------------------*/  
  18.     Mat xProjectResult(nHeight/2,nWidth,CV_8U,Scalar(WHITE));  
  19.   
  20.     for (i = 0; i < xProjectResult.cols-1; ++i)  
  21.     {  
  22.         int begin,end;  
  23.         if(xNum[i] > xNum[i+1])  
  24.         {  
  25.             begin = xNum[i+1];  
  26.             end = xNum[i];  
  27.         }  
  28.         else {  
  29.             begin = xNum[i];  
  30.             end = xNum[i+1];  
  31.         }  
  32.         for (j = begin; j <= end; ++j)  
  33.         {  
  34.             *(xProjectResult.data+xProjectResult.step[0]*(nHeight/2 - j - 1)+i) = BLACK;  
  35.         }  
  36.     }  
  37.   
  38.     std::cout << "The porject of BLACK pixel in X coordinate is in the window" << std::endl;  
  39.     namedWindow("xProjectResult");  
  40.     imshow("xProjectResult",xProjectResult);  
  41.     waitKey();  
  42.     /*-----------------show x project map-------------------*/  
  43.   
  44.     /*-------------------divide the map---------------------*/  
  45.     vector<int> vPoint;  
  46.     int nMin,nIndex;  
  47.     if (xNum[0] > BOUNDRY_NUM) vPoint.push_back(0);  
  48.     for(i = 1;i < nWidth-1 ;)  
  49.     {  
  50.         if( xNum[i] < BOUNDRY_NUM)  
  51.         {  
  52.             i++;  
  53.             continue;  
  54.         }  
  55.         vPoint.push_back(i);  
  56.         //find minimum between the min_thsd and max_thsd  
  57.         nIndex = i+nMin_thsd;  
  58.         nMin = xNum[nIndex];  
  59.         for(j = nIndex;j<i+nMax_thsd;j++)  
  60.         {  
  61.             if (xNum[j] < nMin)  
  62.             {  
  63.                 nMin = xNum[j];  
  64.                 nIndex = j;   
  65.             }  
  66.         }  
  67.         vPoint.push_back(nIndex);  
  68.         i = nIndex + 1;  
  69.     }  
  70.     if (xNum[nWidth-1] > BOUNDRY_NUM) vPoint.push_back(nWidth-1);  
  71.       
  72.     //save the divided characters in map vector  
  73.     int ch_width = nWidth / (vPoint.size()/2) + EXPAND_WIDTH;  
  74.     vector<Image> vImage;  
  75.     for (j = 0; j < (int)vPoint.size(); j += 2)  
  76.     {  
  77.         Mat *mCharacter = new Mat(nHeight,ch_width,CV_8U,Scalar(WHITE));  
  78.         for (i = 0; i < nHeight; ++i)  
  79.             memcpy(mCharacter->data+i*ch_width+EXPAND_WIDTH/2,m_Mat.data+i*nWidth+vPoint.at(j),vPoint.at(j+1)-vPoint.at(j));  
  80.         Image::ContoursRemoveNoise(*mCharacter,2.5);  
  81.         Mat *mResized = new Mat(SCALE,SCALE,CV_8U);  
  82.         resize(*mCharacter,*mResized,cv::Size(SCALE,SCALE),0,0,CV_INTER_AREA);  
  83.         Image iCh(*mResized);  
  84.         vImage.push_back(iCh);  
  85.         delete mCharacter;  
  86.     }  
  87.     //show divided characters  
  88.     char window_name[12];  
  89.     for (i = 0; i < (int)vImage.size(); ++i)  
  90.     {  
  91.         sprintf(window_name,"Character%d",i);  
  92.         //vImage.at(i).NaiveRemoveNoise(1.0f);  
  93.         vImage.at(i).ShowInWindow(window_name);  
  94.     }  
  95.   
  96.     delete []xNum;  
  97. }  


代码首先统计每个x坐标对应的黑色点的数目,然后根据参数提供的阀值,找到字符之间的分割点,然后将分割点入栈,如果有4个字符,就入栈8个边界。最后每次出栈两个x值,将这两个x值之间的所有像素都拷贝到一个新的Mat对象中去,这样就得到了一个独立的字符图片。

 

下面给出X像素投影法的运行结果图:

本文转载自:http://daye.haoshihaoci.com/browse.php?u=y0dL5tQjVB0mlNO%2F8vpPi1G%2FZBHpmKin4Woxyrp0sPWnkmH3CSfQDvb

共有 人打赏支持
moki_oschina
粉丝 24
博文 169
码字总数 22482
作品 0
成都
程序员
使用OpenCV+Keras轻松破解验证码

选自Medium 作者:Adam Geitgey 机器之心编译 参与:李泽南、蒋思源 登录网站时必须输入的图片验证码可以用来识别访问者到底是人还是机器——这同时也是某种程度上的「图灵测试」,人工智能研...

机器之心 ⋅ 2017/12/14 ⋅ 0

仅需15分钟,使用OpenCV+Keras轻松破解验证码

  选自Medium   作者:Adam Geitgey   机器之心编译   参与:李泽南、蒋思源      登录网站时必须输入的图片验证码可以用来识别访问者到底是人还是机器——这同时也是某种程度上...

机器之心 ⋅ 2017/12/14 ⋅ 0

图像验证码识别(八)——字符归一化

前面提到了将验证码上的字符分割成一个单独的字符图片并且保存,但是扣下来的字符串可能会有倾斜的现象,因为现在很多网页验证码为了防止破解都对字符进行了一定的扭曲和旋转,即使是同一个网...

moki_oschina ⋅ 2016/11/29 ⋅ 0

车牌识别SDK开发包智能神器OCR工具

  新闻:【交警新标配“手机神器”违法车信息可拍照录入】北京交警的“新武器”——对着违法车辆一拍,就能全盘掌握车辆和驾驶人的信息;哪里有事故发生民警只需一键导航就可快速赶到;事故...

人工智能专家刘飞 ⋅ 2017/08/24 ⋅ 0

Python 实现识别弱图片验证码

图片来自 unsplash 目前,很多网站为了防止爬虫肆意模拟浏览器登录,采用增加验证码的方式来拦截爬虫。验证码的形式有多种,最常见的就是图片验证码。其他验证码的形式有音频验证码,滑动验证...

猴哥Yuri ⋅ 2017/12/23 ⋅ 0

不想再输入验证码?教你如何十五分钟黑掉这个插件

     大数据文摘作品   编译:Katrine Ren、朝夕、钱天培   验证码这种东西真的是反人类。虽然它在保证账号安全、反作弊以及反广告有着至关重要的作用,但对于普通用户来说,输验证码...

大数据文摘 ⋅ 01/02 ⋅ 0

手把手丨输验证码输到崩溃?教你15分钟黑掉全球最流行的验证码插件

验证码这种东西真的是反人类。虽然它在保证账号安全、反作弊以及反广告有着至关重要的作用,但对于普通用户来说,输验证码很多时候实在是让人抓狂。 文摘菌18岁的时候帮朋友刷QQ空间留言就天...

技术小能手 ⋅ 01/02 ⋅ 0

python版 —— 验证码校验 打码兔平台的使用介绍

python版 —— 验证码校验 打码兔平台的使用介绍 1. 背景 验证码(CAPTCHA)的全称是全自动区分计算机和人类的图灵测试(Completely Automated Public Turing Test to tell Computers and H...

zwq912318834 ⋅ 2017/11/23 ⋅ 0

MentalTrotter 称成功破解谷歌reCAPTCHA验证码

MentalTrotter 写道 "中国人工智能极客团队MentalTrotter宣布其自主研发的模拟人脑图像识别算法已经成功破解了google的reCAPTCHA验证码,该算法只需要较小的资源但有较高较理想的识别率,算法...

oschina ⋅ 2014/04/08 ⋅ 39

验证码的现在与未来

一则有关验证码的笑话 什么是验证码 ”验证码“( CAPTCHA )其实并不是各位网友总是在不同网站上看到的难以辨认的字母组合的代名词,而是“全自动区分计算机和人类的图灵测试”的俗称,顾名...

小卒过河 ⋅ 2011/11/02 ⋅ 21

没有更多内容

加载失败,请刷新页面

加载更多

下一页

开启Swarm集群以及可视化管理

在搭建的两台coreos服务器上开启swarm集群 前置条件: docker均开启2375端口 同一个局域网内 主服务器上安装Portainer容器 安装Portainer容器执行: docker run -d -p 9000:9000 --restart=a...

ykbj ⋅ 11分钟前 ⋅ 0

单例设计模式

1、单例模式确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例 2、饿汉式单例类 在这个类被加载时,静态变量instance会被初始化,此时类的私有构造子会被调用 饿汉式是典型...

职业搬砖20年 ⋅ 16分钟前 ⋅ 0

前端基础(四):前端国际规范收集

字数:1142 阅读时间:5分钟 前言 由于前端技术的灵活性和杂乱性,导致网上的许多解决方案不够全面甚至是完全错误,容易起到误导作用。所以,我对搜索到的解决方案往往是存疑态度。那么,如何...

老司机带你撸代码 ⋅ 18分钟前 ⋅ 0

Failed to open/create Network-VirtualBox Host-Only

虚拟机版本 : Oracle Vm VirtualBox 5.2.12 报错时机:开网卡二,重启虚拟机报错 "Failed to open/create the internal network 'HostInterfaceNetworking-VirtualBox Host-Only Ethernet Ada......

p至尊宝 ⋅ 21分钟前 ⋅ 0

三分钟学会如何在函数计算中使用 puppeteer

摘要: 使用 puppeteer 结合函数计算,可以快速的构建弹性的服务完成各种功能,包括:生成网页截图或者 PDF、高级爬虫,可以爬取大量异步渲染内容的网页、模拟键盘输入、表单自动提交、登录网...

阿里云云栖社区 ⋅ 24分钟前 ⋅ 0

springMVC接收表单时 Bean对象有Double Int Char类型的处理

前台ajax提交表单price为double类型 后台controller就介绍不到 400错误 前台 实体类: public class ReleaseMapIconConfig{ private String id; private long maxValue; private long minVal......

废柴 ⋅ 27分钟前 ⋅ 0

ZOOKEEPER安装

工作需要在ubuntu上配置了一个zookeeper集群,有些问题记录下来。 1. zookeeper以来java,所以首先要安装java。但是ubuntu系统有自带的jdk,需要通过命令切换java版本: $ sudo update-alter...

恰东 ⋅ 30分钟前 ⋅ 0

linux 进程地址空间的一步步探究

我们知道,在32位机器上linux操作系统中的进程的地址空间大小是4G,其中0-3G是用户空间,3G-4G是内核空间。其实,这个4G的地址空间是不存在的,也就是我们所说的虚拟内存空间。 那虚拟内存空间...

HelloRookie ⋅ 30分钟前 ⋅ 0

myatis #{}与${}区别及原理

https://blog.csdn.net/wo541075754/article/details/54292751

李道福 ⋅ 33分钟前 ⋅ 0

三分钟学会如何在函数计算中使用 puppeteer

摘要: 使用 puppeteer 结合函数计算,可以快速的构建弹性的服务完成各种功能,包括:生成网页截图或者 PDF、高级爬虫,可以爬取大量异步渲染内容的网页、模拟键盘输入、表单自动提交、登录网...

猫耳m ⋅ 34分钟前 ⋅ 0

没有更多内容

加载失败,请刷新页面

加载更多

下一页

返回顶部
顶部