最近有需要做银行卡上黑色磁条的提取的工作。因为这是比较典型的轮廓检测问题。用DL的方法需要大量的标注数据集,所以想到用openCV来做。下面梳理一下流程:
这篇博文的目的是应用计算机视觉和图像处理技术,展示一个银行卡上黑色磁条的基本实现。
需要注意的是,这个算法并不是对所有银行卡都有效,但会给你基本的关于应用什么类型的技术的直觉,这种感觉的积累对于解决工程问题来说,是有益的。
假设我们要检测下图银行卡中的黑色磁条:
下面的代码是基于新版的opencv3.3的。
opencv3.x跟网上很多基于opencv2.x版本的api和调用都有不同了,注意调整。
代码放在这里:tomguluson92/card_detection
还有一个效果比较好的用canny算子得到的边缘。
import numpy as np
import cv2
img = cv2.imread(image) # image为你的图片地址
grey = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # 转换了灰度化
gradX = cv2.Sobel(grey, ddepth=cv2.CV_32F, dx=1, dy=0, ksize=-1)
gradY = cv2.Sobel(grey, ddepth=cv2.CV_32F, dx=0, dy=1, ksize=-1)
gradient = cv2.subtract(gradX, gradY)
gradient = cv2.convertScaleAbs(gradient)
这一步,使用Scharr操作(指定使用ksize = -1)构造灰度图在水平和竖直方向上的梯度幅值表示。Scharr操作之后,我们从x-gradient中减去y-gradient,通过这一步减法操作,最终得到包含高水平梯度和低竖直梯度的图像区域。
# blur and threshold the image
blurred = cv2.blur(gradient, (16, 16))
retval, grey = cv2.threshold(blurred, 30, 255, cv2.THRESH_BINARY_INV)
这里要做的第一件事是使用16 * 16的内核对梯度图进行平均模糊,这将有助于平滑梯度表征的图形中的高频噪声。对我这个例子,越大越好。
此外,cv2.THRESH_BINARY_INV是跟cv.THRESH_BINARY正好相反的:作用是把像素点(pixel)低于30全转成0(黑色),其他的像素点(pixel)变成白色。
为了消除这些缝隙,并使我们的算法更容易检测到条形码中的“斑点”状区域,我们需要进行一些基本的形态学操作:
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (25, 10))
closed = cv2.morphologyEx(grey, cv2.MORPH_CLOSE, kernel)
上面代码的含义是:我们首先使用cv2.getStructuringElement构造一个长方形内核kernel。这个内核的宽度大于长度,因此我们可以消除条形码中垂直条之间的缝隙。
然后把这个kernel扔进去,进行形态学操作。将上一步得到的内核应用到我们的二值图中,以此来消除竖杠间的缝隙。
当然,现在图像中还有一些小斑点,不属于真正条形码的一部分,但是可能影响我们的轮廓检测。
让我们来消除这些小斑点:
closed = cv2.erode(closed, None, iterations=5)
grey = cv2.dilate(closed, None, iterations=5)
我们这里所做的是首先进行5次腐蚀(erosion),然后进行5次膨胀(dilation)。腐蚀操作将会腐蚀图像中白色像素,以此来消除小斑点,而膨胀操作将使剩余的白色像素扩张并重新增长回去。
如果小斑点在腐蚀操作中被移除,那么在膨胀操作中就不会再出现。
经过我们这一系列的腐蚀和膨胀操作,可以看到我们已经成功地移除小斑点并得到条形码区域。
image, contours, hierarchy = cv2.findContours(grey.copy(), cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)
# 找到图像中的最大轮廓
c = sorted(contours, key=cv2.contourArea, reverse=True)[0]
rect = cv2.minAreaRect(c)
box = np.int0(cv2.boxPoints(rect))
cv2.drawContours(img, [box], -1, (0, 0, 255), 3)
cv2.imshow('Image', img)
cv2.waitKey()
这一部分比较容易,我们简单地找到图像中的最大轮廓,如果我们正确完成了图像处理步骤,这里应该对应于磁条区域。
然后用cv2.minAreaRect为最大轮廓确定最小边框。
最后就是显示检测到的磁条:
虽然不是完全取到,但是思路是这样的。
cv2.findContours接收的参数中,第一个参数是要检索的图片,必须是为二值图,即黑白的(不是灰度图)。第二个参数是轮廓的检索模式:
第三个参数为轮廓的近似方法:
cv2.drawContours在图像上绘制轮廓。
第一个参数是指明在哪幅图像上绘制轮廓
第二个参数是轮廓本身,在Python中是一个list
第三个参数指定绘制轮廓list中的哪条轮廓,如果是-1,则绘制其中的所有轮廓
第四个参数是轮廓线条的颜色 ((0, 0, 255), (0, 255, 0), …)
第五个参数是轮廓线条的粗细 (1,2,3…)
算法概要如下:
需要注意的是,该方法做了关于图像梯度表示的假设,因此只对水平磁条有效。
如果你想实现一个更加鲁棒的条形码检测算法,你需要考虑图像的方向,或者更好的,应用机器学习技术如Haar级联或者HOG + Linear SVM去扫描图像的磁条区域。
[1] 用 Python 和 OpenCV 检测图片上的条形码
[2] 使用Python和OpenCV检测图像中的物体并将物体裁剪下来
[3] 《Opencv2 Computer Vision Application Programming Cookbook》
[4] OpenCV-Python教程(4、形态学处理)