預處理內容介紹
我們在真正的對二維碼圖形進行分割解碼之前,需要將圖形轉換成我們需求的形態:
1.只關注二維碼部分
2.排除掉其他顏色的干擾信息
3.圖片轉換成完整的正方形
二維碼切分
從紙質發票的實際情況來看,所有的發票的二維碼部分都是藍色的。顏色與針式打印機沒有太大關系,國稅的專票和普票的第一聯是采用的壓敏紙,針式打印機的針頭落下的時候壓敏紙背面的顏色會印記到第二聯和第三聯上,而所有的發票紙張都是國稅監制的,所以基本上初始打印的顏色是沒有太大差異的。只可能因為針頭的力度不足而發生部分欠色,以及由於長時間的不妥善保管導致的褪色。
在不考慮特殊情況的前提下,我們目前需要找出當前圖片中的一個藍色正方形,且排除掉頁面上的其他藍色信息。
顏色空間
參考《RGB、YUV和HSV顏色空間模型》:https://www.cnblogs.com/justkong/p/6570914.html
顏色通常用三個獨立的屬性來描述,三個獨立變量綜合作用,自然就構成一個空間坐標,這就是顏色空間。
顏色空間按照基本機構可以分為兩大類:基色顏色空間和色、亮分離顏色空間。前者典型的是RGB,后者典型的是HSV。
在RGB顏色空間中,任意色光F都可以用R(Red 紅色)、G(Green 綠色)、B(Blue 藍色)三色不同分量的相加混合而成,RGB色彩空間采用物理三基色表示,因而物理意義很清楚,適合彩色顯象管工作。然而這一體制並不適應人的視覺特點。所以無法用RGB色彩空間很好的描述人肉眼中的藍色這個顏色在不同光照下的展現結果
HSV是一種將RGB色彩空間中的點在倒圓錐體中的表示方法。HSV即色相(Hue)、飽和度(Saturation)、明度(Value),又稱HSB(B即Brightness)。色相是色彩的基本屬性,就是平常說的顏色的名稱,如紅色、黃色等。飽和度(S)是指色彩的純度,越高色彩越純,低則逐漸變灰,取0-100%的數值。明度(V),取0-max(計算機中HSV取值范圍和存儲的長度有關)。
HSV顏色空間,更類似於人類感覺顏色的方式,封裝了關於顏色的信息:“這是什么顏色?深淺如何?明暗如何?”
GRAY顏色空間只有一個灰度的顏色通道
多個顏色空間可以互相轉換
二值化
圖像的二值化是將圖像上的像素點的灰度值設置為0或255,也就是將整個圖像呈現出明顯的黑白效果。圖片二值化的結果可以作為掩模使用。
掩模
參考《OpenCV探索之路(十三):詳解掩膜mask》:https://www.cnblogs.com/skyfsm/p/6894685.html
OpenCV中有一個概念:ROI(感興趣區域:Region of interest),但是只支持少量規則圖形,例如矩形。我們需要對發票上的不規則區域進行裁剪,需要借助掩模(mask)的力量。他可以屏蔽圖片上無關區域,只凸顯出感興趣區域。簡單點,就是可以用來摳圖。
按位與
在opencv中,圖像其實是一個矩陣,當為處於RGB顏色空間時,是一個三維矩陣,矩陣的維度分別為:[圖片的高度,圖片的寬度,顏色通道的數量]
當使用掩模進行摳圖操作時,調用的是opencv的bitwise_and(按位與)函數,實際上就是將某個點的圖像各通道的二進制值與相同點位的二進制值進行了按位與操作
例如,某個點的顏色信息為(120,155,100)在掩模上是一個黑色的點(0,0,0),那么執行完按位與操作后
120 -> 0111 1000 & 0000 0000 => 0000 0000
155 -> 1001 1011 & 0000 0000 => 0000 0000
100 -> 0110 0100 & 0000 0000 => 0000 0000
這個點在執行完按位與操作后完全變成了黑色
同樣,某個點與掩模上的白色點(255,255,255)執行完按位與操作后,仍然保留了原來的顏色
120 -> 0111 1000 & 1111 1111 => 0111 1000
155 -> 1001 1011 & 1111 1111 => 1001 1011
100 -> 0110 0100 & 1111 1111 => 0110 0100
輪廓
輪廓可以簡單認為成將連續的點(連着邊界)連在一起的曲線,具有相同 的顏色或者灰度。輪廓在形狀分析和物體的檢測和識別中很有用。
- 為了更加准確,要使用二值化圖像。在尋找輪廓之前,要進行閾值化處理 或者 Canny 邊界檢測。
- 查找輪廓的函數會修改原始圖像。如果你在找到輪廓之后還想使用原始圖 像的話,你應該將原始圖像存儲到其他變量中。
- 在 OpenCV 中,查找輪廓就像在黑色背景中超白色物體。你應該記住, 要找的物體應該是白色而背景應該是黑色。
形態學操作
參考《形態學圖像處理(一):膨脹與腐蝕》:https://blog.csdn.net/poem_qianmo/article/details/23710721
參考《形態學圖像處理(二):開運算、閉運算、形態學梯度、頂帽、黑帽合輯》:https://blog.csdn.net/poem_qianmo/article/details/23710721
參考《OpenCV官方教程中文版(For Python)pdf版》
形態學操作前提:黑色背景,白色物體
膨脹:白色物體膨脹邊緣侵占黑色背景
----->
腐蝕:黑色背景膨脹邊緣侵占白色物體
----->
開運算:去除黑色背景上的白色噪點
閉運算:去除白色物體內的黑色小洞
獲取藍色區域的代碼實現
我們將圖片轉換到HSV空間后,通過設置顏色的上下限來過濾圖片中的藍色顏色區域:
參考《【OpenCV】HSV顏色識別-HSV基本顏色分量范圍》:https://blog.csdn.net/taily_duan/article/details/51506776
# HSV顏色空間下的藍色上下限
lower_blue = np.array([80, 43, 46])
upper_blue = np.array([130, 255, 255])
# 兩個問題,一個是蒙版范圍,一個是膨脹的卷積核的大小
# 將BGR顏色空間的圖片轉換為HSV顏色空間
hsv_image = cv2.cvtColor(image, cv2.COLOR_BGR2HSV)
獲取到的藍色圖像的二值化圖像為:
通過按位與操作將圖片中的有效信息摳出的圖像為:
# 使用蒙版將原圖中的區域摳出
part_blue = cv2.bitwise_and(image, image, mask=mask_blue)
將獲取到的圖像轉換為灰度圖:
# 將BGR顏色空間轉換到灰度顏色空間
gray_image = cv2.cvtColor(part_blue, cv2.COLOR_BGR2GRAY)
將獲取到的灰度圖進行二值化處理
(T, thresh_image) = cv2.threshold(gray_image, 55, 255, cv2.THRESH_BINARY)
可以看到,二值化之后的圖像其實並非連續的,二維碼部分的圖形處於一個分離狀態。從輪廓的定義來看,必須是連續的圖像才能算作一個輪廓。我們需要對整體圖片進行一次閉運算,消除掉白色物體之間的黑色縫隙,使之連續
#卷積核大小需要根據圖像本身的尺寸和圖片上的相鄰圖形的間隔進行調整
kernel = np.ones((30, 30), np.uint8)
close_image = cv2.morphologyEx(thresh_image, cv2.MORPH_CLOSE, kernel)
從結果來看,圖片上存在多個白色物體,需要對其進行篩選。從物體的形狀有多種篩選的依據:
1.白色圖形面積最大
2.白色圖形的輪廓對應的長寬比接近1
因為輪廓本身是一串連續的點,沒有真正的長寬意義,需要重新繪制出最小外接矩形,再來計算外接矩形的長寬比,邏輯比較復雜,所以我選擇了通過面積計算來過濾
# 尋找輪廓
close_image, contours, hierarchy = cv2.findContours(close_image, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
# 刪選輪廓
# 當前圖片中面積最大的輪廓即為二維碼
max_area = 0
max_index = 0
for i in range(0, len(contours)):
area = cv2.contourArea(contours[i]) # 外框面積 #外界矩陣的面積
if area > max_area:
max_index = i
max_area = area
if len(contours) == 0:
return None
qr_cnt = contours[max_index]
將尋找到的合適的輪廓進行描邊,有兩種描邊方案
1.輪廓本身有一個drawContours的方法
2.對輪廓進行多邊形擬合,然后對擬合的多邊形進行繪圖
我選擇的是多邊形擬合再繪圖的方案(具體原因后續講)
# 通過多邊形擬合找到當前二維碼輪廓的多邊形,並對其描邊 2px
epsilon = 1
approx = cv2.approxPolyDP(qr_cnt, epsilon, True)
poly_image = np.ones(close_image.shape, np.uint8)
cv2.polylines(poly_image, [approx], True, (255, 255, 255), 2)
對這張黑色背景的圖片取反,獲得對應的白色圖片
bit_not_poly_image = cv2.bitwise_not(poly_image)
將白色圖片的黑色線框中填充黑色線段,由於opencv自帶的多邊形填充fillPoly在遇到內凹圖形的時候會導致填充斷線,所以寫了一段遞歸的邏輯,不斷尋找最小的輪廓面積進行填充,直到輪廓數量<=2
fill_image = self.fill_black_poly(bit_not_poly_image)
# 遞歸填充黑色多邊形
def fill_black_poly(self, poly_image):
# 傳入黑色多邊形
# 如果當前多邊形找到的輪廓數量小於等於2,說明當前圖形已經被完全填充,無需再進行填充
# 否則說明當前多邊形還存在空白區域,需要繼續填充
poly_image, contours, hierarchy = cv2.findContours(poly_image, cv2.RETR_TREE, cv2.CHAIN_APPROX_NONE)
if len(contours) <= 2:
return poly_image
else:
minArea = -1;
# 從輪廓中找到面積最小的輪廓進行填充
for i in range(0, len(contours)):
if cv2.contourArea(contours[i]) < minArea or minArea == -1:
minArea = cv2.contourArea(contours[i])
minIndex = i
cv2.fillConvexPoly(poly_image, contours[minIndex], (0, 0, 0), 8, 0)
if self.trace_image:
cv2.imwrite(self.trace_path + "008_fill_poly_" + str(minArea) + self.image_name, poly_image)
return self.fill_black_poly(poly_image)
將填充完成的圖片按位取反,並進行二值化處理,作為截取二維碼圖片的蒙版,並執行圖片截取
# 將填充完成的圖片按位取反並進行二值化處理,作為截取二維碼區域的蒙版
qr_mask_image = cv2.bitwise_not(fill_image)
ret, qr_mask = cv2.threshold(qr_mask_image, 175, 255, cv2.THRESH_BINARY)
# 通過蒙版截取圖片二維碼部分
cut_image = cv2.bitwise_and(image, image, mask=qr_mask)
截取圖片后,將圖片的全黑部分修改為全白,避免由於圖片黑色過多導致后續二值化處理異常
# 將黑色部分修改為白色,方便后續處理
cut_image = np.where(cut_image == 0, 255, cut_image)