熱線電話:0755-23712116
郵箱:contact@shuangyi-tech.com
地址:深圳市寶安區(qū)沙井街道后亭茅洲山工業(yè)園工業(yè)大廈全至科技創(chuàng)新園科創(chuàng)大廈2層2A
綜述
2012年iOS應(yīng)用商店中發(fā)布了一個名為FuelMate的Gas跟蹤應(yīng)用。小伙伴們可以使用該應(yīng)用程序跟蹤汽油行駛里程,以及有一些有趣的功能,例如Apple Watch應(yīng)用程序、vin.li集成以及基于趨勢mpg的視覺效果。
燃料伴侶
對此我們有一個新想法,該如何添加一個功能幫助我們在泵中掃描燃油,并在應(yīng)用程序中輸入燃油信息?讓我們深入研究如何實現(xiàn)這一目標(biāo)。
技術(shù)
對于這個項目的我們首先應(yīng)該編寫一個簡單的Python應(yīng)用程序以拍攝汽油泵的圖像,然后嘗試從中讀取數(shù)字。OpenCV是用于計算機視覺應(yīng)用程序的流行的跨平臺庫。它包括各種圖像處理實用程序以及某些機器學(xué)習(xí)功能。除此之外我們希望可以先使用Python對其進行原型設(shè)計,然后將處理代碼轉(zhuǎn)換為C ++以在iOS應(yīng)用程序上運行。
目標(biāo)
我們首先要考慮以下兩個問題:
1.我們可以從圖像中分離出數(shù)字嗎?
2.我們可以確定圖像代表哪個數(shù)字嗎?
數(shù)字分割
如何確定圖像中的數(shù)字有多種方法,但是我提出了使用簡單的圖像閾值法來嘗試查找數(shù)字的方法。
圖像閾值化的基本思想是將圖像轉(zhuǎn)換為灰度,然后說灰度值小于某個常數(shù)的任何像素,則該像素為一個值,否則為另一個。最后,您得到的二進制圖像只有兩種顏色,在大多數(shù)情況下只是黑白圖像。
這個概念在OCR應(yīng)用中非常有效,但是主要問題是決定對該閾值使用什么。我們可以選擇一些常量,也可以使用OpenCV選擇其他一些選項。我們可以使用自適應(yīng)閾值而不是使用常數(shù),這將使用圖像的較小部分并確定要使用的不同閾值。這在具有不同照明情況的應(yīng)用中特別有用,特別是在掃描氣泵中。
將圖像設(shè)置為閾值后,可以使用OpenCV的findContours方法查找圖像中連接了白色像素部分的區(qū)域。繪制輪廓后,便可以裁剪出這些區(qū)域并確定它們是否可能是數(shù)字以及它是什么數(shù)字。
基本圖像處理流程
這是我在測試圖像處理中使用的原始圖像。它有一些眩光點,但是圖像相當(dāng)干凈。讓我們逐步完成獲取此源圖像的過程,并嘗試將其分解為單個數(shù)字。
原始圖片
影像準(zhǔn)備
在開始圖像處理流程之前,我們決定先調(diào)整一些圖像屬性,然后再繼續(xù)。這有點試驗和錯誤,但注意到,當(dāng)我們調(diào)整圖像的曝光度時,可以獲得更好的結(jié)果。下面是使用Python調(diào)整后的圖像,相當(dāng)于曝光(阿爾法)的圖像cv::Mat::convertTo這是剛剛在圖像墊乘法操作cv2.multiply(some_img, np.array([some_alpha]),
調(diào)整曝光
灰階
將圖像轉(zhuǎn)換為灰度。
轉(zhuǎn)換為灰度
模糊
模糊圖像以減少噪點。我們嘗試了許多不同的模糊選項,但僅用輕微的模糊就找到了最佳結(jié)果。
稍微模糊
閾值圖像轉(zhuǎn)換為黑白圖像
在下圖中,使用cv2.adaptiveThreshold帶有cv2.ADAPTIVE_THRES_GAUSSIAN_C選項的方法。此方法采用兩個參數(shù),塊大小和要調(diào)整的常數(shù)。確定這兩者需要一些試驗和錯誤,更多有關(guān)優(yōu)化部分的內(nèi)容。
閾值為黑/白
填補空白
由于大多數(shù)燃油泵都使用某種7段LCD顯示屏,因此數(shù)字中存在一些細微的間隙,無法使用輪廓繪制方法,因此我們需要使這些段看起來相連。在這種情況下,我們將轉(zhuǎn)到erode圖像來彌補這些差距。由于大家可能希望使用,所以這似乎向后看,dilate但是這些方法通常適用于圖像的白色部分。在我們的案例中,我們正在“侵蝕”白色背景以使數(shù)字看起來更大。
侵蝕出來的數(shù)字
反轉(zhuǎn)圖像
在嘗試在圖像中查找輪廓之前,我們需要反轉(zhuǎn)顏色,因為該findContours方法將找到白色的連接部分,而當(dāng)前的數(shù)字是黑色。
顏色反轉(zhuǎn)
在圖像上找到輪廓
下圖顯示了我們的原始圖像,該圖像在上圖的每個輪廓上都有包圍框。大家可以看到它找到了數(shù)字,但也找到了一堆不是數(shù)字的東西,因此我們需要將它們過濾掉。
紅色框顯示所有找到的輪廓
輪廓過濾
1.現(xiàn)在我們有了許多輪廓,我們需要找出我們關(guān)心的輪廓。瀏覽了一堆氣泵的顯示和場景后,使用一套適用于輪廓的快速規(guī)則。
2.收集所有我們將分類為潛在小數(shù)的正方形輪廓。
3.扔掉任何不是正方形或高矩形的東西。
4.使輪廓與某些長寬比匹配。LCD顯示屏中的十個數(shù)字中有九個數(shù)字的長寬比類似于下面的藍色框高光之一。該規(guī)則的例外是數(shù)字“ 1”,其長寬比略有不同。通過使用一些樣本輪廓,我將0–9!1方面確定為0.6,將1方面確定為0.3。它將使用這些比率和+/-緩沖區(qū)來確定輪廓是否是我們想要的東西,并收集這些輪廓。
5.對潛在數(shù)字應(yīng)用一組附加規(guī)則,在這里我們將確定輪廓邊界是否偏離所有其他潛在數(shù)字的平均高度或垂直位置。由于數(shù)字的大小應(yīng)相同,并且在相同的Y上對齊,因此我們可以丟棄它認(rèn)為是數(shù)字的任何輪廓,但不能像其他輪廓那樣將其對齊和調(diào)整大小。
藍色矩形顯示我們的數(shù)字/十進制,紅色被忽略
預(yù)測
有兩個等高線輪廓,一個帶潛在位數(shù),一個帶潛在小數(shù)位,我們可以使用這些輪廓邊界裁剪圖像,并將其輸入經(jīng)過訓(xùn)練的系統(tǒng)中以預(yù)測其值。有關(guān)此過程的更多信息,請參見“數(shù)字培訓(xùn)”部分。
查找小數(shù)
在圖像中查找小數(shù)點是要解決的另一個問題。由于它很小,有時會連接到它旁邊的手指,因此使用我們在手指上使用的方法來確定它似乎有問題。當(dāng)我們過濾輪廓時,我們收集了可能是十進制的正方形輪廓。從上一步獲得經(jīng)過驗證的數(shù)字輪廓之后,我們將找到數(shù)字的最左x位置和最右x位置,以確定我們期望的小數(shù)位數(shù)。然后,我們將遍歷那些潛在的小數(shù),確定它是否在該空間以及該空間的下半部分,并將其分類為小數(shù)。找到小數(shù)點后,我們可以將其插入到我們上面預(yù)測的數(shù)字字符串中。
只在黃色部分中查找小數(shù)
數(shù)字培訓(xùn)
在機器學(xué)習(xí)的世界中,解決OCR問題是一個分類問題。我們建立了一組訓(xùn)練有素的數(shù)據(jù),例如圖像處理中的數(shù)字,將它們分類為某種東西,然后使用該數(shù)據(jù)來匹配任何新圖像。一旦基本的圖像隔離功能開始工作,我就創(chuàng)建了一個腳本,該腳本可以遍歷圖像文件夾,運行數(shù)字隔離代碼,然后將裁剪的數(shù)字保存到新文件夾中供我查看。運行完之后,我會有一個未經(jīng)訓(xùn)練的數(shù)字文件夾,然后可以用來訓(xùn)練系統(tǒng)。
由于OpenCV已經(jīng)包含了k近鄰(k-NN)實現(xiàn),因此無需引入任何其他庫。為了進行訓(xùn)練,我們?yōu)g覽了數(shù)字作物的文件夾,然后將其放入標(biāo)有0–9的新文件夾中,因此每個文件夾中都有一個數(shù)字的不同版本的集合。我們沒有大量的這些圖像,但是有足夠的證據(jù)來證明這是可行的。由于這些數(shù)字是相當(dāng)標(biāo)準(zhǔn)的,我認(rèn)為我不需要大量訓(xùn)練有素的圖像就可以相當(dāng)準(zhǔn)確。
k-NN工作原理的基礎(chǔ)是,我們將以黑白方式加載每個圖像,將該圖像存儲在每個像素處于打開或關(guān)閉狀態(tài)的數(shù)組中,然后將這些打開/關(guān)閉像素與特定的數(shù)字相關(guān)聯(lián)。然后,當(dāng)我們要預(yù)測一個新圖像時,它將找出哪個訓(xùn)練圖像與這些像素最匹配,然后向我們返回最接近的值。
整理好數(shù)字后,將創(chuàng)建一個新的腳本,該腳本將遍歷這些文件夾,獲取每個圖像并將該圖像與數(shù)字關(guān)聯(lián)。到目前為止,在大多數(shù)代碼中,一般的圖像處理概念在Python和C ++中都應(yīng)用相同,但是在這里會有細微的差別。
在大多數(shù)此類應(yīng)用程序的Python示例中,分類被寫入兩個文件,一個包含分類,另一個包含該分類的圖像內(nèi)容。通常使用NumPy和標(biāo)準(zhǔn)文本文件完成此操作。但是,由于我想在iOS應(yīng)用程序上重用該系統(tǒng),因此我需要想出一種可以擁有跨平臺分類文件的方式。當(dāng)時,我什么都找不到,因此最終編寫了一個快速實用程序,該實用程序?qū)腜ython中獲取分類數(shù)據(jù)并將其序列化為JSON文件,我可以在OpenCV的FileStorage系統(tǒng)的C ++端使用它。這不漂亮,但是我寫了一個簡單的MatPython中的序列化方法,它將為OpenCV創(chuàng)建合適的結(jié)構(gòu)以在iOS端讀取。現(xiàn)在,當(dāng)我訓(xùn)練數(shù)字時,我將獲得NumPy文件供我的Python測試使用,然后獲取一個JSON文檔,我可以將其拖到我的iOS應(yīng)用程序中。您可以在此處看到該代碼。
優(yōu)化
一旦確定了數(shù)字隔離和預(yù)測的兩個目標(biāo),就需要對算法進行優(yōu)化,以預(yù)測泵的新圖像上的數(shù)字。
在優(yōu)化的初始階段,創(chuàng)建了一個簡單的Playground應(yīng)用程序,其中使用了OpenCV提供的一些簡單的UI組件。使用這些組件,可以創(chuàng)建一些簡單的軌跡欄,以左右滑動并更改不同的值并重新處理圖像。圍繞該cv2.imshow方法創(chuàng)建了一個小包裝程序,該方法可以平鋪顯示的窗口,因為我討厭總是重新放置它們,
嘗試不同的變量
我們可以加載不同的圖像,并在圖像處理中嘗試變量的不同變化,并確定最佳的組合。
自動化
在每個圖像上測試不同的變量是上手的好方法,但是我們想要一種更好的方法來驗證是否更改了一個圖像的變量是否會對其他任何圖像產(chǎn)生影響。為此,我們想出了針對這些圖像進行一些自動化測試的系統(tǒng)。
我拍攝了每個測試圖像,并將它們放在文件夾中。然后,我用圖像中期望的數(shù)字來命名每個文件,并用小數(shù)點“ A”表示。應(yīng)用程序可以加載該目錄中的每個圖像并預(yù)測數(shù)字,然后將其與文件名中的數(shù)字進行比較以確定是否匹配。這使我們可以針對所有不同的圖像快速嘗試更改。
自動測試輸出
更進一步,我創(chuàng)建了此腳本的不同版本,該腳本將嘗試對這組圖像進行模糊,閾值等變量的幾乎每種組合,并找出最優(yōu)化的變量集將具有最佳的性能。準(zhǔn)確性。該腳本在計算機上花費了相當(dāng)長的時間才能運行,大約需要7個小時,但是最后提出了一組不同的變量,這些變量在我們手動測試時找不到。
結(jié)論
這是否是任何人實際上都會使用的功能尚待確定,但這在實現(xiàn)某些機器學(xué)習(xí)概念和使用OpenCV方面是一個有趣的練習(xí)。到目前為止,在我們的測試中,應(yīng)用程序最大的問題是泵顯示屏上的眩光。根據(jù)泵上的照明和手機的角度,可能會導(dǎo)致某些掃描失效。
# train_model.py
import os
import cv2
import numpy as np
# Current version of training
version = '_2_1'
RESIZED_IMAGE_WIDTH = 20
RESIZED_IMAGE_HEIGHT = 30
int_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 digit
def 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 string
def 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