綜述
2012年iOS應用商店中釋出了一個名為FuelMate的Gas跟蹤應用。小夥伴們可以使用該應用程式跟蹤汽油行駛里程,以及有一些有趣的功能,例如Apple Watch應用程式、vin.li整合以及基於趨勢mpg的視覺效果。
燃料伴侶
對此我們有一個新想法,該如何新增一個功能幫助我們在泵中掃描燃油,並在應用程式中輸入燃油資訊?讓我們深入研究如何實現這一目標。
技術
對於這個專案的我們首先應該編寫一個簡單的Python應用程式以拍攝汽油泵的影象,然後嘗試從中讀取數字。OpenCV是用於計算機視覺應用程式的流行的跨平臺庫。它包括各種影象處理實用程式以及某些機器學習功能。除此之外我們希望可以先使用Python對其進行原型設計,然後將處理程式碼轉換為C ++以在iOS應用程式上執行。
目標
我們首先要考慮以下兩個問題:
1.我們可以從影象中分離出數字嗎?
2.我們可以確定影象代表哪個數字嗎?
數字分割
如何確定影象中的數字有多種方法,但是我提出了使用簡單的影象閾值法來嘗試查詢數字的方法。
影象閾值化的基本思想是將影象轉換為灰度,然後說灰度值小於某個常數的任何畫素,則該畫素為一個值,否則為另一個。最後,您得到的二進位制影象只有兩種顏色,在大多數情況下只是黑白影象。
這個概念在OCR應用中非常有效,但是主要問題是決定對該閾值使用什麼。我們可以選擇一些常量,也可以使用OpenCV選擇其他一些選項。我們可以使用自適應閾值而不是使用常數,這將使用影象的較小部分並確定要使用的不同閾值。這在具有不同照明情況的應用中特別有用,特別是在掃描氣泵中。
將影象設定為閾值後,可以使用OpenCV的findContours方法查詢影象中連線了白色畫素部分的區域。繪製輪廓後,便可以裁剪出這些區域並確定它們是否可能是數字以及它是什麼數字。
基本影象處理流程
這是我在測試影象處理中使用的原始影象。它有一些眩光點,但是影象相當乾淨。讓我們逐步完成獲取此源影象的過程,並嘗試將其分解為單個數字。
原始圖片
影像準備
在開始影象處理流程之前,我們決定先調整一些影象屬性,然後再繼續。這有點試驗和錯誤,但注意到,當我們調整影象的曝光度時,可以獲得更好的結果。下面是使用Python調整後的影象,相當於曝光(阿爾法)的影象cv::Mat::convertTo這是剛剛在影象墊乘法操作cv2.multiply(some_img, np.array([some_alpha]),
調整曝光
灰階
將影象轉換為灰度。
轉換為灰度
模糊
模糊影象以減少噪點。我們嘗試了許多不同的模糊選項,但僅用輕微的模糊就找到了最佳結果。
稍微模糊
閾值影象轉換為黑白影象
在下圖中,使用cv2.adaptiveThreshold帶有cv2.ADAPTIVE_THRES_GAUSSIAN_C選項的方法。此方法採用兩個引數,塊大小和要調整的常數。確定這兩者需要一些試驗和錯誤,更多有關最佳化部分的內容。
閾值為黑/白
填補空白
由於大多數燃油泵都使用某種7段LCD顯示屏,因此數字中存在一些細微的間隙,無法使用輪廓繪製方法,因此我們需要使這些段看起來相連。在這種情況下,我們將轉到erode影象來彌補這些差距。由於大家可能希望使用,所以這似乎向後看,dilate但是這些方法通常適用於影象的白色部分。在我們的案例中,我們正在“侵蝕”白色背景以使數字看起來更大。
侵蝕出來的數字
反轉影象
在嘗試在影象中查詢輪廓之前,我們需要反轉顏色,因為該findContours方法將找到白色的連線部分,而當前的數字是黑色。
顏色反轉
在影象上找到輪廓
下圖顯示了我們的原始影象,該影象在上圖的每個輪廓上都有包圍框。大家可以看到它找到了數字,但也找到了一堆不是數字的東西,因此我們需要將它們過濾掉。
紅色框顯示所有找到的輪廓
輪廓過濾
1.現在我們有了許多輪廓,我們需要找出我們關心的輪廓。瀏覽了一堆氣泵的顯示和場景後,使用一套適用於輪廓的快速規則。
2.收集所有我們將分類為潛在小數的正方形輪廓。
3.扔掉任何不是正方形或高矩形的東西。
4.使輪廓與某些長寬比匹配。LCD顯示屏中的十個數字中有九個數字的長寬比類似於下面的藍色框高光之一。該規則的例外是數字“ 1”,其長寬比略有不同。透過使用一些樣本輪廓,我將0–9!1方面確定為0.6,將1方面確定為0.3。它將使用這些比率和+/-緩衝區來確定輪廓是否是我們想要的東西,並收集這些輪廓。
5.對潛在數字應用一組附加規則,在這裡我們將確定輪廓邊界是否偏離所有其他潛在數字的平均高度或垂直位置。由於數字的大小應相同,並且在相同的Y上對齊,因此我們可以丟棄它認為是數字的任何輪廓,但不能像其他輪廓那樣將其對齊和調整大小。
藍色矩形顯示我們的數字/十進位制,紅色被忽略
預測
有兩個等高線輪廓,一個帶潛在位數,一個帶潛在小數位,我們可以使用這些輪廓邊界裁剪影象,並將其輸入經過訓練的系統中以預測其值。有關此過程的更多資訊,請參見“數字培訓”部分。
查詢小數
在影象中查詢小數點是要解決的另一個問題。由於它很小,有時會連線到它旁邊的手指,因此使用我們在手指上使用的方法來確定它似乎有問題。當我們過濾輪廓時,我們收集了可能是十進位制的正方形輪廓。從上一步獲得經過驗證的數字輪廓之後,我們將找到數字的最左x位置和最右x位置,以確定我們期望的小數位數。然後,我們將遍歷那些潛在的小數,確定它是否在該空間以及該空間的下半部分,並將其分類為小數。找到小數點後,我們可以將其插入到我們上面預測的數字字串中。
只在黃色部分中查詢小數
數字培訓
在機器學習的世界中,解決OCR問題是一個分類問題。我們建立了一組訓練有素的資料,例如影象處理中的數字,將它們分類為某種東西,然後使用該資料來匹配任何新影象。一旦基本的影象隔離功能開始工作,我就建立了一個指令碼,該指令碼可以遍歷影象資料夾,執行數字隔離程式碼,然後將裁剪的數字儲存到新資料夾中供我檢視。執行完之後,我會有一個未經訓練的數字資料夾,然後可以用來訓練系統。
由於OpenCV已經包含了k近鄰(k-NN)實現,因此無需引入任何其他庫。為了進行訓練,我們瀏覽了數字作物的資料夾,然後將其放入標有0–9的新資料夾中,因此每個資料夾中都有一個數字的不同版本的集合。我們沒有大量的這些影象,但是有足夠的證據來證明這是可行的。由於這些數字是相當標準的,我認為我不需要大量訓練有素的影象就可以相當準確。
k-NN工作原理的基礎是,我們將以黑白方式載入每個影象,將該影象儲存在每個畫素處於開啟或關閉狀態的陣列中,然後將這些開啟/關閉畫素與特定的數字相關聯。然後,當我們要預測一個新影象時,它將找出哪個訓練影象與這些畫素最匹配,然後向我們返回最接近的值。
整理好數字後,將建立一個新的指令碼,該指令碼將遍歷這些資料夾,獲取每個影象並將該影象與數字關聯。到目前為止,在大多數程式碼中,一般的影象處理概念在Python和C ++中都應用相同,但是在這裡會有細微的差別。
在大多數此類應用程式的Python示例中,分類被寫入兩個檔案,一個包含分類,另一個包含該分類的影象內容。通常使用NumPy和標準文字檔案完成此操作。但是,由於我想在iOS應用程式上重用該系統,因此我需要想出一種可以擁有跨平臺分類檔案的方式。當時,我什麼都找不到,因此最終編寫了一個快速實用程式,該實用程式將從Python中獲取分類資料並將其序列化為JSON檔案,我可以在OpenCV的FileStorage系統的C ++端使用它。這不漂亮,但是我寫了一個簡單的MatPython中的序列化方法,它將為OpenCV建立合適的結構以在iOS端讀取。現在,當我訓練數字時,我將獲得NumPy檔案供我的Python測試使用,然後獲取一個JSON文件,我可以將其拖到我的iOS應用程式中。您可以在此處看到該程式碼。
最佳化
一旦確定了數字隔離和預測的兩個目標,就需要對演算法進行最佳化,以預測泵的新影象上的數字。
在最佳化的初始階段,建立了一個簡單的Playground應用程式,其中使用了OpenCV提供的一些簡單的UI元件。使用這些元件,可以建立一些簡單的軌跡欄,以左右滑動並更改不同的值並重新處理影象。圍繞該cv2.imshow方法建立了一個小包裝程式,該方法可以平鋪顯示的視窗,因為我討厭總是重新放置它們,
嘗試不同的變數
我們可以載入不同的影象,並在影象處理中嘗試變數的不同變化,並確定最佳的組合。
自動化
在每個影象上測試不同的變數是上手的好方法,但是我們想要一種更好的方法來驗證是否更改了一個影象的變數是否會對其他任何影象產生影響。為此,我們想出了針對這些影象進行一些自動化測試的系統。
我拍攝了每個測試影象,並將它們放在資料夾中。然後,我用影象中期望的數字來命名每個檔案,並用小數點“ A”表示。應用程式可以載入該目錄中的每個影象並預測數字,然後將其與檔名中的數字進行比較以確定是否匹配。這使我們可以針對所有不同的影象快速嘗試更改。
自動測試輸出
更進一步,我建立了此指令碼的不同版本,該指令碼將嘗試對這組影象進行模糊,閾值等變數的幾乎每種組合,並找出最最佳化的變數集將具有最佳的效能。準確性。該指令碼在計算機上花費了相當長的時間才能執行,大約需要7個小時,但是最後提出了一組不同的變數,這些變數在我們手動測試時找不到。
結論
這是否是任何人實際上都會使用的功能尚待確定,但這在實現某些機器學習概念和使用OpenCV方面是一個有趣的練習。到目前為止,在我們的測試中,應用程式最大的問題是泵顯示屏上的眩光。根據泵上的照明和手機的角度,可能會導致某些掃描失效。
# train_model.pyimport osimport cv2import numpy as np# Current version of trainingversion = '_2_1'RESIZED_IMAGE_WIDTH = 20RESIZED_IMAGE_HEIGHT = 30int_classifications = []npa_flattened_images = np.empty((0, RESIZED_IMAGE_WIDTH * RESIZED_IMAGE_HEIGHT))npa_classifications = []trained_folder = 'knn'trained_json_path = 'training' + version + '.json'# Classify a digitdef train_file(file_path, char): global npa_flattened_images, int_classifications, npaRawFlattenedImages if char == 'dot': char = 'A' img = cv2.imread(file_path) imgGray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) imgThreshCopy = imgGray.copy() imgROIResized = cv2.resize(imgThreshCopy, (RESIZED_IMAGE_WIDTH, RESIZED_IMAGE_HEIGHT)) int_classifications.append(ord(char)) npaFlattenedImage = imgROIResized.reshape((1, RESIZED_IMAGE_WIDTH * RESIZED_IMAGE_HEIGHT)) npa_flattened_images = np.append(npa_flattened_images, npaFlattenedImage, 0)# Write out the dictionary as a stringdef serialize_dict(dict): output = '{' count = 1 proplen = len(dict) for key in dict: vals = dict[key] output += '"{}": {}'.format(key, vals) if count < proplen: output += ',' count += 1 output += '}' return output# Write out the image mat data, to the format OpenCV expectsdef serialize_mat(mat): type_id = 'opencv-matrix' dt = 'f' # TODO: Be smarter about the data type return '{{"type_id":"{}", "dt":"{}", "data":{}, "rows":{}, "cols": {}}}\n' \ .format(type_id, dt, serialize_array(mat), mat.shape[0], mat.shape[1])# Write out an array into a string for use in serializationdef serialize_array(arr): output = '[' for value in arr: for element in value: output += str(element) + ',' #'%.18e' % thing2 + ',\n' output = output[:-1] output += ']' return outputdef main(): training_dir = "training" for fname in os.listdir(training_dir): path = os.path.join(training_dir, fname) if os.path.isdir(path): print('Training ' + fname) tfiles = os.listdir(path) for tfile in tfiles: if not tfile.startswith('.'): train_file(path + '/' + tfile, fname) # Save the classifications for use in Python fltClassifications = np.array(int_classifications, np.float32) npaClassifications = fltClassifications.reshape((fltClassifications.size, 1)) np.savetxt(trained_folder + "/classifications" + version + ".txt", npaClassifications) np.savetxt(trained_folder + "/flattened_images" + version + ".txt", npa_flattened_images) # Save the classifications into a JSON file for use in C++/iOS data = { 'classifications': serialize_mat(npaClassifications), 'flattened_images': serialize_mat(npa_flattened_images) } with open(trained_folder + '/' + trained_json_path, 'w') as outfile: outfile.write(serialize_dict(data))if __name__ == "__main__": main()
程式碼連結:https://github.com/kazmiekr/GasPumpOCR