目次
はじめに
こんにちは.
はんぺんです.
今回はPythonを使ってテニスの試合の動画をいじってみました.
具体的には,試合のラリーの中でボールの位置を取得して動画に描画して,グラフにしてみるという感じです.
処理の流れ
動画から画像を取得
まずテニスの動画を用意いたします.
今回はフェデラーとナダルのオーストラリアンオープンでの試合の一部を使います.
ウィンブルドンはグラスコートでボールとの判別が難しいので,試してみるなら青色のハードコートがおすすめです.
1 2 3 4 |
files_name = os.listdir('../image/sample_image') files_name = sorted(files_name) img = cv2.imread('../image/sample_image/'+files_name[0]) movie_name = 'tennis_006' |
動画を読み込んで画像を取得して”sample_image”として保存します.
HSVの閾値で画像を二値化
画像に閾値を設定して,色付きの画像を白黒に変換します.
このとき閾値の設定はgithubで公開されているHSVTrackerを使用しました.
1 2 |
lower = np.array([70, 103,60]) upper = np.array([145,137,255]) |
こちらが今回用いた値です.
この値を使ってボールが白く,他が出来るだけ黒くなるように設定します(この時点で思ったより自動化はできないなーと思った.笑).
さらにモルフォロジー変換を使ってこれらの白い部分を大きくしていきます.
これは簡単に言えば,白の部分を人回り拡大させる処理です.
(オープンキャンパスの時に隣の研究室の人に相談したら教えてもらった.笑)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
start = time.time() cap = cv2.VideoCapture('../movie/'+ movie_name +'.mp4') fps = cap.get(cv2.CAP_PROP_FPS) canvas = np.zeros((image_height, image_width, 3), np.uint8) mask_binary = [] #モルフォロジー変換後のマスク count = 0 #Get default camera window size ret, frame = cap.read() #Height, Width = frame.shape[:2] while (True): # Capture webcame frame ret, frame = cap.read() contours_buf2 = [] if ret == True: canvas = np.zeros((image_height, image_width, 3), np.uint8) count += 1 #line_draw(frame) gaus_image = cv2.GaussianBlur(frame,(5,5),0) hsv_img = cv2.cvtColor(gaus_image, cv2.COLOR_BGR2HSV) # 閾値で二値変換 mask = cv2.inRange(hsv_img, lower, upper) # コートの線をマスキング #line_draw(mask) #Mathematical Morphology kernel = np.ones((5,5),np.uint8) iterations = 2 mask = cv2.dilate(mask,kernel,iterations = iterations) mask = cv2.erode(mask,kernel,iterations = iterations - 1) mask_binary.append(mask) if ret == False: #13 is the Enter Key #cv2.destroyAllWindows() #cv2.namedWindow('test') break print(round(time.time() - start, 2), 'sec') |

オリジナル画像

HSV変換後の画像

モルフォロジー変換後の画像
オリジナル,HSV変換後,モルフォロジー変換後の画像の比較です.
これでボールとその他を白いオブジェクトの大きさとして区別しやすくなります.
閾値より大きいオジェクトを除去
つぎに白いオブジェクトを塊ごとにラベル付します.
1 2 3 4 5 6 7 |
start = time.time() white_object_labels = [] for i in xrange(len(mask_binary)): white_object_labels.append(measure.label(mask_binary[i], background=0)) print(round(time.time() - start, 2), 'sec') |
そして画像にある,それぞれの白いオブジェクトのピクセル数を数えます
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
start = time.time() white_object_total = [] for i in range(len(white_object_labels)): #347 #for i in xrange(0,10): white_object_total_buf1 = np.zeros(np.max(white_object_labels[i]) + 1, np.uint8) for j in xrange(len(white_object_labels[i])): white_object = np.zeros(1, np.uint8) for k in xrange(np.max(white_object_labels[i]) + 1): #for l in set(white_object_labels[i][j]): if k == 0: continue #elif k == l: elif any(np.array(list(set(white_object_labels[i][j]))) == k) == True: white_object = np.append(white_object, [len(np.where(white_object_labels[i][j]==k)[0])]) else: white_object = np.append(white_object, [0]) white_object_total_buf1 = white_object_total_buf1 + white_object white_object_total.append(white_object_total_buf1) print(round(time.time() - start, 2), 'sec') |
これがめちゃくちゃ時間かかる.
5秒の動画で30分かかった.
そして白いオブジェクトで400ピクセル以上のものを除去(黒く)します.
1 2 3 4 5 6 7 8 9 10 11 |
start = time.time() mask_white_object_removed = [] for i in xrange(len(white_object_total)): white_object_labels_buf1 = copy.copy(white_object_labels[i]) for j in np.where(white_object_total[i] > 400)[0]: white_object_labels_buf1[white_object_labels_buf1 == j] = 0 white_object_labels_buf1[white_object_labels_buf1 != 0] = 255 mask_white_object_removed.append(white_object_labels_buf1.astype(np.uint8)) print(round(time.time() - start, 2), 'sec') |
前後の50フレームにおいて動かないオブジェクトを除去
前後1秒ずつで,位置が異胴していない白い物体を除去します.
ボールは前後1秒で移動しないことはないからです.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 |
start = time.time() canvas = np.zeros((image_height, image_width, 3), np.uint8) count = 0 contours = [] #輪郭 ball_position = [] #座標 ball_radius = [] # 半径 ball_area = [] # 面積 mask_binary = [] #モルフォロジー返還後のマスク #Get default camera window size ret, frame = cap.read() Height, Width = frame.shape[:2] frame_count = 0 frame = mask_white_object_removed for frame_count in range(len(frame)): # Capture webcame frame count += 1 mask = frame[frame_count] contours_buf2 = [] if ret == True: # 輪郭を見つける _, contours_buf1, _ = cv2.findContours(mask.copy(), cv2.RETR_CCOMP, cv2.CHAIN_APPROX_SIMPLE) #空のリストを生成(buf2) ball_position_buf2 = [] ball_radius_buf2 = [] ball_area_buf2 = [] for i in xrange(len(contours_buf1)): c = contours_buf1[i] (x,y), radius_buf1 = cv2.minEnclosingCircle(c) ball_area_buf1 = cv2.contourArea(c) ball_position_buf1 = (int(x), int(y)) ball_position_buf2.append(ball_position_buf1) ball_radius_buf2.append(radius_buf1) ball_area_buf2.append(ball_area_buf1) ball_radius.append(ball_radius_buf2) ball_position.append(ball_position_buf2) ball_area.append(ball_area_buf2) contours.append(contours_buf1) #画像の表示 cv2.imshow("Object Tracker", mask) #cv2.imshow("Object Tracker", hsv_img) k = cv2.waitKey(1) & 0xff #画像の保存 cv2.imwrite('../image/image_buf/' + movie_name + '_' + str("{0:05d}".format(count)) +'.jpg', mask) print(round(time.time() - start, 2), 'sec') |
これをピクセルカウントの前にやったほうがよさそうですね.

できた画像がこちらになります.
真ん中の白い点がボールになります.
動画の出力
出来上がった動画がこちらになります.
ノイズひどいですねー笑
グラフに描画
ちなみにこれをグラフにまとめたらこうなります.

わかりにくくて恐縮ですが,x,y軸がボールの位置で,z軸が時間変化軸です.
なんとなくボールの位置がわかるので,各点を何かしらの手法でフィッティングすればボールの位置だけ抽出できるそうです.
まとめ
いかかでしたでしょうか.
正直,ディープラーニングを用いない画像処理には限界を感じました.笑
あとから読み直すとわかるのですが,我ながらコード読みにくくいです.
早急にリーダブルコード読みます.
それでは失礼いたします.
参考
https://github.com/botforge/ColorTrackbar
いいですね、ボールのトラッキング。自分も動画でテニスの試合を解析できるようにしようとしてて、選手のトラッキングは難なくできてるのですがボールがなかなか難しい。説明にあったx y tの3次元グラフみて、ボールの軌道とノイズを切り分けるのを機械学習使ってできないかなあと思った次第です。
返信ありがとうございます。私はあえて機械学習を使わず簡単な変換や色の閾値のみで検知を行なっていました。たしかにグラフを機械学習使ってノイズと切り分けられるかもしれません。
また、以下の動画で紹介されている論文でうまくきりだせるモデルがあるようなので、もし興味があれば参照ください。
https://www.youtube.com/watch?v=iRlWw8GD0xc