Opencv学习笔记(3)---纸牌数字识别练习实践项目

Opencv学习笔记(3)—纸牌数字识别练习

本来我以为会很简单的,然后实际做发现对我来说还是有点问题,我最初只是想着使用透视变换对不同角度拍照的纸牌首先进行变化,然后直接使用pytesseract库就行了,然后实际操作中发现并不能直接进行OCR变化,没有办法,最后使用模板匹配的方法进行,这次练习最大的收获是发现实操跟看视频差别很大。。。
最后附代码和图片的下载方式

第一步 制作数字和花色模板

先对纸牌进行规范命名:例如K-4,名字+花色,其中 1为红桃 2为方块 3 为梅花 4为黑桃,例图如下:
Opencv学习笔记(3)---纸牌数字识别练习实践项目_第1张图片
然后进行图片预处理,形态学操作,首先把图片经过透视变换转换为规整图像,如下图:
Opencv学习笔记(3)---纸牌数字识别练习实践项目_第2张图片
然后我把图片的中间部分给扣了出来填补为白色,同时只处理图片的上半部分,因为卡牌的左上角跟右下角有相同的花色和卡牌数字,只处理上半部分更方便些:
Opencv学习笔记(3)---纸牌数字识别练习实践项目_第3张图片
然后转换为灰度图,进行二值化处理,因为有的数字特征不太明显,可以进行写形态学操作,我这里使用了膨胀操作,迭代次数为5开始查找轮廓特征,分别框选出数字特征和花色特征,找到后进行规范命名并保存。我筛选的方式是通过轮廓矩形的面积:
Opencv学习笔记(3)---纸牌数字识别练习实践项目_第4张图片
通过这种方法进行13次对13张卡牌(剔除大小鬼牌了)分别操作,得到了卡牌与花色的图片,从1-17进行命名,方便生成模板:
Opencv学习笔记(3)---纸牌数字识别练习实践项目_第5张图片
然后通过Make_Template.py文件对每个小模板进行resize成相同大小,并且拼接为新模版,保存文件:
在这里插入图片描述
到这里,模板生成操作就结束了

第二步 模板操作和模板匹配

首先查找模板特征,得到下面的结果:在这里插入图片描述
然后,先按照面积进行排序,并且取前17个特征,这时候的特征是外面的方块,然后再次按照从左到右的顺序进行排序,这个时候排序的依据按照x的大小进行排序,保证特征的顺序正好从左到右是A到黑桃。
在这里插入图片描述
然后,遍历把上面的结果的每个特征保存起来,然后对于数字和花色分别进行模板匹配就行了,最后得到输出结果。
测试的输入图象:
Opencv学习笔记(3)---纸牌数字识别练习实践项目_第6张图片
测试的输出结果:
在这里插入图片描述

参考代码

代码写的很烂。。。因为把一个main函数完成了很多功能,许多变量都重复使用了
main.py

import numpy as np
import cv2
import argparse
import pytesseract
import os
from PIL import Image

ap = argparse.ArgumentParser()
ap.add_argument("-i","--image",required=True,
                help="Path to image to be scanned")
ap.add_argument("-t", "--template", required=True,
	help="path to template OCR-A image")
args = vars(ap.parse_args())
def cv_show(name,file):
    cv2.imshow(name, file)
    cv2.waitKey(0)
    cv2.destroyAllWindows()
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==None and height ==None:
        return image
    if width==None:
        r = height / float(h)
        dim = (int(w * r),height)
    else:
        r = width / float(w)
        dim = (w,int(h * r))
    resizes = cv2.resize(image,dim,interpolation=inter)

    return resizes
def order_points(pts):
    rect = np.zeros((4,2),dtype="float32")

    s = pts.sum(axis=1)
    rect[0]  = pts[np.argmin(s)]
    rect[2] = pts[np.argmax(s)]

    diff = np.diff(pts,axis=1)
    rect[1] = pts[np.argmin(diff)]
    rect[3] = pts[np.argmax(diff)]

    return rect

def four_point_transform(image,pts):
    rect = order_points(pts)
    (tl,tr,bl,br) = rect
    # 计算输入的w和h的值

    widthA = np.sqrt(((br[0] - bl[0]) ** 2) + ((br[1] - bl[1]) ** 2))
    widthB = np.sqrt(((tr[0] -tl[0]) ** 2) + ((tr[1] - tl[1]) ** 2 ))
    maxWidth = max(int(widthA),int(widthB))


    heightA = np.sqrt(((tr[0] - br[0]) ** 2) + ((tr[1] - br[1]) ** 2))
    heightB = np.sqrt(((tl[0] - bl[0]) ** 2) + ((tl[1] - bl[1]) ** 2))
    maxHeight = max(int(heightA), int(heightB))

    # 变换后对应坐标位置
    dst = np.array([
        [0,0],
        [maxWidth - 1,0],
        [maxWidth - 1,maxHeight - 1],
        [0,maxHeight - 1]],dtype = "float32")

    M = cv2.getPerspectiveTransform(rect,dst)
    warped = cv2.warpPerspective(image,M,(maxWidth,maxHeight))

    return warped


image = cv2.imread(args["image"])
#cv_show("image",image)
ratio = image.shape[0] / 500
orig = image.copy()
image  = resize(image.copy(),height=500)
gray = cv2.cvtColor(image,cv2.COLOR_BGR2GRAY)
edged = cv2.Canny(gray,75,200)
#cv_show("edged",edged)
# 查找轮廓,并且去除扑克牌中间的轮廓,只保留花色和数字
cnts = cv2.findContours(edged.copy(),cv2.RETR_LIST,cv2.CHAIN_APPROX_SIMPLE)[1]
cnts = sorted(cnts,key=cv2.contourArea,reverse=True)[:10]
draw_img = image.copy()
for c in cnts:
   # dra = cv2.drawContours(draw_img,c,-1,(0,0,255),2)
   # cv_show("res",dra)
   peri = cv2.arcLength(c,True)

   approx = cv2.approxPolyDP(c,0.02*peri,True)

   if len(approx) ==4:
       screenCnt = approx
       print(np.array(screenCnt.size))
       print(np.array(screenCnt))
       break
# 展示结果
#cv2.drawContours(image,[screenCnt],-1,(0,0,255),2)
#cv_show("outline",image)

wraped = four_point_transform(orig,screenCnt.reshape(4,2) * ratio)
#cv_show("wraped",wraped)
wraped = resize(wraped,height=500)
wraped = cv2.medianBlur(wraped,3)

cv_show("gray",gray)
#cv_show("img",wraped)
#gray = cv2.cvtColor(wraped,cv2.COLOR_BGR2GRAY)

ref = cv2.threshold(gray,100,255,cv2.THRESH_BINARY)[1]

edged = cv2.Canny(ref,75,200)
#cv_show("ref",edged)
cnts = cv2.findContours(edged,cv2.RETR_LIST,cv2.CHAIN_APPROX_SIMPLE)[1]
cnts = sorted(cnts,key=cv2.contourArea,reverse=True)[:10]
draw_img = wraped.copy()
peri = cv2.arcLength(cnts[0],True)
approx = cv2.approxPolyDP(cnts[0],0.02 * peri,True)
#cv2.drawContours(draw_img,[approx],-1,(0,0,255),2)
print("---------------------")
#print([approx])
#cv_show("Outline",draw_img)
print(draw_img.shape[:2])
print(approx)    # 根据这个输出的结果确定mask
mask_img = draw_img
mask_img[96:412,64:218] = 255 # 这个范围是根据approx的结果得出来的大概值,把卡牌中间花的部位变为白色
mask_img = mask_img[:int(mask_img.shape[0]/2),:]
cv_show("1",mask_img)
mask_img = cv2.cvtColor(mask_img,cv2.COLOR_BGR2GRAY)
clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8,8))
mask_img = clahe.apply(mask_img)
ref = cv2.threshold(mask_img,70,255,cv2.THRESH_BINARY)[1] # 大多数牌为75的时候就行,但是有些需要阈值设为100,或者125等。并且有的时候75 与 70得到的结果非常不相同
ref = cv2.medianBlur(ref,3)
cv_show("ref",ref)
#ref = cv2.Canny(ref,55,200)
kernel = np.ones((3,3),np.uint8)
ero = cv2.erode(ref,kernel,iterations = 4)
cv_show("ref",ero)
cnts = cv2.findContours(ero,cv2.RETR_LIST,cv2.CHAIN_APPROX_SIMPLE)[1]
cnts = sorted(cnts,key=cv2.contourArea,reverse=True)[:5]

#cv2.drawContours(draw_img,cnts,-1,(0,0,255),2)
#cv_show("the page",draw_img)
draw_img = ref.copy()
save_img1 = []
save_img2 = []
for c in cnts:

    x,y,w,h = cv2.boundingRect(c)
    area = w * h # 通过面积来寻找要求的轮廓   5917左右为数字  3366左右为花色信息
   # if ar > 0.56 and ar < 0.63:
    draw_img = cv2.rectangle(draw_img, (x, y), (x + w, y + h), (0, 255, 0), 3)
    cv_show("draw_img", draw_img)

    if area > 5600 and area < 7000:
        draw_img  = cv2.rectangle(draw_img,(x,y),(x+w,y+h),(0,255,0),2)
        cv_show("draw_img",draw_img)
        #print("draw_img",draw_img[y:y+h,x:x+w])
        save_img1 =draw_img[y:y+h,x:x+w]
        #cv2.imwrite("2.png",save_img1)
    if area > 2700 and area < 4600:
        draw_img = cv2.rectangle(draw_img, (x, y), (x + w, y + h), (0, 255, 0), 1)
        cv_show("draw_img", draw_img)
        save_img2 =draw_img[y:y+h,x:x+w]
        #cv2.imwrite("2.png",save_img2)


'''
下面的代码是开始进行模板匹配了,上面的代码如果用来生成模板的话,取消cv2.imwrite的注释即可,然后注释掉下面的代码

模板匹配时,现在通过上面的代码已经得到了当前要检测的特征存放在save_img1中数字特征,save_img2中花色特征
'''
#cv_show("data",save_img1)
template = cv2.imread(args["template"])
ref = cv2.cvtColor(template,cv2.COLOR_BGR2GRAY)
ref = cv2.threshold(ref,10,255,cv2.THRESH_BINARY)[1]
cv_show("ref",ref)
cnts = cv2.findContours(ref.copy(),cv2.RETR_LIST,cv2.CHAIN_APPROX_SIMPLE)[1]
cnts = sorted(cnts,key=cv2.contourArea,reverse=1)[:17] # 提取面积最大的17个特征,就是外框特征,剔除具体的数字和花色特征
cnts = sort_contours(cnts, method="left-to-right")[0]#排序,从左到右,从上到下 这次排序主要是看距离最左边点的距离来排序,这样就可以找出每个框的特征是什么了
cv2.drawContours(template,cnts,-1,(0,0,255),3)
cv_show('template',template)
draw_img = template.copy()
digits_and_type = {
     } # 存放数字模板和花色模板
for (i,c) in enumerate(cnts):
    x,y,w,h = cv2.boundingRect(c)
    # cv2.rectangle(draw_img,(x,y),(x+w,y+h),(0,255,0),2)
    # cv_show("draw_img",draw_img)
    roi = ref[y:y+h,x:x+w]

    digits_and_type[i] = roi

'''
第三步 进行模板匹配
'''
OutPut = []
scores = []
save_img2 = cv2.resize(save_img2,(60,90))
save_img1 = cv2.resize(save_img1,(60,90))
#save_img1 = cv2.threshold(save_img1,0,255,
#                          cv2.THRESH_BINARY | cv2.THRESH_OTSU)[1]
#save_img2 = cv2.threshold(save_img2,0,255,
#                          cv2.THRESH_BINARY | cv2.THRESH_OTSU)[1]
for (digit,digitROI) in digits_and_type.items():
    # 模板匹配
    result = cv2.matchTemplate(save_img1,digitROI,cv2.TM_CCOEFF)
    (_,score,_,_) = cv2.minMaxLoc(result)
    scores.append(score)
OutPut.append(np.argmax(scores)+1)
scores = [] # 清空
for (digit,digitROI) in digits_and_type.items():
    # 模板匹配
    result = cv2.matchTemplate(save_img2,digitROI,cv2.TM_CCOEFF)
    (_,score,_,_) = cv2.minMaxLoc(result)
    scores.append(score)
OutPut.append(np.argmax(scores)+1)

dict = {
     1:"A",2:"2",3:"3",4:"4",5:"5",6:"6",7:"7",8:"8",9:"9",10:"10",11:"J",12:"Q",13:"K",14:"红桃",15:"方块",16:"梅花",17:"黑桃"}
print("the result:")
print("当前检测卡牌的数字为" + dict[OutPut[0]] +"       当前检测卡牌的花色为" + dict[OutPut[1]])

Make_Template.py

import numpy as np
import cv2

import matplotlib.pyplot as plt
from PIL import Image


def cv_show(name,file):
    cv2.imshow(name,file)
    cv2.waitKey(0)
    cv2.destroyAllWindows()
for num in range(1,18):
    #print(num)
    file_name = r"Images/" + str(num) + ".png"
    img = Image.open(file_name)
    #img = np.array(img)
    img = img.resize((60,90))
    plt.imshow(img)
    plt.show()
    #print(img)
    img.save(str(num) + ".png")

new_image = np.zeros([100,70 * 17,3],np.uint8) # 66为留出几个像素的小空,高度同理

for num in range(1,18):
    file_name = str(num) + ".png"
    img = cv2.imread(file_name)
    new_image[5:95,(num-1) * 70+5:(num) * 70-5,:] = img

    #print("OK")
cv_show("new",new_image)
cv2.imwrite("template.png",new_image)

代码及附件下载地址:

CSDN下载地址
或者csdn私聊我百度网盘哈(实在是缺积分了最近。。就不直接给网盘咯)

可能遇到的错误

Pycharm下python使用argparse报错: error: the following arguments are required: -i/–image

总结

  • 我在测试过程中不太明白为啥不能用库直接进行识别数字。。然后学到的很关键的一点就是图片很受光照的影响;刚开始我拍照是直接在桌子上拍的卡牌,然后发现有的图片对于卡牌的最外轮廓识别不出来,然后就直接换为了在一个黑皮笔记本上拍照,这个时候解决了最外轮廓的问题。
  • 中间遇到一个非常头疼的问题就是有的数字特征,经过二值化后,可能会消失,因为图片在拍照的时候光线有的很弱,然后二值化的不同阈值就会造成影响,今天上午找到了一个很好的解决办法:自适应直方图均衡化处理,然后就改善了结果(因为没有再特殊在暗光或者什么地方测试过,不过我自己又拍的都成功了)
  • 待改进方面: 这个是一个已知的BUG,应该有的地方还是会遇到,就是在选取数字和花色特征的面积的范围那里,有的图片可能特征面积更大或者更小,就导致不在选取范围内出现BUG,这个时候可以Debug一下,查看这个特征的面积多大,然后修改一下阈值,但是这个还算BUG,因为不能保证每次都要修改,所以可以改进的地方是在进行图片读取后,都resize一下,或者图片进行透视变换后,resize一下规范化,这样就可以保证不同的输入图片都不会出现这个问题,同时在把卡牌中间部位变为白色时的结果也更好
  • 代码有点乱,仅供参考。我也是初学者,这个帖子主要记录下思路,并且提供一个想出来的小练手项目和数据,想了解更多代码的思路可以私聊哈:D

你可能感兴趣的:(opencv,opencv,python)