一、前言及思路簡析
目前車牌識別系統在各小區門口隨處可見,識別效果貌似都還可以。查閱資料后,發現整個過程又可以細化為車牌定位、畸變校正、車牌分割和內容識別四部分。本篇隨筆主要介紹車牌定位及畸變校正兩部分,在python環境下通過opencv實現。
1.1 車牌定位
目前主流的車牌定位方法從大的方面來說可以分為兩類:一種是基於車牌的背景顏色特征;另一種基於車牌的輪廓形狀特征。基於顏色特征的又可分為兩類:一種在RGB空間識別,另一種在HSV空間識別。經測試后發現,單獨使用任何一種方法,效果均不太理想。目前比較普遍的做法是幾種定位方法同時使用,或用一種識別,另一種驗證。本文主要通過顏色特征對車牌進行定位,以HSV空間的H分量為主,以RGB空間的R分量和B分量為輔,后續再用車牌的長寬比例排除干擾。
1.2 畸變校正
在車牌的圖像采集過程中,相機鏡頭通常都不是垂直於車牌的,所以待識別圖像中車牌或多或少都會有一定程度的畸變,這給后續的車牌內容識別帶來了一定的困難。因此需要對車牌進行畸變校正,消除畸變帶來的不利影響。
二、代碼實現
2.1 車牌定位
2.1.1 通過顏色特征選定可疑區域
取了不同光照環境下車牌的圖像,截取其背景顏色,利用opencv進行通道分離和顏色空間轉換,經試驗后,總結出車牌背景色的以下特征:
(1)在HSV空間下,H分量的值通常都在115附近徘徊,S分量和V分量因光照不同而差異較大(opencv中H分量的取值范圍是0到179,而不是圖像學中的0到360;S分量和V分量的取值范圍是到255);
(2)在RGB空間下,R分量通常較小,一般在30以下,B分量通常較大,一般在80以上,G分量波動較大;
(3)在HSV空間下對圖像進行補光和加飽和度處理,即將圖像的S分量和V分量均置為255,再進行色彩空間轉換,由HSV空間轉換為RGB空間,發現R分量全部變為0,B分量全部變為255(此操作會引入較大的干擾,后續沒有使用)。
根據以上特征可初步篩選出可疑的車牌區域。隨后對灰度圖進行操作,將可疑位置的像素值置為255,其他位置的像素值置為0,即根據特征對圖像進行了二值化。二值化圖像中,可疑區域用白色表示,其他區域均為黑色。隨后可通過膨脹腐蝕等操作對圖像進一步處理。
for i in range(img_h): for j in range(img_w): # 普通藍色車牌,同時排除透明反光物質的干擾 if ((img_HSV[:, :, 0][i, j]-115)**2 < 15**2) and (img_B[i, j] > 70) and (img_R[i, j] < 40): img_gray[i, j] = 255 else: img_gray[i, j] = 0
2.1.2 尋找車牌外圍輪廓
選定可疑區域並將圖像二值化后,一般情況下,圖像中就只有車牌位置的像素顏色為白,但在一些特殊情況下還會存在一些噪聲。如上圖所示,由於圖像右上角存在藍色支架,與車牌顏色特征相符,因此也被當做車牌識別了出來,由此引入了噪聲。
經過觀察可以發現,車牌區域與噪聲之間存在較大的差異,且車牌區域特征比較明顯:
(1)根據我國常規車牌的形狀可知,車牌的形狀為扁平矩形,長寬比約為3:1;
(2)車牌區域面積遠大於噪聲區域,一般為圖像中最大的白色區域。
可以通過cv2.findContours()函數尋找二值化后圖像中白色區域的輪廓。
注意:在opencv2和opencv4中,cv2.findContours()的返回值有兩個,而在opencv3中,返回值有3個。視opencv版本不同,代碼的寫法也會存在一定的差異。
# 檢測所有外輪廓,只留矩形的四個頂點 # opencv4.0, opencv2.x contours, _ = cv2.findContours(img_bin, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE) # opencv3.x _, contours, _ = cv2.findContours(img_bin, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)
在本例中,因為二值化圖像中共有三塊白色區域(車牌及兩處噪聲),因此返回值contours為長度為3的list。list內裝有3個array,每個array內各存放着一塊白色區域的輪廓信息。每個array的shape均為(n, 1, 2),即每個array存放着對應白色區域輪廓上n個點的坐標。
目前得到了3個array,即3組輪廓信息,但我們並不清楚其中哪個是車牌區域對應的那一組輪廓信息。此時可以根據車牌的上述特征篩選出車牌區域的輪廓。
#形狀及大小篩選校驗 det_x_max = 0 det_y_max = 0 num = 0 for i in range(len(contours)): x_min = np.min(contours[i][ :, :, 0]) x_max = np.max(contours[i][ :, :, 0]) y_min = np.min(contours[i][ :, :, 1]) y_max = np.max(contours[i][ :, :, 1]) det_x = x_max - x_min det_y = y_max - y_min if (det_x / det_y > 1.8) and (det_x > det_x_max ) and (det_y > det_y_max ): det_y_max = det_y det_x_max = det_x num = i # 獲取最可疑區域輪廓點集 points = np.array(contours[num][:, 0])
最終得到的points的shape為(n, 2),即存放了n個點的坐標,這n個點均分布在車牌的邊緣上。
2.1.3 車牌區域定位
獲取車牌輪廓上的點集后,可用cv2.minAreaRect()獲取點集的最小外接矩形。返回值rect內包含該矩形的中心點坐標、高度寬度及傾斜角度等信息,使用cv2.boxPoints()可獲取該矩形的四個頂點坐標。
# 獲取最小外接矩陣,中心點坐標,寬高,旋轉角度 rect = cv2.minAreaRect(points) # 獲取矩形四個頂點,浮點型 box = cv2.boxPoints(rect) # 取整 box = np.int0(box)
但我們並不清楚這四個坐標點各對應着矩形的哪一個頂點,因此無法充分地利用這些坐標信息。
可以從坐標值的大小特征入手,將四個坐標與矩形的四個頂點匹配起來:在opencv的坐標體系下,縱坐標最小的是top_point,縱坐標最大的是bottom_point, 橫坐標最小的是left_point,橫坐標最大的是right_point。
# 獲取四個頂點坐標 left_point_x = np.min(box[:, 0]) right_point_x = np.max(box[:, 0]) top_point_y = np.min(box[:, 1]) bottom_point_y = np.max(box[:, 1]) left_point_y = box[:, 1][np.where(box[:, 0] == left_point_x)][0] right_point_y = box[:, 1][np.where(box[:, 0] == right_point_x)][0] top_point_x = box[:, 0][np.where(box[:, 1] == top_point_y)][0] bottom_point_x = box[:, 0][np.where(box[:, 1] == bottom_point_y)][0] # 上下左右四個點坐標 vertices = np.array([[top_point_x, top_point_y], [bottom_point_x, bottom_point_y], [left_point_x, left_point_y], [right_point_x, right_point_y]])
2.2 畸變校正
2.2.1 畸變后車牌頂點定位
要想實現車牌的畸變矯正,必須找到畸變前后對應點的位置關系。
可以看出,本是矩形的車牌畸變后變成了平行四邊形,因此車牌輪廓和得出來的矩形輪廓並不契合。但有了矩形的四個頂點坐標后,可以通過簡單的幾何相似關系求出平行四邊形車牌的四個頂點坐標。
在本例中,平行四邊形四個頂點與矩形四個頂點之間有如下關系:矩形頂點Top_Point、Bottom_Point與平行四邊形頂點new_top_point、new_bottom_point重合,矩形頂點Top_Point的橫坐標與平行四邊形頂點new_right_point的橫坐標相同,矩形頂點Bottom_Point的橫坐標與平行四邊形頂點new_left_point的橫坐標相同。
但事實上,由於拍攝的角度不同,可能出現兩種不同的畸變情況。可以根據矩形傾斜角度的不同來判斷具體是哪種畸變情況。
判斷出具體的畸變情況后,選用對應的幾何相似關系,即可輕易地求出平行四邊形四個頂點坐標,即得到了畸變后車牌四個頂點的坐標。
要想實現車牌的校正,還需得到畸變前車牌四個頂點的坐標。因為我國車牌的標准尺寸為440X140,因此可規定畸變前車牌的四個頂點坐標分別為:(0,0),(440,0),(0,140),(440,140)。順序上需與畸變后的四個頂點坐標相對應。
# 畸變情況1 if rect[2] > -45: new_right_point_x = vertices[0, 0] new_right_point_y = int(vertices[1, 1] - (vertices[0, 0]- vertices[1, 0]) / (vertices[3, 0] - vertices[1, 0]) * (vertices[1, 1] - vertices[3, 1])) new_left_point_x = vertices[1, 0] new_left_point_y = int(vertices[0, 1] + (vertices[0, 0] - vertices[1, 0]) / (vertices[0, 0] - vertices[2, 0]) * (vertices[2, 1] - vertices[0, 1])) # 校正后的四個頂點坐標 point_set_1 = np.float32([[440, 0],[0, 0],[0, 140],[440, 140]]) # 畸變情況2 elif rect[2] < -45: new_right_point_x = vertices[1, 0] new_right_point_y = int(vertices[0, 1] + (vertices[1, 0] - vertices[0, 0]) / (vertices[3, 0] - vertices[0, 0]) * (vertices[3, 1] - vertices[0, 1])) new_left_point_x = vertices[0, 0] new_left_point_y = int(vertices[1, 1] - (vertices[1, 0] - vertices[0, 0]) / (vertices[1, 0] - vertices[2, 0]) * (vertices[1, 1] - vertices[2, 1])) # 校正后的四個頂點坐標 point_set_1 = np.float32([[0, 0],[0, 140],[440, 140],[440, 0]]) # 校正前平行四邊形四個頂點坐標 new_box = np.array([(vertices[0, 0], vertices[0, 1]), (new_left_point_x, new_left_point_y), (vertices[1, 0], vertices[1, 1]), (new_right_point_x, new_right_point_y)]) point_set_0 = np.float32(new_box)
2.2.2 校正
該畸變是由於攝像頭與車牌不垂直而引起的投影造成的,因此可用cv2.warpPerspective()來進行校正。
# 變換矩陣 mat = cv2.getPerspectiveTransform(point_set_0, point_set_1) # 投影變換 lic = cv2.warpPerspective(img, mat, (440, 140))
2.3 源碼分享

1 # Created by 秋沐霖 on 2019/3/19. 2 import cv2 3 import numpy as np 4 5 # 預處理 6 def imgProcess(path): 7 img = cv2.imread(path) 8 # 統一規定大小 9 img = cv2.resize(img, (640,480)) 10 # 高斯模糊 11 img_Gas = cv2.GaussianBlur(img,(5,5),0) 12 # RGB通道分離 13 img_B = cv2.split(img_Gas)[0] 14 img_G = cv2.split(img_Gas)[1] 15 img_R = cv2.split(img_Gas)[2] 16 # 讀取灰度圖和HSV空間圖 17 img_gray = cv2.cvtColor(img_Gas, cv2.COLOR_BGR2GRAY) 18 img_HSV = cv2.cvtColor(img_Gas, cv2.COLOR_BGR2HSV) 19 return img, img_Gas, img_B, img_G, img_R, img_gray, img_HSV 20 21 # 初步識別 22 def preIdentification(img_gray, img_HSV, img_B, img_R): 23 for i in range(480): 24 for j in range(640): 25 # 普通藍色車牌,同時排除透明反光物質的干擾 26 if ((img_HSV[:, :, 0][i, j]-115)**2 < 15**2) and (img_B[i, j] > 70) and (img_R[i, j] < 40): 27 img_gray[i, j] = 255 28 else: 29 img_gray[i, j] = 0 30 # 定義核 31 kernel_small = np.ones((3, 3)) 32 kernel_big = np.ones((7, 7)) 33 34 img_gray = cv2.GaussianBlur(img_gray, (5, 5), 0) # 高斯平滑 35 img_di = cv2.dilate(img_gray, kernel_small, iterations=5) # 腐蝕5次 36 img_close = cv2.morphologyEx(img_di, cv2.MORPH_CLOSE, kernel_big) # 閉操作 37 img_close = cv2.GaussianBlur(img_close, (5, 5), 0) # 高斯平滑 38 _, img_bin = cv2.threshold(img_close, 100, 255, cv2.THRESH_BINARY) # 二值化 39 return img_bin 40 41 # 定位 42 def fixPosition(img, img_bin): 43 # 檢測所有外輪廓,只留矩形的四個頂點 44 contours, _ = cv2.findContours(img_bin, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE) 45 #形狀及大小篩選校驗 46 det_x_max = 0 47 det_y_max = 0 48 num = 0 49 for i in range(len(contours)): 50 x_min = np.min(contours[i][ :, :, 0]) 51 x_max = np.max(contours[i][ :, :, 0]) 52 y_min = np.min(contours[i][ :, :, 1]) 53 y_max = np.max(contours[i][ :, :, 1]) 54 det_x = x_max - x_min 55 det_y = y_max - y_min 56 if (det_x / det_y > 1.8) and (det_x > det_x_max ) and (det_y > det_y_max ): 57 det_y_max = det_y 58 det_x_max = det_x 59 num = i 60 # 獲取最可疑區域輪廓點集 61 points = np.array(contours[num][:, 0]) 62 return points 63 64 65 #img_lic_canny = cv2.Canny(img_lic_bin, 100, 200) 66 67 68 def findVertices(points): 69 # 獲取最小外接矩陣,中心點坐標,寬高,旋轉角度 70 rect = cv2.minAreaRect(points) 71 # 獲取矩形四個頂點,浮點型 72 box = cv2.boxPoints(rect) 73 # 取整 74 box = np.int0(box) 75 # 獲取四個頂點坐標 76 left_point_x = np.min(box[:, 0]) 77 right_point_x = np.max(box[:, 0]) 78 top_point_y = np.min(box[:, 1]) 79 bottom_point_y = np.max(box[:, 1]) 80 81 left_point_y = box[:, 1][np.where(box[:, 0] == left_point_x)][0] 82 right_point_y = box[:, 1][np.where(box[:, 0] == right_point_x)][0] 83 top_point_x = box[:, 0][np.where(box[:, 1] == top_point_y)][0] 84 bottom_point_x = box[:, 0][np.where(box[:, 1] == bottom_point_y)][0] 85 # 上下左右四個點坐標 86 vertices = np.array([[top_point_x, top_point_y], [bottom_point_x, bottom_point_y], [left_point_x, left_point_y], [right_point_x, right_point_y]]) 87 return vertices, rect 88 89 def tiltCorrection(vertices, rect): 90 # 畸變情況1 91 if rect[2] > -45: 92 new_right_point_x = vertices[0, 0] 93 new_right_point_y = int(vertices[1, 1] - (vertices[0, 0]- vertices[1, 0]) / (vertices[3, 0] - vertices[1, 0]) * (vertices[1, 1] - vertices[3, 1])) 94 new_left_point_x = vertices[1, 0] 95 new_left_point_y = int(vertices[0, 1] + (vertices[0, 0] - vertices[1, 0]) / (vertices[0, 0] - vertices[2, 0]) * (vertices[2, 1] - vertices[0, 1])) 96 # 校正后的四個頂點坐標 97 point_set_1 = np.float32([[440, 0],[0, 0],[0, 140],[440, 140]]) 98 # 畸變情況2 99 elif rect[2] < -45: 100 new_right_point_x = vertices[1, 0] 101 new_right_point_y = int(vertices[0, 1] + (vertices[1, 0] - vertices[0, 0]) / (vertices[3, 0] - vertices[0, 0]) * (vertices[3, 1] - vertices[0, 1])) 102 new_left_point_x = vertices[0, 0] 103 new_left_point_y = int(vertices[1, 1] - (vertices[1, 0] - vertices[0, 0]) / (vertices[1, 0] - vertices[2, 0]) * (vertices[1, 1] - vertices[2, 1])) 104 # 校正后的四個頂點坐標 105 point_set_1 = np.float32([[0, 0],[0, 140],[440, 140],[440, 0]]) 106 107 # 校正前平行四邊形四個頂點坐標 108 new_box = np.array([(vertices[0, 0], vertices[0, 1]), (new_left_point_x, new_left_point_y), (vertices[1, 0], vertices[1, 1]), (new_right_point_x, new_right_point_y)]) 109 point_set_0 = np.float32(new_box) 110 return point_set_0, point_set_1, new_box 111 112 def transform(img, point_set_0, point_set_1): 113 # 變換矩陣 114 mat = cv2.getPerspectiveTransform(point_set_0, point_set_1) 115 # 投影變換 116 lic = cv2.warpPerspective(img, mat, (440, 140)) 117 return lic 118 119 def main(): 120 path = 'F:\\Python\\license_plate\\test\\9.jpg' 121 # 圖像預處理 122 img, img_Gas, img_B, img_G, img_R, img_gray, img_HSV = imgProcess(path) 123 # 初步識別 124 img_bin = preIdentification(img_gray, img_HSV, img_B, img_R) 125 points = fixPosition(img, img_bin) 126 vertices, rect = findVertices(points) 127 point_set_0, point_set_1, new_box = tiltCorrection(vertices, rect) 128 img_draw = cv2.drawContours(img.copy(), [new_box], -1, (0,0,255), 3) 129 lic = transform(img, point_set_0, point_set_1) 130 # 原圖上框出車牌 131 cv2.namedWindow("Image") 132 cv2.imshow("Image", img_draw) 133 # 二值化圖像 134 cv2.namedWindow("Image_Bin") 135 cv2.imshow("Image_Bin", img_bin) 136 # 顯示校正后的車牌 137 cv2.namedWindow("Lic") 138 cv2.imshow("Lic", lic) 139 # 暫停、關閉窗口 140 cv2.waitKey(0) 141 cv2.destroyAllWindows() 142 143 if __name__ == '__main__': 144 main()
三、后記
由於能力學識有限,代碼只能識別最常見的藍色車牌,對其他車牌無識別能力,后續會持續改進完善項目。最后附上幾張實驗圖片: