DataTennis.NET

データテニスドットネット

【OpenCVを使ったスポーツ画像解析3(モーショントラッキング)】選手やテニスボールの移動を自動で検出して移動軌跡をトラッキング

公開日:
最終更新日:2018/07/27

      2018/07/27

モーショントラッキングの続きです。

先回【モーショントラッキング2】フェデラー選手の移動軌跡をグラフ化してみましたでは、選手の動きをトラッキングしましたが、
最初の位置は自分で指定する必要がありました。ポイントごとに毎回初期位置を設定するのは結構な手間になります。

ということで、今回は動画から動いているものを検出して、初期位置をみつけてからトラッキングできるようにしようかと思います。

そして、選手の動きだけでなく、ボールの軌道も検出できるようにします。

解析した動画は↓のようになります。
動いているものだけを検出できるように白黒画像(2値化)に変換されています。
(元動画は、【モーショントラッキング】テニス選手の移動量や軌跡をデータ化するでご覧になってください。)

静止画ではこんな感じになります。

ballTrack

選手の動きとボールの軌跡をうまくトラッキングできていることがわかります。
どうやってやったかについて説明しましょう。

まずは、前の画像と今の画像との差分画像を作成し、動いているもの(選手やボール)を検出できるようにします。

差分画像は↓のようになります。
color_diff

うっすらとですが、選手とボールが表示されていることがわかります。
次に検出した画像を2値化(白黒画像に変換)し、findContoursを用いて輪郭検出を行いますが、選手が複数の物体に分けて検出されてしまうという問題がでてきます。
notdilation

そこで、2値化画像に、追加で膨張処理を施すことで分割された領域をくっつけます。↓のような画像に変換されます。
dilation

これで、選手2人とボールを検出することができます。

あと、ボールと選手、そしてそれ以外のノイズをちゃんと分けて検出できるように、以下の条件分岐を施してます。
①ボールと選手を分けて検出するために、輪郭内の面積を計算し面積が1000以上かどうかで、ボールと選手を判別しています。
②コート外のボールパーソンやライン審判の移動も検出されてしまわないよう、コートの中にある場合だけ、ボールとして判定します。
③奥の選手と前の選手を分けて検出するために、y座標が185より小さい座標は奥の選手、大きい座標は手前の選手として検出します。

と、こんな感じで、差分画像から移動しているものを抽出し、少しの判定処理で選手とボールの移動をトラッキングできるようにしました。

選手だけでなく、ボールもトラッキングできるようにしたので、動画からボールのコースやネットミス、どちらの選手がどういうポイントの取り方をしたのか、とかいろいろ解析できることが増えそうです。

スポーツ画像解析リンク
・【OpenCVを使ったスポーツ画像解析1】テニス選手の移動量や軌跡をデータ化する
・【OpenCVを使ったスポーツ画像解析2】フェデラー選手の移動軌跡をグラフ化してみました
・【OpenCVを使ったスポーツ画像解析3】選手やテニスボールの移動を自動で検出して移動軌跡をトラッキング
・【OpenCVを使ったスポーツ画像解析4】テニスコートのラインの自動検出
・【OpenCVを使ったスポーツ画像解析5】画像からテニス選手の位置を検出する
・【OpenCVを使ったスポーツ画像解析6】深層学習(ディープラーニング)を用いてテニス選手とボールをトラッキング

コード(python+opencv)は↓に記載しています。

import time
import math
import cv2
import numpy as np

def isCourt(p):#ボール座標がコートの中にあるかを判定
    from sympy.geometry import Point, Polygon
    poly = Polygon((229,90), (429,91), (508,283), (147,280))
    point=p
    return poly.encloses_point(point)

def dilation(dilationSize,kernelSize,img):#膨張処理
    kernel=np.ones((kernelSize,kernelSize),np.uint8)
    element=cv2.getStructuringElement(cv2.MORPH_RECT,(2*dilationSize+1,2*dilationSize+1),(dilationSize,dilationSize))
    dilation_img = cv2.dilate(img,kernel,element)
    
    return dilation_img
    
def detect(gray_diff):#
    retval, black_diff = cv2.threshold(gray_diff, 30, 255, cv2.THRESH_BINARY)
    img=black_diff

    dilation_img=dilation(18,20,img)
    
    img2=dilation_img.copy()
    img2=cv2.cvtColor(img2,cv2.COLOR_GRAY2RGB)

    image,contours, hierarchy = cv2.findContours(dilation_img,cv2.RETR_EXTERNAL,cv2.CHAIN_APPROX_SIMPLE)

    temp=[]
    p_array=[]
    area_array=[]
    ball_p=[]
    up_p=[]
    down_p=[]
    for i in range(len(contours)):
        
        count=len(contours[i])
        temp.append(count)
        area = cv2.contourArea(contours[i])#面積計算
        area_array.append(area)
        x=0.0
        y=0.0
        for j in range(count):
            x+=contours[i][j][0][0]
            y+=contours[i][j][0][1]
                
        x/=count
        y/=count
        x=int(x)
        y=int(y)
        p_array.append([x,y])
        
        if(area<1000):#面積が一定以下
            if(isCourt([x,y])):#コート内かどうか
                cv2.drawContours(img2,contours,i,(0,255,255),2)#黄
                ball_p=[x,y]
        else:
            if(y<185 and y>45):#コートの上側
                cv2.drawContours(img2,contours,i,(0,0,255),2)#赤
                up_p=[x,y]
            elif(y>=185):#コートの下側 
                cv2.drawContours(img2,contours,i,(255,0,0),2)#青
                down_p=[x,y]
    return img2,p_array,area_array,ball_p,up_p,down_p


VIDEO_DATA = "aus-tennis.mp4"
ESC_KEY = 0x1b
DURATION = 1.0
cv2.namedWindow("motion")
video = cv2.VideoCapture(VIDEO_DATA)

fps    = video.get(cv2.CAP_PROP_FPS)
height = video.get(cv2.CAP_PROP_FRAME_HEIGHT)
width  = video.get(cv2.CAP_PROP_FRAME_WIDTH)
# 形式はMP4Vを指定
fourcc = cv2.VideoWriter_fourcc(*'MJPG')
# 出力先のファイルを開く
out = cv2.VideoWriter('output.avi',fourcc,20.0, (int(width), int(height)))

# 最初のフレームの読み込み
end_flag, frame_next = video.read()#read() 1つ1つのフレームを読み込む
height, width, channels = frame_next.shape
motion_history = np.zeros((height, width), np.float32)
frame_pre = frame_next.copy()

p=[]
a=[]
ball=[]
up=[]
down=[]

while(end_flag):
#for i in range(150):    
    color_diff = cv2.absdiff(frame_next, frame_pre)# フレーム間の差分計算
    gray_diff = cv2.cvtColor(color_diff, cv2.COLOR_BGR2GRAY)# グレースケール変換
    retval, black_diff = cv2.threshold(gray_diff, 100, 1, cv2.THRESH_BINARY)# 2値化
    
    proc_time = time.clock()# プロセッサ処理時間(sec)を取得
    cv2.motempl.updateMotionHistory(black_diff, motion_history, proc_time, DURATION)# モーション履歴画像の更新
    hist_color = np.array(np.clip((motion_history - (proc_time - DURATION)) / DURATION, 0, 1) * 255, np.uint8)# 古いモーションの表示を経過時間に応じて薄くする
    hist_gray = cv2.cvtColor(hist_color, cv2.COLOR_GRAY2BGR)# グレースケール変換 
    img,p_temp,a_temp,ball_temp,up_temp,down_temp=detect(gray_diff)
    
    #if(i>50):
    p.append(p_temp)
    a.append(a_temp)
    if(len(ball_temp)>0):
        ball.append(ball_temp)
    if(len(up_temp)>0):
        up.append(up_temp)
    if(len(down_temp)>0):
        down.append(down_temp)
    
    
    ball_array=np.array(ball)
    up_array=np.array(up)
    down_array=np.array(down)
    court_array=np.array([[229,90], [429,91], [508,283], [147,280]])
    img = cv2.polylines(img,[ball_array],False,(0,255,255))
    img = cv2.polylines(img,[up_array],False,(0,0,255))
    img = cv2.polylines(img,[down_array],False,(255,0,0))
    img=cv2.polylines(img,[court_array],True,(0,255,0))
    
    cv2.imshow("motion", img)# モーション画像を表示 
    out.write(img)
    
    if cv2.waitKey(20) == ESC_KEY:# Escキー押下で終了
        break

    # 次のフレームの読み込み
    frame_pre = frame_next.copy()
    end_flag, frame_next = video.read()

# 終了処理
cv2.destroyAllWindows()
out.release()
video.release()


 - blog, スポーツ画像解析, データ分析, テニス×テクノロジー