【实战】OpenCV+Python项目实战--信用卡数字识别


参考网址: https://blog.csdn.net/qiao_lili/article/details/89371418

0 摘要

对信用卡图片上数字进行识别。

1 准备工作(python)

1.1 argparse用法

使用argparse模块创建一个ArgumentParser解析对象,可以理解成一个容器,将包含将命令行解析为Python数据类型所需的所有信息。
parse_args()是将之前add_argument()定义的参数进行赋值,并返回相关的namespace。

import argparse

parser = argparse.ArgumentParser()
# # 括号少了会报错AttributeError: 'str' object has no attribute 'prefix_chars'
parser.add_argument("-i", "--image", required=True, help="path to input image")
# -i可以理解为标签,--image既可以是标签又是属性
parser.add_argument("-t", "--template", required=True, help="path to template OCR-A image")
args = vars(parser.parse_args()) # print(args["image"])
# args为字典{'image': 'images/credit_card_01.png', 'template': 'images/ocr_a_reference.png'}
args = parser.parse_args()  # print(args.image)
# Namespace(image='images/credit_card_01.png', template='images/ocr_a_reference.png')
# -t 和 --train两种情况,在bat文件和pycharm配置种注意区分前面的两个--还是一个-

pycharm下可以通过argparse模块完成参数设置,即生成全局变量

在这里插入图片描述

1.2 zip 与 zip*用法

zip() 函数用于将可迭代的对象直观理解就是能用for循环进行迭代的对象就是可迭代对象。比如:字符串,列表,元祖,字典,集合等等,都是可迭代对象。)作为参数,将对象中对应的元素打包成一个个元组,然后返回由这些元组组成的列表。

如果各个迭代器的元素个数不一致,则返回列表长度与最短的对象相同,利用 * 号操作符,可以将元组解压为列表。(个人理解,将两组元素,分别各成元元组组合成一个列表)

zip 方法在 Python 2 和 Python 3 中的不同:在 Python 3.x 中为了减少内存,zip()
返回的是一个对象。如需展示列表,需手动 list() 转换。

a = [(1, 2), (2, 3), (3, 4)]  
# 类似这些都是可以迭代的元祖,字符串等等。a = ((1, 2), (2, 3), (3, 4)), a="abc"
b = [(5, 6), (7, 8), (9, 9)]
print(zip(a, b))  # <zip object at 0x000001B5EB0CA0C8>
ret = list(zip(a, b))  
# 输出 :[((1, 2), (5, 6)), ((2, 3), (7, 8)), ((3, 4), (9, 9))]
ret1 = list(zip(*ret))  
# 或者写成这样:ret1 = list(zip(*(zip(z,b)))  
# 输出: [((1, 2), (2, 3), (3, 4)), ((5, 6), (7, 8), (9, 9))]
1.3 sorted用法

#基础用法
#传进去一个可迭代的数据,返回一个新的列表,按照从小到大排序,注意,是新的列表!

a = [1, 4, 6, 8, 9, 3, 5]
b = "aghdb"
sorted(a)  # print(a)不变,返回sorted(g)变; [1, 3, 4, 5, 6, 8, 9]
sorted(b)  # 返回['a', 'b', 'd', 'g', 'h']
sorted(a, reverse=True)  # 逆序排序; [9, 8, 6, 5, 4, 3, 1]

高级用法
列表里面的每一个元素都为二维元组,key参数传入了一个lambda函数表达式,其x就代表列表里的每一个元素,然后分别利用索引返回元素内的第一个和第二个元素,这就代表了sorted()函数根据哪一个元素进行排列。reverse参数起逆排的作用,默认为False,从小到大顺序。

c = [("a", 1), ("e", 2), ("c", 4)]
print(sorted(c, key=lambda x: x[0]))
print(sorted(c, key=lambda x: x[1]))
print(sorted(c, key=lambda x: x[0], reverse=True))
1.4 items()用法

D.items()
Python 字典 items() 方法,以列表形式返回可遍历的(键, 值) 元组数组,(并非直接的列表,若要返回列表值还需调用list函数)。

D = {'Google': 'www.google.com', 'Runoob': 'www.runoob.com', 'taobao': 'www.taobao.com'}
print(D.items())
print(list(D.items()))
# 遍历字典列表
for key, value in D.items():
    print(key, value)
# dict_items([('Google', 'www.google.com'), ('Runoob', 'www.runoob.com'), ('taobao', 'www.taobao.com')])
# [('Google', 'www.google.com'), ('Runoob', 'www.runoob.com'), ('taobao', 'www.taobao.com')]
# Google www.google.com   Runoob www.runoob.com  taobao www.taobao.com
1.5 join()用法

join()方法——用于将序列中的元素以指定的字符连接生成一个新的字符串
join()方法语法:str.join(sequence),sequence为要连接的元素序列。

str = "-"
seq = ("a", "b", "c") # 字符串序列
# c = [1, 2, 3] 数字不行,变成# c = ["1", "2", "3"]
print(str.join(seq))  # 输出结果为a-b-c
# print("-".join(seq))  直接这样写也行
# print(“”.join(seq))  输出结果为abc
1.6 extend()用法

extend() 函数用于在列表末尾一次性追加另一个序列中的多个值(用新列表扩展原来的列表)。
extend()方法语法:list.extend(seq)

a = [1, "a", "ad", "fd"]
b = ["d", "d", "d"]

a.extend(b)  # 注意这个函数没有返回值,直接在a上面变化。
print(a)

a1 = []
a1.extend(a)
print(a1)
1.7 format用法

一种格式化字符串的函数 str.format()
format 函数可以接受不限个参数,位置可以不按顺序。

print("hello:{}{}".format("111", "222"))
# hello:111222
print("hello:{1}{0}".format("111", "222"))
# hello:222111可以改变顺序
print("hello:{name},{age}".format(name="111", age="222"))
# hello:111,222可以设置参数
print("{:.2f}".format(3.123222))  # 3.12

2 准备工作(opencv)

2.1 cv2.getStructuringElement()

kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE,(11,11))

这个函数的第一个参数表示内核的形状,有三种形状可以选择。

矩形:MORPH_RECT;

交叉形:MORPH_CROSS;

椭圆形:MORPH_ELLIPSE;

第二和第三个参数分别是内核的尺寸以及锚点的位置。一般在调用erode以及dilate函数之前,先定义一个Mat类型的变量来获得

getStructuringElement函数的返回值:
对于锚点的位置,有默认值Point(-1,-1),表示锚点位于中心点。element形状唯一依赖锚点位置,其他情况下,锚点只是影响了形态学运算结果的偏移。

rectKernel = cv2.getStructuringElement(cv2.MORPH_RECT, (9, 3))  
# (9, 3)是指的矩阵的宽w和高h
# rectKernel为:
# [[1 1 1 1 1 1 1 1 1]
#  [1 1 1 1 1 1 1 1 1]
#  [1 1 1 1 1 1 1 1 1]]
# 形状(3, 9)
# 可以用于初始化卷积核
2.2 cv2.resize()

cv2.resize(InputArray src, OutputArray dst, Size, fx, fy, interpolation)

InputArray src 输入图片
OutputArray dst 输出图片 (可省)
Size 输出图片尺寸 (w, h)宽和高
fx, fy 沿x轴,y轴的缩放系数 interpolation 插入方式
interpolation 选项所用的5种插值方法:
cv2.INTER_NEAREST 最近邻插值
cv2.INTER_LINEAR 双线性插值(默认设置)
cv2.INTER_AREA 使用像素区域关系进行重采样。
cv2.INTER_CUBIC 4x4像素邻域的双三次插值
cv2.INTER_LANCZOS4 8x8像素邻域的Lanczos插值

import cv2
image = cv2.imread("images/credit_card_01.png")
cv2.imshow("image", image)
image_resize = cv2.resize(image, (200, 100), interpolation=cv2.INTER_LINEAR)
cv2.imshow("image_resize", image_resize)
image_resizes = cv2.resize(image, (0, 0), fx=0.5, fy=1)  # x轴缩放0.5,y不进行缩放
cv2.imshow("image_resizes", image_resizes)
cv2.waitKey(0)
2.3 cv2.findContours() & cv2.drawContours()

binary, contours, hierarchy = cv2.findContours(src_bi, cv2.RETR_TREE, cv2.CHAIN_APPROX_NONE)

src_bi二值图,轮廓检测针对二值图像来做

在这里插入图片描述
NONE输出所有点,SIMPLE只保留他们的终点。
binary是返回的二值图
contours返回一个list,list中每个元素都是图像中的一个轮廓,用numpy中的ndarray表示。
hierarchy结果,这是一个ndarray,其中的元素个数和轮廓个数相同,每个轮廓contours[i]对应4个hierarchy元素hierarchy[i][0] ~hierarchy[i][3],分别表示后一个轮廓、前一个轮廓、父轮廓、内嵌轮廓的索引编号,如果没有对应项,则该值为负数

cv2.drawContours(image, contours, contourIdx, color[, thickness[,
lineType[, hierarchy[, maxLevel[, offset ]]]]])

第一个参数是指明在哪幅图像上绘制轮廓; 第二个参数是轮廓本身,在Python中是一个list。
第三个参数指定绘制轮廓list中的哪条轮廓,如果是-1,则绘制其中的所有轮廓。后面的参数很简单。其中thickness表明轮廓线的宽度,如果是-1(cv2.FILLED),则为填充模式。绘制参数将在以后独立详细介绍。

src = cv2.imread("pic/c.jpg")
src_gray = cv2.cvtColor(src, cv2.COLOR_BGR2GRAY)
ret, src_bi = cv2.threshold(src_gray, 127, 256, cv2.THRESH_BINARY)
binary, contours, hierarchy = cv2.findContours(src_bi, cv2.RETR_TREE, cv2.CHAIN_APPROX_NONE)  # binary就是灰度图
print(src_bi == binary)  # 全是True,说明binary就是,src_bi
src1 = src.copy()  # 需要复制一下原图,否则在原图上画轮廓的时候会改变原图
res = cv2.drawContours(src1, contours, -1, (255, 0, 0), 2) 
# -1表示画出所有的轮廓,若-1换成是0,1,2..就是画出所有图像的边缘。也可以将contours换成contours[0],contours[1]来显示第几张图

cont = contours[0]  # 0表示第0个特征,将具体的每一个轮廓
print(cv2.contourArea(cont))  # 计算面积
print(cv2.arcLength(cont, True))  # 计算周长,True表示闭合
cv2.imshow("src", src)
cv2.imshow("res", res)

3 代码实现

具体过程不讲述,代码种有详细注释,一步一步的调试

3.1文件目录与图片展示

images目录中ocr_a_reference.png是模板图片,其余图片是待检测的图片,代码中,只使用了credit_card_01.png,其余代码可用于测试。
1.jpg是代码执行时生成的图片,用于测试图中,待检测部分的像素点,保存图片用画图软件打开即可查看。
2主程序:为ocr_template_match.py
3辅助代码:myutils.py(注意!这个程序不是python的包)

图片展示:

  1. 信用卡照片credit_card_01.png
  1. 数字模板照片ocr_a_reference.png
  1. 代码生成图片1.jpg
3.2代码展示

1. ocr_template_match.py

# 导入工具包
from imutils import contours
import numpy as np
import argparse
import cv2
import myutils

# 设置参数
# 在pycharm中,Edit Configurations中配置下面参数
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")
ar = ap.parse_args()
args = vars(ap.parse_args())  # print(args["image"])
# "第一种方式:"
# args = vars(ap.parse_args())
# args为字典{'image': 'images/credit_card_01.png', 'template': 'images/ocr_a_reference.png'}
# 调用方式:print(args["image"])
# "第二种方式:"
# args = ap.parse_args()
# Namespace(image='images/credit_card_01.png', template='images/ocr_a_reference.png')
# 调用方式:print(args.image)


# 指定信用卡类型
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"])
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]  # 法1c
ret, ref = cv2.threshold(ref, 10, 255, cv2.THRESH_BINARY_INV)  # 法2, 10值越小,保留的黑越黑,INV后变成白色
# ref要么是0要么是255
# ret返回的是阈值;ref返回的是二值化后的图像矩阵,是二维矩阵。
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)
ref_, refCnts, hierarchy = cv2.findContours(ref.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)
# ref_二值图, refCnts是边缘轮廓,返回值形状为(10,),表示10个形状,每个里面是一个列表,列表里是具体每一个轮廓的值。
# cv2.RETR_EXTERNAL获取外轮廓, cv2.RETR_TREE获取所有轮廓
cv2.drawContours(img, refCnts, -1, (0, 0, 255), 3)  # -1表示画出所有轮廓
cv_show('img', img)
print(np.array(refCnts).shape)
refCnts = myutils.sort_contours(refCnts, method="left-to-right")[0]  # 排序,指定 从左到右 还是 从上到下
# refCnts找到的轮廓不一定就是,从左往右123,现在找到的10个轮廓顺序乱序,需要将轮廓从左往右对应拍成0,1,2..等等
digits = {}

# 遍历每一个轮廓
for (i, c) in enumerate(refCnts):  # (i, c)加不加括号都是一样的
	# 计算外接矩形并且resize成合适大小
	(x, y, w, h) = cv2.boundingRect(c)
	roi = ref[y:y + h, x:x + w]
	roi = cv2.resize(roi, (57, 88), cv2.INTER_LINEAR)
	cv_show("roi", roi)
	# 每一个数字对应每一个模板
	digits[i] = roi
print(digits)

# 初始化卷积核
rectKernel = cv2.getStructuringElement(cv2.MORPH_RECT, (9, 3))
# (9, 3)是指的矩阵的宽w和高h, 9是因为想让横向的信息多保留?
# rectKernel为:
# [[1 1 1 1 1 1 1 1 1]
#  [1 1 1 1 1 1 1 1 1]
#  [1 1 1 1 1 1 1 1 1]]
# 形状(3, 9)

sqKernel = cv2.getStructuringElement(cv2.MORPH_RECT, (5, 5))

# 读取输入图像,预处理
image = cv2.imread(args["image"])
cv_show('image', image)
image = myutils.resize(image, width=300)  # resize前(368, 583, 3),resize后(189, 300, 3)
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
# cv_show('gray', gray)
cv_show('gray', gray)

# gradX = cv2.morphologyEx(tophat, cv2.MORPH_CLOSE, rectKernel)
# # 此处的卷积核为(9, 3),全1
# cv_show('gradX', gradX)  # 此处做闭操作也行
# 闭操作就是把字连接起来

# 顶帽操作,突出更明亮的区域;为了突出字体,所以核(9,3)和字体部分差不多比例
tophat = cv2.morphologyEx(gray, cv2.MORPH_TOPHAT, rectKernel)   # 可以对灰度图进行操作
cv_show('tophat', tophat)

# gradX = cv2.morphologyEx(tophat, cv2.MORPH_CLOSE, rectKernel)
# # 此处的卷积核为(9, 3),全1
# cv_show('gradX', gradX)  # 此处做闭操作也行

gradX = cv2.Sobel(tophat, ddepth=cv2.CV_32F, dx=1, dy=0, ksize=-1)  # ksize=-1相当于用3*3的
cv_show('gradX1', gradX)
gradX = np.absolute(gradX)  # 这个是(0,3040)之间的浮点数
(minVal, maxVal) = (np.min(gradX), np.max(gradX))
gradX = (255 * ((gradX - minVal) / (maxVal - minVal)))  # 这个是(0,255)之间的浮点数
gradX = gradX.astype("uint8")
cv_show('gradX', gradX)

# 通过闭操作(先膨胀,再腐蚀)将数字连在一起
gradX = cv2.morphologyEx(gradX, cv2.MORPH_CLOSE, rectKernel)
# 此处的卷积核为(9, 3),全1
cv_show('gradX', gradX)

# THRESH_OTSU会自动寻找合适的阈值,适合双峰,需把阈值参数设置为0
thresh = cv2.threshold(gradX, 0, 255, cv2.THRESH_BINARY+cv2.THRESH_OTSU)[1]
# cv_show('thresh', thresh)
cv2.imshow('thresh2', 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)
cv2.imwrite("1.jpg", 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)  # 由于使用的是相关性,所以是去最大值即可
			# min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(result)
			# # 返回的res 中min_loc代表每一个滑动窗口左上角坐标的值,min_val代表损失
			# print(score)
			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)
	# cv2.FONT_HERSHEY_SIMPLEX为字体,常用还有cv2.FONT_HERSHEY_COMPLEX

	# 得到结果
	print(groupOutput)
	output.extend(groupOutput)
	print(output)

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

2. 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
    # print(cv2.boundingRect(cnts[0]))  # (730, 20, 54, 85)
    print(list(zip(cnts, boundingBoxes)))
    print(zip(cnts, boundingBoxes))
    (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

3. 识别效果图
在这里插入图片描述

  • 10
    点赞
  • 86
    收藏
    觉得还不错? 一键收藏
  • 9
    评论
评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值