自从发表了用于验证码图片识别的类(C#代码)后,不断有网友下载这个类后,问如何用于一些特定的验证码。总结一下网友们的提问,很多都是不会从复杂背景中提到干净的字符图片来,这主要就是一个去噪问题,即除去图片上的背景、干扰点、干扰线等信息。这当中要用到很多图像学数学算法,首先声明,本人不是学图像学的,以下方法理论说得不对,敬请多批评指正。
1、如何设前景/背景的分界值
UnCodebase类中有一个GetPicValidByValue( int dgGrayValue) 函数,可以得到前景的有效区域,常有人问我前景/背景的分界值dgGrayValue是如何确定的(常用的是灰度128)。这个值的获取是有数学算法,叫最大类间方差法,即图像的前后景的平方差为最大时的值就是我们关心的分界值,对付如 这样较复杂的背景非常管用,下面是具体的C#代码。
/// <summary>
/// 得到灰度图像前景背景的临界值 最大类间方差法,yuanbao,2007.08
/// </summary>
/// <returns> 前景背景的临界值 </returns>
public int GetDgGrayValue()
{
int [] pixelNum = new int [ 256 ]; // 图象直方图,共256个点
int n, n1, n2;
int total; // total为总和,累计值
double m1, m2, sum, csum, fmax, sb; // sb为类间方差,fmax存储最大方差值
int k, t, q;
int threshValue = 1 ; // 阈值
int step = 1 ;
// 生成直方图
for ( int i = 0 ; i < bmpobj.Width ; i ++ )
{
for ( int j = 0 ; j < bmpobj.Height; j ++ )
{
// 返回各个点的颜色,以RGB表示
pixelNum[bmpobj.GetPixel(i,j).R] ++ ; // 相应的直方图加1
}
}
// 直方图平滑化
for (k = 0 ; k <= 255 ; k ++ )
{
total = 0 ;
for (t = - 2 ; t <= 2 ; t ++ ) // 与附近2个灰度做平滑化,t值应取较小的值
{
q = k + t;
if (q < 0 ) // 越界处理
q = 0 ;
if (q > 255 )
q = 255 ;
total = total + pixelNum[q]; // total为总和,累计值
}
pixelNum[k] = ( int )(( float )total / 5.0 + 0.5 ); // 平滑化,左边2个+中间1个+右边2个灰度,共5个,所以总和除以5,后面加0.5是用修正值
}
// 求阈值
sum = csum = 0.0 ;
n = 0 ;
// 计算总的图象的点数和质量矩,为后面的计算做准备
for (k = 0 ; k <= 255 ; k ++ )
{
sum += ( double )k * ( double )pixelNum[k]; // x*f(x)质量矩,也就是每个灰度的值乘以其点数(归一化后为概率),sum为其总和
n += pixelNum[k]; // n为图象总的点数,归一化后就是累积概率
}
fmax = - 1.0 ; // 类间方差sb不可能为负,所以fmax初始值为-1不影响计算的进行
n1 = 0 ;
for (k = 0 ; k < 256 ; k ++ ) // 对每个灰度(从0到255)计算一次分割后的类间方差sb
{
n1 += pixelNum[k]; // n1为在当前阈值遍前景图象的点数
if (n1 == 0 ) { continue ; } // 没有分出前景后景
n2 = n - n1; // n2为背景图象的点数
if (n2 == 0 ) { break ; } // n2为0表示全部都是后景图象,与n1=0情况类似,之后的遍历不可能使前景点数增加,所以此时可以退出循环
csum += ( double )k * pixelNum[k]; // 前景的“灰度的值*其点数”的总和
m1 = csum / n1; // m1为前景的平均灰度
m2 = (sum - csum) / n2; // m2为背景的平均灰度
sb = ( double )n1 * ( double )n2 * (m1 - m2) * (m1 - m2); // sb为类间方差
if (sb > fmax) // 如果算出的类间方差大于前一次算出的类间方差
{
fmax = sb; // fmax始终为最大类间方差(otsu)
threshValue = k; // 取最大类间方差时对应的灰度的k就是最佳阈值
}
}
return threshValue;
}
2、如何去除干扰点/干扰线
2.1 干扰点/干扰线的特征分析
现在网上的大多数的验证码都是加了干扰的,一般分为干扰点和干扰线,如下图。标用1、2、3的分别为点、线、字符。
去干扰,一般是逐点分析,这三种情况下,每一点及周边8个点的情况都不一样(分别为1点,3点,8点),这是一种干扰信息的粒度比字符的粒度小的典型情况。现在就可以动手编写去杂代码了。
2.2 根据周边有效点数去噪函数
/// <summary>
/// 去掉杂点(适合杂点/杂线粗为1)
/// </summary>
/// <param name="dgGrayValue"> 背前景灰色界限 </param>
/// <returns></returns>
public void ClearNoise( int dgGrayValue, int MaxNearPoints)
{
Color piexl;
int nearDots = 0 ;
int XSpan, YSpan, tmpX, tmpY;
// 逐点判断
for ( int i = 0 ; i < bmpobj.Width; i ++ )
for ( int j = 0 ; j < bmpobj.Height; j ++ )
{
piexl = bmpobj.GetPixel(i, j);
if (piexl.R < dgGrayValue)
{
nearDots = 0 ;
// 判断周围8个点是否全为空
if (i == 0 || i == bmpobj.Width - 1 || j == 0 || j == bmpobj.Height - 1 ) // 边框全去掉
{
bmpobj.SetPixel(i, j, Color.FromArgb( 255 , 255 , 255 ));
}
else
{
if (bmpobj.GetPixel(i - 1 , j - 1 ).R < dgGrayValue) nearDots ++ ;
if (bmpobj.GetPixel(i, j - 1 ).R < dgGrayValue) nearDots ++ ;
if (bmpobj.GetPixel(i + 1 , j - 1 ).R < dgGrayValue) nearDots ++ ;
if (bmpobj.GetPixel(i - 1 , j).R < dgGrayValue) nearDots ++ ;
if (bmpobj.GetPixel(i + 1 , j).R < dgGrayValue) nearDots ++ ;
if (bmpobj.GetPixel(i - 1 , j + 1 ).R < dgGrayValue) nearDots ++ ;
if (bmpobj.GetPixel(i, j + 1 ).R < dgGrayValue) nearDots ++ ;
if (bmpobj.GetPixel(i + 1 , j + 1 ).R < dgGrayValue) nearDots ++ ;
}
if (nearDots < MaxNearPoints)
bmpobj.SetPixel(i, j, Color.FromArgb( 255 , 255 , 255 )); // 去掉单点 && 粗细小3邻边点
}
else // 背景
bmpobj.SetPixel(i, j, Color.FromArgb( 255 , 255 , 255 ));
}
}
2.3 滤波算法去噪函数
图像预处理中有多种滤波算法,其原理与方法分别为
1 ) 中值滤波
它通过从图像中的某个采样窗口取出奇数个数据进行排序得到的结果。顾名思义,所谓中值就是窗口中奇数个数据按大小顺序排列后处于中心位置的那个数。中值滤波以窗口的中值作为处理结果。
实现起来很简单
1 :先对窗口排序
2 :用排序后的中值取代要处理的数据即可
注意事项:
1 :注意图像边缘数据的处理
2 :对于不同的目的选用不同的窗体,一般有3× 3 , 5 ×5等等
Code
/// <summary>
/// 3×3中值滤波除杂,yuanbao,2007.10
/// </summary>
/// <param name="dgGrayValue"></param>
public void ClearNoise( int dgGrayValue)
{
int x, y;
byte [] p = new byte [ 9 ]; // 最小处理窗口3*3
byte s;
// byte[] lpTemp=new BYTE[nByteWidth*nHeight];
int i, j;
// --!!!!!!!!!!!!!!下面开始窗口为3×3中值滤波!!!!!!!!!!!!!!!!
for (y = 1 ; y < bmpobj.Height - 1 ; y ++ ) // --第一行和最后一行无法取窗口
{
for (x = 1 ; x < bmpobj.Width - 1 ; x ++ )
{
// 取9个点的值
p[ 0 ] = bmpobj.GetPixel(x - 1 , y - 1 ).R;
p[ 1 ] = bmpobj.GetPixel(x, y - 1 ).R;
p[ 2 ] = bmpobj.GetPixel(x + 1 , y - 1 ).R;
p[ 3 ] = bmpobj.GetPixel(x - 1 , y).R;
p[ 4 ] = bmpobj.GetPixel(x, y).R;
p[ 5 ] = bmpobj.GetPixel(x + 1 , y).R;
p[ 6 ] = bmpobj.GetPixel(x - 1 , y + 1 ).R;
p[ 7 ] = bmpobj.GetPixel(x, y + 1 ).R;
p[ 8 ] = bmpobj.GetPixel(x + 1 , y + 1 ).R;
// 计算中值
for (j = 0 ; j < 5 ; j ++ )
{
for (i = j + 1 ; i < 9 ; i ++ )
{
if (p[j] > p[i])
{
s = p[j];
p[j] = p[i];
p[i] = s;
}
}
}
// if (bmpobj.GetPixel(x, y).R < dgGrayValue)
bmpobj.SetPixel(x, y, Color.FromArgb(p[ 4 ], p[ 4 ], p[ 4 ])); // 给有效值付中值
}
}
}
经过实际运行证实,中值滤波能有效去除图像中的噪声点,特别是在一片连续变化缓和的区域中(比如人的衣服,皮肤),几乎100%去除灰度突变点(可以认为是噪声点),也因为如此,中值滤波不适合用在一些细节多,如细节点,细节线多的图像中,因为细节点有可能被当成噪声点去除。
中值滤波的窗口还可以有多种形状,上面程序选择的是矩形(容易计算),其实窗口还可以是菱形,圆形,十字形等等,不同的窗口形状有不同的滤波效果,对有缓慢且有较长轮廓线的物体适合用矩形或者原型窗口,对于有尖顶角物体的图像适合采用十字形窗口。
中值滤波可以进行线性组合,不同窗口形状的滤波器可以线性组合
改进中值滤波方法:
对一些内容复杂的图像,可以使用复合型中值滤波。如, 中值滤波线性组合、高阶中值滤波组合、加权中值滤波以及迭代中值滤波等。
中值滤波的线性组合是将几种窗口尺寸大小和形状不同的中值滤波器复合使用,只要各窗口都与中心对称,滤波输出可保持几个方向上的边缘跳变,而且跳变幅度可调节。
高阶中值滤波组合可以使输入图像中任意方向的细线条保持不变。
为了在一定的条件下尽可能去除噪声,又有效保持图像细节,可以对中值滤波器参数进行修正, 如加权中值滤波, 也就是对输入窗口进行加权。
也可以是对中值滤波器的使用方法进行变化, 保证滤波的效果, 还可以和其他滤波器联合使用。
2).均值滤波(模糊算法)
均值滤波是典型的线性滤波算法,它是指在图像上对待处理的像素给一个模板,该模板包括了其周围的临近像素。将模板中的全体像素的均值来代替原来的像素值的方法。
3)维纳(Wiener)滤波
维纳(Wiener)滤波是对退化图像进行恢复处理的另一种常用算法,是一种有约束的恢复处理方法,其采用的维纳滤波器是一种最小均方误差滤波器,其数学形式比较复杂:
F(u,v)=[(1/H(u,v))*(|H(u,v)|2)/(|H(u,v)|2+s*[Sn(u,v)/Sf(u,v)])]*G(u,v)
& nbsp; 当s为1时,上式就是普通的维纳滤波;如果s为变量,则为参数维纳滤波,如果没有噪声干扰,即Sn (u,v)=0时,上式实际就是前面的逆滤波。从其数学形式可以看出:维纳滤波比逆滤波在对噪声的处理方面要强一些。以上只是理论上的数学形式,在进行实际处理时,往往不知道噪声函数Sn(u,v)和Sf(u,v)的分布情况,因此在实际应用时多用下式进行近似处理:
F(u,v)=[(1/H(u,v))* (|H(u,v)|2)/(|H(u,v)|2+K)]*G(u,v)
其中K是一个预先设定的常数。