文章目录
信用卡图片
数字模板图片
一、数字模板图片处理步骤
1,读取模板图片
2,灰度处理图片
3,二值化处理图片
4,检测数字轮廓图片
二、识别信用卡图片处理步骤
1,图片信用卡
2,图片灰度处理
3,图片礼帽操作
4,图片sobel算子检测轮廓
5,图片字符矩形区域
6,图片闭操作
7,图片计算轮廓
7,图片分割字符
下面的图片和上面类似,因此省略
8,图片识别结果
9,图片打印结果
Credit Card Type: Visa
Credit Card #: 4000123456789010
文字
#import myutils # 我将唐雨迪这个myutils.py复制到了imutils文件夹下
#通过from imutils import myutils调用myutils.py模块/我是天才!
# ocr信用卡数字识别/形态学操作
# 模板匹配/模板匹配要求必须标准数字标准模板/
# 模板匹配对模板图片要求高
# 模板数字不太规则/不能使用轮廓检测/使用轮廓外接矩形或者外接圆此时是外接矩形
# 首先轮廓检测会得到/内轮廓和外轮廓我们需要外轮廓/进行匹配之前需要进行诸多前期步骤
# 灰度图到二值图到
# 模板信用卡数字是白色,二值化图片数字也是白色,其余黑色
"""""""""
#使用ecplisseIDE来写/适用ecplisseIDE
# 设置参数/模板匹配的工作/两个参数/一个是输入/一个是模板/下面代码不适合pycharmIDE
ap = argparse.ArgumentParser() #
ap.add_argument("-i", "--image", required=True,# i必须是一个字符
help="path to input image")# help相当于注释
ap.add_argument("-t", "--template", required=True,# t必须是一个字符
help="path to template OCR-A image")# help相当于注释
args = vars(ap.parse_args())
"""""""""
"""
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
# 下面这一步计算外接矩形,xy是矩形左上角坐标点,hw是垂直高度水平宽度,boundingBoxes是一个元组元组包含四个值
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
resize(src,dst,Size dsize, double fx=0,double fy=0,int interpolation=INTER_LINEAR);
dsize——输出图像大小Size(width,height),若为Size(),则由下式计算:
dsize=Size(round(fx*src.cols),round(fy*src.rows)) 其中,dsize、fx、fy都不为0
fx,fy——沿x、y方向的缩放系数,默认0,默认计算如下:
(double)dsize.width/src.cols (double)dsize.height/src.rows
nterpolation——插值方式,默认INTER_LINEAR线性插值【放大图像,推荐】
其他可选插值方式:
INTER_NEAREST 最邻近插值i
INTER_AREA 区域插值(利用像素区域关系的重采样插值)【缩小图像,推荐】
INTER_CUBIC 三次样条插值(超过4*4像素邻域内的双三次插值)
INTER_LANCZOS4——Lanczos插值(超过8*8像素邻域的Lanczos插值)
OpenCV提供了resize函数来改变图像的大小,函数原型如下:
void resize(InputArray src, OutputArray dst, Size dsize, double fx=0, double fy=0, int interpolation=INTER_LINEAR );
先解释一下各个参数的意思:
src:输入,原图像,即待改变大小的图像;
dst:输出,改变大小之后的图像,这个图像和原图像具有相同的内容,只是大小和原图像不一样而已;
dsize:输出图像的大小。如果这个参数不为0,那么就代表将原图像缩放到这个Size(width,height)指定的大小;
如果这个参数为0,那么原图像缩放之后的大小就要通过下面的公式来计算:
dsize = Size(round(fx*src.cols), round(fy*src.rows))
其中,fx和fy就是下面要说的两个参数,是图像width方向和height方向的缩放比例。
fx:width方向的缩放比例,如果它是0,那么它就会按照(double)dsize.width/src.cols来计算;
fy:height方向的缩放比例,如果它是0,那么它就会按照(double)dsize.height/src.rows来计算;
使用注意事项:
1. dsize和fx/fy不能同时为0,要么你就指定好dsize的值,让fx和fy空置直接使用默认值,就像
resize(img, imgDst, Size(30,30));
要么你就让dsize为0,指定好fx和fy的值,比如fx=fy=0.5,那么就相当于把原图两个方向缩小一倍!
2. 至于最后的插值方法,正常情况下使用默认的双线性插值就够用了。
几种常用方法的效率是:最邻近插值>双线性插值>双立方插值>Lanczos插值;
但是效率和效果成反比,所以根据自己的情况酌情使用。
3. 正常情况下,在使用之前dst图像的大小和类型都是不知道的,类型从src图像继承而来,大小也是从原图像根据参数计算出来。
但是如果你事先已经指定好dst图像的大小,那么你可以通过下面这种方式来调用函数:
"""
代码(个人注释版本)
# 导入工具包
from imutils import contours
from imutils import myutils
import numpy as np
import argparse
import cv2
# 指定信用卡类型
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("E:/jupyter-notebook/ocr_a_reference.png")# 读模板图像/img边界都是255白色
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所以要copy一下/检测外轮廓为了检测外接矩形/返回值与输入参数没有关系
# 保留轮廓坐标点,CHAIN_APPROX_SIMPLE执行完之后返回三个参数,我们只需要轮廓refCnts,其余两个不用
ref_, refCnts, hierarchy = cv2.findContours(ref.copy(), cv2.RETR_EXTERNAL,cv2.CHAIN_APPROX_SIMPLE)
cv2.drawContours(img,refCnts,-1,(0,0,255),3)# 外轮廓只保留红色通道/refCnts是个三维矩阵
print("1",ref_)# 二值图
print("2",refCnts)# 一堆轮廓点 列表格式
print("3",hierarchy)# 层级用不到
"""
1)这里必须将灰度图转为彩色图(例如该例中的BGR),若无,则无法绘制轮廓线。//
2)下面代码语句是等效的,因函数 drawContours() 会修改原/ image 值(等于返回值)/,
先检测轮廓,再绘制轮廓//
cv2.drawContours(color1, contours, -1, (0,255,0), 2)
color2 = cv2.drawContours(color, contours, -1, (0,255,0), 2)
print(id(color1))
print(id(color2))
运行
2524168987312
2524168987312
findContours函数和drawContours函数是画轮廓很方便的函数
drawContours函数参数详解:
其中第一个参数image表示目标图像,
第二个参数contours表示输入的轮廓组,每一组轮廓由点vector构成,
第三个参数contourIdx指明画第几个轮廓,如果该参数为负值,则画全部轮廓,
第四个参数color为轮廓的颜色,
第五个参数thickness为轮廓的线宽,如果为负值或CV_FILLED表示填充轮廓内部,
第六个参数lineType为线型,
第七个参数为轮廓结构信息,
第八个参数为maxLevel
"""
cv_show('img',img)
print (np.array(refCnts).shape)# 打印出shape==(10,)/就是10个轮廓/这个形式是元组
# 说明排sort序,就是将检测的第一个轮廓赋值1后面模板匹配表示它就是数字1,以此类推,myutils包我已放在最下面
refCnts = myutils.sort_contours(refCnts, method="left-to-right")[0] #排序,从左到右,从上到下
print("5555",refCnts) # 我明白了,处理后的refCnts是一个元组,里面是是个array[]数组是三维数组,处理前refCnts是列表
digits = {} #是字典
# 遍历每一个轮廓
for (i, c) in enumerate(refCnts):# i=0,c=轮廓 此时refCnts是元组,默认i从0标号遍历
# 计算外接矩形并且resize成合适大小
(x, y, w, h) = cv2.boundingRect(c) #(x, y, w, h)类似元组不可改变,将xywh都赋值
roi = ref[y:y + h, x:x + w]# roi就是框住我们感兴趣区域 xy与hw
roi = cv2.resize(roi, (57, 88))# 放缩一下这个框大小,测试了一下,55是x方向像素点,88是y方向像素点
# cv_show('image222', roi)
# 每一个数字对应每一个模板
digits[i] = roi # 字典映射,这一个是将0对应0模板图像矩阵放到数组中,1对应等等,执行完i=10for循环跳出,所以是集合模板不能重复
# 初始化卷积核,用于形态学操作
rectKernel = cv2.getStructuringElement(cv2.MORPH_RECT, (9, 3))# 9*3卷积核算子
sqKernel = cv2.getStructuringElement(cv2.MORPH_RECT, (5, 5))# 5*5卷积核算子
#读取输入图像,预处理
#image = cv2.imread(args["image"])
image = cv2.imread("E:/jupyter-notebook/credit_card_01.png")
print(type(image)) # 打印输出变量类型
print(image.shape) # 打印输出变量高宽通道数目/就是图片高宽/RGB三通道
print(image.size) # 大小==高*宽*通道
print(image.dtype) # 字节位数占多少/就是每个像素点8位表示
cv_show('image',image)
image = myutils.resize(image, width=300)# 这一步缩小了图片,按比例放缩,原图是(368, 583, 3)
# 输入300就是原来图像宽度变为300,然后300/原来宽度*原来高度就是现在的高度值
#cv_show('sss',image)
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)# 得到灰度图
cv_show('gray',gray)
#礼帽操作,突出更明亮的区域,也可以不进行这一步或者用其他方法
#这一步过后白色字体更显白,其余背景变为黑色,礼帽得到迪哥两字的毛刺
tophat = cv2.morphologyEx(gray, cv2.MORPH_TOPHAT, rectKernel)
cv_show('tophat',tophat)
#图像梯度算子,用于边缘检测,右边-左边,这里作者发现只用效果更好,CV_32F是32位单精度通常为-1
#负数截断,大于255为255,ddepth=cv2.CV_32F是高级写法表示能表示负数就是位数更多也表示输入输出图像深度相同
gradX = cv2.Sobel(tophat, ddepth=cv2.CV_32F, dx=1, dy=0, #ksize=-1相当于用3*3的
ksize=-1)
# cv_show('tophat111',gradX)
gradX = np.absolute(gradX)# 求绝对值
(minVal, maxVal) = (np.min(gradX), np.max(gradX))# 双阈值检测
gradX = (255 * ((gradX - minVal) / (maxVal - minVal)))
gradX = gradX.astype("uint8") # 将所有算子边界右-左差值都按比例变为255以内值
print (np.array(gradX).shape)# (189, 300)
cv_show('gradX',gradX)# 得到所有字符的轮廓
#通过闭操作(先膨胀,再腐蚀)将数字连在一起,迪哥就是白字黑背景,闭操作就是增加毛刺程度
gradX = cv2.morphologyEx(gradX, cv2.MORPH_CLOSE, rectKernel)
cv_show('gradX',gradX)# 这一步之后,将白色轮廓模糊在了一起,就像连在了一块
#THRESH_OTSU会自动寻找合适的阈值,适合双峰,需把阈值参数设置为0,就是自动找出适合的阈值进行二值分割
#详情参考这篇博客https://blog.csdn.net/liyuanbhu/article/details/49387483?utm_source=app
thresh = cv2.threshold(gradX, 0, 255,# opencv系统自动判断阈值
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 # threshCnts是计算出的轮廓值,cnts是列表
#print("cnts",cnts)
cur_img = image.copy()# 不会影响到image图像,copy原因是因为执行完之后画轮廓,cur_img会改变并且保存下来防止这个出现
cv2.drawContours(cur_img,cnts,-1,(0,0,255),3)# 画出轮廓,用红色通道画,3是线宽
cv_show('img',cur_img)# 是将计算出的轮廓画在初始图像当中,cur_img是cope复制
locs = []# 待会将一些数值填进来有价值数据/定义列表
"""
draw_img = img.copy() # 上面与这些等价
res = cv2.drawContours(draw_img, [cnt], -1, (0, 0, 255), 2)# 会改变输入图draw_img/-1所有轮廓/0第一个轮廓外圈/1第一个轮廓内圈/
cv_show(res,'res')# 2第二个轮廓外圈/...... # 左下角轮廓开始向右数 0就是左下角第一个轮廓外圈
"""
# 遍历轮廓
for (i, c) in enumerate(cnts):# 1从0开始,c就是各个轮廓,c就是xy ccc (93, 155, 96, 11),此时cnts是列表也是符合for循环
# for (i, c) in enumerate(cnts,1)代表从1开始打印默认为0,上面是从0开始打印就是,0对应cnts中第一个
# 计算矩形
(x, y, w, h) = cv2.boundingRect(c)# c经过这个函数后返回xywh
# print("ccc",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))#详见博客https://blog.csdn.net/qq_36357820/article/details/77527081?utm_source=app
# 将符合的轮廓从左到右排序
locs = sorted(locs, key=lambda x:x[0])# x[0]意思是按照二维数组每个元素的第一个数字进行排序也就是xywh中x,x[1]是第二个
output = []# 定义列表,locs是上面定义的列表
print("locs",locs)# locs [(24, 102, 50, 14), (90, 102, 51, 14), (157, 101, 51, 15), (224, 102, 52, 14)]
# 遍历每一个轮廓中的数字
for (i, (gX, gY, gW, gH)) in enumerate(locs):# 默认从0开始遍历losc中转到(gX, gY, gW, gH)此时locs是列表
# initialize the list of group digits
groupOutput = []
# 根据坐标提取每一个组
group = gray[gY - 5:gY + gH + 5, gX - 5:gX + gW + 5]# gray是最上面的灰度图,只不过此时缩小框住了数字部分
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,# 现在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(): # digits是最开始模板中数字的字典,定义也是字典,item()是将字典中key和value组成一个元组
# 模板匹配 digit对应字典中钥匙,digitROI是钥匙对应的值
result = cv2.matchTemplate(roi, digitROI,
cv2.TM_CCOEFF)# 6个方法,
(_, score, _, _) = cv2.minMaxLoc(result)
scores.append(score)# append是向列表最后一位添加元素就是分数
# 得到最合适的数字
groupOutput.append(str(np.argmax(scores)))#取最大的数//最终这个数组会存储12个数字也就是12个最大符合度
# 画出来
cv2.rectangle(image, (gX - 5, gY - 5),
(gX + gW + 5, gY + gH + 5), (0, 0, 255), 1)#红色框,1是线宽 gX这四个是最开始的四个大矩形框
cv2.putText(image, "".join(groupOutput), (gX, gY - 15),# 输出文本数字
cv2.FONT_HERSHEY_SIMPLEX, 0.65, (0, 0, 255), 2)
# 得到结果
output.extend(groupOutput)#因为列表groupOutput中有12个数据,所以必须使用extend()函数进行导入到output列表里
# 打印结果
print("Credit Card Type: {}".format(FIRST_NUMBER[output[0]]))# 意思是根据output[0]数值来打印信用卡类型
print("Credit Card #: {}".format("".join(output)))
cv2.imshow("Image", image)
cv2.waitKey(0)
代码(原版,无个人注释)
# 导入工具包
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("E:/4-Project/4-OpenCV-pic/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("E:/4-Project/4-OpenCV-pic/credit_card_01.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)