search
LoginSignup
1

posted at

updated at

スマブラのオンライン対戦の連勝&戦闘力を自動でカウントするプログラムを作った話

追記

OBSを28.0.0以降にアップデートすると、今までのバージョンのVirtualCamプラグインによる仮想カメラが、OpenCVで拾えなくなる問題が発生しています。

なので、以下のサイトからobs-virtualcam-3.0.0-Windows-installer.exeをダウンロードして、再度プラグインのインストールを行ってください。

謝辞

プログラムのコード整形と、一部機能の修正方法の提案をしてくださったNeo様(@Glassesman10)に感謝致します。

関連ファイル

以下のGithubレポジトリからダウンロードできます。詳しくはプログラムとその他関連ファイルのダウンロードで説明します。

実際の動作

以下がプログラムを実際に動かした動画です。画面左下がコマンドプロンプトの標準出力、右下がOBSのテキスト表示となっています。

目次

はじめに

去年の12月くらいに作ったやつを今更記事にしました。

背景としては、スマブラのVIP連勝配信をしている際、OBSにいちいち連勝数を書き込まないといけないのが面倒だったので、放置していても勝手に連勝数をファイルに書き込んでくれたらいいな~と思い、作ってみた次第です。

また、戦闘力のカウントもあれば便利かな~とも思い、ついでにそれも実装してみようと思いました。

一からこの記事と同じように実装しようとするとかなり面倒なので、「こういうのもあるんだな~」という感覚で参考程度に読んでいただければと思います。

プログラミング初心者&Qiita初投稿なので拙い記事になりますが、温かい目で読んでいただければ幸いです。

実行環境

以降、Windowsユーザー向けに書いていきます。ご了承ください。

以下に私の実行環境を示します。バージョンはそれほど厳密でなくていいと思います。

環境 バージョン
OS Windows10 Home
CPU Corei7-10875H
GPU RTX 3060 LaptopGPU
Python(Anaconda仮想環境) 3.6.9
OBS 27.2.4
Tesseract 5.0.0

また、Pythonのプログラムに必要なライブラリとバージョンは以下のようになっています。こちらもバージョンは同じにする必要はないと思います。

ライブラリ バージョン
OpenCV 4.5.2.52
PyOCR 0.8
Pillow 8.4.0
Numpy 1.19.5

実行環境の準備

とりあえず、大体は参考文献だけ載せて説明は省略します。

主に以下の準備をします。

プログラムとその他関連ファイルのダウンロード

以下のページに飛び、「Code」→「Download ZIP」を押してZIPファイルをダウンロードします。その後、任意の場所にZIPを展開します。

image.png

一旦このままにして先に進みます。

Pythonのインストール

最初にPythonをインストールします。私は3.6.9のバージョンを使っていますが、最新のものをインストールしても問題ないと思います。

以下のサイトを参考にすればインストールできると思います。

Pythonライブラリのインストール

実行環境の2つ目の表に記してあるライブラリをインストールします。

手っ取り早く済ませたい方は、以下の手順でライブラリのインストールを行ってください。

  1. プログラムとその他関連ファイルのダウンロードでダウンロードして展開したZIPファイルの中に

    • requirements_same.txt :私と同じバージョンをインストールしたい場合
    • requirements_latest.txt :最新のバージョンをインストールしたい場合

    が入っているので、どちらかを選び、shiftキー+右クリック→パスのコピー でファイルのパスをコピーする。また、ファイルの中身は以下のようになっている。

    requirements_same.txt
    numpy==1.19.5
    opencv-python==4.5.2.52
    pyocr==0.8
    Pillow==8.4.0
    
    requirements_latest.txt
    numpy
    opencv-python
    pyocr
    Pillow
    
  2. Windowsキー+Rキー を押した後、入力画面で「cmd」と打ちコマンドプロンプトを開く

  3. 以下のコマンドを打つ。「コピーしたファイルのパス」には1.でコピーしたファイルのパスをペーストする

    pip install -r コピーしたファイルのパス
    

その後問題が無ければ必要なライブラリが一括でインストールされます。

この方法が心配な方は一つずつ調べてインストールしてください。

Tesseractのインストール

Tesseractは、オープンソースの文字認識エンジンです。今回は戦闘力の文字認識に使用します。

以下の記事の「tesseractインストール」の部分を参考にTesseractをインストールします。

インストールしたら、以下の手順でTesseractを環境変数に登録します。

  1. Windowsキー+IキーでWindowsの設定画面を開き、「システム」を押す
    image.png

  2. 「詳細情報」を選択し、「システムの詳細設定」を押す
    image.png

  3. 「環境変数」を押す
    image.png

  4. 「○○○のユーザー環境変数」の「Path」を選び、「編集」を押す
    image.png

  5. 「新規」を押し、「C:\Program Files\Tesseract-OCR」と入力し、「OK」を押し続けて全てのウィンドウを閉じる
    image.png

以上でTesseractの準備は終了です。

OBSの設定

OBSはインストールしている前提で話を進めていきます。

まず、OBSのプラグインである「VirtualCam」をインストールします。現在のOBSはデフォルトで仮想カメラ機能がありますが、その仮想カメラではOpenCVでの動画の読み込みができないため、プラグインでの仮想カメラを使用する必要があります。

以下のryomatu様の記事を参考にすればインストールできると思います。

※OBSのバージョンが28.0.0以降の場合

従来のバージョンのVirtualCamではOpenCVで仮想カメラが読み込めない状態となっているので、以下のサイトからobs-virtualcam-3.0.0-Windows-installer.exeをダウンロード、インストールを行ってください。

次に、OBSの設定をしていきます。OBSにゲーム画面が映っている状態で、ゲーム画面に該当するソースを選び、「フィルタ」を押します。
image.png

フィルタの設定画面が出てくるので、左下の「+」を押し、「VirtualCam」を選びます。押したら名前の変更画面が出てきますが、デフォルトでも変えていただいても大丈夫です。
image.png

その後、以下のような画面が出てきます。ここの「Start」を押すと、OBSの仮想カメラが起動します。今後自動化プログラムを使って配信をしたい場合は、配信する前にこのStartボタンを押す必要があります。(AutoStart機能付けてほしい。。)
image.png

ここまでフィルタを使って仮想カメラを起動する手順を説明しましたが、以下の画像のように、「ツールタブからVirtualCam開けばAutoStart(OBS起動時に自動で仮想カメラを起動する設定)使えるのになんでそうしないの?」と思われた方もいるかもしれないですが、理由はフィルタから仮想カメラを起動する理由で説明します。
image.png

次に連勝数と戦闘力を表示します。「ソース」の右下の「+」を押し、「テキスト」を選択します。
image.png

先に連勝数のテキストを作成するので、ここでは「連勝数」という名前でOKを押します。
image.png

以下のような画面が出てくるので、ここで「ファイルからの」読み取りをチェックし、「参照」を押します。
image.png

ここで、プログラムとその他関連ファイルのダウンロードで展開した「vip_auto_count-main」の中にある「vipc.txt」を選択し、「開く」を押します。
image.png

連勝数の書かれたファイルの中身が表示されるので、適当に色とかを変えてもらって「OK」を押します。その後、文字の大きさを適当に変更します。
image.png

戦闘力のテキストも連勝数と同様の操作で追加できます。戦闘力のファイルは同梱されている「rate.txt」を選択してください。

フィルタから仮想カメラを起動する理由

どうでもいい方は読み飛ばしてください。

まず、勝敗の結果は以下の画像と、それに該当する位置でのゲーム画面の切り抜き画像の一致率で判断します。
win.png          lose.png
 win.png             lose.png
映像キャプチャデバイスのフィルタから仮想カメラを起動すれば、仮想カメラは下図の赤枠のゲーム画面の部分だけを移すので、映像キャプチャデバイスのサイズを変更してもプログラムで読み込むゲーム画面のサイズは変わらないです。
image.png

しかし、ツールからVirtualCamを選択して仮想カメラを起動すると下図のように赤枠全体を移すので、映像キャプチャデバイスのサイズを変更するとプログラムで読み込むゲーム画面のサイズが変わってしまい、上のwin.png,lose.pngと比較するための画像が全く違う位置で映したような画像に切り抜かれてしまいます。
image.png

ゲーム画面のサイズを変えない方ならその問題はないので ツール→VirtualCam でAutoRunを実行して仮想カメラを自動起動した方が便利ですが、ゲーム画面のサイズを変える方はエラーが起きてしまうので、フィルタから仮想カメラを起動する必要があります。

プログラムの実行

プログラムを実行していきます。プログラムファイルは プログラムとその他関連ファイルのダウンロードでダウンロードした中にある「vipcount.py」です。

  1. エクスプローラーを開き、vipcount.pyが入っている「vip_auto_count-main」フォルダを探し、下図の赤枠の部分をクリックします。そしたら、そのフォルダのパスが表示されるので、それをコピーします。
    image.png
    image.png

  2. 次に、Windowsキー+Rキーで入力画面が出てくるので、「cmd」と入力してコマンドプロンプトを開きます。

  3. コマンドプロンプトが開いたら、以下のようにコマンドを打ちます。

    cd 1でコピーしたフォルダのパス
    

    自分の場合は以下のようになっています。
    image.png

  4. 以下のようにキャラを選択した状態にします。この時、赤枠の戦闘力を読み込むので、ポインターが赤枠の中に入らないように注意してください。
    image.png

  5. OBSを開いて仮想カメラを開いている状態で、コマンドプロンプトに以下のコマンドを打ちます。そしたらプログラムが実行します。

    python vipcount.py
    

以下のようにプログラムを実行して、Trueと連勝数と戦闘力が表示されたら成功です。エラーが起きて強制終了したり、連勝数などが表示されなかった場合は、エラーが起きる場合を読んでください。
image.png

以下がプログラムの動作確認です。

エラーが起きる場合

エラーが起きて強制終了したり、連勝数などが表示されなかった場合は、vipcount.pyの69行目にある「cam_id = 0」の0の部分を1~3の数字に置き換えて再度実行してください。

vipcount.py
import cv2
import pyocr
import pyocr.builders
from PIL import Image
import numpy as np
import os

def write_wins(wins: int):
    # vipc.txtに連勝数を書き込む
    with open('vipc.txt',"w",encoding="utf-8") as f:
        f.write("%s連勝" % wins)

def get_wins() -> str:
    ## vipc.txtから連勝数を取得する
    with open('vipc.txt',"r",encoding="utf-8") as f:
        return f.read()

def add_wins():
    # vipc.txtの連勝数に+1してまた書き込む
    s = get_wins()
    index = s.find('連勝')
    r = s[:index]
    wins = int(r) + 1
    write_wins(wins)

def reset_wins():
    # 連勝数をリセットする
    write_wins(0)

def read_first_rate_from_image(frame) -> str:
    # キャラ選択画面から戦闘力を取得する
    crop = frame[722:770, 1544:1797]
    rate_img = Image.fromarray(crop)
    return remove_non_numbers(tool.image_to_string(rate_img, lang="eng", builder=builder))

def read_rate_from_image(frame) -> str:
    # リザルト画面から戦闘力を取得する
    crop = frame[265:300, 667:854]
    rate_img = Image.fromarray(crop)
    return remove_non_numbers(tool.image_to_string(rate_img, lang="eng", builder=builder))

def write_rate(rate: str):
    # rate.txtに戦闘力を書き込む
    with open('rate.txt',"w",encoding="utf-8") as f:
        f.write(add_commas(rate))

def get_rate() -> str:
    ## rate.txtから戦闘力を取得する
    with open('rate.txt',"r",encoding="utf-8") as f:
        return f.read()

def remove_non_numbers(rate: str) -> str:
    ## 数字以外を削除する
    return ''.join(filter(str.isdigit, rate))

def add_commas(rate: str) -> str:
    ## 数字をカンマ区切りにする
    rate = "{:,}".format(int(rate))
    return rate


os.chdir(os.path.dirname(os.path.abspath(__file__))) #実行ファイルのあるディレクトリに移動

# OCRエンジンの取得
tools = pyocr.get_available_tools()
tool = tools[0]
builder = pyocr.builders.TextBuilder()

cam_id = 0 # 自分の仮想カメラのデバイスID (大体0~3)
cap = cv2.VideoCapture(cam_id)
cap.set(cv2.CAP_PROP_FRAME_WIDTH, 1920) # カメラ画像の横幅を1920に設定
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 1080) # カメラ画像の縦幅を1080に設定

win_image = cv2.imread('win.png') # 自分が勝ったときのリザルト
lose_image = cv2.imread('lose.png') # 自分が負けたときのリザルト

win_is_counted: bool = False
first_rate_is_counted: bool = False

while True:
    ret, frame = cap.read()

    # 読み込んでいるWebカメラを表示
    # cv2.imshow('video', frame)

    # 読み込んでいるwebカメラの縦横幅を表示
    # print('width:' + str(cap.get(cv2.CAP_PROP_FRAME_WIDTH)))
    # print('height:' + str(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)))

    if not first_rate_is_counted: # キャラ選択画面で戦闘力を取得する
        first_rate = read_first_rate_from_image(frame)
        write_rate(first_rate)
        print(True)
        print(get_wins())
        print(get_rate())
        first_rate_is_counted = True
        
    win_frame = frame[14:194, 682:784] # キャプチャしているフレームをwin_imageと同じ領域に切り取る
    lose_frame = frame[39:189, 720:825] # キャプチャしているフレームをlose_imageと同じ領域に切り取る
    # frame[y:y, x:x]

    # win_frameとlose_frameを画像として保存。win_imageとlose_imageはこの2つのフレームから切り取った画像である。
    # cv2.imwrite('win_frame.png', win_frame)
    # cv2.imwrite('lose_frame.png', lose_frame)

    # imageとframeの一致率を表示
    # print('win:' + str(np.count_nonzero(win_image == win_frame) / win_image.size))
    # print('lose:' + str(np.count_nonzero(lose_image == lose_frame) / lose_image.size))

    if np.count_nonzero(win_image == win_frame) / win_image.size > 0.8: # もしwin_imageとwin_frameの一致率が0.8以上だった場合
        result = read_rate_from_image(frame) # 戦闘力取得
        write_rate(result)
        print(add_commas(result))

        if not win_is_counted:
            add_wins() # 連勝数+1
            print(get_wins())
            win_is_counted = True

    elif np.count_nonzero(lose_image == lose_frame) / lose_image.size > 0.8: # もしlose_imageとlose_frameの一致率が0.8以上だった場合
        result = read_rate_from_image(frame) # 戦闘力取得
        write_rate(result)
        print(add_commas(result))

        if not win_is_counted:
            reset_wins() # 連勝数=0
            print(get_wins())
            win_is_counted = True

    else:
        win_is_counted = False

プログラムの概要

上記のソースコードのコメントに簡単に書いてはおりますが、一応こちらでもざっくり説明します。

関数

実は最初にプログラムを完成させた当時、恥ずかしながら関数の使い方がよくわかっておらず混沌としたコードになっていましたが、Neo様(@Glassesman10)が関数を使った綺麗なコードに整形してくださいました。。本当にありがとうございます。m(_ _)m

以下に関数の簡単な説明を示します。

  • write_wins():vipc.txtに連勝数を書き込む

  • get_wins():vipc.txtから連勝数の文字列を取得する

  • add_wins():vipc.txtの連勝数を+1する

  • reset_wins():vipc.txtの連勝数を0にする

  • read_first_rate_from_image():キャラ選択画面から戦闘力を取得する。以下の図の赤枠の部分。
    image.png

  • read_rate_from_image():リザルト画面から戦闘力を取得する。以下の図の青枠の部分。
    image.png

  • write_rate():rate.txtに戦闘力を書き込む

  • get_rate():rate.txtから連勝数の文字列を取得する

  • remove_non_numbers():戦闘力の数字以外を削除する

  • add_commas():数字をカンマ区切りにする

remove_non_numbers()add_commas()は、戦闘力の「,(カンマ)」が「.(ピリオド)」などに誤認識してしまったものを直すための関数です。数字以外を削除してまた新たにカンマを振り直すことで正常に戦闘力が表示されるようにしています。これもNeo様(@Glassesman10)からの提案です。感謝。。

プログラムの大まかな流れ

  1. cam_id = xのxにOBS仮想カメラに該当する番号を指定する

  2. キャラ選択画面で戦闘力を取得してプログラムが動く

  3. win_framelose_frameに、フレームから以下の図の赤枠、青枠の部分を切り取り代入する。赤枠がwin_frame、青枠がlose_frameである。
    image.png
    image.png

  4. win_imagewin_framelose_imagelose_frameをそれぞれ比較し、一致率が0.8以上の場合は戦闘力を取得し、勝ちならば+1連勝、負けなら0連勝にする。

以降、3,4の繰り返し

その他説明

このプログラムはゲーム画面のフレームの1つ1つを処理しているため、かなりPCに負荷のかかるものとなっています。

また、戦闘力の増減だけで勝敗は判別できないのか、という疑問が生じた方もおられたかもしれません。自分も最初は戦闘力だけ読み取って、上がれば勝ち、下がれば負け、という風にしようと思っていたのですが、その場合ですと、相手との戦闘力に差がありすぎて戦闘力が変動しない場合に勝敗の判別がつかないという問題が発生してしまうため、やむを得ず画像処理で勝敗を判別するという形にしました。

おわりに

配布用に作っていなかったため、プログラムを実行するための環境構築がかなり面倒です。
なので、時間があれば環境構築せずにexeファイルなどで簡単に実行できるようにして配布できるようにしたいです。
ただ、代わりにそれをやってくださる方がいればそちらを尊重します。

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
What you can do with signing up
1