1. 前言
文本識別早已經不是問題了,不過卻不能直接應用於象棋棋子的識別,因爲棋盤上的棋子是隨機擺放上去的,不能保證棋子上的文字總是保持一個固定的角度。識別棋子的關鍵是找到具有“旋轉不變性”的特徵——無論棋子旋轉多少度,其特徵總是穩定的。
2. 圖像的矩特徵
矩是概率與統計中的一個概念,是隨機變量的一種數字特徵。如果把二維灰度圖像視爲有質量的平板,像素的灰度值代表平板的密度,那麼灰度圖像就可以用二維灰度密度函數 來表示。通過計算圖像的零階矩、一階矩、二階矩、三階矩,即可獲得被稱爲不變矩的七個高度濃縮的圖像特徵。不變矩具有平移、灰度、尺度、旋轉不變性。
以下代碼給出了計算圖像不變矩的函數,基於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]]]
['象', '象', '象', '象', '象', '象', '象', '象', '象', '象']
['卒', '卒', '卒', '卒', '卒', '卒', '卒', '卒', '卒', '卒']
['車', '車', '車', '車', '車', '車', '車', '士', '車', '車']
['馬', '馬', '馬', '馬', '炮', '馬', '馬', '馬', '馬', '馬']
['相', '相', '相', '相', '相', '相', '相', '相', '相', '相']
['士', '士', '士', '士', '士', '士', '士', '士', '士', '士']
['將', '將', '將', '將', '將', '將', '將', '將', '將', '將']
['帥', '帥', '帥', '帥', '帥', '帥', '帥', '帥', '帥', '帥']
['兵', '兵', '兵', '兵', '兵', '兵', '兵', '兵', '兵', '兵']
['炮', '炮', '炮', '炮', '炮', '炮', '炮', '炮', '炮', '炮']