预处理内容介绍
我们在真正的对二维码图形进行分割解码之前,需要将图形转换成我们需求的形态:
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)