记录一下第一次手动'机器学习'过程
源项目来自B站Python-木子的视频
信用卡/银行卡识别
主体思路
-
指定模板
-
读入模板图片以及该模板图片的灰度图
-
灰度图进行阈值处理
-
查找轮廓并绘制(绘制轮廓只是方便我们用户查看是否查找轮廓正确)
-
按轮廓制作数字模板并排序保存
-
-
目标图片
-
读入目标图片以及该目标图片的灰度图片
-
对图片进行预处理操作(阈值处理,梯度计算,形态学处理)
-
查找轮廓并绘制(绘制轮廓只是方便我们用户查看是否查找轮廓正确)
-
按轮廓筛选并分割成大模板并排序
-
大模板继续分割成一个个单独的数字图片
-
进行模板匹配
-
输出结果
-
具体代码
头文件
import numpy as np import cv2 import myutils
预设函数
def show(img, name='demo'): cv2.imshow(name, img) cv2.waitKey(0) cv2.destroyAllWindows()
对模板图片进行一系列操作
读入模板
template = cv2.imread('../res/template.png')#python读取文件的方式 template_gray = cv2.imread('../res/template.png', 0)#转换为灰度图,当然你也可以使用cv2.cvtColor()进行灰度处理 show(template_gray)
对灰度图进行阈值处理
原图的作用:绘制模板查看具体情况
灰度图的作用:图像处理
template_gray = cv2.threshold(template_gray, 10, 255, cv2.THRESH_BINARY_INV)[1]#注意这个[1],因为该函数返回2个值 show(template_gray)
查找轮廓
template_cnts = cv2.findContours(template_gray.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)[0]
绘制轮廓
cv2.drawContours(template, template_cnts, -1, (0, 0, 255), 2)
定义字典
template_dic = {}#该字典用于存放 每一个轮廓对应的(x,y,w,h)
模板制作与保存
template_cnts = myutils.sort_contours(template_cnts)[0]#自定义sort函数,将轮廓图从左到右排序
# 遍历每一个轮廓,找到每一个模板,并将模板装入到 template_dic 字典中 for (i, template_single_contour) in enumerate(template_cnts):#推荐使用enumerate()将下标值标识为i (x, y, w, h) = cv2.boundingRect(template_single_contour)#boundingRect()函数基于轮廓得到其外围矩形的基本信息 single_template = template_gray[y:y + h, x:x + w]#截图,一般都是处理灰度图 single_template = cv2.resize(single_template, (57, 88)) template_dic[i] = single_template#字典中存放的是 基于轮廓得到的矩形 所绘制的灰度图的部分截图,而不是存放轮廓!
对目标图片进行处理
读入图片
image = cv2.imread('../res/card.png') image = cv2.resize(image, (400, 300))#根据个人喜好进行设置 image_gray = cv2.imread('../res/card.png', 0) image_gray = cv2.resize(image_gray, (400, 300))
自定义卷积核
方便后续图像处理涉及的卷积计算
rectKernel = cv2.getStructuringElement(cv2.MORPH_RECT, (12, 4)) sqKernel = cv2.getStructuringElement(cv2.MORPH_RECT, (5, 5))
#为什么不用rectKernel = np.ones((12, 4), np.uint8)作为卷积? #np.ones()得到的卷积 按行排列 #cv2.getStructuringElement()得到的卷积 按列排列,一般涉及OpenCV我们只需要使用cv2库里面的卷积 #由于它们的排列方式不同,因此产生了不同的效果。为了避免这种情况,建议始终使用cv2.getStructuringElement()函数来创建卷积核,这样可以确保得到的卷积核是按列优先存储的,与OpenCV中其他函数的默认行为相匹配
图片礼帽操作
#对图片进行礼帽处理: #礼帽: 原始输入 - 开运算结果 #目的: 保留并突出 图片中 较为明亮的部分 tophat = cv2.morphologyEx(image_gray, cv2.MORPH_TOPHAT, rectKernel)
梯度计算
计算梯度 类似 查找边缘 , 方便我们后续进行阈值处理
一般用Sobel算子就好了
Scharr算子更加细腻,能捕捉更多细节,更加丰富,计算规则同Sobel
gradX = cv2.Sobel(tophat, ddepth=cv2.CV_32F, dx=1, dy=0, ksize=3) gradX = cv2.convertScaleAbs(gradX) # 这里不能直接np.abs,凡是涉及CV的,一律用CV的库函数,避免出现莫名其妙的BUG gradY = cv2.Sobel(tophat, ddepth=cv2.CV_32F, dx=0, dy=1, ksize=3) gradY = cv2.convertScaleAbs(gradY) gradXY = cv2.addWeighted(gradX, 0.5, gradY, 0.5, 0)#分开计算X,Y的梯度,再加起来,处理效果最好
归一化操作
为什么要归一化操作?
归一化是一种常见的操作,通常用于将图像的像素值范围调整到特定的区间内。
这是因为在进行图像处理算法时,不同的像素值范围可能会导致算法的不稳定性或不准确性。
(minVal, maxVal) = (np.min(gradXY), np.max(gradXY)) gradXY = (255 * ((gradXY - minVal) / (maxVal - minVal)))#这个计算规则常用~ gradXY = cv2.convertScaleAbs(gradXY)#取绝对值 show(gradXY)
闭运算
闭运算:填充图片中的黑色,目的是将目标区域连成一块,方便后续进行轮廓查找
image_close = cv2.morphologyEx(gradXY, cv2.MORPH_CLOSE, rectKernel)
阈值处理
image_threshold = cv2.threshold(image_close, 0, 255, cv2.THRESH_BINARY | cv2.THRESH_OTSU)[1]#让CV算法自己去识别处理
再次闭运算
# 若第一次闭运算填充效果不好,可以再进行几次闭运算,直到目标区域连在一起,方便后续轮廓查找与切割 image_close2 = cv2.morphologyEx(image_threshold, cv2.MORPH_CLOSE, sqKernel)
查找轮廓
image_contours = cv2.findContours(image_close2.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)[0] cv2.drawContours(image, image_contours, -1, (0, 0, 255), 2) show(image)#检查轮廓是否正确
自定义存储变量并保存
locs = [] #用于存放后续的目标矩形(x,y,w,h)
for c in image_contours: (x, y, w, h) = cv2.boundingRect(c) ar = w / float(h) #带一个float 可以直接让 ar 也变为 float类型,更加精确 #经测量得目标区域轮廓的基本范围,我们只需要提取出我们需要的那4个矩形轮廓区域即可 if 2.25 < ar < 3.0: if 25 < h < 30 and 65 < w < 80: locs.append((x, y, w, h)) locs.sort()#内置的sort也会根据位置进行排序,这里是一种取巧的做法,保险起见还是调用myutils里面的sort()方法
图片分割处理与模板匹配
locs_final = [] result = [] temp_max = 0 temp_num = 0
for (i, (gx, gy, gw, gh)) in enumerate(locs):#拿到存放的矩形的信息 group = image[gy - 5:gy + gh + 5, gx - 5:gx + gw + 5]#截图常用语法~ group_gray = image_gray[gy - 5:gy + gh + 5, gx - 5:gx + gw + 5] group_gray = cv2.threshold(group_gray, 0, 255, cv2.THRESH_BINARY | cv2.THRESH_OTSU)[1] show(group_gray)#不出错即为 '4000' group_cnts = cv2.findContours(group_gray.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)[0] cv2.drawContours(group, group_cnts, -1, (0, 0, 255), 2) show(group)#不出意外为 彩图,且'4000'已被识别 并红笔描边~ for c in group_cnts:#对于 '4000' 处理后得到的 4个轮廓 依次处理 (x, y, w, h) = cv2.boundingRect(c)#得到每一个轮廓的外接矩形的信息并用locs_final存储起来 locs_final.append((x, y, w, h)) locs_final.sort()#取巧的排序 for (fx, fy, fw, fh) in locs_final:#这里我们得到了 '4','0','0','0'四个轮廓的外置矩形信息,那么依据它进行模板匹配 ROI = group_gray[fy:fy+fh,fx:fx+fw]#截图 一般是截灰度图,因为我们上面制作的模板截图也是灰度图 ROI = cv2.resize(ROI,(57,88))#模板匹配必须要 统一大小! show(ROI)#逐个展示字符'4','0','0','0' for j in range(10):#这里我们进行模板匹配,由于此前template的模板制备符合数字排序,我们只需要range(10)即可 match = cv2.matchTemplate(ROI,template_dic[j],cv2.TM_CCOEFF)#CCOEFF计算相关系数,越大越好 score = cv2.minMaxLoc(match)[1]#提取两图匹配后的最大值 if score > temp_max: temp_max = score temp_num = j#若从0~9之间哪一个匹配度最高,那么我们就可以确定其为源目标图片该区域的数字 print(temp_num) result.append(temp_num)#这里result列表用来存放得到的结果 temp_max = temp_num = 0 locs_final.clear()#一次模板匹配完后清空换人~
最后识别结果: 4 0 0 0
myutils附录
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