基于opencv4x --- python3.11实现信用卡卡号识别

1.导言:

        版本:opencv-python      4.8.1.78      python  3.11(csdn也有类似的例子,不过版本比较古早,还是自己动手实现了一下)

        这个小的功能demo主要是使用cv的模板匹配功能matchTemplate函数,对于一些有固定模板的文字、图片,可以参考下述代码来利用模板实现。(ps:虽然使用torch来训练模型可以获得更具有泛用性功能,但是对于信用卡这类有个固定文字格式的图片,使用模板匹配来的更加方便些)

2.实现思路     

        首先,下图是一张visa的信用卡,这便是我们的输入,我们想要获取的便是红色框内的数字

        那么我们第一步就是要想着如何去获取这个红色的框,cv中一般我们可以利用边界检测来获取到边界信息,最好的预计情况就是将背景的灰色去除,留下白色、金色的文字信息,此时文字会有大段的边界是紧挨着的,我们可以利用膨胀后腐蚀,也就是闭操作来获取紧挨着的边界块。然后更具边界块的大小进行筛选出我们所需要的文字部分。最后利用模板数字来实现文字识别。

模板数字:

3.代码实现

3.1 一些常用函数申明

import cv2
import numpy as np
#基本就这两个库的使用,后面代码就不重复强调了哈
#展示cv图片的
def cv_show(name,img):
    cv2.imshow(name,img)
    cv2.waitKey(0)
    cv2.destroyWindow(name)
#根据给的width或height做等比例缩放
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

#用于对轮廓进行排序的函数
def sort_contours(contours,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 contours] #用一个最小的矩形,把找到的形状包起来x,y,h,w
    (cnts, boundingBoxes) = zip(*sorted(zip(contours, boundingBoxes),
                                        key=lambda b: b[1][i], reverse=reverse))

    return cnts, boundingBoxes

简单说下:
cv_show:用来展示图片

sort_contours(contours,method = 'left-to-right')  ---->cnts,boundingBoxes :
用来对传入的轮廓进行从左到右/从上到下的排序,cnts:是排序后的轮廓列表,boundingBoxes  是对应的边界矩形列表

resize(image, width=None, height=None, inter=cv2.INTER_AREA)----->image  :
用来对传入图像按给出的长或宽进行等比例缩放,输出为缩放后的图像

3.2 模板的特征提取:
        

        首先来看一下我们的模板,很明显我们可以通过边缘检测,来拉取文字的边界,最后只要更具轮廓来画出边界矩形列表,我们就能将其分割为0-9模板了。

代码实现:

temp_img = cv2.imread(r'.\images\ocr_a_reference.png')
gray_temp_img = cv2.cvtColor(temp_img,cv2.COLOR_BGR2GRAY)
#阈值处理
ref = cv2.threshold(gray_temp_img,10,255,cv2.THRESH_BINARY_INV)[1]#二值化,反着来的
#边缘检测
contours,_= cv2.findContours(ref.copy(),cv2.RETR_EXTERNAL,cv2.CHAIN_APPROX_SIMPLE)
cv2.drawContours(temp_img,contours,-1,(0,0,255),3)
#轮廓排序
contours,_ = sort_contours(contours,method='left-to-right')
digits = {}
for (i,c) in enumerate(contours):
    (x,y,w,h) = cv2.boundingRect(c)
    roi = ref[y:y+h,x:x+w]
    roi = cv2.resize(roi,(57,88)) #把框放大点
    # cv_show('roi',roi)
    digits[i] = roi

简要分析:

1.temp_img用来存储读取的模板图片,然后将其转换成灰度图(这一步是不能省的,看似转换后都一样,实际上灰度图只有单色通道,后面的阈值处理也必须是灰度图的输入)。
2.然后进行阈值处理。(ps:cv2.threshold返回值有两个,我们只需要第二个返回值也就是处理后的对象即可。)
3.cv2.findContours完成边缘检测,并将边缘绘制出来(画不画无所谓,但可以看效果)


4.调用sort_contours(自定义的排序函数)对检测出的边缘排序
5.最后我们对排序后的轮廓,利用boundingRect函数来计算边界矩形,将每个数字对应的矩形存储进digits字典中。这样我们就获得了数字模板。大致如下图所示:

3.3 预处理信用卡图片:

代码实现:

img = cv2.imread(r'.\images\credit_card_04.png')
img = resize(img,width=300 )#按比例缩放,给出wid或length就像
img_copy = img.copy()
gray_img = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)
#突出特征,礼帽操作
tophat = cv2.morphologyEx(gray_img,cv2.MORPH_TOPHAT,rectKernel)
#边界检测
gradX = cv2.Sobel(tophat,ddepth=cv2.CV_32F,dx=1,dy=0,ksize=-1)#ksize=-1 用3*3的
#取绝对值
gradX = np.absolute(gradX)
#归一化
(minVal,maxVal) = (np.min(gradX),np.max(gradX))
gradX = (255*(gradX-minVal)/(maxVal-minVal))
gradX = gradX.astype("uint8")

        这个过程没啥好说的了基本注释都写了,唯一的注意点是,一般来说我们使用Sobel算子来计算边界的时候,都会是先对x计算再对y计算,然后两者按比例结合,不过这里对y使用Sobel算子,效果很差,所以我就只使用了x方向的。

礼帽后图像:

边界提取后:

        看上去很模糊对吗,但是别忘了,我们不是要准确的提取每个有效的轮廓,而是要确定它们所在的位置。

3.4 确定有效数字位置

        还记的开头所说的么,你看我们获得上图那个模糊的轮廓图后,我们一旦使用闭操作(先膨胀后腐蚀),那么是不是只要我们选择的卷积核大小合适,就能够把连串的轮廓合并到一块。(ps:卷积核大小是按经验和实际效果来设定的)

代码实现:

#定义了两种不同的卷积核
rectKernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE,(9,3))
sqeKernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE,(7,7))

gradX = cv2.morphologyEx(gradX,cv2.MORPH_CLOSE,rectKernel)
#二值化
thresh = cv2.threshold(gradX,0,255,cv2.THRESH_BINARY|cv2.THRESH_OTSU)[1]
#存在空隙,感觉不想一个大的矩形,那就再来一个闭操作
thresh = cv2.morphologyEx(thresh,cv2.MORPH_CLOSE,sqeKernel)

这是闭操作后的图像:

        我们已经大致的获得了一些块区域了,但似乎不是那么饱满,那么我们可以再来一次闭操作,来使内容更加饱满些,以便于后面更精确的框定区域

第二次闭操作:

        是不是这样看起来就顺眼多了,那么正常来说需要多少次闭操作才能够比较完美的获取,这就需要针对样本做一下小批量的实验了,就我手中数据,我选了30个左右,包含了所有0-9的数字,基本两次就有不错的效果了。获得了上图我们该如何获取区域呢,哎,对了再来一次边界检测不就好了。

thresh_contours,_ = cv2.findContours(thresh.copy(),cv2.RETR_EXTERNAL,cv2.CHAIN_APPROX_SIMPLE)
thresh_img=img_copy
cv2.drawContours(thresh_img,thresh_contours,-1,(0,0,255),2)

3.5 候选框筛选

        获得上图后,我们只需要筛选每个边框哪些符合我们的需求不就好了,我这里使用了实际的宽高比,宽度、长度3个条件进行筛选,当然可以更具情况设定不同的筛选条件。

代码实现:

locs=[] #存储想要的区域
for (i,c) in enumerate(thresh_contours):
    (x,y,w,h) = cv2.boundingRect(c)
    ar = w/float(h)  #宽高比
    if(w>40 and w<55) and (h>10 and h<20):
        locs.append((x,y,w,h))
#获取了4个轮廓,顺便排个序,其实也就是4个参数,x,y以及宽和高
locs= sorted(locs,key=lambda x:x[0])

3.6 卡号识别

        还记得吗,第一步我们利用模板提取了特征存贮到了roi参数中

代码实现:

output = []#存储卡号
for (i,(gX,gY,gW,gH)) in enumerate(locs):
    groupOutput=[]
    group = gray_img[gY-5:gY+gH+5,gX-5:gX+gW+5] #放宽了5个像素,防止要素缺失
    # cv_show('',group)
    #trheshold 0 表示系统自动获取阈值
    group = cv2.threshold(group,0,255,
                          cv2.THRESH_BINARY|cv2.THRESH_OTSU)[1]
    cv_show('group',group)
    group_contours,_ = cv2.findContours(group.copy(),cv2.RETR_EXTERNAL,
                                        cv2.CHAIN_APPROX_SIMPLE)
    digit_contours = sort_contours(group_contours,method='left-to-right')[0]
    for c in digit_contours:
        (x, y, w, h) = cv2.boundingRect(c)
        roi = group[y:y + h, x:x + w]
        roi = cv2.resize(roi, (57, 88)) #和模板resize成一致的大小
        # 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(img, (gX - 5, gY - 5),
                  (gX + gW + 5, gY + gH + 5), (0, 0, 255), 1)
    cv2.putText(img, "".join(groupOutput), (gX, gY - 15),
                cv2.FONT_HERSHEY_SIMPLEX, 0.65, (0, 0, 255), 2)

# 得到结果
    output.extend(groupOutput)

简单说下过程:
1.我们通过之前存储的x,y,w,h信息,把对应的框从图上抠出来


2.在for c in digit_contours:循环中,我们将扣除来的图,在进行分割,还是老套路,边缘提取,然后画矩形,最后根据矩形把数字给抠出来,这里有个注意点,我们之前在做模板特征提取的是时候,其实有一个步骤是将图片给扩大到57*88的大小,我们既然要做模板匹配,也要同步大小才行。


3.最后用matchTemplate函数做一个匹配度测算,然后选取最高的作为识别结果,然后输出即可,效果图:

完整代码附上:这个数据集太好找了,网上随便找点信用卡图片就行,我就不贴上了

import cv2
import numpy as np

def sort_contours(contours,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 contours] #用一个最小的矩形,把找到的形状包起来x,y,h,w
    (cnts, boundingBoxes) = zip(*sorted(zip(contours, 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
#指定信用卡类型
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.destroyWindow(name)
temp_img = cv2.imread(r'.\images\ocr_a_reference.png')
# test = cv2.imread(r'.\images\credit_card_05.png')
# cv_show('img',temp_img)
gray_temp_img = cv2.cvtColor(temp_img,cv2.COLOR_BGR2GRAY)
# cv_show('gray',gray_temp_img)
#二值化
ref = cv2.threshold(gray_temp_img,10,255,cv2.THRESH_BINARY_INV)[1]#二值化,反着来的
# cv_show('ref',ref)
#轮廓检测CHAIN_APPROX_SIMPLE只记录端点  RETR_EXTERNAL只计算外轮廓
contours,_ = cv2.findContours(ref.copy(),cv2.RETR_EXTERNAL,cv2.CHAIN_APPROX_SIMPLE)
#(0,0,255)描画颜色 3 划线线段像素
cv2.drawContours(temp_img,contours,-1,(0,0,255),2)
# cv_show('img',temp_img )
contours,_ = sort_contours(contours,method='left-to-right')#对轮廓进行排序,注意idex=0表示的是9的轮廓
digits = {}
# cv2.drawContours(copy,contours,contourIdx=0,color = (0,0,255),thickness=3)
# cv_show('',copy)
#获取到了模板的图片
for (i,c) in enumerate(contours):
    (x,y,w,h) = cv2.boundingRect(c)
    roi = ref[y:y+h,x:x+w]
    roi = cv2.resize(roi,(57,88)) #把框放大点
    # cv_show('roi',roi)
    digits[i] = roi
#生成一个形态学操作中的结构元素(类似于卷积核)
rectKernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE,(9,3))
sqeKernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE,(7,7))



img = cv2.imread(r'.\images\credit_card_04.png')
# cv_show('credit',img)
img = resize(img,width=300 )#按比例缩放,给出wid或length就像
img_copy = img.copy()
gray_img = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)
# cv_show('gray',gray_img)

#礼帽操作,突出更明亮的区域
tophat = cv2.morphologyEx(gray_img,cv2.MORPH_TOPHAT,rectKernel)
# _,threshold_img = cv2.threshold(gray_img,150,255,cv2.THRESH_BINARY)
# tophat = threshold_img
cv_show('tophat',tophat)
# cv_show('',threshold_img)

#边界检测
gradX = cv2.Sobel(tophat,ddepth=cv2.CV_32F,dx=1,dy=0,ksize=-1)#ksize=-1 用3*3的
#300*188
#绝对值化
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)
thresh = cv2.threshold(gradX,0,255,cv2.THRESH_BINARY|cv2.THRESH_OTSU)[1]
cv_show('thresh',thresh)
#存在空隙,感觉不想一个大的矩形,那就再来一个闭操作
thresh = cv2.morphologyEx(thresh,cv2.MORPH_CLOSE,sqeKernel)
cv_show('thresh2',thresh)

#然后再生成轮廓框
thresh_contours,_ = cv2.findContours(thresh.copy(),cv2.RETR_EXTERNAL,cv2.CHAIN_APPROX_SIMPLE)
thresh_img=img_copy
cv2.drawContours(thresh_img,thresh_contours,-1,(0,0,255),2)
cv_show('img',thresh_img)

locs=[] #存储想要的区域
for (i,c) in enumerate(thresh_contours):
    (x,y,w,h) = cv2.boundingRect(c)
    ar = w/float(h)  #宽高比
    if(w>40 and w<55) and (h>10 and h<20):
        locs.append((x,y,w,h))
#获取了4个轮廓
locs= sorted(locs,key=lambda x:x[0])


output = []
for (i,(gX,gY,gW,gH)) in enumerate(locs):
    groupOutput=[]
    group = gray_img[gY-5:gY+gH+5,gX-5:gX+gW+5] #放宽了5个像素,防止要素缺失
    # cv_show('',group)
    #trheshold 0 表示系统自动获取阈值
    group = cv2.threshold(group,0,255,
                          cv2.THRESH_BINARY|cv2.THRESH_OTSU)[1]
    cv_show('group',group)
    group_contours,_ = cv2.findContours(group.copy(),cv2.RETR_EXTERNAL,
                                        cv2.CHAIN_APPROX_SIMPLE)
    digit_contours = sort_contours(group_contours,method='left-to-right')[0]
    for c in digit_contours:
        (x, y, w, h) = cv2.boundingRect(c)
        roi = group[y:y + h, x:x + w]
        roi = cv2.resize(roi, (57, 88)) #和模板resize成一致的大小
        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(img, (gX - 5, gY - 5),
                  (gX + gW + 5, gY + gH + 5), (0, 0, 255), 1)
    cv2.putText(img, "".join(groupOutput), (gX, gY - 15),
                cv2.FONT_HERSHEY_SIMPLEX, 0.65, (0, 0, 255), 2)

# 得到结果
    output.extend(groupOutput)
print(output)
# 打印结果
# print("Credit Card Type: {}".format(FIRST_NUMBER[output[0]]))
# print("Credit Card #: {}".format("".join(output)))
cv2.imshow("Image", img)
cv2.waitKey(0)

ps:emmm都看到这里了,点个赞呗

        好吧,再写点现在想到的扩充,譬如我们可以通过Torch来训练一个模型,这个模型可以找到一张图片中的信用卡的区域,并识别是否是正面,label大概就是4个坐标点+0表示反面 1 表示正面如果是正面,我们做个透视变换,然后套上去,也就是一个拍照获取卡号的小项目了。

  • 1
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

渊兮旷兮

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值