Opencv學習筆記(3)---紙牌數字識別練習實踐專案

瞲_大河彎彎發表於2020-09-24

Opencv學習筆記(3)—紙牌數字識別練習

本來我以為會很簡單的,然後實際做發現對我來說還是有點問題,我最初只是想著使用透視變換對不同角度拍照的紙牌首先進行變化,然後直接使用pytesseract庫就行了,然後實際操作中發現並不能直接進行OCR變化,沒有辦法,最後使用模板匹配的方法進行,這次練習最大的收穫是發現實操跟看視訊差別很大。。。
最後附程式碼和圖片的下載方式

第一步 製作數字和花色模板

先對紙牌進行規範命名:例如K-4,名字+花色,其中 1為紅桃 2為方塊 3 為梅花 4為黑桃,例圖如下:
在這裡插入圖片描述
然後進行圖片預處理,形態學操作,首先把圖片經過透視變換轉換為規整影像,如下圖:
在這裡插入圖片描述
然後我把圖片的中間部分給扣了出來填補為白色,同時只處理圖片的上半部分,因為卡牌的左上角跟右下角有相同的花色和卡牌數字,只處理上半部分更方便些:
在這裡插入圖片描述
然後轉換為灰度圖,進行二值化處理,因為有的數字特徵不太明顯,可以進行寫形態學操作,我這裡使用了膨脹操作,迭代次數為5開始查詢輪廓特徵,分別框選出數字特徵和花色特徵,找到後進行規範命名並儲存。我篩選的方式是通過輪廓矩形的面積:
在這裡插入圖片描述
通過這種方法進行13次對13張卡牌(剔除大小鬼牌了)分別操作,得到了卡牌與花色的圖片,從1-17進行命名,方便生成模板:
在這裡插入圖片描述
然後通過Make_Template.py檔案對每個小模板進行resize成相同大小,並且拼接為新模版,儲存檔案:
在這裡插入圖片描述
到這裡,模板生成操作就結束了

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

首先查詢模板特徵,得到下面的結果:在這裡插入圖片描述
然後,先按照面積進行排序,並且取前17個特徵,這時候的特徵是外面的方塊,然後再次按照從左到右的順序進行排序,這個時候排序的依據按照x的大小進行排序,保證特徵的順序正好從左到右是A到黑桃。
在這裡插入圖片描述
然後,遍歷把上面的結果的每個特徵儲存起來,然後對於數字和花色分別進行模板匹配就行了,最後得到輸出結果。
測試的輸入圖象:
在這裡插入圖片描述
測試的輸出結果:
在這裡插入圖片描述

參考程式碼

程式碼寫的很爛。。。因為把一個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私聊我百度網盤哈(實在是缺積分了最近。。就不直接給網盤咯)

總結

  • 我在測試過程中不太明白為啥不能用庫直接進行識別數字。。然後學到的很關鍵的一點就是圖片很受光照的影響;剛開始我拍照是直接在桌子上拍的卡牌,然後發現有的圖片對於卡牌的最外輪廓識別不出來,然後就直接換為了在一個黑皮筆記本上拍照,這個時候解決了最外輪廓的問題。
  • 中間遇到一個非常頭疼的問題就是有的數字特徵,經過二值化後,可能會消失,因為圖片在拍照的時候光線有的很弱,然後二值化的不同閾值就會造成影響,今天上午找到了一個很好的解決辦法:自適應直方圖均衡化處理,然後就改善了結果(因為沒有再特殊在暗光或者什麼地方測試過,不過我自己又拍的都成功了)
  • 待改進方面: 這個是一個已知的BUG,應該有的地方還是會遇到,就是在選取數字和花色特徵的面積的範圍那裡,有的圖片可能特徵面積更大或者更小,就導致不在選取範圍內出現BUG,這個時候可以Debug一下,檢視這個特徵的面積多大,然後修改一下閾值,但是這個還算BUG,因為不能保證每次都要修改,所以可以改進的地方是在進行圖片讀取後,都resize一下,或者圖片進行透視變換後,resize一下規範化,這樣就可以保證不同的輸入圖片都不會出現這個問題,同時在把卡牌中間部位變為白色時的結果也更好
  • 程式碼有點亂,僅供參考。我也是初學者,這個帖子主要記錄下思路,並且提供一個想出來的小練手專案和資料,想了解更多程式碼的思路可以私聊哈:D

相關文章