车牌、验证码识别的普通方法为:
1.
将图片灰度化与二值化。
2.
去噪,然后切割成一个一个的字符。
3.提取每一个字符的特征,生成特征矢量或特征矩阵。
4.
分类与学习。将特征矢量或特征矩阵与样本库进行比对,挑选出相似的那类样本,将这类样本的值作为输出结果。
下面借着代码,描述一下上述过程。
1.
图片的灰度化与二值化
这样做的目的是将图片的每一个象素变成
0
或者255
,以便以计算。同时,也可以去除部分噪音。
图片的灰度化与二值化的前提是
bmp
图片,如果不是,则需要首先转换为
bmp
图片。
1
protected
static
Color Gray(Color c)
2 { 3 int rgb = Convert.ToInt32(( double ) ((( 0.3 * c.R) + ( 0.59 * c.G)) + ( 0.11 * c.B))); 4 return Color.FromArgb(rgb, rgb, rgb); 5 } 6 |
通过将图片灰度化,每一个象素就变成了一个
0-255
的灰度值。然后是将灰度值二值化为 0 或 255 。一般的处理方法是设定一个区间,比如, [a,b] ,将 [a,b] 之间的灰度全部变成 255 ,其它的变成 0 。这里我采用的是网上广为流行的自适应二值化算法。
1
public
static
void
Binarizate(Bitmap map)
2 { 3 int tv = ComputeThresholdValue(map); 4 int x = map.Width; 5 int y = map.Height; 6 for ( int i = 0 ; i < x; i ++ ) 7 { 8 for ( int j = 0 ; j < y; j ++ ) 9 { 10 if (map.GetPixel(i, j).R >= tv) 11 { 12 map.SetPixel(i, j, Color.FromArgb( 0xff , 0xff , 0xff )); 13 } 14 else 15 { 16 map.SetPixel(i, j, Color.FromArgb( 0 , 0 , 0 )); 17 } 18 } 19 } 20 } 21 22 private static int ComputeThresholdValue(Bitmap img) 23 { 24 int i; 25 int k; 26 double csum; 27 int thresholdValue = 1 ; 28 int [] ihist = new int [ 0x100 ]; 29 for (i = 0 ; i < 0x100 ; i ++ ) 30 { 31 ihist[i] = 0 ; 32 } 33 int gmin = 0xff ; 34 int gmax = 0 ; 35 for (i = 1 ; i < (img.Width - 1 ); i ++ ) 36 { 37 for ( int j = 1 ; j < (img.Height - 1 ); j ++ ) 38 { 39 int cn = img.GetPixel(i, j).R; 40 ihist[cn] ++ ; 41 if (cn > gmax) 42 { 43 gmax = cn; 44 } 45 if (cn < gmin) 46 { 47 gmin = cn; 48 } 49 } 50 } 51 double sum = csum = 0.0 ; 52 int n = 0 ; 53 for (k = 0 ; k <= 0xff ; k ++ ) 54 { 55 sum += k * ihist[k]; 56 n += ihist[k]; 57 } 58 if (n == 0 ) 59 { 60 return 60 ; 61 } 62 double fmax = - 1.0 ; 63 int n1 = 0 ; 64 for (k = 0 ; k < 0xff ; k ++ ) 65 { 66 n1 += ihist[k]; 67 if (n1 != 0 ) 68 { 69 int n2 = n - n1; 70 if (n2 == 0 ) 71 { 72 return thresholdValue; 73 } 74 csum += k * ihist[k]; 75 double m1 = csum / (( double ) n1); 76 double m2 = (sum - csum) / (( double ) n2); 77 double sb = ((n1 * n2) * (m1 - m2)) * (m1 - m2); 78 if (sb > fmax) 79 { 80 fmax = sb; 81 thresholdValue = k; 82 } 83 } 84 } 85 return thresholdValue; 86 } 87 |
灰度化与二值化之前的图片:
灰度化与二值化之后的图片:
注:对于车牌识别来说,这个算法还不错。对于验证码识别,可能需要针对特定的网站设计特殊的二值化算法,以过滤杂色。
2. 去噪,然后切割成一个一个的字符
上面这张车牌切割是比较简单的,从左到右扫描一下,碰见空大的,咔嚓一刀,就解决了。但有一些车牌,比如这张:
简单的扫描就解决不了。因此需要一个比较通用的去噪和切割算法。 这里我采用的是比较朴素的方法: 将上面的图片看成是一个平面。将图片向水平方向投影,这样有字的地方的投影值就高,没字的地方投影得到的值就低。
然后,用一根扫描线 从下向上扫描。这个扫描线会与图中曲线存在交点,这些交点会将山头分割成一个又一个区域。车牌图片一般是 7 个字符,因此,当扫描线将山头分割成七个区域时停止。然后根据这七个区域向水平线的投影的坐标就可以将图片中的七个字符分割出来。
但是,现实是复杂的。比如,“川”字,它的水平投影是三个山头。按上面这种扫描方法会将它切开。因此,对于上面的切割,需要加上约束条件:每个山头有一个中心线,山头与山头的中心线的距离必需在某一个值之上,否则,则需要将这两个山头进行合并。加上这个约束之后,便可以有效的切割了。
以上是水平投影。然后还需要做垂直投影与切割。这里的垂直投影与切割就一个山头,因此好处理一些。
水平投影及切割代码:
1
public
static
IList
<
Bitmap
>
Split(Bitmap map,
int
count)
2 { 3 if (count <= 0 ) 4 { 5 throw new ArgumentOutOfRangeException( " Count 必须大于0. " ); 6 } 7 IList < Bitmap > resultList = new List < Bitmap > (); 8 int x = map.Width; 9 int y = map.Height; 10 int splitBitmapMinWidth = 4 ; 11 int [] xNormal = new int [x]; 12 for ( int i = 0 ; i < x; i ++ ) 13 { 14 for ( int j = 0 ; j < y; j ++ ) 15 { 16 if (map.GetPixel(i, j).R == CharGrayValue) 17 { 18 xNormal[i] ++ ; 19 } 20 } 21 } 22 Pair pair = new Pair(); 23 for ( int i = 0 ; i < y; i ++ ) 24 { 25 IList < Pair > pairList = new List < Pair > (count + 1 ); 26 for ( int j = 0 ; j < x; j ++ ) 27 { 28 if (xNormal[j] >= i) 29 { 30 if ((j == (x - 1 )) && (pair.Status == PairStatus.Start)) 31 { 32 pair.End = j; 33 pair.Status = PairStatus.End; 34 if ((pair.End - pair.Start) >= splitBitmapMinWidth) 35 { 36 pairList.Add(pair); 37 } 38 pair = new Pair(); 39 } 40 else if (pair.Status == PairStatus.JustCreated) 41 { 42 pair.Start = j; 43 pair.Status = PairStatus.Start; 44 } 45 } 46 else if (pair.Status == PairStatus.Start) 47 { 48 pair.End = j; 49 pair.Status = PairStatus.End; 50 if ((pair.End - pair.Start) >= splitBitmapMinWidth) 51 { 52 pairList.Add(pair); 53 } 54 pair = new Pair(); 55 } 56 if (pairList.Count > count) 57 { 58 break ; 59 } 60 } 61 if (pairList.Count == count) 62 { 63 foreach (Pair p in pairList) 64 { 65 if (p.Width < (map.Width / 10 )) 66 { 67 int width = (map.Width / 10 ) - p.Width; 68 p.Start = Math.Max( 0 , p.Start - (width / 2 )); 69 p.End = Math.Min(( int ) (p.End + (width / 2 )), ( int ) (map.Width - 1 )); 70 } 71 } 72 foreach (Pair p in pairList) 73 { 74 int newMapWidth = (p.End - p.Start) + 1 ; 75 Bitmap newMap = new Bitmap(newMapWidth, y); 76 for ( int ni = p.Start; ni <= p.End; ni ++ ) 77 { 78 for ( int nj = 0 ; nj < y; nj ++ ) 79 { 80 newMap.SetPixel(ni - p.Start, nj, map.GetPixel(ni, nj)); 81 } 82 } 83 resultList.Add(newMap); 84 } 85 return resultList; 86 } 87 } 88 return resultList; |
代码中的
Pair,
代表扫描线与曲线的一对交点:
1
private
class
Pair
2 { 3 public Pair(); 4 public int CharPixelCount { get ; set ; } 5 public int CharPixelXDensity { get ; } 6 public int End { get ; set ; } 7 public int Start { get ; set ; } 8 public BitmapConverter.PairStatus Status { get ; set ; } 9 public int Width { get ; } 10 } |
PairStatus 代表 Pair 的状态。
1
private
enum
PairStatus
2 { 3 JustCreated, 4 Start, 5 End 6 } |
3.
提取每一个字符的特征,生成特征矢量或特征矩阵将切割出来的字符,分割成一个一个的小块,比如 3 × 3 , 5 × 5 ,或 3 × 5 ,或 10 × 8 ,然后统计一下每小块的值为 255 的像素数量,这样得到一个矩阵 M ,或者将这个矩阵简化为矢量 V
通过以上 3 步,就可以将一个车牌中的字符数值化为矢量了。
4. 分类与学习。将特征矢量或特征矩阵与样本库进行比对,挑选出相似的那类样本,将这类样本的值作为输出结果。