信用卡数字识别 opencv python 实现 形态学操作、轮廓提取,模板匹配

基于python和opencv实现信用卡数字识别

本项目和源码来源于唐宇迪opencv项目实战

1.准备工作

  • python3.5
  • opencv4.2
  • 同一字体,相似类型的信用卡图片
  • 字体模板图片信用卡图片字体模板图片

2.本项目总体可分为两部分:

  • 对模板图像的处理
  • 对信用卡图像的处理

3.应用到的数字图像处理基础知识

  • 图像灰度处理
  • 图像二值化处理
  • 检测轮廓,画出轮廓
  • 画图形的外接矩形
  • 图像的形态学操作

4.先放代码, 我把每一步生成的图片都显示出来,以便观察每一步操作所起的作用

# 导入工具包
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"
}
# 参数为读取的图像image和模板图像template
template = cv2.imread(args["template"])
# 将模板转化为灰度图
template_gray = cv2.cvtColor(template, cv2.COLOR_BGR2GRAY)
# 将模板转化为二值图像
# threshold函数应该返回两个值,第二个值为输出图像,所以有[1]
# threshold操作,超过thresh的部分取最大值,否则取零,然后再对图像进行反转
template_thresh = cv2.threshold(template_gray, 10, 255, cv2.THRESH_BINARY_INV)[1]


# 计算轮廓,findcontours()函数只接受二值图像作为参数
refCnts, hierarchy = cv2.findContours(template_thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
template_c = template.copy()
# 参数resCnts表示存储在列表中的提取到的所有轮廓,参数-1表示默认画出所有轮廓
template_draw = cv2.drawContours(template_c, refCnts, -1, (255, 170, 0), 3)
print('refCnts', np.array(refCnts).shape)
# 轮廓的排序方式,从左到右,从上到下
# 对轮廓进行排序, 为了将轮廓和对应的数字匹配,即在图像中与数字的模板匹配则显示对应的数字结果
# 根据外接矩形返回的值,比较横坐标大小,对轮廓进行排序
refCnts = myutils.sort_contours(refCnts, method="left-to-right")[0]
# 新建一个空字典
digits = {}
# 用循环遍历每一个轮廓
# 此时轮廓的列表中内容已排序完成,
# i表示下标,c表示i对应数值的模板的图像
for (i, c) in enumerate(refCnts):
    # 计算外接矩形并且resize成合适大小
    (x, y, w, h) = cv2.boundingRect(c)
    # 用外接矩形当做每一个数字的模板
    # 利用外接矩形返回的值制作出每一个数字的模板
    roi = template_thresh[y:y + h, x:x + w]
    roi = cv2.resize(roi, (57, 88))
    # 每个数字对应一个模板
    digits[i] = roi
    cv2.imshow("roi", roi)
    cv2.waitKey(100)

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

#读取输入的图像进行预处理,进行一系列形态学操作
image = cv2.imread(args["image"])
image_resize = myutils.resize(image, width = 300)
image_gray = cv2.cvtColor(image_resize, cv2.COLOR_BGR2GRAY)

# 礼帽操作,突出明亮的区域,滤除部分干扰信息
# 其中卷积核的大小由要保留的字体大小的具体情况决定
image_tophat = cv2.morphologyEx(image_gray, cv2.MORPH_TOPHAT, rectKernel)
#ksize=-1相当于用3*3的,锐化操作
image_sobel = cv2.Sobel(image_tophat, ddepth=cv2.CV_32F, dx=1, dy=0,
	ksize=-1)

gradX = np.absolute(image_sobel)
(minVal, maxVal) = (np.min(gradX), np.max(gradX))
gradX = (255 * ((gradX - minVal) / (maxVal - minVal)))
gradX = gradX.astype("uint8")
print ("gradx.shape", np.array(gradX).shape)
# 通过闭操作(先膨胀,再腐蚀)将数字连在一起
image_close = cv2.morphologyEx(
    gradX, cv2.MORPH_CLOSE, rectKernel)
#THRESH_OTSU会自动寻找合适的阈值,适合双峰,让opencv自动判断阈值,所以第二个参数为0
image_thresh = cv2.threshold(image_close, 0, 255,
	cv2.THRESH_BINARY | cv2.THRESH_OTSU)[1]
# 再进行一个闭操作
image_close_2 = cv2.morphologyEx(image_thresh, cv2.MORPH_CLOSE, sqKernel) #再来一个闭操作


# 计算轮廓
threshCnts, hierarchy = cv2.findContours(image_close_2.copy(), cv2.RETR_EXTERNAL,
	cv2.CHAIN_APPROX_SIMPLE)

cnts = threshCnts
cur_img = image_resize.copy()
cv2.drawContours(cur_img, cnts, -1, (0, 0, 255), 3)
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))
print('locs', len(locs))
# 将符合的轮廓从左到右排序
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 = image_gray[gY - 5:gY + gH + 5, gX - 5:gX + gW + 5]
    group_thresh = cv2.threshold(group, 0, 255,
        cv2.THRESH_BINARY | cv2.THRESH_OTSU)[1]
    cv2.imshow("groupthresh", group_thresh)
    cv2.waitKey(1000)
    # 计算每一组的轮廓
    digitCnts, hierarchy = cv2.findContours(group_thresh.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))
        cv2.imshow("roi_r", roi)
        cv2.waitKey(500)
        # 计算匹配得分
        scores = []
        # 在模板中计算每一个得分
        for (digit, digitROI) in digits.items():
            # 模板匹配
            result = cv2.matchTemplate(roi, digitROI,
                                       cv2.TM_CCOEFF)
            #返回四个值min,max,minlocation, maxlocation
            (_, score, _, _) = cv2.minMaxLoc(result)
            scores.append(score)

        # 得到最合适的数字
        # 函数np.argmax()作用是输出最大值所对应的索引
        # 而索引与模板表示的值相对应
        groupOutput.append(str(np.argmax(scores)))
    # 画出轮廓
    image_loc = image_resize.copy()
    cv2.rectangle(image_resize, (gX - 5, gY - 5), (gX+gW+5, gY+gH +5), (0, 0, 255), 2)
    cv2.putText(image_resize, "".join(groupOutput), (gX, gY - 15),
                cv2.FONT_HERSHEY_SIMPLEX, 0.65, (13, 158, 153), 2)
    # 得到结果
    output.extend(groupOutput)


# 打印结果
print("Credit Card Type: {}".format(FIRST_NUMBER[output[0]]))
print("Credit Card #: {}".format("".join(output)))


cv2.imshow("1template", template)
cv2.imshow("2template_gray", template_gray)
cv2.imshow("3template_thresh", template_thresh)
cv2.imshow("4template_draw", template_draw)
cv2.imshow("5original_image", image)
cv2.imshow("6image_gray", image_gray)
cv2.imshow("7image_tophat", image_tophat)
cv2.imshow("8image_sobel", image_sobel)
cv2.imshow("9gradX", gradX)
cv2.imshow("10image_close", image_close)
cv2.imshow("11image_thresh", image_thresh)
cv2.imshow("12image_close_2",image_close_2)
cv2.imshow("13cur_img", cur_img)
cv2.imshow("14group", group)
cv2.imshow("15group_thresh", group_thresh)
cv2.imshow('16roi', roi)
cv2.imshow("Image_loc", image_loc)
cv2.imshow("gradX", gradX)
cv2.waitKey(0)
cv2.destroyAllWindows()
# 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

5.代码实现步骤如图所示

代码实现步骤

2.1 处理模板图片

处理模板图片最终目的是用每一个数字的模板,与待识别的信用卡图像进行模板匹配,最后识别出具体的数字。
我们要做的就是把模板图片中的每一个数字分割开,并把每一个数字图像与其所代表的值对应起来。

第一步 提取轮廓

使用opencv中 的findContours()函数检测轮廓,由于该函数只能输入二值图像,所以用cvtcolor(img, cv.GBR2GRAY)函数将图像转化为灰度图,再用thresh()函数将图像转化成二值图像。由于thresh函数有两个返回值,我们需要的只有返回的二值图像,所以在程序中该条语句的末尾加上了[1],就是将第二个返回值传递给了img_thresh。
使用cv2.drawContours()函数画出轮廓,参数-1表示画出所有轮廓,在绘制轮廓,要将原图复制,否则原图会被更改。
绘制轮廓结果如图所示
轮廓检测

第二步 轮廓排序与数字匹配

提取到的轮廓都保存在了refCnts中,但我们想要轮廓以0数字的轮廓、1数字的轮廓、2数字的轮廓…9数字的轮廓的顺序排列,以便后续将模板与数字对应。
在自己定义的myutils.py ,resize函数中,前面已经得到了图像中的所有轮廓,resize()函数中做出每一个轮廓的外接矩形,有4个返回值,前两个返回值表示外接矩形左上角的点。由于本实例中模板图像中数字横向排列,我们只需提取出每个轮廓外接矩形左上角点的横坐标,从小到大,即从左到右进行排列。
建立一个空的字典,在循环中枚举所有轮廓和其对应的索引,之前已经返回了每一个轮廓,在循环中做每一个小轮廓的外接矩形,返回值是每个小轮廓的外接矩形左上角的坐标点和矩形的宽和高。

for (i, c) in enumerate(refCnts):
    # 计算外接矩形并且resize成合适大小
    (x, y, w, h) = cv2.boundingRect(c)
    # 用外接矩形当做每一个数字的模板
    # 利用外接矩形返回的值制作出每一个数字的模板
    roi = template_thresh[y:y + h, x:x + w]
    roi = cv2.resize(roi, (57, 88))
    # 每个数字对应一个模板
    digits[i] = roi
    cv2.imshow("roi", roi)
    cv2.waitKey(100)

图像切片的操作就是把模板图像中的每一个数字单独抠出来,变成单独的一张图片,接着对每一张小的图片resize(),这里没有特别要求,建议电脑屏幕放得下就行。但是这个大小后面还会用到,digits是刚刚定义的一个空的字典,因为前面已经对轮廓排好了顺序,这里只需要将这些轮廓依照下标索引放进digits中,然后在整个程序运行的时候,会一次从0到9显示每张小图片。waitKey()参数设置成100了。
到这里就处理完模板图片了。

2.2 处理信用卡图片

信用卡图片的处理比处理模板麻烦一丢丢。

第一步 读取信用卡图像进行一系列的形态学操作

在程序中首先定义了两个卷积核,用于后续的TOPHAT顶帽操作、和形态学闭操作。resize()后width=300,则整张图按照原比例缩小。
顶帽操作突出明亮区域,滤除干扰信息。sobel算子滤波进行锐化,本实例中的sobel只对水平方向进行滤波。结果如图所示
水平方向锐化滤波结果
进行二值化的程序中用到了THRESH_OTSU(大津法) 这是一种图像二值化的方法,又称为最大类间方差法。这种方法使得前景和背景类间方差最大。这种方法适合双峰,能够自动寻找合适的阈值,第二个参数设为0,并不是将阈值设为0,而是让opencv自动检测阈值。
https://www.cnblogs.com/ranjiewen/p/6385564.html

image_thresh = cv2.threshold(image_close, 0, 255,
	cv2.THRESH_BINARY | cv2.THRESH_OTSU)[1]

做形态学闭操作,相邻较近 的图像连在一起,根据信用卡数字的特征将每四个数字连成一个整体,再进行二值化。结果如图所示
形态学闭操作结果
我们关注的是数字部分的图像,为填补数字部分图像的空洞,确保数字部分连成一个整体,再进行一次形态学闭操作,结果如图所示:
第二次形态学闭操作结果

第二步 只保留数字部分的图像,滤除其他干扰

二值化以后的图像进行轮廓检测画出图像,步骤与模板图像的轮廓检测类似。但此时图像中不仅有数字部分图像还有其他的图案。我们只关注数字部分的图像确切地说是四个数字连在一起的那部分图像,要将数字图像与其他图像分离,就要找出我们要保留的部分有什么特征。它们可以看做是长宽比固定的矩形,根据这一特征我们将数字部分的图像与干扰图像分割开,滤除干扰图像。
做出每一个提取到的轮廓的外接矩形,并且计算每一个外接矩形的长宽比,如果满足条件则是我们希望保留的数字部分的图像。将数字部分的(四个数字连成一组的)图像外接矩形保存在locs中,这里我们打印了一下len(locs),如果为4则正确。
同样按照横坐标从左到右的方法对locs中的外接矩形进行排序
接着遍历locs中的每一个外接矩形,对每一个外接矩形进行进一步操作。因为此时每一个外接矩形内部都包含4个数字。我们对resize 后的源图像进行操作。上一步保存了四个外接矩形的定位点和长宽值。
这段程序是对四个数字一组的轮廓标记在resize后的灰度图上,此时应该用切片提取出这四个图像。此时(发现上一步进行的一系列操作就是为了返回四个外接矩形信息,用来进一步提取出每一个数字的图像)

for (i, (gX, gY, gW, gH)) in enumerate(locs):
    # initialize the list of group digits
    groupOutput = []
    # 从灰度图像中 将每一个数字分割开
    group = image_gray[gY - 5:gY + gH + 5, gX - 5:gX + gW + 5]
    group_thresh = cv2.threshold(group, 0, 255,
        cv2.THRESH_BINARY | cv2.THRESH_OTSU)[1]
    cv2.imshow("groupthresh", group_thresh)
    cv2.waitKey(1000)

运行结果如图(这里只显示其中的一张图片):
上一步的轮廓截取的数字图片
接下来终于到了最后一层。对于上一步的到的四个数字组成的图像,首先对其进行轮廓检测,画出轮廓, 同样的,得到的轮廓我们要用的是它的返回值,利用这个返回值进行对检测到的轮廓按照横坐标进行排序,同样利用轮廓的返回值检测外接矩形(此时外接矩形中 只包含一个数字了。)
还记得在分割模板数字图像时resize的大小吗?没错现在需要把分割完的图像也resize成与模板图像相同的大小(57, 88)

第三步 进行模板匹配

现在我们得到了每一个数字的模板图片和信用卡图片中的每一个数字的图片
模板匹配的程序段

scores = []
        # 在模板中计算每一个得分
        for (digit, digitROI) in digits.items():
            # 模板匹配
            result = cv2.matchTemplate(roi, digitROI,
                                       cv2.TM_CCOEFF)
            #返回四个值min,max,minlocation, maxlocation
            (_, score, _, _) = cv2.minMaxLoc(result)
            scores.append(score)

模板匹配选择较大结果还是较小的结果与选择的方法有关。共有6种方法,本实例中选择的是cv2.TM_CCOEFF,该方法计算相关系数,值越大越相关。
最后,将resize后的原图像上画出每一个数字的外接矩形轮廓(这里外接矩形向外延伸了5个像素点)
在图像上外接矩形上方15个像素点的位置添加文字

join()方法——用于将序列中的元素以指定的字符连接生成一个新的字符串
join()方法语法:str.join(sequence),sequence为要连接的元素序列。 一种格式化字符串的函数
str.format() format 函数可以接受不限个参数,位置可以不按顺序。

https://blog.csdn.net/weixin_41874898/article/details/99624454
最后现实的结果如图所示:

显示结果

补充内容:
这里用python切片操作对图像进行分割,这里切片操作纵坐标在前,横坐标参数在后
在这里还验证了一下参数的位置

import cv2
img = cv2.imread("xxx.jpg")
print(img.shape)
qiepian = img[0:100, 100:200]
qiepian_1 = img[100:200, 0:100]
cv2.imshow("original", img)
cv2.imshow("qiepian", qiepian)
cv2.imshow("qiepian_1", qiepian_1)
cv2.waitKey(0)
cv2.destroyAllWindows()

因为切片从开始就一定是在图像的边缘,运行结果如图所示:
验证切片操作
因为shape()函数返回的就是(h, w, c)嘛
imshow(“qiepian”, qiepian)截取的是眼睛那部分,纵坐标从0开始,验证完毕,切片操作参数是纵坐标在前横坐标在后。黄轩好帅:)

至此,信用卡数字识别内容完成,感谢为我提供帮助的博客,谢谢你们的辛勤耕耘,受益匪浅。

武汉加油!

发布了10 篇原创文章 · 获赞 0 · 访问量 219

猜你喜欢

转载自blog.csdn.net/weixin_43227526/article/details/104653957