文章目录
- 代码
- 视频讲解
- 模板匹配文件
- 主程序(ocr_template_match.py)
- myutils.py
代码
链接: https://pan.baidu.com/s/1KjdiqkyYGfHk97wwgF-j3g?pwd=hhkf 提取码: hhkf
视频讲解
链接: https://pan.baidu.com/s/1PZ6w5NcSOuKusBTNa3Ng2g?pwd=79wr 提取码: 79wr
模板匹配文件
主程序(ocr_template_match.py)
# 导入工具包
from imutils import contours # 从imutils库导入contours模块,imutils是一个方便的图像处理库,contours模块主要用于处理和操作图像轮廓。
import numpy as np
import argparse # 处理命令行参数
import cv2
import myutils# 设置参数
ap = argparse.ArgumentParser() # 创建一个ArgumentParser对象,这个对象会保存命令行参数和程序文档等信息# 添加一个命令行参数,参数的短名称是"-i",长名称是"--image",这个参数是必须的(required=True),参数的含义是输入图像的路径。
ap.add_argument("-i", "--image", required=True,help="path to input image")# 添加另一个命令行参数,参数的短名称是"-t",长名称是"--template",这个参数也是必须的,参数的含义是模板图像的路径。
# 这个模板图像是OCR字体的图像,其中包含了0-9的数字。
ap.add_argument("-t", "--template", required=True,help="path to template OCR-A image")# 解析命令行参数,然后将结果转为一个字典,字典的键是参数的名称,值是参数的值。
args = vars(ap.parse_args())
print('\n', args) # {'image': 'images/credit_card_03.png', 'template': 'images/ocr_a_reference.png'}# 指定信用卡类型
"""
定义了一个名为FIRST_NUMBER的字典,这个字典中的键是信用卡卡号的首位数字的字符串形式,对应的值是信用卡的类型。
这是因为信用卡的种类可以通过卡号的首位数字来区分。
比如credit_card_03.png里面的信用卡的首位数字是5,那么这张信用卡就是MasterCard
"""
FIRST_NUMBER = {"3": "American Express","4": "Visa","5": "MasterCard","6": "Discover Card"
}
# 绘图展示函数
def cv_show(name,img):cv2.imshow(name, img) # name: 窗口名称; img: 要展示的图像cv2.waitKey(0)cv2.destroyAllWindows()cv2.waitKey(1) # 对于MacOS系统,cv2.waitKey(1)是必要的,用于处理一些特定的系统事件,但在Windows上不一定需要。# 读取一个模板图像。使用OpenCV的imread函数读取模板图像,图像的路径从前面解析的命令行参数args中获取。
img = cv2.imread(args["template"])
cv_show('img',img)# 灰度图。将模板图像转换为灰度图像,使用OpenCV的cvtColor函数,COLOR_BGR2GRAY表示从BGR颜色空间转换到灰度空间。
ref = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
cv_show('ref',ref) # 展示灰度图像"""
将灰度图像转换为二值图像,使用OpenCV的threshold函数。
10是阈值,255是当像素值高于(对于THRESH_BINARY_INV)或者小于(对于THRESH_BINARY)阈值时应给予的新的像素值 ,
THRESH_BINARY_INV表示进行反二进制阈值化(低于阈值的像素设置为255,高于阈值的像素设置为0)。
"""
ref = cv2.threshold(ref, 10, 255, cv2.THRESH_BINARY_INV)[1]
cv_show('ref',ref) # 展示二值图像"""
计算轮廓
使用OpenCV的findContours函数计算二值图像的轮廓,RETR_EXTERNAL表示只检测外轮廓,CHAIN_APPROX_SIMPLE表示只保留终点坐标
返回的refCnts中每个元素都是图像中的一个轮廓
"""
refCnts, hierarchy = cv2.findContours(ref.copy(), cv2.RETR_EXTERNAL,cv2.CHAIN_APPROX_SIMPLE)
"""
cv2.drawContours函数用来在图像上画出找到的轮廓。第一个参数img是要绘制轮廓的图像,第二个参数refCnts是从前面找到的轮廓列表,
第三个参数-1表示要绘制所有轮廓,(0,0,255)是轮廓的颜色(这里是红色),3是线宽。
"""
cv2.drawContours(img, refCnts, -1, (0, 0, 255), 3)
cv_show('img', img) # 展示绘制了轮廓的图像"""
refCnts = myutils.sort_contours(refCnts, method="left-to-right")[0] 这行代码的主要作用是对模板图像中的轮廓进行排序。在这里,myutils.sort_contours(refCnts, method="left-to-right")[0] 是调用 sort_contours 函数,对 refCnts(模板图像中的轮廓)进行排序。method="left-to-right" 参数表示轮廓应该按照从左到右的顺序排序。这对于在图像中识别和分类对象(如数字)时非常有用,因为我们通常会按照从左到右的顺序进行阅读和识别。sort_contours 函数的返回值是一个元组,其中包含两个元素:排序后的轮廓和对应的边界框。由于我们只关心排序后的轮廓,所以使用 [0] 索引来获取第一个元素,即排序后的轮廓。因此,这行代码的结果 refCnts 是一个列表,其中的轮廓已经按照从左到右的顺序进行了排序。这样处理之后,程序可以按照这个顺序逐个处理和识别每个数字。
"""
refCnts = myutils.sort_contours(refCnts, method="left-to-right")[0] # 排序,从左到右,从上到下
# cv2.drawContours(img, refCnts, 0, (255, 0, 0), 2)
# cv_show('img', img) # 同学们这里可以修改method看看输出会有什么区别digits = {} # 定义一个空字典digits,将用来存储每个数字模板。
# 遍历每一个轮廓
for (i, c) in enumerate(refCnts): # 遍历轮廓列表 refCnts。在每一次循环中,索引 i 和对应的轮廓 c 都被提取出来。# 计算外接矩形并且resize成合适大小(x, y, w, h) = cv2.boundingRect(c) # 对于每一个轮廓 c,计算其外接矩形,外接矩形的坐标被存储在 (x, y) 中,矩形的宽度和高度分别是 w 和 h# 使用刚刚计算出来的外接矩形的坐标和大小,从灰度图像 ref 中提取出对应的区域,这就是我们的 ROI。# 在OpenCV和很多其他图像处理库中,图像的索引方式通常为 [y:y+h, x:x+w],这样的顺序:先行(高度)后列(宽度)roi = ref[y:y + h, x:x + w]roi = cv2.resize(roi, (57, 88)) # 调整 ROI 的大小,使得每一个数字模板的大小都是一致的(这里是 57x88 像素)。# 将提取和调整大小后的 ROI 存储在字典 digits 中,字典的键是数字的索引 i,值是对应的数字模板。digits[i] = roi# 初始化卷积核
# 这两行代码生成了两个矩形的结构元素(也称为卷积核或结构核),分别为9x3和5x5的大小。这些结构元素将在接下来的形态学操作中使用。
rectKernel = cv2.getStructuringElement(cv2.MORPH_RECT, (9, 3))
sqKernel = cv2.getStructuringElement(cv2.MORPH_RECT, (5, 5))#读取输入图像,预处理
image = cv2.imread(args["image"])
cv_show('image',image)
image = myutils.resize(image, width=300) # 用自定义的resize函数将图像的宽度缩放到300像素,高度按比例缩放
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) # 将缩放后的彩色图像转换为灰度图像。在OpenCV中,颜色图像通常是BGR格式,而在很多图像处理任务中,我们只需要灰度图像。
cv_show('gray',gray)# 礼帽操作:原始输入-开运算(先腐蚀,在膨胀)的结果,突出更明亮的区域;黑帽操作常常用于强调比周围暗的区域,或者是强调比较小,且比周围区域暗的细节部分。
# 这行代码对灰度图像执行了一个名为"礼帽"的形态学操作,这是通过使用前面定义的9x3的矩形卷积核对图像进行开运算,然后从原始图像中减去结果来实现的
tophat = cv2.morphologyEx(gray, cv2.MORPH_TOPHAT, rectKernel)
cv_show('tophat',tophat)# 这行代码对进行了"礼帽"操作的图像应用Sobel算子,以计算图像的x方向(水平方向)的梯度。
# 这里ksize=-1表示使用3x3的Sobel滤波器。
gradX = cv2.Sobel(tophat, ddepth=cv2.CV_32F, dx=1, dy=0, #ksize=-1相当于用3*3的ksize=-1)
gradX = np.absolute(gradX) # 这行代码将Sobel梯度的值转换为绝对值,因为梯度值可能是正值也可能是负值,转换为绝对值使所有梯度值都为正,方便后续处理。
"""
这三行代码对梯度图像进行了归一化和量化处理。
首先找到梯度图像的最小值和最大值,然后将梯度图像的亮度值线性映射到0-255的范围,以便显示和保存。
最后,将浮点型的梯度图像转换为8位无符号整型。
"""
(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)# 通过闭操作(先膨胀,再腐蚀)将数字连在一起
# 使用闭操作来连结图像中的数字。闭操作是先进行膨胀,然后进行腐蚀。这样可以将断开的部分连在一起,主要用来关闭前景物体内部的小孔或者小黑点。这里使用的是前面定义的rectKernel,一个长方形的结构元素。
gradX = cv2.morphologyEx(gradX, cv2.MORPH_CLOSE, rectKernel)
cv_show('gradX',gradX)"""
使用阈值化操作来将图像转换为二值图像。其中,cv2.THRESH_BINARY表示大于阈值的像素点设为最大值(这里是255),小于阈值的设为0;
cv2.THRESH_OTSU表示使用Otsu算法来自动确定最优阈值,该方法适合对于双峰图像,需要将阈值参数设置为0。
输出的thresh是一个二值化后的图像。
"""
thresh = cv2.threshold(gradX, 0, 255,cv2.THRESH_BINARY | cv2.THRESH_OTSU)[1]
cv_show('thresh',thresh)# 再进行一次闭操作。这次使用的结构元素是sqKernel,一个正方形的结构元素,其目的是使得图像中的数字更加紧密,减小数字间的空隙。
thresh = cv2.morphologyEx(thresh, cv2.MORPH_CLOSE, sqKernel) #再来一个闭操作
cv_show('thresh',thresh)"""
计算图像的轮廓。cv2.findContours函数用来寻找二值图像中的轮廓。
这里使用的是cv2.RETR_EXTERNAL模式,表示只查找最外层的轮廓;cv2.CHAIN_APPROX_SIMPLE表示轮廓的存储方式,这种方式会存储轮廓的转折点,从而节省内存空间。
thresh.copy()是为了保护原图,因为findContours函数会修改输入的图像。
"""
threshCnts, hierarchy = cv2.findContours(thresh.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
cnts = threshCnts
# 在原图上画出找到的轮廓。
# cv2.drawContours函数用来在图像上画出轮廓,-1表示画出所有轮廓,(0,0,255)是轮廓的颜色,这里是红色,3是轮廓的宽度。
cur_img = image.copy()
cv2.drawContours(cur_img,cnts,-1,(0,0,255),3)
cv_show('img',cur_img)# 下面这段代码的主要目的是从所有找到的轮廓中筛选出数字的轮廓,并将这些轮廓按照从左到右的顺序排序。
locs = [] # 初始化一个空列表 locs,用于存储选出来的轮廓。
# 遍历找到的所有轮廓
for (i, c) in enumerate(cnts):# 对于每个轮廓,先计算其外接矩形,得到矩形的坐标 (x, y) 和宽高 (w, h)(x, y, w, h) = cv2.boundingRect(c)"""计算矩形的长宽比(宽度除以高度)。如果一个轮廓的外接矩形的长宽比在一定范围内(比如这里的2.5到4.0),并且宽度和高度也在一定范围内(比如这里的宽度40到55,高度10到20),就认为这个轮廓可能是一个数字的轮廓,将其保存到 locs 中。"""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))# 展示此时的结果clone = image.copy()cv2.rectangle(clone, (x, y), (x + w, y + h), (0, 255, 0), 2)cv_show("Selection", clone)# 对 locs 中的轮廓按照 x 坐标(即从左到右)进行排序。
locs = sorted(locs, key=lambda x:x[0]) # locs是一个包含4个元素的列表
# 展示排序后的结果
for (i, (gX, gY, gW, gH)) in enumerate(locs):clone = image.copy()cv2.rectangle(clone, (gX, gY), (gX + gW, gY + gH), (0, 255, 0), 2)cv_show("Sorted", clone)# 这段代码的目的是遍历存储的符合条件的轮廓(即信用卡上的数字组),对每一组数字进行处理以识别出其中的数字,并将识别的数字显示在图像上。
output = []
# 遍历 locs 列表,它包含了一系列的轮廓(或者说是数字组)。每个轮廓都有自己的坐标 (gX, gY) 和尺寸 (gW, gH)
for (i, (gX, gY, gW, gH)) in enumerate(locs):# 初始化一个空列表 groupOutput,用于保存当前轮廓(数字组)内识别出的数字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)# 在二值化后的图像中寻找轮廓,这个轮廓就对应于信用卡上的每一个数字。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]# 将切出的数字图像的尺寸调整为57x88,这是为了和模板的尺寸一致,从而可以进行模板匹配。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 是最大匹配得分(_, score, _, _) = cv2.minMaxLoc(result)scores.append(score) # 将当前模板的匹配得分添加到得分列表中# 在所有模板的匹配得分中找出最大的那个,最大得分对应的数字就是识别出来的数字。将识别出来的数字添加到当前数字组的输出列表中。groupOutput.append(str(np.argmax(scores)))"""这两行代码的主要目的是在原始图像上标记出识别的数字,并且显示识别结果。1. cv2.rectangle(image, (gX - 5, gY - 5), (gX + gW + 5, gY + gH + 5), (0, 0, 255), 1):这行代码用于在原始图像上画出一个矩形,表示每一组的数字区域。cv2.rectangle()函数的参数是:图像、左上角坐标、右下角坐标、颜色和线条宽度。(gX - 5, gY - 5) 和 (gX + gW + 5, gY + gH + 5) 分别是矩形的左上角和右下角坐标,(0, 0, 255) 是矩形的颜色(这里是红色,因为在OpenCV中,颜色是以BGR而非RGB表示的),1 是线条的宽度。2. cv2.putText(image, "".join(groupOutput), (gX, gY - 15), cv2.FONT_HERSHEY_SIMPLEX, 0.65, (0, 0, 255), 2): 这行代码是将识别的数字添加到图像上。cv2.putText()函数的参数是:图像、文本、位置、字体、字体大小、颜色和线条宽度。"".join(groupOutput) 是要添加的文本,它是将每一组识别的数字拼接成一个字符串;(gX, gY - 15) 是文本的位置,这里选择的是每一组数字的左上角稍微向上一点的位置;cv2.FONT_HERSHEY_SIMPLEX 是字体;0.65 是字体的大小;(0, 0, 255) 是字体的颜色(这里选择的也是红色);2 是线条的宽度。"""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)# 打印出识别出来的信用卡类型和信用卡号。这里 FIRST_NUMBER 是一个字典,它将信用卡号的第一位数字映射到对应的信用卡类型。
print("Credit Card Type: {}".format(FIRST_NUMBER[output[0]]))
print("Credit Card #: {}".format("".join(output)))
cv_show("Image", image) # 显示处理后的图像,可以看到识别出的信用卡号已经被画在了原图上。
myutils.py
import cv2"""
sort_contours函数的作用是对轮廓和其外接矩形进行排序。让我们一步一步地解读:- zip(cnts, boundingBoxes)将轮廓列表cnts和外接矩形列表boundingBoxes合并成一个列表,其中每个元素都是一个由轮廓和其外接矩形组成的元组。- sorted(zip(cnts, boundingBoxes), key=lambda b: b[1][i], reverse=reverse)使用Python的sorted函数对上述列表进行排序。- 排序的关键是key=lambda b: b[1][i],其中b是列表中的一个元素,也就是一个由轮廓和其外接矩形组成的元组,b[1]是外接矩形,b[1][i]是外接矩形的x或y坐标,取决于i的值。- 如果reverse=True,则对结果进行反向排序,即从大到小排序;如果reverse=False,则直接按照从小到大的顺序排序。- zip(*sorted(zip(cnts, boundingBoxes), key=lambda b: b[1][i], reverse=reverse))使用zip函数和*操作符将排序后的列表转换回原来的形式,即分别是轮廓列表和外接矩形列表。- (cnts, boundingBoxes) = zip(*sorted(zip(cnts, boundingBoxes), key=lambda b: b[1][i], reverse=reverse))将返回的轮廓列表和外接矩形列表分别赋值给cnts和boundingBoxes。所以,这行代码的总体作用是按照x或y坐标对轮廓和其外接矩形进行排序,然后返回排序后的轮廓列表和外接矩形列表。怕同学们忘记,补充一个点:
cv2.boundingRect()函数是一个计算轮廓外接矩形的函数,它的参数是一个轮廓。该函数返回一个表示外接矩形的四元组,这四个值分别为(x, y, w, h)。- x 和 y 表示外接矩形左上角点的坐标。
- w 和 h 分别表示外接矩形的宽度和高度。"""
def sort_contours(cnts, method="left-to-right"):reverse = Falsei = 0if method == "right-to-left" or method == "bottom-to-top":reverse = Trueif method == "top-to-bottom" or method == "bottom-to-top":i = 1boundingBoxes = [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"""
resize函数用于调整图像的大小下面这段代码定义了一个叫做resize的函数,这个函数接收一个图像和一个宽度或高度的值(或两者都有),并将这个图像缩放到指定的尺寸。如果没有给出宽度和高度值,那么函数将返回原图。插值方法默认为cv2.INTER_AREA。
"""
def resize(image, width=None, height=None, inter=cv2.INTER_AREA):dim = None # 这里初始化了一个变量dim,它将在后续的代码中被用作储存图像新的尺寸(h, w) = image.shape[:2] # 这里从输入图像的形状(即尺寸)中提取出图像的高度(h)和宽度(w)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))"""这段if-else代码的作用是在保持图像原有长宽比(即纵横比)的条件下,根据给出的新的宽度或高度来计算出新的图像尺寸。例子1:如果只给出了新的高度值 height(即 width 是 None),那么我们需要计算出新的宽度值,方法是:首先,我们计算出新的高度值与原高度 h 的比例 r = height / float(h)。然后,我们根据这个比例来计算新的宽度值 w * r,并将其转化为整数(因为像素值必须是整数)。最后,我们得到新的图像尺寸 dim = (int(w * r), height)。例子2:如果给出了新的宽度值 width,那么我们需要计算出新的高度值,方法是:首先,我们计算出新的宽度值与原宽度 w 的比例 r = width / float(w)。然后,我们根据这个比例来计算新的高度值 h * r,并将其转化为整数。最后,我们得到新的图像尺寸 dim = (width, int(h * r))。这样做的好处是可以保证图像在缩放过程中不会发生扭曲,也就是保持了图像的原有长宽比。"""# 调用cv2.resize函数,将图像缩放到新的尺寸resized = cv2.resize(image, dim, interpolation=inter)"""cv2.resize()是OpenCV库中的一个函数,用于调整图像的大小。该函数接收三个参数:- 第一个参数 image:需要调整大小的原始图像。- 第二个参数 dim:一个包含两个元素的元组,表示新的图像大小。这个大小是你要调整到的目标大小。- 第三个参数 interpolation:插值方法,也就是当我们调整图像大小时,如何计算新的像素值。 cv2.INTER_AREA 是一种插值方法,适合于图像缩小。OpenCV还提供了其他插值方法,如 cv2.INTER_LINEAR(线性插值,也是默认方法)、cv2.INTER_CUBIC(立方插值)等,这些方法在图像放大时可能会有较好的效果。函数的返回值 resized 就是调整大小后的新图像。这个新图像的大小就是 dim 所指定的大小。举个例子,如果你有一个大小为1000x1000的图像,你希望将它缩小到500x500,那么你可以这样调用这个函数:resized_image = cv2.resize(original_image, (500, 500), interpolation=cv2.INTER_AREA)在这个例子中,resized_image 就是大小为500x500的新图像。"""return resized # 返回缩放后的图像