都說深度學習的出現極力地打壓着傳統機器學習算法的地位,作爲一個二刷機器學習經典算法的小夥伴告訴你:還真多半是這樣,咳,畢竟還是差距較大,深度學習處理真實世界多維度的問題更權威!不過,有的事情還是機器學習能做的,經典永不過時,下面我們來做一個實踐。
前兩篇博客比較倉促,今天我把全部整理了一遍,流程圖過程也全部展現,讓大家更好的明白流程
,篇幅字數上萬,建議細品!
我們來看看車牌檢測基本的識別流程:
我使用的是OpenCv自帶的SVM模型,由於SVM的突出表現,得到了更多官方的青睞,就誕生出了很多方便使用的封裝,正如Opencv的SVM封裝。
核心代碼介紹:
調用Opencv的SVM
OpenCV(開源計算機視覺庫:http://opencv.org)是一個bsd授權的開源庫,包含數百種計算機視覺算法。該文件描述了所謂的opencv2。x API,本質上是一個c++ API,而不是基於C的OpenCV 1。x API(自OpenCV 2.4發佈以來,C API被棄用,並且沒有使用“C”編譯器進行測試)
Python與C/ c++這樣的語言相比,Python要慢一些,但是Python可以很容易地使用C/ c++進行擴展,這允許我們用C/ c++編寫計算密集型代碼,並創建可以用作Python模塊的Python包裝器。這給了我們兩個好處:首先,代碼和原始的C/c++代碼一樣快(因爲Python在後臺工作的代碼實際上是c++代碼);其次,用Python編寫代碼比用C/c++更容易。
OpenCV支持多種編程語言,如c++、Python、Java等,可以在不同的平臺上使用,包括Windows、Linux、OS X、Android和iOS。基於CUDA和OpenCL的高速GPU操作接口也在積極開發中。
OpenCV-Python是OpenCV的Python API,是原始OpenCV c++實現的Python包裝器。結合了OpenCV c++ API和Python語言的最佳特性。
支持向量機(Support Vector Machine, SVM)的基本模型是在特徵空間上找到最佳的分離超平面使得訓練集上正負樣本間隔最大。SVM是用來解決二分類問題的有監督學習算法(實際上還有多分類),在引入了核方法之後SVM也可以用來解決非線性問題。
一般SVM有下面三種:
硬間隔支持向量機(線性可分支持向量機):當訓練數據線性可分時,可通過硬間隔最大化學得一個線性可分支持向量機。
軟間隔支持向量機:當訓練數據近似線性可分時,可通過軟間隔最大化學得一個線性支持向量機。
非線性支持向量機:當訓練數據線性不可分時,可通過核方法以及軟間隔最大化學得一個非線性支持向量機。
它在解決小樣本、非線性及高維模式識別中表現出許多特有的優勢,並能夠推廣應用到函數擬合等其他機器學習問題中。
支持向量機方法是建立在統計學習理論的VC維理論和結構風險最小原理基礎上的,根據有限的樣本信息在模式的複雜性(即對特定訓練樣本的學習精度,Accurary)和學習能力(即無錯誤地識別任意樣本的能力)之間尋求最佳折衷,以期獲得最好的推廣能力。
SVM實質上是一個類分類器,是一個能夠將不同類樣本在樣本空間分隔的超平面。
換句話說,給定一些標記(label)號的訓練樣本,SVM算法輸出一個最優化的分隔超平面。
class SVM(StatModel):
def __init__(self, C = 1, gamma = 0.5):
self.model = cv2.ml.SVM_create()
self.model.setGamma(gamma)
self.model.setC(C)
self.model.setKernel(cv2.ml.SVM_RBF)
self.model.setType(cv2.ml.SVM_C_SVC)
這邊的cv2.ml.SVM_create()生成一個SVM模型
setGamma(gamma),設置Gamma參數,demo中是0.5
setC(C), 設置懲罰項, 爲:1
setKernel(cv2.ml.SVM_RBF):設置核函數:RBF
setType(cv2.ml.SVM_C_SVC):設置SVM的模型類型:SVC是分類模型,SVR是迴歸模型
接下來繼續走:
訓練svm
1:定義
class SVM(StatModel):
def __init__(self, C = 1, gamma = 0.5):
self.model = cv2.ml.SVM_create()
self.model.setGamma(gamma)
self.model.setC(C)
self.model.setKernel(cv2.ml.SVM_RBF)
self.model.setType(cv2.ml.SVM_C_SVC)
#訓練svm
def train(self, samples, responses):
self.model.train(samples, cv2.ml.ROW_SAMPLE, responses)
2:
調用方法,並且喂數據:
def train_svm(self):
#識別英文字母和數字
self.model = SVM(C=1, gamma=0.5)
#識別中文
self.modelchinese = SVM(C=1, gamma=0.5)
if os.path.exists("svm.dat"):
self.model.load("svm.dat")
這邊已經訓練好模型就執行IF語句中的load操作(常用的調取持久化模型的方法),否則要是沒有模型就開始訓練(喂數據):
else:
chars_train = []
chars_label = []
for root, dirs, files in os.walk("train\\chars2"):
if len(os.path.basename(root)) > 1:
continue
root_int = ord(os.path.basename(root))
for filename in files:
filepath = os.path.join(root,filename)
digit_img = cv2.imread(filepath)
digit_img = cv2.cvtColor(digit_img, cv2.COLOR_BGR2GRAY)
chars_train.append(digit_img)
#chars_label.append(1)
chars_label.append(root_int)
chars_train = list(map(deskew, chars_train))
chars_train = preprocess_hog(chars_train)
#chars_train = chars_train.reshape(-1, 20, 20).astype(np.float32)
chars_label = np.array(chars_label)
print(chars_train.shape)
self.model.train(chars_train, chars_label)
調用的相對路徑是:"train\chars2是數據集:
這是從1-9,A到Z的數據集~
有點像MNIST手寫數字體有木有!
比如A:
這些是字母的訓練數據,同樣的還有我們車牌的省份簡寫:
來看一個:廣西的簡稱:桂
在此分成了SVC分別訓練省份簡稱和右邊的英文字符和數字
def train_svm(self):
#識別英文字母和數字
self.model = SVM(C=1, gamma=0.5)
#識別中文
self.modelchinese = SVM(C=1, gamma=0.5)
if os.path.exists("svm.dat"):
self.model.load("svm.dat")
else:
chars_train = []
chars_label = []
for root, dirs, files in os.walk("train\\chars2"):
if len(os.path.basename(root)) > 1:
continue
root_int = ord(os.path.basename(root))
for filename in files:
filepath = os.path.join(root,filename)
digit_img = cv2.imread(filepath)
digit_img = cv2.cvtColor(digit_img, cv2.COLOR_BGR2GRAY)
chars_train.append(digit_img)
#chars_label.append(1)
chars_label.append(root_int)
chars_train = list(map(deskew, chars_train))
chars_train = preprocess_hog(chars_train)
#chars_train = chars_train.reshape(-1, 20, 20).astype(np.float32)
chars_label = np.array(chars_label)
print(chars_train.shape)
self.model.train(chars_train, chars_label)
if os.path.exists("svmchinese.dat"):
self.modelchinese.load("svmchinese.dat")
else:
chars_train = []
chars_label = []
for root, dirs, files in os.walk("train\\charsChinese"):
if not os.path.basename(root).startswith("zh_"):
continue
pinyin = os.path.basename(root)
index = provinces.index(pinyin) + PROVINCE_START + 1 #1是拼音對應的漢字
for filename in files:
filepath = os.path.join(root,filename)
digit_img = cv2.imread(filepath)
digit_img = cv2.cvtColor(digit_img, cv2.COLOR_BGR2GRAY)
chars_train.append(digit_img)
#chars_label.append(1)
chars_label.append(index)
chars_train = list(map(deskew, chars_train))
chars_train = preprocess_hog(chars_train)
#chars_train = chars_train.reshape(-1, 20, 20).astype(np.float32)
chars_label = np.array(chars_label)
print(chars_train.shape)
self.modelchinese.train(chars_train, chars_label)
同上的,先判斷我們本地是否訓練好了,免得多此一舉,同樣的
解讀一下:
os.walk方法,主要用來遍歷一個目錄內各個子目錄和子文件。
os.walk(top, topdown=True, onerror=None, followlinks=False)
可以得到一個三元tupple(dirpath, dirnames, filenames), 我們這裏換名了: root, dirs, files
第一個爲起始路徑,第二個爲起始路徑下的文件夾,第三個是起始路徑下的文件。
dirpath 是一個string,代表目錄的路徑,
dirnames 是一個list,包含了dirpath下所有子目錄的名字。
filenames 是一個list,包含了非目錄文件的名字。
這些名字不包含路徑信息,如果需要得到全路徑,需要使用os.path.join(dirpath, name).
os.path.basename(),返回path最後的文件名。若path以/或\結尾,那麼就會返回空值。
標好了訓練集和標籤,就可以“喂”給分類器了:
self.modelchinese.train(chars_train, chars_label)
特徵提取:獲取車牌的可能位置(以下爲根據車牌顏色再定位,縮小邊緣非車牌邊界)
def accurate_place(self, card_img_hsv, limit1, limit2, color):
row_num, col_num = card_img_hsv.shape[:2]
xl = col_num
xr = 0
yh = 0
yl = row_num
#col_num_limit = self.cfg["col_num_limit"]
row_num_limit = self.cfg["row_num_limit"]
col_num_limit = col_num * 0.8 if color != "green" else col_num * 0.5#綠色有漸變
for i in range(row_num):
count = 0
for j in range(col_num):
H = card_img_hsv.item(i, j, 0)
S = card_img_hsv.item(i, j, 1)
V = card_img_hsv.item(i, j, 2)
if limit1 < H <= limit2 and 34 < S and 46 < V:
count += 1
if count > col_num_limit:
if yl > i:
yl = i
if yh < i:
yh = i
for j in range(col_num):
count = 0
for i in range(row_num):
H = card_img_hsv.item(i, j, 0)
S = card_img_hsv.item(i, j, 1)
V = card_img_hsv.item(i, j, 2)
if limit1 < H <= limit2 and 34 < S and 46 < V:
count += 1
if count > row_num - row_num_limit:
if xl > j:
xl = j
if xr < j:
xr = j
print('size111', xl, xr, yh, yl)
return xl, xr, yh, yl
接下來:測試
def predict(self, car_pic):
if type(car_pic) == type(""):
img = imreadex(car_pic)
else:
img = car_pic
pic_hight, pic_width = img.shape[:2]
if pic_width > MAX_WIDTH:
resize_rate = MAX_WIDTH / pic_width
img = cv2.resize(img, (MAX_WIDTH, int(pic_hight*resize_rate)), interpolation=cv2.INTER_AREA)
print('tuxing', img.shape[0],img.shape[1])
大致是做了以下步驟:
傳入車子圖片
判定圖片完整性
處理或重讀圖片
使用img.shape方式獲取圖片的高和寬
超出自定義最大高寬,就resize操作,接下來 邊緣計算:
blur = self.cfg["blur"]
#高斯去噪
if blur > 0:
img = cv2.GaussianBlur(img, (blur, blur), 0)#圖片分辨率調整
oldimg = img
img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
#equ = cv2.equalizeHist(img)
#img = np.hstack((img, equ))
#去掉圖像中不會是車牌的區域
kernel = np.ones((20, 20), np.uint8)
img_opening = cv2.morphologyEx(img, cv2.MORPH_OPEN, kernel)
img_opening = cv2.addWeighted(img, 1, img_opening, -1, 0);
#找到圖像邊緣
ret, img_thresh = cv2.threshold(img_opening, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
img_edge = cv2.Canny(img_thresh, 100, 200)
#使用開運算和閉運算讓圖像邊緣成爲一個整體
kernel = np.ones((self.cfg["morphologyr"], self.cfg["morphologyc"]), np.uint8)
img_edge1 = cv2.morphologyEx(img_edge, cv2.MORPH_CLOSE, kernel)
img_edge2 = cv2.morphologyEx(img_edge1, cv2.MORPH_OPEN, kernel)
高斯去噪:
高斯濾波器是一種線性濾波器,能夠有效的抑制噪聲,平滑圖像。其作用原理和均值濾波器類似,都是取濾波器窗口內的像素的均值作爲輸出。其窗口模板的係數和均值濾波器不同,均值濾波器的模板係數都是相同的爲1;而高斯濾波器的模板係數,則隨着距離模板中心的增大而係數減小。所以,高斯濾波器相比於均值濾波器對圖像個模糊程度較小。
高斯濾波的基本思想是: 圖像上的每一個像素點的值,都由其本身和鄰域內其他像素點的值經過加權平均後得到。其具體操作是,用一個核(又稱爲卷積核、掩模、矩陣)掃描圖像中每一個像素點,將鄰域內各個像素值與對應位置的權值相稱並求和。從數學的角度來看,高斯濾波的過程是圖像與高斯正態分佈做卷積操作。
注意: 高斯濾波是將二維高斯正態分佈放在圖像矩陣上做卷積運算。考慮的是鄰域內像素值的空間距離關係,因此對彩色圖像處理時應分通道進行操作,也就是說操作的圖像原矩陣時用單通道數據,最後合併爲彩色圖像。
效果如圖:右邊是高斯模糊降噪處理:
圖片分辨率調整
灰度處理:
灰度是指把白色與黑色之間按對數關係分爲若干個等級,一般我們將灰度分爲256階(0-255)。用灰度表示的圖像就叫做灰度圖。
圖像裁剪(去掉圖像中不會是車牌的區域)
使用開運算和閉運算讓圖像邊緣成爲一個整體:
圖像開運算與閉運算與膨脹和腐蝕運算有關,由膨脹和腐蝕兩個運算的複合與集合操作(並、交、補等)組合成的運算構成。開運算與閉運算依據腐蝕和膨脹演變而來。
1)開運算:先對圖像腐蝕後膨脹。
A○S= (AΘS)⊕ S
作用:用來消除小的物體,平滑形狀邊界,並且不改變其面積。可以去除小顆粒噪聲,斷開物體之間的粘連。
2)閉運算:先對圖像膨脹後腐蝕
A●S= (A⊕S)Θ S
作用:用來填充物體內的小空洞,連接鄰近的物體,連接斷開的輪廓線,平滑其邊界的同時不改變面積。
融合開運算和閉運算:
二值化操作找到圖像邊緣:
上面這兩步是視覺的常用的基本操作,經常用於數據(圖像)預處理
接下來繼續車牌檢測~
查找圖像邊緣整體形成的矩形區域,可能有很多,車牌就在其中一個矩形區域中:
例如:
try:
contours, hierarchy = cv2.findContours(img_edge2, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
except ValueError:
image, contours, hierarchy = cv2.findContours(img_edge2, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
contours = [cnt for cnt in contours if cv2.contourArea(cnt) > Min_Area]
需要注意的是cv2.findContours()函數接受的參數爲二值圖,即黑白的(不是灰度圖),所以讀取的圖像要先轉成灰度的,再轉成二值圖!
結果篩選(原因是上述的多可能性情況):
car_contours = []
for cnt in contours:
rect = cv2.minAreaRect(cnt)
area_width, area_height = rect[1]
if area_width < area_height:
area_width, area_height = area_height, area_width
wh_ratio = area_width / area_height
#print(wh_ratio)
#要求矩形區域長寬比在2到5.5之間,2到5.5是車牌的長寬比,其餘的矩形排除
if wh_ratio > 2 and wh_ratio < 5.5:
car_contours.append(rect)
box = cv2.boxPoints(rect)
box = np.int0(box)
接下來:
矩形區域可能是傾斜的矩形,需要矯正,以便使用顏色定位
for rect in car_contours:
if rect[2] > -1 and rect[2] < 1:#創造角度,使得左、高、右、低拿到正確的值
angle = 1
else:
angle = rect[2]
rect = (rect[0], (rect[1][0]+5, rect[1][1]+5), angle)#擴大範圍,避免車牌邊緣被排除
box = cv2.boxPoints(rect)
heigth_point = right_point = [0, 0]
left_point = low_point = [pic_width, pic_hight]
for point in box:
if left_point[0] > point[0]:
left_point = point
if low_point[1] > point[1]:
low_point = point
if heigth_point[1] < point[1]:
heigth_point = point
if right_point[0] < point[0]:
right_point = point
if left_point[1] <= right_point[1]:#正角度
new_right_point = [right_point[0], heigth_point[1]]
pts2 = np.float32([left_point, heigth_point, new_right_point])#字符只是高度需要改變
pts1 = np.float32([left_point, heigth_point, right_point])
M = cv2.getAffineTransform(pts1, pts2)
dst = cv2.warpAffine(oldimg, M, (pic_width, pic_hight))
point_limit(new_right_point)
point_limit(heigth_point)
point_limit(left_point)
card_img = dst[int(left_point[1]):int(heigth_point[1]), int(left_point[0]):int(new_right_point[0])]
card_imgs.append(card_img)
elif left_point[1] > right_point[1]:#負角度
new_left_point = [left_point[0], heigth_point[1]]
pts2 = np.float32([new_left_point, heigth_point, right_point])#字符只是高度需要改變
pts1 = np.float32([left_point, heigth_point, right_point])
M = cv2.getAffineTransform(pts1, pts2)
dst = cv2.warpAffine(oldimg, M, (pic_width, pic_hight))
point_limit(right_point)
point_limit(heigth_point)
point_limit(new_left_point)
card_img = dst[int(right_point[1]):int(heigth_point[1]), int(new_left_point[0]):int(right_point[0])]
card_imgs.append(card_img)
開始使用顏色定位,排除不是車牌的矩形,目前識別藍、綠、黃車牌
colors = []
for card_index,card_img in enumerate(card_imgs):
green = yello = blue = black = white = 0
card_img_hsv = cv2.cvtColor(card_img, cv2.COLOR_BGR2HSV)
#有轉換失敗的可能,原因來自於上面矯正矩形出錯
if card_img_hsv is None:
continue
row_num, col_num= card_img_hsv.shape[:2]
card_img_count = row_num * col_num
for i in range(row_num):
for j in range(col_num):
H = card_img_hsv.item(i, j, 0)
S = card_img_hsv.item(i, j, 1)
V = card_img_hsv.item(i, j, 2)
if 11 < H <= 34 and S > 34:#圖片分辨率調整
yello += 1
elif 35 < H <= 99 and S > 34:#圖片分辨率調整
green += 1
elif 99 < H <= 124 and S > 34:#圖片分辨率調整
blue += 1
if 0 < H <180 and 0 < S < 255 and 0 < V < 46:
black += 1
elif 0 < H <180 and 0 < S < 43 and 221 < V < 225:
white += 1
color = "no"
limit1 = limit2 = 0
if yello*2 >= card_img_count:
color = "yello"
limit1 = 11
limit2 = 34#有的圖片有色偏偏綠
elif green*2 >= card_img_count:
color = "green"
limit1 = 35
limit2 = 99
elif blue*2 >= card_img_count:
color = "blue"
limit1 = 100
limit2 = 124#有的圖片有色偏偏紫
elif black + white >= card_img_count*0.7:#TODO
color = "bw"
print(color)
colors.append(color)
print(blue, green, yello, black, white, card_img_count)
cv2.imshow("color", card_img)
cv2.waitKey(1110)
if limit1 == 0:
continue
#以上爲確定車牌顏色
#以下爲根據車牌顏色再定位,縮小邊緣非車牌邊界
xl, xr, yh, yl = self.accurate_place(card_img_hsv, limit1, limit2, color)
if yl == yh and xl == xr:
continue
need_accurate = False
if yl >= yh:
yl = 0
yh = row_num
need_accurate = True
if xl >= xr:
xl = 0
xr = col_num
need_accurate = True
card_imgs[card_index] = card_img[yl:yh, xl:xr] if color != "green" or yl < (yh-yl)//4 else card_img[yl-(yh-yl)//4:yh, xl:xr]
if need_accurate:#可能x或y方向未縮小,需要再試一次
card_img = card_imgs[card_index]
card_img_hsv = cv2.cvtColor(card_img, cv2.COLOR_BGR2HSV)
xl, xr, yh, yl = self.accurate_place(card_img_hsv, limit1, limit2, color)
print('size', xl,xr,yh,yl)
if yl == yh and xl == xr:
continue
if yl >= yh:
yl = 0
yh = row_num
if xl >= xr:
xl = 0
xr = col_num
card_imgs[card_index] = card_img[yl:yh, xl:xr] if color != "green" or yl < (yh-yl)//4 else card_img[yl-(yh-yl)//4:yh, xl:xr]
核心部分來了,詳解一下:
predict_result = []
roi = None
card_color = None
for i, color in enumerate(colors):
if color in ("blue", "yello", "green"):
card_img = card_imgs[i]
gray_img = cv2.cvtColor(card_img, cv2.COLOR_BGR2GRAY)
#黃、綠車牌字符比背景暗、與藍車牌剛好相反,所以黃、綠車牌需要反向
if color == "green" or color == "yello":
gray_img = cv2.bitwise_not(gray_img)
ret, gray_img = cv2.threshold(gray_img, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
#查找水平直方圖波峯
x_histogram = np.sum(gray_img, axis=1)
x_min = np.min(x_histogram)
x_average = np.sum(x_histogram)/x_histogram.shape[0]
x_threshold = (x_min + x_average)/2
wave_peaks = find_waves(x_threshold, x_histogram)
if len(wave_peaks) == 0:
print("peak less 0:")
continue
#認爲水平方向,最大的波峯爲車牌區域
wave = max(wave_peaks, key=lambda x:x[1]-x[0])
gray_img = gray_img[wave[0]:wave[1]]
#查找垂直直方圖波峯
row_num, col_num= gray_img.shape[:2]
#去掉車牌上下邊緣1個像素,避免白邊影響閾值判斷
gray_img = gray_img[1:row_num-1]
y_histogram = np.sum(gray_img, axis=0)
y_min = np.min(y_histogram)
y_average = np.sum(y_histogram)/y_histogram.shape[0]
y_threshold = (y_min + y_average)/5#U和0要求閾值偏小,否則U和0會被分成兩半
wave_peaks = find_waves(y_threshold, y_histogram)
#for wave in wave_peaks:
# cv2.line(card_img, pt1=(wave[0], 5), pt2=(wave[1], 5), color=(0, 0, 255), thickness=2)
#車牌字符數應大於6
if len(wave_peaks) <= 6:
print("peak less 1:", len(wave_peaks))
continue
wave = max(wave_peaks, key=lambda x:x[1]-x[0])
max_wave_dis = wave[1] - wave[0]
#判斷是否是左側車牌邊緣
if wave_peaks[0][1] - wave_peaks[0][0] < max_wave_dis/3 and wave_peaks[0][0] == 0:
wave_peaks.pop(0)
#組合分離漢字
cur_dis = 0
for i,wave in enumerate(wave_peaks):
if wave[1] - wave[0] + cur_dis > max_wave_dis * 0.6:
break
else:
cur_dis += wave[1] - wave[0]
if i > 0:
wave = (wave_peaks[0][0], wave_peaks[i][1])
wave_peaks = wave_peaks[i+1:]
wave_peaks.insert(0, wave)
#去除車牌上的分隔點
point = wave_peaks[2]
if point[1] - point[0] < max_wave_dis/3:
point_img = gray_img[:,point[0]:point[1]]
if np.mean(point_img) < 255/5:
wave_peaks.pop(2)
if len(wave_peaks) <= 6:
print("peak less 2:", len(wave_peaks))
continue
part_cards = seperate_card(gray_img, wave_peaks)
for i, part_card in enumerate(part_cards):
#可能是固定車牌的鉚釘
if np.mean(part_card) < 255/5:
print("a point")
continue
part_card_old = part_card
w = abs(part_card.shape[1] - SZ)//2
part_card = cv2.copyMakeBorder(part_card, 0, 0, w, w, cv2.BORDER_CONSTANT, value = [0,0,0])
part_card = cv2.resize(part_card, (SZ, SZ), interpolation=cv2.INTER_AREA)
#part_card = deskew(part_card)
part_card = preprocess_hog([part_card])
if i == 0:
resp = self.modelchinese.predict(part_card)
charactor = provinces[int(resp[0]) - PROVINCE_START]
else:
resp = self.model.predict(part_card)
charactor = chr(resp[0])
#判斷最後一個數是否是車牌邊緣,假設車牌邊緣被認爲是1
if charactor == "1" and i == len(part_cards)-1:
if part_card_old.shape[0]/part_card_old.shape[1] >= 7:#1太細,認爲是邊緣
continue
predict_result.append(charactor)
roi = card_img
card_color = color
break
return predict_result, roi, card_color#識別到的字符、定位的車牌圖像、車牌顏色
部分代碼有註釋,大致說說:
這是識別車牌中的字符
gray_img = cv2.bitwise_not(gray_img)
這個是掩膜方法,我們後續再介紹吧, 大致思路就是把原圖中要放logo的區域摳出來,再把logo放進去就行了。
根據設定的閾值和圖片直方圖,找出波峯,用於分隔字符
def find_waves(threshold, histogram):
up_point = -1#上升點
is_peak = False
if histogram[0] > threshold:
up_point = 0
is_peak = True
wave_peaks = []
for i,x in enumerate(histogram):
if is_peak and x < threshold:
if i - up_point > 2:
is_peak = False
wave_peaks.append((up_point, i))
elif not is_peak and x >= threshold:
is_peak = True
up_point = i
if is_peak and up_point != -1 and i - up_point > 4:
wave_peaks.append((up_point, i))
return wave_peaks
根據找出的波峯,分隔圖片,從而得到逐個字符圖片:
def seperate_card(img, waves):
part_cards = []
for wave in waves:
part_cards.append(img[:, wave[0]:wave[1]])
return part_cards
def deskew(img):
m = cv2.moments(img)
if abs(m['mu02']) < 1e-2:
return img.copy()
skew = m['mu11']/m['mu02']
M = np.float32([[1, skew, -0.5*SZ*skew], [0, 1, 0]])
img = cv2.warpAffine(img, M, (SZ, SZ), flags=cv2.WARP_INVERSE_MAP | cv2.INTER_LINEAR)
return img
最後,結果篩選:
#車牌字符數應大於6
if len(wave_peaks) <= 6:
print("peak less 1:", len(wave_peaks))
continue
wave = max(wave_peaks, key=lambda x:x[1]-x[0])
max_wave_dis = wave[1] - wave[0]
#判斷是否是左側車牌邊緣
if wave_peaks[0][1] - wave_peaks[0][0] < max_wave_dis/3 and wave_peaks[0][0] == 0:
wave_peaks.pop(0)
#組合分離漢字
cur_dis = 0
for i,wave in enumerate(wave_peaks):
if wave[1] - wave[0] + cur_dis > max_wave_dis * 0.6:
break
else:
cur_dis += wave[1] - wave[0]
if i > 0:
wave = (wave_peaks[0][0], wave_peaks[i][1])
wave_peaks = wave_peaks[i+1:]
wave_peaks.insert(0, wave)
#去除車牌上的分隔點
point = wave_peaks[2]
if point[1] - point[0] < max_wave_dis/3:
point_img = gray_img[:,point[0]:point[1]]
if np.mean(point_img) < 255/5:
wave_peaks.pop(2)
if len(wave_peaks) <= 6:
print("peak less 2:", len(wave_peaks))
continue
part_cards = seperate_card(gray_img, wave_peaks)
for i, part_card in enumerate(part_cards):
#可能是固定車牌的鉚釘
if np.mean(part_card) < 255/5:
print("a point")
continue
part_card_old = part_card
w = abs(part_card.shape[1] - SZ)//2
part_card = cv2.copyMakeBorder(part_card, 0, 0, w, w, cv2.BORDER_CONSTANT, value = [0,0,0])
part_card = cv2.resize(part_card, (SZ, SZ), interpolation=cv2.INTER_AREA)
#part_card = deskew(part_card)
part_card = preprocess_hog([part_card])
if i == 0:
resp = self.modelchinese.predict(part_card)
charactor = provinces[int(resp[0]) - PROVINCE_START]
else:
resp = self.model.predict(part_card)
charactor = chr(resp[0])
#判斷最後一個數是否是車牌邊緣,假設車牌邊緣被認爲是1
if charactor == "1" and i == len(part_cards)-1:
if part_card_old.shape[0]/part_card_old.shape[1] >= 7:#1太細,認爲是邊緣
continue
predict_result.append(charactor)
roi = card_img
card_color = color
break
return predict_result, roi, card_color
返回識別到的字符、定位的車牌圖像、車牌顏色:
main函數:
if __name__ == '__main__':
c = CardPredictor()
c.train_svm()
r, roi, color = c.predict("test//car7.jpg")
print(r, roi.shape[0],roi.shape[1],roi.shape[2])
img = cv2.imread("test//car7.jpg")
img = cv2.resize(img,(480,640),interpolation=cv2.INTER_LINEAR)
r = ','.join(r)
r = r.replace(',', '')
print(r)
from PIL import Image, ImageDraw, ImageFont
cv2img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) # cv2和PIL中顏色的hex碼的儲存順序不同
pilimg = Image.fromarray(cv2img)
# PIL圖片上打印漢字
draw = ImageDraw.Draw(pilimg) # 圖片上打印
font = ImageFont.truetype("simhei.ttf", 30, encoding="utf-8") # 參數1:字體文件路徑,參數2:字體大小
draw.text((0, 0), r, (255, 0, 0), font=font) # 參數1:打印座標,參數2:文本,參數3:字體顏色,參數4:字體
# PIL圖片轉cv2 圖片
cv2charimg = cv2.cvtColor(np.array(pilimg), cv2.COLOR_RGB2BGR)
# cv2.imshow("圖片", cv2charimg) # 漢字窗口標題顯示亂碼
cv2.imshow("photo", cv2charimg)
cv2.waitKey(0)
cv2.destroyAllWindows()
小結一下:
OPENCV的SVM的SVC訓練模型
OpenCv進行圖像採集/控制攝像頭
圖像預處理(二值化操作,邊緣計算等)
定位車牌位置,並正放置處理——>確定車牌顏色
根據車牌顏色再定位,縮小邊緣非車牌邊界
以下爲識別車牌中的字符
返回結果
最後ptrdict返回識別到的字符、定位的車牌圖像、車牌顏色
結果顯示,並使用PIL方法顯示中文
最後使用UI的庫:tkinter繪製一個簡易界面:
新能源的綠牌汽車車牌檢測成功
黃車車牌檢測成功
最後說說識別準確率,我在數百張驗證集中測試,準確率大致在75左右!這是使用經典機器學習算法帶來的缺點,不過他的優點是十分迅速,在極端情況下只需要0.3秒檢測成功,但是他容易欠擬合,泛化能力不強,這也是需要改進的,比如數據增強,加大訓練集等!繼續進步
打字不易,看到最後,別忘記了關注一下~,另外歡迎大家進入我的機器&深度學習,大佬雲集,大家一起進步!
上海第二工業大學 18智能A1 周小夏(CV調包俠)