#弓道の採点簿とは
飛ばした矢が的にあったか外したかを記録する冊子です。
この結果の電子化に挑戦してみました。
拙いコードですが、誰かの参考になればと思います。
本プログラムの構成
google collaboratery の利用
google collaboratery を利用する前提でプログラムを作成しました。
google collab は完全にクラウドで実行される Jupyter ノートブック環境です。
設定不要で無料で利用できるので、パソコンがない!というときや、パソコンにpython入れてない!という場合でも簡単に使えます。
さらに、自分のパソコンにはライブラリをインストールする手間が必要ですが、google collab には大体のライブラリがインストール済みなので脳死で実行できるのも意外と大きな利点です。
今回は画像を読み書きするので、google Drive と連携させることにしました。
#電子データ化計画
本コードの設計は次の通りです。
1. グーグルドライブと連携、フォルダの作成
2. 画像の取得、画像サイズ変更
3. 採点簿のフレームを認識
4. 採点簿の赤色の円を認識
5. 円の位置情報を配列化
6. Excelに書き込む
冊子の外枠の認識
ある条件で直線を認識させ、認識された直線の中で最も画像の端にある縦線、横線を戻り値として指定しました。
- ハフ変換による直線検出を用いています。
def resize_im(self, im): # 画像のサイズを固定
# --------------------------------------------
size = self.x_pixel
h, w, c = im.shape
width,height = size, round(size * ( h / w ))
im_resize = cv2.resize(im,(width, height))
return im_resize
def detect_line(self): # フレームを検出する
# -----------------------------------------
im = cv2.imread(path_Now_Projects + self.FileName)
im_resize = self.resize_im(im)
# parameter
G = 1 + 2 * self.nomalization(10)
T1 = 1 + 2 * self.nomalization(7)
T2 = 1 + 2 * self.nomalization(2)
# 画像を加工する(ノイズ除去、ぼかし、二値化)
im_denoise = cv2.fastNlMeansDenoising(im_resize)
im_gray = cv2.cvtColor(im_denoise, cv2.COLOR_BGR2GRAY)
im_gau = cv2.GaussianBlur(im_gray,(G,G),0)
im_th = cv2.adaptiveThreshold(im_gau, 255, cv2.ADAPTIVE_THRESH_MEAN_C, cv2.THRESH_BINARY,T1,T2)
if detail2 == True:
cv2.imwrite(self.path_project + self.ImName + "_th.jpg", im_th)
# 直線を抽出する。
imgEdge = cv2.Canny(im_th,50,150,apertureSize = 3) # キャニー法によるエッジの検出
minLineLength = self.nomalization(200) # 検出する直線の長さの閾値(画素数依存)
maxLineGap = self.nomalization(20) # 直線が連続とみなせる隙間の最長距離(画素数依存)
th = self.nomalization(50)
lines = cv2.HoughLinesP(imgEdge,2,np.pi/180,th,minLineLength=minLineLength,maxLineGap=maxLineGap) # ハフ近似による直線の検出
# 直線を青で描画しつつ、フレームの直線を選別する。
im_line = im_resize
frame_left,frame_under, frame_over, frame_right = [10000]*4,[1]*4, [10000]*4, [1]*4 # 初期値の設定
# 全ての直線を描画
for i in range(len(lines)):
for x1,y1,x2,y2 in lines[i]:
cv2.line(im_line,(x1,y1),(x2,y2),(255,0,0),2)
# フレームの直線の選別
if frame_left[0] > x1 and abs(y1-y2) >3*abs(x1-x2) : # x座標が最も小さい縦線
frame_left = [x1,y1,x2,y2]
if frame_under[1] < y1 and 3*abs(y1-y2) < abs(x1-x2) : # y座標が最も大きい横線
frame_under = [x1,y1,x2,y2]
if frame_over[1] > y1 and 3*abs(y1-y2) < abs(x1-x2) : # y座標が最も小さい横線
frame_over = [x1,y1,x2,y2]
if frame_right[0] < x1 and abs(y1-y2) >3*abs(x1-x2) : # x座標が最も大きい縦線
frame_right = [x1,y1,x2,y2]
# フレームを示す直線を緑で描画する。
cv2.line(im_line,(frame_left[0], frame_left[1]),(frame_left[2], frame_left[3]),(0,255,0),2)
cv2.line(im_line,(frame_under[0], frame_under[1]),(frame_under[2], frame_under[3]),(0,255,0),2)
cv2.line(im_line,(frame_over[0], frame_over[1]),(frame_over[2], frame_over[3]),(0,255,0),2)
cv2.line(im_line,(frame_right[0], frame_right[1]),(frame_right[2], frame_right[3]),(0,255,0),2)
if detail2 == True: # デバック用の画像を保存する。
cv2.imwrite(self.path_project + self.ImName + "_line.jpg", im_line)
return frame_left, frame_under, frame_over, frame_right
4つの直線それぞれの交点を求める
def cross_point(self, p1, p2): # 2点を通る直線2本の交点の導出
# -----------------------------------------------------------
return solve( [ solve(p1,[1,1]), solve(p2,[1,1]) ], [1,1] )
def get_4point(self, f_under, f_left,f_over,f_right):# 2点を通る直線4本の交点4つを取得
# ------------------------------------------------------------------------------------
f_under = np.array([f_under[0:2], f_under[2:4]])
f_left = np.array([f_left[0:2], f_left[2:4]])
f_over = np.array([f_over[0:2], f_over[2:4]])
f_right = np.array([f_right[0:2], f_right[2:4]])
UL = self.cross_point(f_under, f_left)
OL = self.cross_point(f_over , f_left)
UR = self.cross_point(f_under, f_right)
OR = self.cross_point(f_over, f_right)
return [OL, OR, UL, UR]
任意の4点での四角形のトリミング
def transform_by4(self, points):# 任意の4点から長方形にトリミング
# --------------------------------------------------------------
im = cv2.imread(path_Now_Projects + self.FileName)
im_resize = self.resize_im(im)
points = sorted(points, key=lambda x:x[1]) # yが小さいもの順に並び替え。
top = sorted(points[:2], key=lambda x:x[0]) # 前半二つは四角形の上。xで並び替えると左右も分かる。
bottom = sorted(points[2:], key=lambda x:x[0], reverse=True) # 後半二つは四角形の下。同じくxで並び替え。
points = np.array(top + bottom, dtype='float32') # 分離した二つを再結合。
width = max(np.sqrt(((points[0][0]-points[2][0])**2)*2), np.sqrt(((points[1][0]-points[3][0])**2)*2))
height = max(np.sqrt(((points[0][1]-points[2][1])**2)*2), np.sqrt(((points[1][1]-points[3][1])**2)*2))
dst = np.array([
np.array([0, 0]),
np.array([width-1, 0]),
np.array([width-1, height-1]),
np.array([0, height-1]),
], np.float32)
trans = cv2.getPerspectiveTransform(points, dst) # 変換前の座標と変換後の座標の対応を渡すと、透視変換行列を作ってくれる。
im_trimming = cv2.warpPerspective(im_resize, trans, (int(width), int(height))) # 透視変換行列を使って切り抜く。
if detail2 == True:
cv2.imwrite(self.path_project + self.ImName +'_trimming.jpg', im_trimming)
return im_trimming
赤い円の抽出
赤い色の抽出の仕方は、hsv色空間のある範囲でmask処理をしたりとめんどくさいことをします。
- マスク処理で参照したサイト
def detect_red(self, im_trimming):# 赤色のみを抽出
# ------------------------------------------------
im = im_trimming
im_resize = self.resize_im(im)
# 赤色 (Hが0~30,150~180の範囲が赤)のマスクを用意
hsv = cv2.cvtColor(im_resize, cv2.COLOR_BGR2HSV)
lower1 = np.array([150, 30, 100]) # HSV
upper1 = np.array([179, 255, 255]) # HSV
img_mask1 = cv2.inRange(hsv, lower1, upper1)
lower2 = np.array([0, 30, 100]) # HSV
upper2 = np.array([30, 255, 255]) # HSV
img_mask2 = cv2.inRange(hsv, lower2, upper2)
# 2つの赤用マスクを結合させる
mask = cv2.bitwise_or(img_mask1, img_mask2)
# マスクをかけ、赤の円のみを残す
im_red = cv2.bitwise_and(im_resize, im_resize, mask=mask)
if detail2 == True: # デバッグ用の画像を保存
cv2.imwrite(self.path_project + self.ImName + "_red.jpg", im_red)
return im_red
円検出
赤だけ抽出した画像に対して円検出を行います。条件の設定をミスるとあらゆる模様を円として認識してしまうので、条件だしは必須。
今回も直線検出と同様、ハフ関数を用いています。
- ハフ変換による円検出
def detect_circle(self, im_trimming):# 円の位置を取得
# ---------------------------------------------------
# parameter
minD = self.nomalization(58)
p2= self.nomalization(12)
minR = self.nomalization(30)
maxR = self.nomalization(36)
Lx0 = self.nomalization(10)
Ly0 = self.nomalization(86)
Lx = self.nomalization(90)
Ly = self.nomalization(72)
# 赤抽出の画像から円を検出する。
im_red = self.detect_red(im_trimming)
im_gray = cv2.cvtColor(im_red,cv2.COLOR_BGR2GRAY)
# 検出する円の大きさを画素数に基づいた円の大きさ前後に設定
circles = cv2.HoughCircles(im_gray,
cv2.HOUGH_GRADIENT,
dp=1,
minDist = minD, # 検出を許す円ごとの間隔
param1=1,
param2=p2, # 検出しきい値
minRadius=minR, # 検出する半径の最小
maxRadius=maxR) # 検出する半径の最大
全コード
# coding: utf-8
# 採点簿の的中の写真を電子化する
# ________________________________
# 出力ユーザー設定 "True"or"False"
detail1 = True
detail2 = True
# 1 = 確認用の画像
# 2 = パラメータ調整用の画像
# ________________________________
# import一覧
import numpy as np
from numpy.linalg import solve
import os
import cv2
import sys
import pandas as pd
import openpyxl as excel
from pandas import ExcelWriter
import matplotlib.pyplot as plt
# google drive と連携する
from google.colab import drive
drive.mount('/content/drive')
# pathリスト
path_Now_Projects = 'drive/My Drive/OU_kyudo/Now_Projects/'
path_Past_Projects = 'drive/My Drive/OU_kyudo/Past_Projects/'
# フォルダ作成する
def make_folder(path):
if os.path.exists(path)==False:
os.mkdir(path)
make_folder(path_Now_Projects)
make_folder(path_Past_Projects)
# 画像名を取得する
files = []
for filename in os.listdir(path_Now_Projects):
if os.path.isfile(os.path.join(path_Now_Projects, filename)): #ファイルのみ取得
files.append(filename)
if len(files)==0:
print("画像をNow_Projectsフォルダに入れてください。")
sys.exit()
#=============================
#<<<<<< C l a s s >>>>>>>>>>
class Tekichu(object): # 初期化する。
# --------------------------------
def __init__(self):
# 画像名(拡張子あり)
self.FileName = ""
# 画像名(拡張子なし)
self.ImName, self.ext = "",""
# project名とそのpath名
self.project = ""
self.path_project = ""
# 画像の横方向の画素数
self.x_pixel = 1800
def set_variable(self, file): # 画像の名前をセットする
# ----------------------------------------------------
# project名とそのpath名
self.project = input("画像("+ file +") の project名 を入力 : ")
self.path_project = "drive/My Drive/OU_kyudo/Now_Projects/" + self.project +"/"
# project名のフォルダを作成
if os.path.exists(self.path_project)==False:
os.mkdir(self.path_project)
# 画像名(拡張子あり)
self.FileName = file
# 画像名(拡張子なし)
self.ImName, self.ext = os.path.splitext(file)
# 画素で変動するパラメータを基準値を用いて正規化
def nomalization(self, val):
return int(self.x_pixel *(val / 1200))
def resize_im(self, im): # 画像のサイズを固定
# --------------------------------------------
size = self.x_pixel
h, w, c = im.shape
width,height = size, round(size * ( h / w ))
im_resize = cv2.resize(im,(width, height))
return im_resize
def detect_line(self): # フレームを検出する
# -----------------------------------------
im = cv2.imread(path_Now_Projects + self.FileName)
im_resize = self.resize_im(im)
# parameter
G = 1 + 2 * self.nomalization(10)
T1 = 1 + 2 * self.nomalization(7)
T2 = 1 + 2 * self.nomalization(2)
# 画像を加工する(ノイズ除去、ぼかし、二値化)
im_denoise = cv2.fastNlMeansDenoising(im_resize)
im_gray = cv2.cvtColor(im_denoise, cv2.COLOR_BGR2GRAY)
im_gau = cv2.GaussianBlur(im_gray,(G,G),0)
im_th = cv2.adaptiveThreshold(im_gau, 255, cv2.ADAPTIVE_THRESH_MEAN_C, cv2.THRESH_BINARY,T1,T2)
if detail2 == True:
cv2.imwrite(self.path_project + self.ImName + "_th.jpg", im_th)
# 直線を抽出する。
imgEdge = cv2.Canny(im_th,50,150,apertureSize = 3) # キャニー法によるエッジの検出
minLineLength = self.nomalization(200) # 検出する直線の長さの閾値(画素数依存)
maxLineGap = self.nomalization(20) # 直線が連続とみなせる隙間の最長距離(画素数依存)
th = self.nomalization(50)
lines = cv2.HoughLinesP(imgEdge,2,np.pi/180,th,minLineLength=minLineLength,maxLineGap=maxLineGap) # ハフ近似による直線の検出
# 直線を青で描画しつつ、フレームの直線を選別する。
im_line = im_resize
frame_left,frame_under, frame_over, frame_right = [10000]*4,[1]*4, [10000]*4, [1]*4 # 初期値の設定
# 全ての直線を描画
for i in range(len(lines)):
for x1,y1,x2,y2 in lines[i]:
cv2.line(im_line,(x1,y1),(x2,y2),(255,0,0),2)
# フレームの直線の選別
if frame_left[0] > x1 and abs(y1-y2) >3*abs(x1-x2) : # x座標が最も小さい縦線
frame_left = [x1,y1,x2,y2]
if frame_under[1] < y1 and 3*abs(y1-y2) < abs(x1-x2) : # y座標が最も大きい横線
frame_under = [x1,y1,x2,y2]
if frame_over[1] > y1 and 3*abs(y1-y2) < abs(x1-x2) : # y座標が最も小さい横線
frame_over = [x1,y1,x2,y2]
if frame_right[0] < x1 and abs(y1-y2) >3*abs(x1-x2) : # x座標が最も大きい縦線
frame_right = [x1,y1,x2,y2]
# フレームを示す直線を緑で描画する。
cv2.line(im_line,(frame_left[0], frame_left[1]),(frame_left[2], frame_left[3]),(0,255,0),2)
cv2.line(im_line,(frame_under[0], frame_under[1]),(frame_under[2], frame_under[3]),(0,255,0),2)
cv2.line(im_line,(frame_over[0], frame_over[1]),(frame_over[2], frame_over[3]),(0,255,0),2)
cv2.line(im_line,(frame_right[0], frame_right[1]),(frame_right[2], frame_right[3]),(0,255,0),2)
if detail2 == True: # デバック用の画像を保存する。
cv2.imwrite(self.path_project + self.ImName + "_line.jpg", im_line)
return frame_left, frame_under, frame_over, frame_right
def cross_point(self, p1, p2): # 2点を通る直線2本の交点の導出
# -----------------------------------------------------------
return solve( [ solve(p1,[1,1]), solve(p2,[1,1]) ], [1,1] )
def get_4point(self, f_under, f_left,f_over,f_right):# 2点を通る直線4本の交点4つを取得
# ------------------------------------------------------------------------------------
f_under = np.array([f_under[0:2], f_under[2:4]])
f_left = np.array([f_left[0:2], f_left[2:4]])
f_over = np.array([f_over[0:2], f_over[2:4]])
f_right = np.array([f_right[0:2], f_right[2:4]])
UL = self.cross_point(f_under, f_left)
OL = self.cross_point(f_over , f_left)
UR = self.cross_point(f_under, f_right)
OR = self.cross_point(f_over, f_right)
return [OL, OR, UL, UR]
def transform_by4(self, points):# 任意の4点から長方形にトリミング
# --------------------------------------------------------------
im = cv2.imread(path_Now_Projects + self.FileName)
im_resize = self.resize_im(im)
points = sorted(points, key=lambda x:x[1]) # yが小さいもの順に並び替え。
top = sorted(points[:2], key=lambda x:x[0]) # 前半二つは四角形の上。xで並び替えると左右も分かる。
bottom = sorted(points[2:], key=lambda x:x[0], reverse=True) # 後半二つは四角形の下。同じくxで並び替え。
points = np.array(top + bottom, dtype='float32') # 分離した二つを再結合。
width = max(np.sqrt(((points[0][0]-points[2][0])**2)*2), np.sqrt(((points[1][0]-points[3][0])**2)*2))
height = max(np.sqrt(((points[0][1]-points[2][1])**2)*2), np.sqrt(((points[1][1]-points[3][1])**2)*2))
dst = np.array([
np.array([0, 0]),
np.array([width-1, 0]),
np.array([width-1, height-1]),
np.array([0, height-1]),
], np.float32)
trans = cv2.getPerspectiveTransform(points, dst) # 変換前の座標と変換後の座標の対応を渡すと、透視変換行列を作ってくれる。
im_trimming = cv2.warpPerspective(im_resize, trans, (int(width), int(height))) # 透視変換行列を使って切り抜く。
if detail2 == True:
cv2.imwrite(self.path_project + self.ImName +'_trimming.jpg', im_trimming)
return im_trimming
def detect_red(self, im_trimming):# 赤色のみを抽出
# ------------------------------------------------
im = im_trimming
im_resize = self.resize_im(im)
# 赤色 (Hが0~30,150~180の範囲が赤)のマスクを用意
hsv = cv2.cvtColor(im_resize, cv2.COLOR_BGR2HSV)
lower1 = np.array([150, 30, 100]) # HSV
upper1 = np.array([179, 255, 255]) # HSV
img_mask1 = cv2.inRange(hsv, lower1, upper1)
lower2 = np.array([0, 30, 100]) # HSV
upper2 = np.array([30, 255, 255]) # HSV
img_mask2 = cv2.inRange(hsv, lower2, upper2)
# 2つの赤用マスクを結合させる
mask = cv2.bitwise_or(img_mask1, img_mask2)
# マスクをかけ、赤の円のみを残す
im_red = cv2.bitwise_and(im_resize, im_resize, mask=mask)
if detail2 == True: # デバッグ用の画像を保存
cv2.imwrite(self.path_project + self.ImName + "_red.jpg", im_red)
return im_red
def detect_circle(self, im_trimming):# 円の位置を取得
# ---------------------------------------------------
# parameter
minD = self.nomalization(58)
p2= self.nomalization(12)
minR = self.nomalization(30)
maxR = self.nomalization(36)
Lx0 = self.nomalization(10)
Ly0 = self.nomalization(86)
Lx = self.nomalization(90)
Ly = self.nomalization(72)
# 赤抽出の画像から円を検出する。
im_red = self.detect_red(im_trimming)
im_gray = cv2.cvtColor(im_red,cv2.COLOR_BGR2GRAY)
# 検出する円の大きさを画素数に基づいた円の大きさ前後に設定
circles = cv2.HoughCircles(im_gray,
cv2.HOUGH_GRADIENT,
dp=1,
minDist = minD, # 検出を許す円ごとの間隔
param1=1,
param2=p2, # 検出しきい値
minRadius=minR, # 検出する半径の最小
maxRadius=maxR) # 検出する半径の最大
circle_position = [[0 for i in range(20)] for j in range(13)]
total_number = [0 for i in range(13)]
warning = False
if circles is not None:
circles = circles.squeeze(axis=0) # 円の中心を取得
im_circle = self.resize_im(im_trimming)
# 採点簿の格子に合わせたパラメータ
x_level = [int(Lx0+i*Lx) for i in range(13)]
y_level = [int(Ly0+j*Ly) for j in range(21)]
# 全ての格子を描画する
for i in x_level:
cv2.line(im_circle,(i, 0),(i, int(self.x_pixel * 9/16)),(0,0,255),1)
for j in y_level:
cv2.line(im_circle,(0, j),(self.x_pixel, j),(0,0,255),1)
# 円の中心位置を格子と比較し配列化
for cx, cy, r in circles:
# 円の円周と中心を描画する。
cv2.circle(im_circle, (cx, cy), r, (0, 255, 0), 2)
cv2.circle(im_circle, (cx, cy), 2, (0, 255, 0), 2)
horizontal = int((cx-Lx0) // Lx)
vertical = int((cy-Ly0)// Ly)
# 円が格子をはみ出すと異常を検出して対応する
if vertical >= 20:
vertical = 19
warning = True
# 配列に記録
circle_position[horizontal][vertical] += 1
# 1格子内に2つ以上検出すると異常を記録する
if circle_position[horizontal][vertical] >= 2:
warning = True
if detail1 == True:
cv2.imwrite(self.path_project + self.ImName + "_circles.jpg", im_circle)
# 合計的中を計算
for i in range(13):
total_number[i] = np.sum(circle_position[i])
# 文字化
for i in range(13):
for j in range (20):
if circle_position[i][j] == 1:
circle_position[i][j] = "○"
elif circle_position[i][j] == 0:
circle_position[i][j] = "・"
# 結合
data = np.r_[np.array([total_number]), np.array(circle_position).T] # トータルが0行目、的中が1~20行目になるよう結合
df = pd.DataFrame(data)
# 結果を表示
if warning == True :
print("【警告】結果に誤りがあります。"+ self.FileName)
print(df)
return df
def tekichu_main(self):# class内のメインプログラム
# ------------------------------------------------
f_left, f_under , f_over, f_right = self.detect_line()
box_points = self.get_4point(f_left, f_under , f_over, f_right)
im_trimming = self.transform_by4(box_points)
df = self.detect_circle(im_trimming)
wb = excel.Workbook() # 新規ワークブックを作る
wb.save(self.path_project + self.project +".xlsx")
writer = ExcelWriter(self.path_project + self.project + '.xlsx')
df.to_excel(writer, sheet_name = self.ImName) # Excelに書き込み
writer.save()
return df
#==================================
#>>> mainプログラム >>>>>>>>>>>>>>>
if __name__ == '__main__':
for i in range(len(files)):
tek1 = Tekichu()
tek1.set_variable(files[i])
df = tek1.tekichu_main()
print("正常に終了しました")
実行手順
本当は適当に撮った写真で解析したかった
こちらは断念しました。
的中の円がどの位置にあるかを解析するには、採点簿の格子の位置関係を把握する必要があります。
写真はどんな環境で撮られるか分かりません。
明度の違い、歪み、背景の色や質などが定まらないため、通常のパターン認識では確実な位置を求めるのは至難であり、とともいい方法とは言えません。
ということで「画像を背景の入らないようにトリミングしてもらう」ことを前提とした解析にしました。
ドライブの準備
まずは、本プログラムの仕様上、"kyudo" というフォルダ、その中に ”New_Projects” "Past_Projects" という2つのフォルダを作成します。
”New_Projects”にはいている画像を処理する設計なので、そこに先ほどトリミングした画像を入れます。
ここで、ドライブのマウントを指示されます。Google Collabを開いて最初の実行時は
ドライブのマウントの仕方など、初めての方は一読させると良いかと思います。
知っている方は飛ばしてください。
ここで、自分のアカウントを選択します。
これでマウントは完了です。
続いて、project名を入力してください。
project名を入力後に実行すると、解析した結果が表示され、表のデータがドライブに保存されると思います。
GUI画面で一部をユーザーに処理させたほうが良かった
このあと、Google Colabを使わず、Tkinterを用いてGUI(グラフィックユーザーインターフェイス)で処理をさせるコードも書いてみましたが、精度はそちらの方が良くなりました。