用機器學習的分類算法識別象棋棋子——兼論旋轉不變性

1. 前言

文本識別早已經不是問題了,不過卻不能直接應用於象棋棋子的識別,因爲棋盤上的棋子是隨機擺放上去的,不能保證棋子上的文字總是保持一個固定的角度。識別棋子的關鍵是找到具有“旋轉不變性”的特徵——無論棋子旋轉多少度,其特徵總是穩定的。

2. 圖像的矩特徵

矩是概率與統計中的一個概念,是隨機變量的一種數字特徵。如果把二維灰度圖像視爲有質量的平板,像素的灰度值pp代表平板的密度,那麼灰度圖像就可以用二維灰度密度函數 p=f(x,y)p=f(x,y) 來表示。通過計算圖像的零階矩、一階矩、二階矩、三階矩,即可獲得被稱爲不變矩的七個高度濃縮的圖像特徵。不變矩具有平移、灰度、尺度、旋轉不變性。

以下代碼給出了計算圖像不變矩的函數,基於OpenCV提供的兩個函數實現,返回包含七個高度濃縮的圖像特徵的數組。因爲圖像不變矩的動態範圍過大,默認返回的是不變矩的對數。

import cv2
import numpy as np

def humoments(img_gray, log=True):
    """返回圖像7個不變矩"""
    
    hu = cv2.HuMoments(cv2.moments(img_gray))[:,0]
    if log:
        hu = np.log(np.abs(hu))
    
    return hu

以下面四個角度的棋子“車”爲例,不管角度如何,它們的不變矩具有很強的相關性。
在這裏插入圖片描述

-6.979, -21.904, -29.739, -28.555, -57.703, -39.707, -60.654
-6.945, -21.576, -29.658, -28.057, -57.050, -38.891, -57.633
-6.925, -21.083, -28.879, -27.764, -56.091, -38.464, -58.305
-6.978, -21.966, -29.773, -28.337, -57.442, -39.753, -58.575

3. 採集樣本

選擇不同角度、光照條件,對棋子拍照,不同照片上的棋子儘可能保持相同的尺寸。使用圓檢測技術找到棋子,裁切、高斯模糊、二值化,通過旋轉抖動生成多個樣本,將其不變特徵矩保存到樣本集,其分類保存到標籤集。最後,樣本數據集保存成名爲cchessman.npz的本地文件。

# -*- coding: utf-8 -*-

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

def humoments(img_gray, log=True):
    """返回圖像7個不變矩"""
    
    hu = cv2.HuMoments(cv2.moments(img_gray))[:,0]
    if log:
        hu = np.log(np.abs(hu))
    
    return hu

def jitter(im_cv, theta):
    """隨機旋轉和抖動,返回新的圖像"""
    
    #theta = np.random.random()*360
    dx, dy = np.random.randint(0, 5, 2) - 2
    
    im_pil = Image.fromarray(im_cv)
    im_pil = im_pil.rotate(theta, translate=(dx, dy))
    im = np.array(im_pil)
    im[im==0] = 240
    im = np.where(im>128, 240, 15).astype(np.uint8)
    
    return im

if __name__ == '__main__':
    # 定義一個96x96像素的掩碼,用以濾除棋子周邊的無效像素
    _mask = np.empty((96,96), dtype=np.bool)
    _mask.fill(False)
    for i in range(96):
        for j in range(96):
            if np.hypot((i-48), (j-48)) > 42:
                _mask[i,j] = True

    target = list() # 分類結果集
    data = list() # 樣本數據集
    
    chessman = ['車','馬','炮','兵','卒','士','相','象','將','帥']
    files = [
        ('res/ju.jpg', 0, 12),
        ('res/ma.jpg', 1, 12),
        ('res/pao.jpg', 2, 12),
        ('res/bing.jpg', 3, 15),
        ('res/zu.jpg', 4, 15),
        ('res/shi.jpg', 5, 12),
        ('res/rxiang.jpg', 6, 6),
        ('res/bxiang.jpg', 7, 6),
        ('res/jiang.jpg', 8, 3),
        ('res/shuai.jpg', 9, 3)
    ]
        
    for fn, idx, count in files:
        print('------------------------')
        print(fn)
        
        img = cv2.imread(fn)
        img_gray = cv2.imread(fn, cv2.IMREAD_GRAYSCALE)
        
        # 圓檢測
        circles = cv2.HoughCircles(img_gray, cv2.HOUGH_GRADIENT, 1, 200, param1=100, param2=50, minRadius=90, maxRadius=140)
        circles = np.int_(np.around(circles))
        print(circles)
        for i, j, r in circles[0]:
            cv2.circle(img, (i, j), r, (0, 255, 0), 2)
            cv2.circle(img, (i, j), 2, (0, 0, 255), 3)
            
            piece = cv2.resize(img_gray[j-r:j+r, i-r:i+r], (96,96))
            piece[_mask] = 240
            piece = cv2.GaussianBlur(piece, (5,5), 0)
            piece = np.where(piece>128, 240, 15).astype(np.uint8)
            cv2.imwrite('res/chessman/%d_%d_%d.jpg'%(idx, i, j), piece)
            
             for theta in range(360):
                data.append(humoments(jitter(piece, theta)))
                target.append(idx)
            
        cv2.imshow('image', img)
        cv2.waitKey(0)
        cv2.destroyAllWindows()

    target = np.array(target)
    data = np.stack(data, axis=0)
    
    np.savez('cchessman.npz', target=target, data=data)

代碼運行中,每處理一張圖片,顯式一次圓檢測結果。如下圖所示。
在這裏插入圖片描述

4. 模型訓練

使用隨機森林分類器,簡單設置一下參數,就可以取得不錯的訓練效果。使用交叉驗證的代碼如下。

# -*- coding: utf-8 -*-

import numpy as np
from sklearn.model_selection import cross_val_score, KFold
from sklearn.ensemble import RandomForestClassifier

ds = np.load('cchessman.npz')
X = ds['data']
y = ds['target']

cv = KFold(n_splits=10, shuffle=True, random_state=0)
rfc = RandomForestClassifier()
rfc_scroe = cross_val_score(rfc, X, y, cv=cv)
print(rfc_scroe)
print(rfc_scroe.mean())

交叉驗證結果顯式,模型精度居然高達99.9%。

[1.         0.99971065 0.99971065 0.99971065 0.99971065 0.9994213
 0.9994213  0.99971065 1.         0.9994213 ]
0.9996817129629629

5. 模型應用

新拍一張全家福,驗證一下模型的泛化能力。
在這裏插入圖片描述
總共10個棋子,每個棋子隨機旋轉10次,生成100個測試樣本。測試結果表明,準確率99%。

(34560, 7)
[[[ 534  756  119]
  [ 468  412  128]
  [ 208  640  128]
  [1140  620  128]
  [ 800  524  119]
  [ 270  998  132]
  [ 716  184  122]
  [ 622 1106  123]
  [1046  284  125]
  [ 888  866  134]]]
['象', '象', '象', '象', '象', '象', '象', '象', '象', '象']
['卒', '卒', '卒', '卒', '卒', '卒', '卒', '卒', '卒', '卒']
['車', '車', '車', '車', '車', '車', '車', '士', '車', '車']
['馬', '馬', '馬', '馬', '炮', '馬', '馬', '馬', '馬', '馬']
['相', '相', '相', '相', '相', '相', '相', '相', '相', '相']
['士', '士', '士', '士', '士', '士', '士', '士', '士', '士']
['將', '將', '將', '將', '將', '將', '將', '將', '將', '將']
['帥', '帥', '帥', '帥', '帥', '帥', '帥', '帥', '帥', '帥']
['兵', '兵', '兵', '兵', '兵', '兵', '兵', '兵', '兵', '兵']
['炮', '炮', '炮', '炮', '炮', '炮', '炮', '炮', '炮', '炮']
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章