整体框架来自ysc_ysc大神的图像验证码识别系列
http://blog.csdn.net/ysc6688/article/details/50772382
降噪在计算机和信号学中用到的非常多,在多媒体技术中,降噪主要是去除图片上的干扰噪点,玩过单反的都知道,当ISO调太高的时候,图片会产生颗粒感,这些颗粒就是噪点。当然,验证码图片上的噪点有大有小,只要是用来干扰机器识别的,会对后面的图片处理以及识别造成干扰。
常见的降噪算法一般都是一些滤波算法——均值滤波、中值滤波、自适应维纳滤波器和小波滤波等,不过在这里由于大多数验证码字符本身不是标准的打印体,而且字符相对较小,直线感很强,所以滤波算法降噪的效果不是很好。比如有些字符本身就比较模糊,在笔画弯折的地方很浅,使用滤波算法之后很容易将连贯的字符分成两个部分,另外如果验证码图片很小,有可能滤波之后整个验证码字符都被视为噪点了。
所以这里我自己实现了两种算法,都比较简单,但是经过试验,去除验证码噪点的效果很好,基本上能够应付所有的验证码噪点。
一、8邻域降噪
先介绍第一种方法,这种方法类似均值滤波,不过对于每个pixel,不是取其周围像素的灰度平均值,而是统计其周围像素点的灰度值为0或255的个数。从前面经过二值化处理可知,如果一个pixel是验证码或者干扰因素的一部分,那么这个pixel在二值化结果中其灰度值一定是0,即黑色;如果一个pixel是背景,则其灰度值应该是255是白色。因此对于孤立的噪点,其周围应该都是白色,或者大多数点都是白色pixel,比如下面的图片:
所以对一个噪点来讲,其周围的pixel应该全是白色的背景才对,准确来讲就是一个噪点pixel是黑色的并且外包的8个相邻pixel全是白色。当然,如果图片分辨率够高,一个噪点实际上可能是有很多个pixel组成,所以此时的判断条件应该放宽,即一个pixel是黑色的并且相邻的8个pixel白色的大于一个固定值,那么这个pixel就是噪点。对于不同的验证码,这个阀值是不固定的,所以在这可以设置大小,多试几次,找到最佳的阀值。
经过测试,8领域降噪法对于小的噪点的去除是很有效的,而且计算量不大,下面是一些结果图
,
左边的时原图,右边的是降噪以后的图,当然降噪没有降干净,这是因为这个方法对小噪点比较好,如果阀值设的比较大,很多验证码字符也会受到很大影响,因为验证码可能就是一些断断续续的点连出来的,阀值设太大,尽管噪点没了,验证码也会没了。
代码如下,所有关于像素的处理,都是基于opencv库:
[cpp] view plain copy
- void Image::NaiveRemoveNoise(int pNum)
- {
- //naive remove noise
- int i,j,m,n,nValue,nCount;
- int nWidth = getWidth();
- int nHeight = getHeight();
- //set boundry to be white
- for (i = 0; i < nWidth ; ++i)
- {
- setPixel(i,0,WHITE);
- setPixel(i,nHeight-1,WHITE);
- }
- for (i = 0; i < nHeight ; ++i)
- {
- setPixel(0,i,WHITE);
- setPixel(nWidth-1,i,WHITE);
- }
- //if the neighbor of a point is white but it is black, delete it
- for (j = 1; j < nHeight; ++j)
- for (i = 1; i < nWidth; ++i)
- {
- nValue = getPixel(i,j);
- if ( !nValue )
- {
- nCount = 0;
- for (m = i-1; m <= i+1; ++m)
- for (n = j-1; n <= j+1; ++n)
- {
- if( !getPixel(m,n) )
- nCount++;
- }
- if (nCount <= pNum)
- setPixel(i,j,WHITE);
- }
- else
- {
- nCount = 0;
- for (m = i-1; m <= i+1; ++m)
- for (n = j-1; n <= j+1; ++n)
- {
- if( !getPixel(m,n) )
- nCount++;
- }
- if (nCount >= 7)
- setPixel(i,j,BLACK);
- }
- }
- }
二、连通域降噪
对于较大的噪点,还有一个思路就是求其面积,因为字符pixel大部分都是相互连通的,因此求出每一个相互连通的黑色点的个数,如果个数很多那么就说明这一片pixel很有可能是字符的部分,如果一个连通域的像素个数很少,那么基本可以确定这一片pixel就是噪点。
对于求连通域的面积,opencv是有API可以直接利用的,那就是cvStartFindContours,这里不再过多介绍,其主要思路就是先求出连通域的轮廓,然后用指定的形状拟合,然后求每个连通域的面积。
为了精确性,我这里没有用上面那个API,而是用了另外一个方法——泛水填充法,其API如下:
[cpp] view plain copy
- floodFill(Mat,cvPoint(i,j),cvScalar(color));
其中Mat就是图片对于的矩阵对象,cvPoint(i,j)就是图片种位置为(i,j)的一个点,cvScalar就是颜色对象,这个函数的意思就是将与坐标为cvPoint(i,j)连通的所有的点的颜色都改为cvScalar(color),整个过程就像一张纸第一滴水,水泛染的样子,因故得名。
在计算的过程中,每扫描到一个黑色(灰度值为0)的点,就将与该点连通的所有点的灰度值都改为1,因此这一个连通域的点都不会再次重复计算了。下一个灰度值为0的点所有连通点的颜色都改为2,这样依次递加,知道所有的点都扫描完。接下来再次扫描所有的点,统计每一个灰度值对应的点的个数,每一个灰度值的点的个数对应该连通域的大小,并且不同连通域由于灰度值不同,因此每个点只计算一次,不会重复。这样一来就统计到了每个连通域的大小,再根据预设的阀值,如果该连通域大小小于阀值,则其就为噪点。这个算法比较适合检查大的噪点,与上个算法正好相反。
上面采用8邻域降噪得到的验证码还是保留不少较大的噪点,这里对上面处理过的验证码图片再次使用连通域降噪算法,对其进行2次降噪,得到的结果如下图:
可以看到,此时所有的噪点已经全部去除掉,效果很好。下面给出代码:
[cpp] view plain copy
- void Image::ContoursRemoveNoise(double pArea)
- {
- int i,j;
- int color = 1;
- int nHeight = getHeight();
- int nWidth = getWidth();
- for (i = 0; i < nWidth; ++i)
- for (j = 0; j < nHeight; ++j)
- {
- if ( !getPixel(i,j) )
- {
- //FloodFill each point in connect area using different color
- floodFill(m_Mat,cvPoint(i,j),cvScalar(color));
- color++;
- }
- }
- int ColorCount[255] = { 0 };
- for (i = 0; i < nWidth; ++i)
- {
- for (j = 0; j < nHeight; ++j)
- {
- //caculate the area of each area
- if (getPixel(i,j) != 255)
- {
- ColorCount[getPixel(i,j)]++;
- }
- }
- }
- //get rid of noise point
- for (i = 0; i < nWidth; ++i)
- {
- for (j = 0; j < nHeight; ++j)
- {
- if (ColorCount[getPixel(i,j)] <= pArea)
- {
- setPixel(i,j,WHITE);
- }
- }
- }
- for (i = 0; i < nWidth; ++i)
- {
- for (j = 0; j < nHeight; ++j)
- {
- if (getPixel(i,j) < WHITE)
- {
- setPixel(i,j,BLACK);
- }
- }
- }
- }
三、总结
上面提到的两个降噪算法——8邻域降噪和连通域降噪,分别适合去除小的噪点和大的噪点,因此我在实际应用中,两个算法组合来使用的,对于一个二值化的验证码图片,先采用8邻域算法降噪,然后再使用连通域算法降噪,这样无论大的还是小的噪点都能够去除掉,经过测试,效果很好