在本教程中,我們將使用OpenCV構建一個簡單的手寫數字分類器。我們將共享用C ++和Python編寫.
圖像分類管道
本章節(jié)暫不討論圖像分類管道,大家可以自行搜索相關知識,下次我們補充。
?
我們將使用方向梯度直方圖作為特征描述符和支持向量機(SVM)作為分類的機器學習算法。
使用OpenCV的光學字符識別(OCR)示例(C ++ / Python)
我想與代碼共享一個示例,以使用HOG + SVM演示圖像分類。與此同時,我希望盡可能保持簡單,這樣除了HOG和SVM之外我們不需要太多東西。這個靈感和數據來自OpenCV的教程在這里:
http://docs.opencv.org/trunk/dd/d3b/tutorial_py_svm_opencv.html
原始教程僅在Python中,并且由于一些奇怪的原因實現了它自己的簡單HOG描述符。我們用OpenCV的HOG描述符替換了他們自己開發(fā)的HOG。
OCR的數字數據集
我們將使用上面的圖像作為OpenCV樣本附帶的數據集。它共包含5000張圖像 - 每個數字500張圖像。每幅圖像均為20×20灰度,背景為黑色。這些數字中的4500個將用于訓練,其余500個將用于測試算法的性能。您可以點擊上面的圖片放大。
讓我們完成構建和測試分類器所需的步驟。
第1步:糾偏(預處理)
人們通常將學習算法視為塊框。在一端輸入圖像,在另一端輸出結果。實際上,您可以稍微協助算法,并注意到性能的巨大提升。例如,如果您正在構建面部識別系統,則將圖像與參考面對齊通常會導致性能的顯著提高。典型的對準操作使用面部特征檢測器來對準每個圖像中的眼睛。
在構建分類器之前對齊數字同樣會產生更好的結果。在面部的情況下,對準是相當明顯的 - 您可以對面部圖像應用相似變換,以將眼睛的兩個角對準參考面的兩個角。
歪斜的例子
在手寫數字的情況下,我們沒有明顯的特征,如我們可以用于對齊的眼角。然而,人們寫作的明顯變化是他們的寫作傾向。有些作者有一個向右或向前的傾斜,其中數字向前傾斜,有些具有向后或向左傾斜,有些沒有傾斜。我們可以通過修復這個垂直斜面來幫助算法,因此它不必學習數字的這種變化。左側的圖像顯示第一列中的原始數字,并且它是傾斜(固定)版本。
可以使用圖像矩來實現對簡單灰度圖像的這種偏斜校正。OpenCV有一個瞬間的實現,它在計算有用的信息時很方便,如質心,面積,黑色背景的簡單圖像的偏斜。
事實證明,偏斜的度量是由兩個中心力矩(mu11 / mu02)的比率給出的。如此計算的偏度可以用于計算對圖像進行校正的仿射變換。
偏移的代碼如下:
C++
Mat deskew(Mat& img)
{
Moments m = moments(img);
if(abs(m.mu02) < 1e-2)
{
// No deskewing needed.
return img.clone();
}
// Calculate skew based on central momemts.
double skew = m.mu11/m.mu02;
// Calculate affine transform to correct skewness.
Mat warpMat = (Mat_
(2,3) << 1, skew, -0.5*SZ*skew, 0, 1 , 0);
Mat imgOut = Mat::zeros(img.rows, img.cols, img.type());
warpAffine(img, imgOut, warpMat, imgOut.size(),affineFlags);
return imgOut;
}
Python
def deskew(img):
m = cv2.moments(img)
if abs(m['mu02']) < 1e-2:
# no deskewing needed.
return img.copy()
# Calculate skew based on central momemts.
skew = m['mu11']/m['mu02']
# Calculate affine transform to correct skewness.
M = np.float32([[1, skew, -0.5*SZ*skew], [0, 1, 0]])
# Apply affine transform
img = cv2.warpAffine(img, M, (SZ, SZ), flags=cv2.WARP_INVERSE_MAP | cv2.INTER_LINEAR)
return img
步驟2:計算定向梯度直方圖(HOG)描述符
在此步驟中,我們將使用HOG特征描述符將灰度圖像轉換為特征向量。
我們發(fā)現理論與實踐之間存在巨大差距。獲取知識很容易。我可以閱讀論文和書籍。如果我們不理解這個概念或數學,我們可以閱讀更多的論文和書籍。這很容易。將這些知識付諸實踐的難點。部分原因是很多這些算法在繁瑣的手動操作之后起作用,并且如何設置正確的參數并不明顯。例如,在Harris角點檢測器中,為什么自由參數k設置為0.04?為什么不是1或2或0.34212呢?為什么42是生命,宇宙和一切的答案?
隨著我獲得更多真實世界的經驗,我意識到在某些情況下你可以做出有根據的猜測,但在其他情況下,沒有人知道為什么。人們經常進行參數掃描 - 他們以原則方式改變不同的參數,以查看產生最佳結果的因素。有時,最好的參數有一個直觀的解釋,有時他們沒有。
牢記這一點,讓我們看看為我們的HOG描述符選擇了哪些參數。我們也會嘗試解釋為什么它們有意義,但我會提供有力的手工操作而不是嚴格的證據!
C++
HOGDeor hog(
Size(20,20), //winSize
Size(10,10), //blocksize
Size(5,5), //blockStride,
Size(10,10), //cellSize,
9, //nbins,
1, //derivAper,
-1, //winSigma,
0, //histogramNormType,
0.2, //L2HysThresh,
1,//gammal correction,
64,//nlevels=64
1);//Use signed gradients
Python
winSize = (20,20)
blockSize = (10,10)
blockStride = (5,5)
cellSize = (10,10)
nbins = 9
derivAperture = 1
winSigma = -1.
histogramNormType = 0
L2HysThreshold = 0.2
gammaCorrection = 1
nlevels = 64
signedGradients = True
hog = cv2.HOGDeor(winSize,blockSize,blockStride,cellSize,nbins,derivAperture,winSigma,histogramNormType,L2HysThreshold,gammaCorrection,nlevels, useSignedGradients)
我們不打算來形容derivAperture,winSigma,histogramNormType,L2HysThreshold,伽瑪校正和NLEVELS,因為我從來沒有在使用HOG描述符來改變這些參數。除非您仔細閱讀原始HOG文件,否則我建議您使用默認值。讓我們探討其他參數的選擇。
winSize:此參數設置為20×20,因為我們的數據集中的數字圖像的大小是20×20,我們想要為整個圖像計算一個描述符。
cellSize:我們的數字是20×20灰度圖像。換句話說,我們的圖像由20×20 = 400個數字表示。描述符的大小通常遠小于圖像中的像素數。基于進行分類的重要特征的尺度來選擇cellSize。一個非常小的cellSize會炸掉特征向量的大小,而一個非常大的cellSize可能無法捕獲相關信息。您應該使用本文中共享的代碼自行測試。我們在本教程中選擇了10×10的cellSize。我們可以選擇8嗎?是的,那也行得通。
blockSize:塊的概念存在以解決照明變化。較大的塊大小使本地更改不太重要,而較小的塊大小權重本地更改更多。通常,blockSize設置為2 x cellSize,但在我們的數字分類示例中,照明并不是一個挑戰(zhàn)。在我的實驗中,10×10的blockSize給出了最好的結果。
blockStride:blockStride確定相鄰塊之間的重疊并控制對比度標準化程度。通常,blockStride設置為blockSize的50%。
nbins:nbins設置漸變直方圖中的bin數。HOG論文的作者建議使用值9來捕獲0到180度之間的漸變,以20度為增量。在我的實驗中,將此值增加到18并沒有產生任何更好的結果。
signedGradients:通常,漸變可以具有0到360度之間的任何方向。這些梯度被稱為“有符號”梯度,而不是“無符號”梯度,它們使符號下降并取0到180度之間的值。在原始HOG紙中,無符號梯度用于行人檢測。在我的實驗中,對于這個問題,簽名漸變產生了稍好的結果。
上面定義的HOG描述符可用于使用以下代碼計算圖像的HOG特征。
C++
// im is of type Mat
vector
deors;
hog.compute(im,deor);
Python
deor = hog.compute(im)
對于我們選擇的參數,該描述符的大小為81×1。
第3步:訓練模型(又稱學習分類器)
在此之前,我們已經對原始圖像進行了校正,并為我們的圖像定義了描述符。這使我們能夠將數據集中的每個圖像轉換為大小為81×1的向量。
我們現在準備訓練一個模型,對我們訓練集中的圖像進行分類。為此,我們選擇了支持向量機(SVM)作為我們的分類算法。雖然SVM背后的理論和數學涉及并超出了本教程的范圍,但它的工作原理非常直觀且易于理解。您可以查看我之前解釋線性SVM的帖子。
要快速回顧一下,如果在n維空間中有點并且類標簽附加到點,則線性SVM將使用平面劃分空間,使得不同的類位于平面的不同側。在下圖中,我們有兩個由紅色和藍色圓點表示的類。如果將此數據輸入到線性SVM中,則可以通過查找明確區(qū)分這兩個類的行來輕松構建分類器。有許多行可以分離這些數據。SVM選擇處于任一類的最大距離數據點的那個。
與我們的數字分類問題相比,上圖中顯示的兩類示例可能看起來很簡單,但在數學上它們非常相似。我們的圖像描述符不是二維空間中的點,而是81維空間中的點,因為它們由81×1向量表示。附加到這些點的類標簽是圖像中包含的數字,即0,1,2,... 9.而不是2D中的線,SVM將在高維空間中找到超平面來進行分類。
SVM參數C.
在訓練SVM時您需要了解的兩個常見參數之一稱為C.真實世界數據不像上面所示那樣干凈。有時,訓練數據可能有錯誤標記的示例。在其他時候,一組的一個例子在外觀上可能與另一個例子太接近。例如,手寫數字2可能看起來像3。
在下面的動畫中,我們創(chuàng)建了這個場景。請注意,藍點太靠近紅色簇。選擇默認值C = 1時,藍點被錯誤分類。為C選擇值100將其正確分類。
但是現在由黑線代表的決策邊界太接近其中一個類。您是否愿意選擇C為1,其中一個數據點被錯誤分類,但類之間的分離要好得多(減去一個數據點)?參數C允許您控制此權衡。
那么,你如何選擇C?我們選擇在提供的測試集上提供最佳分類的C. 該組中的圖像未用于訓練。
SVM參數Gamma:非線性SVM
你注意到了,我偷了幾次“線性”這個詞?在分類任務中,如果包含數據的空間可以使用平面(或2D中的線)進行分區(qū)以分隔類,則由多個類組成的數據集稱為線性可分。
如果數據不是線性可分的怎么辦?下圖顯示了使用紅色和藍色點的兩個類,這些點不是線性可分的。您無法在平面上繪制一條線來分隔這兩個類。使用黑線表示的良好分類器更像是一個圓圈。
在現實生活中,數據是混亂的,而不是線性可分的。
我們還可以使用SVM嗎?答案是肯定的!
為此,您使用了一種稱為Kernel Trick的技術。這是一個巧妙的技巧,可將非線性可分離數據轉換為線性可分離數據。在我們的示例中,紅色和藍色點位于2D平面上。讓我們使用以下等式為所有數據點添加第三維。
如果您聽過人們使用帶有高斯核的奇異項徑向基函數(RBF),他們只是在談論上面的等式。RBF只是一個實值函數,它僅取決于與原點的距離(即僅取決于)。的高斯核是指上式的高斯形式。更一般地,RBF可以具有不同種類的內核。你可以在這里看到其中一些。
因此,我們根據其他兩個維度中的數據制作了第三維。下圖顯示了這個三維(x,y,z)數據。我們可以看到它可以被包含黑色圓圈的平面分開!
?
參數Gamma(\伽瑪)控制第三維中的數據拉伸。它有助于分類,但也會扭曲數據。像金發(fā)姑娘一樣,你必須選擇這個參數“恰到好處”。這是人們在訓練SVM時選擇的兩個重要參數之一。
有了這些知識,我們現在準備使用OpenCV訓練SVM。
使用OpenCV訓練和測試SVM
在幕后,OpenCV使用LIBSVM。OpenCV 2.4.x中的SVM仍然使用C API。幸運的是,從3.x開始,OpenCV現在使用了更好的C ++ API。以下是在C ++和Python中使用OpenCV設置SVM的方法。
C++
// Set up SVM for OpenCV 3
Ptr
svm = SVM::create();
// Set SVM type
svm->setType(SVM::C_SVC);
// Set SVM Kernel to Radial Basis Function (RBF)
svm->setKernel(SVM::RBF);
// Set parameter C
svm->setC(12.5);
// Set parameter Gamma
svm->setGamma(0.50625);
// Train SVM on training data
Ptr
td = TrainData::create(trainData, ROW_SAMPLE, trainLabels);
svm->train(td);
// Save trained model
svm->save("digits_svm_model.yml");
// Test on a held out test set
svm->predict(testMat, testResponse);
Python
# Set up SVM for OpenCV 3
svm = cv2.ml.SVM_create()
# Set SVM type
svm.setType(cv2.ml.SVM_C_SVC)
# Set SVM Kernel to Radial Basis Function (RBF)
svm.setKernel(cv2.ml.SVM_RBF)
# Set parameter C
svm.setC(C)
# Set parameter Gamma
svm.setGamma(gamma)
# Train SVM on training data
svm.train(trainData, cv2.ml.ROW_SAMPLE, trainLabels)
# Save trained model
svm->save("digits_svm_model.yml");
# Test on a held out test set
testResponse = svm.predict(testData)[1].ravel()
自動訓練SVM
可以想象,選擇正確的SVM參數C和Gamma可能非常耗時。幸運的是,OpenCV 3.x C ++ API提供了一個功能,可以自動為您執(zhí)行此超參數優(yōu)化,并提供最佳的C和Gamma值。在上面的代碼中,您可以將svm-> train(td)更改為以下內容
svm->trainAuto(td);
這種訓練可能需要很長時間(比svm->訓練多5倍),因為它基本上是多次訓練。
OpenCV SVM錯誤
我們在使用OpenCV SVM時遇到了兩個錯誤。第一個是確認的,但另外兩個不是。
SVM模型不會在Python API中加載。如果您使用的是Python,您剛剛保存的訓練有素的SVM模型將無法加載!錯誤修復會來嗎?不!檢查出來這里
trainAuto似乎沒有通過Python API公開。
帶有RBF內核的SVM在iOS / Android中不起作用。很高興被證明是錯誤的,但在移動平臺(iOS / Android)上,我們無法使用受RBF內核訓練的SVM。SVM響應始終相同。線性SVM模型工作得很好。
結果
經過訓練和一些超參數優(yōu)化,我們在數字分類上達到了98.6%!不是,只需幾秒鐘的培訓就不好了。
在訓練集中的500個圖像中,有7個被錯誤分類。圖像及其錯誤分類的標簽如下所示。就像父親看著他孩子的錯誤一樣,我想說的是這些錯誤是可以理解的。
源碼地址關注微信公眾號:“ 圖像算法 ”或者微信搜索賬號 imalg_cn 關注公眾號 回復 數字分類器
更多文章、技術交流、商務合作、聯系博主
微信掃碼或搜索:z360901061

微信掃一掃加我為好友
QQ號聯系: 360901061
您的支持是博主寫作最大的動力,如果您喜歡我的文章,感覺我的文章對您有幫助,請用微信掃描下面二維碼支持博主2元、5元、10元、20元等您想捐的金額吧,狠狠點擊下面給點支持吧,站長非常感激您!手機微信長按不能支付解決辦法:請將微信支付二維碼保存到相冊,切換到微信,然后點擊微信右上角掃一掃功能,選擇支付二維碼完成支付。
【本文對您有幫助就好】元
