信用卡数字识别OpenCV实现(含代码)【DataWhale项目】

参考课程:Opencv计算机视觉实战(Python版)

基本材料准备

  1. 一张仅含数字的图像作为模板。
    在这里插入图片描述
  2. 一张银行卡的图像作为检测样本。
    在这里插入图片描述

代码编写或者阅读的习惯

  1. 常打断点
  2. 勤展示数组的shape等,以及数组的图像。

思路介绍

模板图像的处理

  1. 模板图像是一张背景色为255的白色且字体为黑色0 但是仍旧是三层通道的BGR彩色图像

  2. 由于图像识别等科学操作,使用彩色图没有一个比较好的效果,而二维的灰度图效果却很不错,因此在读入三维的彩色图像之后,需要转化为灰度图。

  3. 因为做轮廓检测之间需要二值图像,因此需要将灰度图再转化为二值图像
    这里的二值图像转换过程强调一点:二值图像不仅仅只有0和255两种值的情况。原因在于二值图像的实现是借助threshhold方法实现的,而这个方法需要自行指定阈值。所以最后的二值图像是只有0和255还是只有0和阈值等情况
    取决于你的method参数。

  4. 得到二值图像就可以调用findContours方法寻找轮廓,且给返回一个Contours对象。此时注意,findContours方法的参数的格式必须是:二值图像深度复制的副本、mode轮廓检测模式一般选择RETR_EXTERNAL,而method表示轮廓逼近方法:一般选择CHAIN_APPROX_SIMPLE(只保留各个轮廓的部分顶点或者转折点,足够用来描绘出轮廓!)

  5. 为了展示轮廓检测的效果,可以将contours对象作为参数,传入drawContours方法中。

  6. 我们希望在用模板识别信用卡数字时,能够按照0-9的顺序排列,所以需要对Countours对象中的10个轮廓list对象,进行排序。这里是自行写了一个方法。

  7. 排序的思想是:通过boundingRect方法获取到包含了每个轮廓的矩形的左上座标,因为模板图像本身是按照从小到大的顺序排列,因此,通过比较个轮廓矩形的左上座标的横座标即可得到个轮廓的排序的列表refCnts。

  8. 通过定义一个digits字典来通过对refCnts列表的for循环来将数字0123456789与refCnts中的边框在ref图像中的像素点区域(相当于截取了该数字在模板上对应的数字模板)一一对应。

被识别图像的处理

  1. 因为被识别图像的内容比较丰富,此时如果机械地重复模板图像的处理流程(转化为灰度图、二值图像、轮廓检测)则会产生大量的问题。因此结合这个被识别的图像的具体情况,我们也可以针对性的提出一些附加的预处理操作和形态学处理操作。

  2. 形态学的处理操作其实是基于灰度图的因此事先需要先进行这方面的操作。

  3. 形态学的顶帽操作(原始输入–开运算的结果(先腐蚀再膨胀,以突出主体区域)以突出细节区域),另做顶帽操作的卷积核的大小设定,是考虑到自己想要识别的区域的长宽,比如“5412”这四个字的整体区域,因此选择了一个9*3的卷积核。
    其中,膨胀操作会使得像素点值大的得到膨胀,侵蚀像素值小的区域(比如:白色侵蚀黑色)
    腐蚀操作会使得像素点值小的颜色侵蚀像素点值大的区域(比如黑色侵蚀白色),此时也可以看出所谓侵蚀还是膨胀操作是以"像素点值大的区域"为中心进行称谓命名的。(像素值大的区域膨胀、像素值大的区域被侵蚀

#读取输入图像,预处理
image = cv2.imread("./images/credit_card_03.png")
cv_show('image',image)
image = myutils.resize(image, width=300)
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
cv_show('gray',gray)
## 查看腐蚀的结果
erosion_1 = cv2.erode(gray,rectKernel,iterations = 1)
cv_show('erosion_1', erosion_1)
## 查看膨胀的结果
dilate_1 = cv2.dilate(gray,rectKernel,iterations = 1)
cv_show('dilate_1', dilate_1)
## 查看开运算的结果
opening = cv2.morphologyEx(gray, cv2.MORPH_OPEN, rectKernel)
cv_show('opening', opening)
#礼帽操作,突出更明亮的区域
tophat = cv2.morphologyEx(gray, cv2.MORPH_TOPHAT, rectKernel) 
cv_show('tophat',tophat) 

灰度图:
灰度图
腐蚀操作之后:像素点值大的被腐蚀
腐蚀操作
膨胀操作:
膨胀操作
开运算操作:
开运算操作
顶帽操作:
顶帽操作
综合来看:综合
4. 通过sobel算子对输入图像进行处理,得到输入图像的边缘特征(注意,边缘特征杂而乱,不等于轮廓特征的规整。)
而边缘特征进行检测时的流程时,对X方向或者Y方向或者XY方向以此进行Soble计算、绝对值操作、归一化、uint8类型转变。(这里选择的时对X方向进行运算。)
在这里插入图片描述
5. 想要实现一种行为:将数字构成的四个组块提取出来,为此用到闭操作(先膨胀后腐蚀)使得数字连在一起。

闭操作
6. 灰度图的形态学处理完成后,还需要做二值化处理。这里的threashhold方法要采取系统自动指定阈值的方式来进行二值化操作处理。
在这里插入图片描述
7. 为了使得分组的图像中的空洞位置同样被白色填充,我们可以再进行一步闭操作。这样做的好处提升外轮廓定位的准确率
再一次闭操作
8. 调用findContours方法以及显示所有的轮廓。
在这里插入图片描述
9. 因为找到的所有轮廓中包含大量不规则且不是我们想要的边框区域,所以进行过滤操作,而过滤操作的实现思路是,人为地指定四组数字所有的区域的范围的座标,使得Contours对象中的各个轮廓的座标不处于这个范围之内的轮廓被排除掉。处理后,依旧进行排序操作。(此时的排序操作,不需要根据边框的左上点的横座标来排序)。
10.为了使得每一组边框中的图像一一与0-9的数字进行模板匹配,需要再将每一组轮廓中的四个数字再次细分为四个数字,而这一步就类似于模板图像提取每个数据模板的过程(灰度图的基础上二值化处理、轮廓检测、排序、根据每一个轮廓截取二值图像中的相应轮廓的位置)。
在这里插入图片描述
在这里插入图片描述
10. 截至到目前,我们已经获得模板图像中截取出来的10个数字模板。同时也从银行卡中截取到的16个数字且这26个图像的大小经过resize都搞成了一样的shape。此时就可以做模板匹配了。思想即是,对于每一个要验证的数字,使用10个数字模板一一遍历匹配,并计算ROI得分,选择最大值的数字。
11. 当上一步操作结束,即可将加过呈现在原来的图片上。

代码实现

  1. 主程序
# 导入工具包
from imutils import contours
import numpy as np
import argparse
import cv2
import myutils

# 设置参数
# ap = argparse.ArgumentParser()
# ap.add_argument("-i", "--image", required=True,
# 	help="path to input image")
# ap.add_argument("-t", "--template", required=True,
# 	help="path to template OCR-A image")
# args = vars(ap.parse_args())

# 指定信用卡类型
FIRST_NUMBER = {
	"3": "American Express",
	"4": "Visa",
	"5": "MasterCard",
	"6": "Discover Card"
}
# 绘图展示
def cv_show(name,img):
	cv2.imshow(name, img)
	cv2.waitKey(0)
	cv2.destroyAllWindows()
# 读取一个模板图像
# img = cv2.imread(args["template"])
img = cv2.imread("ocr_a_reference.png")
cv_show('img',img)
# 灰度图
ref = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
cv_show('ref',ref)
# 二值图像
ref = cv2.threshold(ref, 10, 255, cv2.THRESH_BINARY_INV)[1]
cv_show('ref',ref)

# 计算轮廓
#cv2.findContours()函数接受的参数为二值图,即黑白的(不是灰度图),cv2.RETR_EXTERNAL只检测外轮廓,cv2.CHAIN_APPROX_SIMPLE只保留终点座标
#返回的list中每个元素都是图像中的一个轮廓

ref_, refCnts, hierarchy = cv2.findContours(ref.copy(), cv2.RETR_EXTERNAL,cv2.CHAIN_APPROX_SIMPLE)

cv2.drawContours(img,refCnts,-1,(0,0,255),3) 
cv_show('img',img)
print (np.array(refCnts).shape)
refCnts = myutils.sort_contours(refCnts, method="left-to-right")[0] #排序,从左到右,从上到下
digits = {}

# 遍历每一个轮廓
for (i, c) in enumerate(refCnts):
	# 计算外接矩形并且resize成合适大小
	(x, y, w, h) = cv2.boundingRect(c)
	roi = ref[y:y + h, x:x + w]
	roi = cv2.resize(roi, (57, 88))

	# 每一个数字对应每一个模板
	digits[i] = roi

# 初始化卷积核
rectKernel = cv2.getStructuringElement(cv2.MORPH_RECT, (9, 3))
sqKernel = cv2.getStructuringElement(cv2.MORPH_RECT, (5, 5))

# ==============================================================================================================================
#读取输入图像,预处理
image = cv2.imread("./images/credit_card_03.png")
cv_show('image',image)
image = myutils.resize(image, width=300)
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
cv_show('gray',gray)

#礼帽操作,突出更明亮的区域
tophat = cv2.morphologyEx(gray, cv2.MORPH_TOPHAT, rectKernel) 
cv_show('tophat',tophat) 
# 
gradX = cv2.Sobel(tophat, ddepth=cv2.CV_32F, dx=1, dy=0, #ksize=-1相当于用3*3的
	ksize=-1)


gradX = np.absolute(gradX)
(minVal, maxVal) = (np.min(gradX), np.max(gradX))
gradX = (255 * ((gradX - minVal) / (maxVal - minVal)))
gradX = gradX.astype("uint8")

print (np.array(gradX).shape)
cv_show('gradX',gradX)

#通过闭操作(先膨胀,再腐蚀)将数字连在一起
gradX = cv2.morphologyEx(gradX, cv2.MORPH_CLOSE, rectKernel) 
cv_show('gradX',gradX)
#THRESH_OTSU会自动寻找合适的阈值,适合双峰,需把阈值参数设置为0
thresh = cv2.threshold(gradX, 0, 255,
	cv2.THRESH_BINARY | cv2.THRESH_OTSU)[1] 
cv_show('thresh',thresh)

#再来一个闭操作

thresh = cv2.morphologyEx(thresh, cv2.MORPH_CLOSE, sqKernel) #再来一个闭操作
cv_show('thresh',thresh)

# 计算轮廓

thresh_, threshCnts, hierarchy = cv2.findContours(thresh.copy(), cv2.RETR_EXTERNAL,
	cv2.CHAIN_APPROX_SIMPLE)

cnts = threshCnts
cur_img = image.copy()
cv2.drawContours(cur_img,cnts,-1,(0,0,255),3) 
cv_show('img',cur_img)
locs = []

# 遍历轮廓
for (i, c) in enumerate(cnts):
	# 计算矩形
	(x, y, w, h) = cv2.boundingRect(c)
	ar = w / float(h)

	# 选择合适的区域,根据实际任务来,这里的基本都是四个数字一组
	if ar > 2.5 and ar < 4.0:

		if (w > 40 and w < 55) and (h > 10 and h < 20):
			#符合的留下来
			locs.append((x, y, w, h))

# 将符合的轮廓从左到右排序
locs = sorted(locs, key=lambda x:x[0])
output = []

# 遍历每一个轮廓中的数字
for (i, (gX, gY, gW, gH)) in enumerate(locs):
	# initialize the list of group digits
	groupOutput = []

	# 根据座标提取每一个组
	group = gray[gY - 5:gY + gH + 5, gX - 5:gX + gW + 5]
	cv_show('group',group)
	# 预处理
	group = cv2.threshold(group, 0, 255,
		cv2.THRESH_BINARY | cv2.THRESH_OTSU)[1]
	cv_show('group',group)
	# 计算每一组的轮廓
	group_,digitCnts,hierarchy = cv2.findContours(group.copy(), cv2.RETR_EXTERNAL,
		cv2.CHAIN_APPROX_SIMPLE)
	digitCnts = contours.sort_contours(digitCnts,
		method="left-to-right")[0]

	# 计算每一组中的每一个数值
	for c in digitCnts:
		# 找到当前数值的轮廓,resize成合适的的大小
		(x, y, w, h) = cv2.boundingRect(c)
		roi = group[y:y + h, x:x + w]
		roi = cv2.resize(roi, (57, 88))
		cv_show('roi',roi)

		# 计算匹配得分
		scores = []

		# 在模板中计算每一个得分
		for (digit, digitROI) in digits.items():
			# 模板匹配
			result = cv2.matchTemplate(roi, digitROI,
				cv2.TM_CCOEFF)
			(_, score, _, _) = cv2.minMaxLoc(result)
			scores.append(score)

		# 得到最合适的数字
		groupOutput.append(str(np.argmax(scores)))

	# 画出来
	cv2.rectangle(image, (gX - 5, gY - 5),
		(gX + gW + 5, gY + gH + 5), (0, 0, 255), 1)
	cv2.putText(image, "".join(groupOutput), (gX, gY - 15),
		cv2.FONT_HERSHEY_SIMPLEX, 0.65, (0, 0, 255), 2)

	# 得到结果
	output.extend(groupOutput)

# 打印结果
print("Credit Card Type: {}".format(FIRST_NUMBER[output[0]]))
print("Credit Card #: {}".format("".join(output)))
cv2.imshow("Image", image)
cv2.waitKey(0)
  1. 其中 myutils.py
import cv2

def sort_contours(cnts, method="left-to-right"):
    reverse = False
    i = 0

    if method == "right-to-left" or method == "bottom-to-top":
        reverse = True

    if method == "top-to-bottom" or method == "bottom-to-top":
        i = 1
    boundingBoxes = [cv2.boundingRect(c) for c in cnts] #用一个最小的矩形,把找到的形状包起来x,y,h,w
    (cnts, boundingBoxes) = zip(*sorted(zip(cnts, boundingBoxes),
                                        key=lambda b: b[1][i], reverse=reverse))

    return cnts, boundingBoxes
def resize(image, width=None, height=None, inter=cv2.INTER_AREA):
    dim = None
    (h, w) = image.shape[:2]
    if width is None and height is None:
        return image
    if width is None:
        r = height / float(h)
        dim = (int(w * r), height)
    else:
        r = width / float(w)
        dim = (width, int(h * r))
    resized = cv2.resize(image, dim, interpolation=inter)
    return resized

====================== T H E E N D ===========================

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章