整体框架来自ysc_ysc大神的图像验证码识别系列
http://blog.csdn.net/ysc6688/article/details/50772382
改写了一部分自己不明白的代码,加了少量注释。对于阀值的选择更加清晰,着重体现算法的优势。
一、8邻域降噪
先介绍第一种方法,这种方法类似均值滤波,不过对于每个pixel,不是取其周围像素的灰度平均值,而是统计其周围像素点的灰度值为0或255的个数。从前面经过二值化处理可知,如果一个pixel是验证码或者干扰因素的一部分,那么这个pixel在二值化结果中其灰度值一定是0,即黑色;如果一个pixel是背景,则其灰度值应该是255是白色。因此对于孤立的噪点,其周围应该都是白色,或者大多数点都是白色pixel,比如下面的图片:
原图:
二值化效果图:
所以对一个噪点来讲,其周围的pixel应该全是白色的背景才对,准确来讲就是一个噪点pixel是黑色的并且外包的8个相邻pixel全是白色。当然,如果图片分辨率够高,一个噪点实际上可能是有很多个pixel组成,所以此时的判断条件应该放宽,即一个pixel是黑色的并且相邻的8个pixel白色的大于一个固定值,那么这个pixel就是噪点。对于不同的验证码,这个阀值是不固定的,所以在这可以设置大小,多试几次,找到最佳的阀值。
经过测试,8领域降噪法对于小的噪点的去除是很有效的,而且计算量不大,下面是一些取不同阀值的效果图结果图:
如图是阀值从4到7。
结果一目了然,阀值为5时最优选择。
当然降噪没有降干净,这是因为这个方法对小噪点比较好,如果阀值设的比较大,很多验证码字符也会受到很大影响,因为验证码可能就是一些断断续续的点连出来的,阀值设太大,尽管噪点没了,验证码也会没了。
代码如下:
#include "stdafx.h"
#include
#include
#include
using namespace cv;
Mat srcImage;
void NaiveRemoveNoise(double pNum);
int main()
{
srcImage = imread("E:\\picture\\mask1.jpg", 0);
imshow("srcImage", srcImage);
adaptiveThreshold(srcImage, srcImage, 255, ADAPTIVE_THRESH_GAUSSIAN_C, THRESH_BINARY, 31, 7);
imshow("tempImage", srcImage);
NaiveRemoveNoise(5);
imshow("dstImage", srcImage);
waitKey();
return 0;
}
void NaiveRemoveNoise(double pNum) {
int m, n, nValue, nCount;
int nCols = srcImage.cols;
int nRows = srcImage.rows;
//set boundry to be white
for (int i = 0; i < nRows; ++i) {
srcImage.at(i, 0) = 255;
srcImage.at(i, nCols - 1) = 255;
}
for (int j = 0; j < nCols; ++j) {
srcImage.at(0, nCols) = 255;
srcImage.at(nRows - 1, nCols) = 255;
}
//if the neighbor of a point is white but it is black, delete it
for (int i = 1; i < nRows; ++i)
for (int j = 1; j < nCols; ++j)
{
nValue = srcImage.at(i, j);
if (nValue == 0) //fine a black point
{
nCount = 0;
for (m = i - 1; m <= i + 1; ++m)
for (n = j - 1; n <= j + 1; ++n)
{
if (srcImage.at(m, n) == 255)
nCount++;
}
if (nCount >= pNum)
srcImage.at(i, j) = 255;
}
}
}
二、连通域降噪
对于较大的噪点,还有一个思路就是求其面积,因为字符pixel大部分都是相互连通的,因此求出每一个相互连通的黑色点的个数,如果个数很多那么就说明这一片pixel很有可能是字符的部分,如果一个连通域的像素个数很少,那么基本可以确定这一片pixel就是噪点。
对于求连通域的面积,opencv是有API可以直接利用的,那就是cvStartFindContours,这里不再过多介绍,其主要思路就是先求出连通域的轮廓,然后用指定的形状拟合,然后求每个连通域的面积。
为了精确性,我这里没有用上面那个API,而是用了另外一个方法——泛水填充法,其API如下:
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次降噪,得到的结果如下图:
可以看到,此时所有的噪点已经全部去除掉,效果很好。下面给出代码:
#include "stdafx.h"
#include
#include
#include
using namespace cv;
void ContoursRemoveNoise(double pArea);
Mat srcImage;
int main()
{
srcImage = imread("E:\\picture\\IDCode\\5.jpg", 0);
imshow("srcImage", srcImage);
imwrite("E:\\picture\\IDCode\\8.jpg", srcImage);
adaptiveThreshold(srcImage, srcImage, 255, ADAPTIVE_THRESH_GAUSSIAN_C, THRESH_BINARY, 31, 7);
imshow("tempImage", srcImage);
imwrite("E:\\picture\\IDCode\\9.jpg", srcImage);
ContoursRemoveNoise(20);
imshow("dstImage", srcImage);
imwrite("E:\\picture\\IDCode\\10.jpg", srcImage);
waitKey();
return 0;
}
void ContoursRemoveNoise(double pArea)
{
int i, j;
int color = 1;
int nRows = srcImage.rows;
int nCols = srcImage.cols;
for (i = 0; i < nRows; ++i)
for (j = 0; j < nCols; ++j){
if (!srcImage.at(i,j)){
//FloodFill each point in connect area using different color
floodFill(srcImage, Point(j, i), Scalar(color)); //注意 point是(_x,_y)形式 所以注意反写行列
color++;
}
}
int ColorCount[255] = { 0 };
for (i = 0; i < nRows; ++i){
for (j = 0; j < nCols; ++j){
//caculate the area of each area
if (srcImage.at(i, j) != 255)
ColorCount[srcImage.at(i, j)]++;
}
}
//get rid of noise point
for (i = 0; i < nRows; ++i){
for (j = 0; j < nCols; ++j){
if (ColorCount[srcImage.at(i, j)] <= pArea)
srcImage.at(i, j) = 255;
else
srcImage.at(i, j) = 0;
}
}
}