轉自:車牌識别及驗證碼識别的一般思路
本文源自我之前花了2天時間做的一個簡單的車牌識别系統。那個項目,時間太緊,樣本也有限,達不到對方要求的95%識别率(主要對于車牌來說,D,0,O,I,1等等太相似了。然後,漢字的識别難度也不小),是以未被對方接受。在此放出,同時描述一下思路及算法。
全文分兩部分,第一部分講車牌識别及普通驗證碼這一類識别的普通方法,第二部分講對類似QQ驗證碼,Gmail驗證碼這一類變态驗證碼的識别方法和思路。
一、車牌/驗證碼識别的普通方法
車牌、驗證碼識别的普通方法為:
(1) 将圖檔灰階化與二值化
(2) 去噪,然後切割成一個一個的字元
(3) 提取每一個字元的特征,生成特征矢量或特征矩陣
(4) 分類與學習。将特征矢量或特征矩陣與樣本庫進行比對,挑選出相似的那類樣本,将這類樣本的值作為輸出結果。
下面借着代碼,描述一下上述過程。因為更新SVN Server,我以前以bdb儲存的代碼通路不了,是以部分代碼是用Reflector反編譯過來的,望見諒。
(1) 圖檔的灰階化與二值化
這樣做的目的是将圖檔的每一個象素變成0或者255,以便以計算。同時,也可以去除部分噪音。
圖檔的灰階化與二值化的前提是bmp圖檔,如果不是,則需要首先轉換為bmp圖檔。
用代碼說話,我的将圖檔灰階化的代碼(算法是在網上搜到的):
protected static Color Gray(Color c)
{
int rgb = Convert.ToInt32((double) (((0.3 * c.R) + (0.59 * c.G)) + (0.11 * c.B)));
return Color.FromArgb(rgb, rgb, rgb);
}
通過将圖檔灰階化,每一個象素就變成了一個0-255的灰階值。
然後是将灰階值二值化為 0 或255。一般的處理方法是設定一個區間,比如,[a,b],将[a,b]之間的灰階全部變成255,其它的變成0。這裡我采用的是網上廣為流行的自适應二值化算法。
public static void Binarizate(Bitmap map)
{
int tv = ComputeThresholdValue(map);
int x = map.Width;
int y = map.Height;
for (int i = 0; i < x; i++)
{
for (int j = 0; j < y; j++)
{
if (map.GetPixel(i, j).R >= tv)
{
map.SetPixel(i, j, Color.FromArgb(0xff, 0xff, 0xff));
}
else
{
map.SetPixel(i, j, Color.FromArgb(0, 0, 0));
}
}
}
}
private static int ComputeThresholdValue(Bitmap img)
{
int i;
int k;
double csum;
int thresholdValue = 1;
int[] ihist = new int[0x100];
for (i = 0; i < 0x100; i++)
{
ihist[i] = 0;
}
int gmin = 0xff;
int gmax = 0;
for (i = 1; i < (img.Width - 1); i++)
{
for (int j = 1; j < (img.Height - 1); j++)
{
int cn = img.GetPixel(i, j).R;
ihist[cn]++;
if (cn > gmax)
{
gmax = cn;
}
if (cn < gmin)
{
gmin = cn;
}
}
}
double sum = csum = 0.0;
int n = 0;
for (k = 0; k <= 0xff; k++)
{
sum += k * ihist[k];
n += ihist[k];
}
if (n == 0)
{
return 60;
}
double fmax = -1.0;
int n1 = 0;
for (k = 0; k < 0xff; k++)
{
n1 += ihist[k];
if (n1 != 0)
{
int n2 = n - n1;
if (n2 == 0)
{
return thresholdValue;
}
csum += k * ihist[k];
double m1 = csum / ((double) n1);
double m2 = (sum - csum) / ((double) n2);
double sb = ((n1 * n2) * (m1 - m2)) * (m1 - m2);
if (sb > fmax)
{
fmax = sb;
thresholdValue = k;
}
}
}
return thresholdValue;
}
灰階化與二值化之前的圖檔:
灰階化與二值化之後的圖檔:
注:對于車牌識别來說,這個算法還不錯。對于驗證碼識别,可能需要針對特定的網站設計特殊的二值化算法,以過濾雜色。
(2) 去噪,然後切割成一個一個的字元
上面這張車牌切割是比較簡單的,從左到右掃描一下,碰見空大的,咔嚓一刀,就解決了。但有一些車牌,比如這張:
簡單的掃描就解決不了。是以需要一個比較通用的去噪和切割算法。這裡我采用的是比較樸素的方法:
将上面的圖檔看成是一個平面。将圖檔向水準方向投影,這樣有字的地方的投影值就高,沒字的地方投影得到的值就低。這樣會得到一根曲線,像一個又一個山頭。下面是我手畫示意圖:
然後,用一根掃描線(上圖中的S)從下向上掃描。這個掃描線會與圖中曲線存在交點,這些交點會将山頭分割成一個又一個區域。車牌圖檔一般是7個字元,是以,當掃描線将山頭分割成七個區域時停止。然後根據這七個區域向水準線的投影的坐标就可以将圖檔中的七個字元分割出來。
但是,現實是複雜的。比如,“川”字,它的水準投影是三個山頭。按上面這種掃描方法會将它切開。是以,對于上面的切割,需要加上限制條件:每個山頭有一個中心線,山頭與山頭的中心線的距離必需在某一個值之上,否則,則需要将這兩個山頭進行合并。加上這個限制之後,便可以有效的切割了。
以上是水準投影。然後還需要做垂直投影與切割。這裡的垂直投影與切割就一個山頭,是以好處理一些。
切割結果如下:
水準投影及切割代碼:
public static IList<Bitmap> Split(Bitmap map, int count)
{
if (count <= 0)
{
throw new ArgumentOutOfRangeException("Count 必須大于0.");
}
IList<Bitmap> resultList = new List<Bitmap>();
int x = map.Width;
int y = map.Height;
int splitBitmapMinWidth = 4;
int[] xNormal = new int[x];
for (int i = 0; i < x; i++)
{
for (int j = 0; j < y; j++)
{
if (map.GetPixel(i, j).R == CharGrayValue)
{
xNormal[i]++;
}
}
}
Pair pair = new Pair();
for (int i = 0; i < y; i++)
{
IList<Pair> pairList = new List<Pair>(count + 1);
for (int j = 0; j < x; j++)
{
if (xNormal[j] >= i)
{
if ((j == (x - 1)) && (pair.Status == PairStatus.Start))
{
pair.End = j;
pair.Status = PairStatus.End;
if ((pair.End - pair.Start) >= splitBitmapMinWidth)
{
pairList.Add(pair);
}
pair = new Pair();
}
else if (pair.Status == PairStatus.JustCreated)
{
pair.Start = j;
pair.Status = PairStatus.Start;
}
}
else if (pair.Status == PairStatus.Start)
{
pair.End = j;
pair.Status = PairStatus.End;
if ((pair.End - pair.Start) >= splitBitmapMinWidth)
{
pairList.Add(pair);
}
pair = new Pair();
}
if (pairList.Count > count)
{
break;
}
}
if (pairList.Count == count)
{
foreach (Pair p in pairList)
{
if (p.Width < (map.Width / 10))
{
int width = (map.Width / 10) - p.Width;
p.Start = Math.Max(0, p.Start - (width / 2));
p.End = Math.Min((int) (p.End + (width / 2)), (int) (map.Width - 1));
}
}
foreach (Pair p in pairList)
{
int newMapWidth = (p.End - p.Start) + 1;
Bitmap newMap = new Bitmap(newMapWidth, y);
for (int ni = p.Start; ni <= p.End; ni++)
{
for (int nj = 0; nj < y; nj++)
{
newMap.SetPixel(ni - p.Start, nj, map.GetPixel(ni, nj));
}
}
resultList.Add(newMap);
}
return resultList;
}
}
return resultList;
}
代碼中的 Pair,代表掃描線與曲線的一對交點:
private class Pair
{
public Pair();
public int CharPixelCount { get; set; }
public int CharPixelXDensity { get; }
public int End { get; set; }
public int Start { get; set; }
public BitmapConverter.PairStatus Status { get; set; }
public int Width { get; }
}
PairStatus代表Pair的狀态。具體哪個狀态是什麼意義,我已經忘了。
private enum PairStatus
{
JustCreated,
Start,
End
}
以上這一段代碼寫的很辛苦,因為要處理很多特殊情況。那個PairStatus 也是為處理特殊情況引進的。
垂直投影與切割的代碼簡單一些,不貼了,見附後的dll的BitmapConverter.TrimHeight方法。
以上用到的是樸素的去噪與切割方法。有些圖檔,尤其是驗證碼圖檔,需要特别的去噪處理。具體操作方法就是,打開CxImage(http://www.codeproject.com/KB/graphics/cximage.aspx),或者Paint.Net,用上面的那些圖檔處理方法,看看能否有效去噪。記住自己的操作步驟,然後翻他們的源代碼,将其中的算法提取出來。還有什麼細化啊,濾波啊,這些處理可以提高圖檔的品質。具體可參考ITK的代碼或圖像處理書籍。
(3) 提取每一個字元的特征,生成特征矢量或特征矩陣
将切割出來的字元,分割成一個一個的小塊,比如3×3,5×5,或3×5,或10×8,然後統計一下每小塊的值為255的像素數量,這樣得到一個矩陣M,或者将這個矩陣簡化為矢量V。
通過以上3步,就可以将一個車牌中的字元數值化為矢量了。
(1)-(3)步具體的代碼流程如下:
BitmapConverter.ToGrayBmp(bitmap); // 圖檔灰階化
BitmapConverter.Binarizate(bitmap); // 圖檔二值化
IList<Bitmap> mapList = BitmapConverter.Split(bitmap, DefaultCharsCount); // 水準投影然後切割
Bitmap map0 = BitmapConverter.TrimHeight(mapList[0], DefaultHeightTrimThresholdValue); // 垂直投影然後切割
ImageSpliter spliter = new ImageSpliter(map0);
spliter.WidthSplitCount = DefaultWidthSplitCount;
spliter.HeightSplitCount = DefaultHeightSplitCount;
spliter.Init();
然後,通過spliter.ValueList就可以獲得 Bitmap map0 的矢量表示。
(4) 分類
分類的原理很簡單。用(Vij,Ci)表示一個樣本。其中,Vij是樣本圖檔經過上面過程數值化後的矢量。Ci是人肉眼識别這張圖檔,給出的結果。Vij表明,有多個樣本,它們的數值化後的矢量不同,但是它們的結果都是Ci。假設待識别的圖檔矢量化後,得到的矢量是V’。
直覺上,我們會有這樣一個思路,就是這張待識别的圖檔,最像樣本庫中的某張圖檔,那麼我們就将它當作那張圖檔,将它識别為樣本庫中那張圖檔事先指定的字元。
在我們眼睛裡,判斷一張圖檔和另一張圖檔是否相似很簡單,但對于電腦來說,就很難判斷了。我們前面已經将圖檔數值化為一個個次元一樣的矢量,電腦是怎樣判斷一個矢量與另一個矢量相似的呢?
這裡需要計算一個矢量與另一個矢量間的距離。這個距離越短,則認為這兩個矢量越相似。
我用 SampleVector<T> 來代表矢量:
public class SampleVector<T>
{
protected T[] Vector { get; set; }
public Int32 Dimension { get { return Vector.Length; } }
……
}
T代表資料類型,可以為Int32,也可以為Double等更精确的類型。
測量距離的公共接口為:IMetric
public interface IMetric<TElement,TReturn>
{
TReturn Compute(SampleVector<TElement> v1, SampleVector<TElement> v2);
}
常用的是 MinkowskiMetric 。
/// <summary>
/// Minkowski 測度。
/// </summary>
public class MinkowskiMetric<TElement> : IMetric<TElement, Double>
{
public Int32 Scale { get; private set; }
public MinkowskiMetric(Int32 scale)
{ Scale = scale; }
public Double Compute(SampleVector<TElement> v1, SampleVector<TElement> v2)
{
if (v1 == null || v2 == null) throw new ArgumentNullException();
if (v1.Dimension != v2.Dimension) throw new ArgumentException("v1 和 v2 的次元不等.");
Double result = 0;
for (int i = 0; i < v1.Dimension; i++)
{
result += Math.Pow(Math.Abs(Convert.ToDouble(v1[i]) - Convert.ToDouble(v2[i])), Scale);
}
return Math.Pow(result, 1.0 / Scale);
}
}
///MetricFactory 負責生産各種次元的MinkowskiMetric:
public class MetricFactory
{
public static IMetric<TElement, Double> CreateMinkowskiMetric<TElement>(Int32 scale)
{
return new MinkowskiMetric<TElement>(scale);
}
public static IMetric<TElement, Double> CreateEuclideanMetric<TElement>()
{
return CreateMinkowskiMetric<TElement>(2);
}
}
MinkowskiMetric是普遍使用的測度。但不一定是最有效的量。因為它對于矢量V中的每一個點都一視同仁。而在圖像識别中,每一個點的重要性卻并不一樣,例如,Q和O的識别,特征在下半部分,下半部分的權重應該大于上半部分。對于這些易混淆的字元,需要設計特殊的測量方法。在車牌識别中,其它易混淆的有D和0,0和O,I和1。Minkowski Metric識别這些字元,效果很差。是以,當碰到這些字元時,需要進行特别的處理。由于當時時間緊,我就隻用了Minkowski Metric
我的代碼中,隻實作了哪個最近,就選哪個。更好的方案是用K近鄰分類器或神經網絡分類器。K近鄰的原理是,找出和待識别的圖檔(矢量)距離最近的K個樣本,然後讓這K個樣本使用某種規則計算(投票),這個新圖檔屬于哪個類别(C);神經網絡則将測量的過程和投票判決的過程參數化,使它可以随着樣本的增加而改變,是這樣的一種學習機。有興趣的可以去看《模式分類》一書的第三章和第四章。
二、 變态字元的識别
有些字元變形很嚴重,有的字元連在一起互相交叉,有的字元被掩蓋在一堆噪音海之中。對這類字元的識别需要用上特殊的手段。
下面介紹幾種幾個經典的處理方法,這些方法都是被證明對某些問題很有效的方法:
(1) 切線距離 (Tangent Distance):可用于處理字元的各種變形,OCR的核心技術之一。
(2) 霍夫變換(Hough Transform):對噪音極其不敏感,常用于從圖檔中提取各種形狀。圖像識别中最基本的方法之一。
(3) 形狀上下文(Shape Context):将特征高維化,對形變不很敏感,對噪音也不很敏感。新世紀出現的新方法。
因為這幾種方法我均未編碼實作過,是以隻簡單介紹下原理及主要應用場景。
(1) 切線距離
前面介紹了MinkowskiMetric。這裡我們看看下面這張圖:一個正寫的1與一個歪着的1.
用MinkowskiMetric計算的話,兩者的MinkowskiMetric很大。
然而,在圖像識别中,形狀形變是常事。理論上,為了更好地識别,我們需要對每一種形變都采足夠的樣,這樣一來,會發現樣本數幾乎無窮無盡,計算量越來越大。
怎麼辦呢?那就是通過計算切線距離,來代替直接距離。切線距離比較抽象,我們将問題簡化為二維空間,以便以了解。
上圖有兩條曲線。分别是兩個字元經過某一形變後所産生的軌迹。V1和V2是2個樣本。V’是待識别圖檔。如果用樣本之間的直接距離,比較哪個樣本離V’最近,就将V’當作哪一類,這樣的話,就要把V’分給V1了。理論上,如果我們無限取樣的話,下面那一條曲線上的某個樣本離V’最近,V’應該歸類為V2。不過,無限取樣不現實,于是就引出了切線距離:在樣本V1,V2處做切線,然後計算V’離這兩條切線的距離,哪個最近就算哪一類。這樣一來,每一個樣本,就可以代表它附近的一個樣本區域,不需要海量的樣本,也能有效的計算不同形狀間的相似性。
深入了解切線距離,可參考這篇文章。Transformation invariance in pattern recognition – tangent distance and tangent propagation (http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.32.9482)這篇文章。
(2) 霍夫變換
霍夫變換出自1962年的一篇專利。它的原理非常簡單:就是坐标變換的問題。
如,上圖中左圖中的直線,對應着有圖中k-b坐标系中的一個點。通過坐标變換,可以将直線的識别轉換為點的識别。點的識别就比直線識别簡單的多。為了避免無限大無限小問題,常用的是如下變換公式:
下面這張圖是wikipedia上一張霍夫變換的示意圖。左圖中的兩條直線變換後正對應着右圖中的兩個亮點。
通過霍夫變換原理可以看出,它的抗幹擾性極強極強:如果直線不是連續的,是斷斷續續的,變換之後仍然是一個點,隻是這個點的強度要低一些。如果一個直線被一個矩形遮蓋住了,同樣不影響識别。因為這個特征,它的應用性非常廣泛。
對于直線,圓這樣容易被參數化的圖像,霍夫變換是最擅長處理的。對于一般的曲線,可通過廣義霍夫變換進行處理。感興趣的可以google之,全是數學公式,看的人頭疼。
(3) 形狀上下文
圖像中的像素點不是孤立的,每個像素點,處于一個形狀背景之下,是以,在提取特征時,需要将像素點的背景也作為該像素點的特征提取出來,數值化。
形狀上下文(Shape Context,形狀背景)就是這樣一種方法:假定要提取像素點O的特征,采用上圖(c)中的坐标系,以O點作為坐标系的圓心。這個坐标系将O點的上下左右切割成了12×5=60小塊,然後統計這60小塊之内的像素的特征,将其數值化為12×5的矩陣,上圖中的(d),(e),(f)便分别是三個像素點的Shape Context數值化後的結果。如此一來,提取的每一個點的特征便包括了形狀特征,加以計算,威力甚大。來看看Shape Context的威力:
上圖中的驗證碼,對Shape Context來說隻是小Case。
看看這幾張圖。嘿嘿,硬是給識别出來了。
Shape Context是新出現的方法,其威力到底有多大目前還未見底。這篇文章是Shape context的必讀文章:Shape Matching and Object Recognitiom using shape contexts(http://www.cs.berkeley.edu/~malik/papers/BMP-shape.pdf)。最後那兩張驗證碼識别圖出自Greg Mori,Jitendra Malik的《Recognizing Objects in Adversarial Clutter:Breaking a Visual CAPTCHA》一文。
===========================================================
附件:第一部分的代碼(vcr.zip). 3個dll檔案,反編譯看的很清晰。源代碼反而沒dll好看,我就不放了。其中,Orc.Generics.dll是幾個泛型類,Orc.ImageProcess.Common.dll 對圖像進行處理和分割,Orc.PatternRecognition.dll 是識别部分。
這三個dll可以直接用在車牌識别上。用于車牌識别,對易混淆的那幾個字元識别率較差,需要補充幾個分類器,現有分類器識别結果為D ,O,0,I,1等時,用新分類器識别。用于識别驗證碼需要改一改。
有個asp.net的調用例子可實作線上上傳圖檔識别,因為其中包含多張車牌資訊,不友善放出來。我貼部分代碼出來:
Global.asax:
void Application_Start(object sender, EventArgs e)
{
log4net.Config.XmlConfigurator.Configure();
Orc.Spider.Vcr.DaoConfig.Init();
Classifier.Update(Server);
}
DaoConfig:
using System;
using Castle.ActiveRecord;
using Castle.ActiveRecord.Framework;
using Castle.ActiveRecord.Framework.Config;
namespace Orc.Spider.Vcr
{
public static class DaoConfig
{
private static Boolean Inited = false;
public static void Init()
{
if (!Inited)
{
Inited = true;
XmlConfigurationSource con = new XmlConfigurationSource(AppDomain.CurrentDomain.BaseDirectory + @"\ActiveRecord.config");
ActiveRecordStarter.Initialize
(con,
typeof(TrainPattern)
);
}
}
}
}
TrainPattern:// TrainPattern存在資料庫裡
[ActiveRecord("TrainPattern")]
public class TrainPattern : ActiveRecordBase<TrainPattern>
{
[PrimaryKey(PrimaryKeyType.Native, "Id")]
public Int32 Id { get; set; }
[Property("FileName")]
public String FileName { get; set; }
[Property("Category")]
public String Category { get; set; }
public static TrainPattern[] FindAll()
{
String hql = "from TrainPattern ORDER BY Category DESC";
SimpleQuery<TrainPattern> query = new SimpleQuery<TrainPattern>(hql);
return query.Execute();
}
}
lassifier://主要調用封裝在這裡
ublic class Classifier
{
protected static Orc.PatternRecognition.KnnClassifier<Int32> DefaultChineseCharClassifier;
protected static Orc.PatternRecognition.KnnClassifier<Int32> DefaultEnglishAndNumberCharClassifier;
protected static Orc.PatternRecognition.KnnClassifier<Int32> DefaultNumberCharClassifier;
public static Int32 DefaultWidthSplitCount = 3;
public static Int32 DefaultHeightSplitCount = 3;
public static Int32 DefaultCharsCount = 7; // 一張圖檔中包含的字元個數
public static Int32 DefaultHeightTrimThresholdValue = 4;
public static ILog Log = LogManager.GetLogger("Vcr");
public static void Update(HttpServerUtility server)
{
TrainPattern[] TPList = TrainPattern.FindAll();
if (TPList == null) return;
DefaultChineseCharClassifier = new KnnClassifier<Int32>(DefaultWidthSplitCount * DefaultHeightSplitCount);
DefaultEnglishAndNumberCharClassifier = new KnnClassifier<Int32>(DefaultWidthSplitCount * DefaultHeightSplitCount);
DefaultNumberCharClassifier = new KnnClassifier<Int32>(DefaultWidthSplitCount * DefaultHeightSplitCount);
foreach (TrainPattern tp in TPList)
{
String path = server.MapPath(".") + "/VcrImage/" + tp.FileName;
using (Bitmap bitmap = new Bitmap(path))
{
TrainPattern<Int32> tpv = CreateTainPatternVector(bitmap, tp.Category.Substring(0, 1));
Char c = tpv.Category[0];
if (c >= '0' && c <= '9')
{
DefaultEnglishAndNumberCharClassifier.AddTrainPattern(tpv);
DefaultNumberCharClassifier.AddTrainPattern(tpv);
}
else if (c >= 'a' && c <= 'z')
DefaultEnglishAndNumberCharClassifier.AddTrainPattern(tpv);
else if (c >= 'A' && c <= 'Z')
DefaultEnglishAndNumberCharClassifier.AddTrainPattern(tpv);
else
DefaultChineseCharClassifier.AddTrainPattern(tpv);
}
}
}
protected static TrainPattern<Int32> CreateTainPatternVector(Bitmap bitmap, String categoryChars)
{
TrainPattern<int> tpv = new TrainPattern<int>( CreateSampleVector(bitmap), categoryChars);
tpv.XNormalSample = CreateXNormalSampleVector(bitmap);
tpv.YNormalSample = CreateYNormalSampleVector(bitmap);
return tpv;
}
protected static SampleVector<Int32> CreateSampleVector(Bitmap bitmap)
{
ImageSpliter spliter = new ImageSpliter(bitmap);
spliter.WidthSplitCount = DefaultWidthSplitCount;
spliter.HeightSplitCount = DefaultHeightSplitCount;
spliter.Init();
return new SampleVector<Int32>(spliter.ValueList);
}
protected static SampleVector<Int32> CreateYNormalSampleVector(Bitmap bitmap)
{
ImageSpliter spliter = new ImageSpliter(bitmap);
spliter.WidthSplitCount = 1;
spliter.HeightSplitCount = DefaultHeightSplitCount;
spliter.Init();
return new SampleVector<Int32>(spliter.ValueList);
}
protected static SampleVector<Int32> CreateXNormalSampleVector(Bitmap bitmap)
{
ImageSpliter spliter = new ImageSpliter(bitmap);
spliter.WidthSplitCount = DefaultWidthSplitCount;
spliter.HeightSplitCount = 1;
spliter.Init();
return new SampleVector<Int32>(spliter.ValueList);
}
public static String Classify(String imageFileName)
{
Log.Debug("識别檔案:" + imageFileName);
String result = String.Empty;
if (DefaultChineseCharClassifier == null || DefaultEnglishAndNumberCharClassifier == null) throw new Exception("識别器未初始化.");
using (Bitmap bitmap = new Bitmap(imageFileName))
{
BitmapConverter.ToGrayBmp(bitmap);
BitmapConverter.Binarizate(bitmap);
IList<Bitmap> mapList = BitmapConverter.Split(bitmap, DefaultCharsCount);
if (mapList.Count == DefaultCharsCount)
{
Bitmap map0 = BitmapConverter.TrimHeight(mapList[0], DefaultHeightTrimThresholdValue);
TrainPattern<Int32> tp0 = CreateTainPatternVector(map0, " ");
String sv0Result = DefaultChineseCharClassifier.Classify(tp0);
Console.WriteLine("識别樣本: " + tp0.Sample.ToString());
result += sv0Result;
for (int i = 1; i < mapList.Count; i++)
{
Bitmap mapi = BitmapConverter.TrimHeight(mapList[i], DefaultHeightTrimThresholdValue);
TrainPattern<Int32> tpi = CreateTainPatternVector(mapi, " ");
Console.WriteLine("識别樣本: " + tpi.Sample.ToString());
if (i < mapList.Count - 3)
result += DefaultEnglishAndNumberCharClassifier.Classify(tpi);
else
result += DefaultNumberCharClassifier.Classify(tpi);
}
}
return result;
}
}
/*
public static IList<Tuple<Double,String>> ComputeDistance(String imageFileName)
{
if (DefaultChineseCharClassifier == null) throw new Exception("識别器未初始化.");
using (Bitmap bitmap = new Bitmap(imageFileName))
{
ImageSpliter spliter = new ImageSpliter(bitmap);
spliter.WidthSplitCount = DefaultWidthSplitCount;
spliter.HeightSplitCount = DefaultHeightSplitCount;
spliter.Init();
SampleVector<Int32> sv = new SampleVector<Int32>(spliter.ValueList);
return DefaultChineseCharClassifier.ComputeDistance(sv);
}
}*/
}