jack blog
作者一覧
サイト移転のお知らせ
jack-webは下記のアドレスに移動しました
2023-12-20

マリオカートでトラッキング入門

マリオカートとは?

みなさんはじめまして! トモキングです! アドベントカレンダーはもう20日目になります!

私は趣味がゲームの大学生で、皆さんも自宅や電車通学・通勤のスキマ時間にゲームで遊ぶと思います。ゲームって飽きなくて面白いですよね。

そんなゲーム界の有名人といえば「マリオ」の赤帽子の黒ヒゲ男。1981年発売のアーケードゲーム「ドンキーコング」で初登場して以降、今なお任天堂を支えるキラーコンテンツです。ゲームを飛び出してリオデジャネイロオリンピック(2016)の開会式やUFJのテーマパークまで登場して、世界規模の人気を獲得し続けています。

突然ですが、問題です!

マリオが登場するゲームソフトの中で全世界売り上げ本数1位はなんでしょうか?

スーパーマリオブラザーズ…違います

スーパーマリオギャラクシー…違います

正解はなんと、「マリオカート8デラックス」で5000万本も売れているんです!

マリオの長〜い歴史の中で、たった6年前に発売されたソフトが1位は意外ですよね。

売れる要因の一つに「ゲームバランスの絶妙さ」が挙げられます。

初心者でも上級者でも両方楽しめるゲームバランスなんですよね。

上級者は操作テクニックを駆使すればレースで1位を目指せるのは当然ですが、初心者でもアイテムを駆使すれば一発逆転を狙えます。他には登場するレースコースが96コースと豊富なところや、レースマシンのカスタマイズ性の高さが売り上げに貢献しています。

(画像引用元 : https://gameseeker.jp/mariokart8dx/strong.html)

さあ皆さん! マリオカート8デラックスを買いましょう! 欲しい方は下のAmazonリンクをクリック! ↓↓↓

https://www.amazon.co.jp/任天堂-マリオカート8-デラックス-Switch/dp/B01N12G06K/ref=sr_1_2?adgrpid=51992317103&gclid=CjwKCAiAvJarBhA1EiwAGgZl0ANNp8XgVn31diJAPCVF8l_l7cz3vmbatd0BamuFSiFA1Ck37wHWohoC4JQQAvD_BwE&hvadid=658798402059&hvdev=c&hvlocphy=1009461&hvnetw=g&hvqmt=e&hvrand=17167431798760396302&hvtargid=kwd-334481892873&hydadcr=8612_13637040&jp-ad-ap=0&keywords=amazon+マリオカート&qid=1701178474&sr=8-2

………

…これではマリオカート8デラックスの宣伝ページになってしまいます。ブログの運営者に怒られちゃうので本題に移りましょう。

ちなみに定価は税込6,578円で、コース追加パスと呼ばれるDLCは税込2,500円です。コース追加パスは48コースの新規コースが追加されるので、ゲームソフトとセットで購入してしまうのがオススメです。

(画像引用元 : https://topics.nintendo.co.jp/article/ad54329a-2ddc-436d-8e96-43139628e6c3)

https://www.amazon.co.jp/任天堂-マリオカート8-デラックス-Switch/dp/B01N12G06K/ref=sr_1_2?adgrpid=51992317103&gclid=CjwKCAiAvJarBhA1EiwAGgZl0ANNp8XgVn31diJAPCVF8l_l7cz3vmbatd0BamuFSiFA1Ck37wHWohoC4JQQAvD_BwE&hvadid=658798402059&hvdev=c&hvlocphy=1009461&hvnetw=g&hvqmt=e&hvrand=17167431798760396302&hvtargid=kwd-334481892873&hydadcr=8612_13637040&jp-ad-ap=0&keywords=amazon+マリオカート&qid=1701178474&sr=8-2

………

今度こそ本題に移ります(笑)


OBSとOpenCVを使ってみよう

この記事ではOBSOpenCVを使って、ゲーム画面に映るキャラアイコンのトラッキングを試していきます。OBSは有名なストリーミング配信・録画ソフトウェアのことで、Nintendo Switchのリアルタイムキャプチャに使用します。OpenCVは画像処理に特化したオープンソースライブラリの一つで、今回はOBSのキャプチャ映像をPythonで処理するために使用します。OBSは仮想Webカメラとして出力してからOpenCV側に入力します。

まずは、ゲーム画面とOBSを接続していきましょう。OBSをパソコン上にインストールしてから、キャプチャボードを介してNintendo Switchとパソコンをケーブルで接続します。OBSのダウンロードリンクは下記の通り。

https://obsproject.com/ja/download

キャプチャボードはAmazonで3,000円程度の安価なものを購入しました。Nintendo Switchの映像出力のHDMIケーブルとパソコン側のHDMIケーブルをキャプチャボードに接続します。商品購入ページには4K 60fps対応の謳い文句があるものの、中華製の安価なモデルは1080p 30fps程度が限界なので注意です。今回はコスパ面を重視して中華製で我慢。

https://www.amazon.co.jp/gp/product/B08CGKJKDS/ref=ppx_yo_dt_b_asin_title_o05_s00?ie=UTF8&th=1

キャプチャボードとSwitch、パソコンをケーブルで繋いでからOBSを起動すると、無事にゲーム画面をキャプチャできました。どうでもいいけどスイカゲームって飽きないよね。

ウィンドウ右下にある「仮想カメラ開始」ボタンをクリックしてみましょう。こうすることで、ゲーム画面をウェブカメラ上の映像とわざと認識させて、OpenCV側でフレームごとのnumpy配列として受け取れるようになります。


テンプレートトラッキングを実装しよう

そもそも、テンプレートトラッキングとは? 例えば動画内で移動している乗り物が、画面のどの辺りにいるかリアルタイムで判断したい場合を考えてみましょう。 動画内から追跡したい乗り物の画像を切り出して、動画をフレームごとに抽出した画像とテンプレート画像の”類似度”を適当な計算式で算出します。この類似度が最も高い位置(または閾値以上の映像領域全て)を追跡したい乗り物として検出されます。つまり、追跡したい乗り物が”テンプレート”の役割を果たして動画内で追跡されます。

今回は、レース中のマップ画面に映る自分のキャラアイコンをテンプレートとして、現在キャラがいる場所をリアルタイムで枠を囲って表示することを目指します。

さて、ソースコードを組んでみましょう。基本的にOpenCVでソースコードを組んでいくのですが、OpenCVで読み込まれた画像ファイルはNumpy配列として受け取るので、画像加工を効率的に行うためにNumpyもインストールする必要があります。pipコマンドでまとめてインストールしちゃいましょう。

pip install opencv-python
pip install numpy

必要なライブラリをインストールしたら、実際にソースコードを書いてみましょう。ライブラリのimport後に仮想webカメラとテンプレート画像の初期設定を行っています。

"""
テンプレートマッチングを実装
"""

import cv2
import numpy as np
from config import *

# カメラ番号は適宜変更
camera = cv2.VideoCapture(CAMERA_NUMBER)

# テンプレート画像
templ_name = input("プレイヤー画像(.png): ") + ".png"
templ = cv2.imread(templ_name)

prev_pos = (0, 0)

メインループ部分です。

上記のapp.pyに追記していきましょう。ここではテンプレートトラッキングを実行して、最もテンプレート画像と似てる箇所を四角で囲んで画像出力しています。qキーを押すとトラッキングを中断できるようにしました。

while True:
    
    # qキーで中断
    if cv2.waitKey(1) & 0xFF == ord('q'):
        break
    
    # obsを取り込む
    _, frame = camera.read()
    if frame is None: continue
    
    # フレームを切り取り
    frame = frame[MAP_CROP[0,0] : MAP_CROP[0,1], MAP_CROP[1,0] : MAP_CROP[1,1]]
    
    # テンプレートマッチング
    result = cv2.matchTemplate(frame, templ, cv2.TM_CCOEFF_NORMED)
    
    # 類似度と移動速度を検出
    _, maxVal, _, maxLoc = cv2.minMaxLoc(result)
    pos     = (maxLoc[0], maxLoc[1])
    end_pos = (maxLoc[0] + templ.shape[1], maxLoc[1] + templ.shape[0])
    dist = np.linalg.norm(np.array(pos) - np.array(prev_pos))
    prev_pos = pos
    
    # 適切ならば四角で囲む
    if maxVal >= THRESHOLD and dist < MAX_VELOCITY:
        cv2.rectangle(frame, pos, end_pos, color=(0, 255, 0), thickness=2)
    
    # 結果を表示
    cv2.imshow('result', frame)

# メモリー解放
camera.release()
cv2.destroyAllWindows()

類似度と移動速度の検出について詳しく解説すると、cv2.matchTemplate関数では「正規化相互相関」の手法で類似度を計算しています。 μI(x,y)\mu_{I(x,y)}は、入力画像の走査位置(x,y)(x, y)のRGB値に対するそれぞれの画素のRGB値の平均で、 μT\mu_T は、テンプレート画像のそれぞれの画素のRGBの平均です。

...

# テンプレートマッチング
    result = cv2.matchTemplate(frame, templ, cv2.TM_CCOEFF_NORMED)

...
μI(x,y)=1wIhIj=0hI1i=0wI1I(x+i,y+j)μT=1whj=0h1i=0w1T(i,j)\mu_{I(x,y)} = \frac{1}{w_Ih_I}\displaystyle\sum^{h_I-1}_{j=0}\sum^{w_I-1}_{i=0}I(x+i, y+j)\\ \mu_T = \frac{1}{wh}\displaystyle\sum^{h-1}_{j=0}\sum^{w-1}_{i=0}T(i, j)
RZNCC(x,y)=j=0h1i=0w1(I(x+i,y+j)μI(x,y))×(T(i,j)μT)j=0h1i=0w1(I(x+i,y+j)μI(x,y))2×j=0h1i=0w1(T(i,j)μT)2R_{ZNCC}(x, y) = \frac{\displaystyle\sum^{h-1}_{j=0} \sum^{w-1}_{i=0}(I(x+i,y+j)-\mu_{I(x,y)})×(T(i, j)-\mu_T)}{\displaystyle\sqrt{\sum^{h-1}_{j=0} \sum^{w-1}_{i=0}{{(I(x+i,y+j)-\mu_{I(x,y)})}^2}}×{\sqrt{\sum^{h-1}_{j=0} \sum^{w-1}_{i=0}{{(T(i,j)-\mu_T)}^2}}}}

…何やら難しい数式なのですが、要するに相関係数と似た数式で、入力フレームの画素単位の類似度をRZNCC(x,y)R_{ZNCC} (x,y)で計算できるというわけです。-1.0~1.0の範囲で計算されて、値が大きければ大きいほど類似していると判定されます。

この他の類似度の検出手法は、下のウェブサイトで詳しくまとめられているのでチェックしてみてね!

https://qiita.com/pachi-dragon/items/394b26b1621de92bfd98#画素値

こうして入力画像を類似度に変換されると、cv2.minMaxLoc関数で類似度が最も高い座標を算出します。返り値のmaxValが類似度の最大値、maxLocが最も類似している座標になります。

...

# 類似度と移動速度を検出
    _, maxVal, _, maxLoc = cv2.minMaxLoc(result)
    pos     = (maxLoc[0], maxLoc[1])
    end_pos = (maxLoc[0] + templ.shape[1], maxLoc[1] + templ.shape[0])

...

実はテンプレートトラッキング以外にも、キャラアイコンの移動速度を使って閾値の判定をしています。これはトラッキングの精度向上のためで、キャラアイコンがほぼ一定速度で移動する性質を利用して、高速で移動する背景部分(レース上の地面や壁)の誤検出を防いでいます。

...

  dist = np.linalg.norm(np.array(pos) - np.array(prev_pos))
  prev_pos = pos

...

仮想カメラなどの重要な設定は、別のソースコードからimportすると便利。app.pyと同じディレクトリでconfig.pyとして環境変数を設定しましょう。

import numpy as np

# カメラ番号を登録
CAMERA_NUMBER = 0

# クロップする領域
MAP_CROP = np.array([[300, 900], [1400, 1900]])

# 類似度の閾値
THRESHOLD = 0.5

# 移動速度の閾値
MAX_VELOCITY = 5

いざ、テンプレートトラッキング実行!

ソースコードが組めたから実行!…する前に、テンプレート画像を作る必要があります。Nintendo Switchの画面をスクショしてキャラアイコン部分を切り出せば十分ですが、作業が面倒な方々のためにマリオのキャラアイコンを下に添付しちゃいます(自分の推しキャラです笑)。キャラクター選択画面でマリオを選択してテンプレートトラッキングを実行していきます。 ※OpenCVの仕様上、pngファイル形式でテンプレート画像の作成・保存を推奨します。

app.py, config.pyと同じディレクトリにアイコン画像を保存したら、ターミナルで下記のコマンドを入力して実行タイム! input関数による指示が出るので、キャラアイコンが保存されている画像ファイル名を拡張子抜きで入力します。

python app.py

>>> プレイヤー画像(.png): mario

Traceback (most recent call last):
  File ".../app.py", line 32, in <module>
    result = cv2.matchTemplate(frame, templ, cv2.TM_CCOEFF_NORMED)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
cv2.error: OpenCV(4.8.1) /Users/runner/work/opencv-python/opencv-python/opencv/modules/imgproc/src/templmatch.cpp:1175: error: (-215:Assertion failed) _img.size().height <= _templ.size().height && _img.size().width <= _templ.size().width in function 'matchTemplate'

…どうやらエラーが出た模様。 config.pyのCAMERA_NUMBERで設定された番号が間違っていたみたい。OBSで取り込まれたキャプチャ画面をOpenCVに読み込ませたいので、この番号を変更していきましょう。 どの番号が正解なのかはしらみつぶしで探るしかなさそう。

...

# カメラ番号を登録
CAMERA_NUMBER = 1

...

値を「1」に変更して再度実行すると、無事にキャプチャ画面が取り込まれました。

このままレース開始しちゃいましょう。

キャラアイコンの移動に合わせて緑枠が移動しているので、テンプレートトラッキング成功です!


おわりに

OpenCV以外にもテンプレートトラッキングは色々開発されています。 テンプレートトラッキングとchatGPTにも使われている深層学習のモデルを合体させた「YOLO」ライブラリは高速×高精度なリアルタイムトラッキングがPython上で可能になります。

https://github.com/ultralytics/ultralytics

このブログ記事でAI全般について興味を持っていただけら嬉しいです〜! ここまで読んでくださり、ありがとうございました〜〜

※本ページの一部画像は、chatGPT-4 / DALL・E 3によるAI生成されたものです。

おすすめ記事

jackFes2023
2023-12-24

jackFes2023

  • アドベントカレンダー2023