Apex Legendsのダメージ表示エリアを取得する

OpenCVでファイルの読み込みのスレッド化ができたので、以前作成したOCRによるダメージ取得を適用してみたが芳しくない。

OpenCVのthreshold関数やinRange関数で2値化して、取得という流れで試してみたもののいくつかの理由で余分な数値を取得してしまう。

一番多いのは背景の一部を数値として取得してしまうことが多いような印象だった。

ダメージが表示される場所は左上固定なので、そこだけ切り抜きたいところではあるのだが、0ダメージの場合はそもそも表示されていない。

なのでダメージが表示されている時だけ処理するように赤枠の部分を目印にすることにする。

ダメージのエリアが表示されているかの目印にするシンボル

このシンボルを画像認識を使用して、場所の取得をすることにします。

ただし、ダメージの表示エリアの白い線の内側は背景を少し透過しており、その影響を受ける。

特に雲を見つめると全体がほぼほぼ白く見えてしまうので、影響を少なくするために2値化を行う。

2値化は、簡単に言うとある特定の閾値で0と255に分けて白黒の画像を作る作業になる。

OpenCVの解説でよく見かけるのはグレースケールに変換後にcv2.threshold関数を用いて2値化が多い。

特に第4引数にcv2.THRESH_OTSUを設定することで、自動で閾値を自動で決めるというやり方が多い。

ret,image = cv2.threshold(im, 0, 255, cv2.THRESH_OTSU)

今回は後述の問題が発生する関係で、HSVというHue(色相),Saturation(彩度),Value(明度)の画像形式に変換し、cv2.splitにより明度のみにすることで作成する。明度の閾値に応じて、閾値より低い値は0にすることでグレースケールのフィルタリングした画像を作成する。

後々、使いそうなので関数化する。

def valuebase_grayscale(img,threshold=None):
    # 画像をBGRからHSVに変換(本来は元の画像のタイプを判別した方が良い)
    hsv_img = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
    # cv.splitでHue,Saturation,Valueする、ただしHue,Saturationは今回は不要なので捨てる
    [_, _, val] = cv2.split(hsv_img)
    if threshold != None:
        # Valueの閾値未満の値を0にする
        return  np.where(val < threshold, 0, val)
    else:
        return val

先ほどの画像に適用するとこのようになる。

threshold=180でフィルタリングした画像

この中からダメージのシンボルマークのみを切り出す。

切り出したダメージのシンボル

位置の特定には、cv2.matchTemplate関数を使う。

こちらも一般的な解説では、実行後にnumpyのnp.where関数を用いて閾値以上の座標のリストを取得後に、zip関数でxとy座標のペアに変換、for文で処理するようなものが多い。

今回の処理では全体必要なく、一番一致率の高い座標を取得できれば良いので、以下のような関数を作成して使用することにする。

def find_symbol(img,templ,threshold=0.8, method=cv2.TM_CCOEFF_NORMED):
    ret = None
    # imgには対象の画像、templには探したい画像を入れる
    res = cv2.matchTemplate(img, templ, method)
    loc = np.where(res>threshold)

    # 閾値での絞り込みの結果が空かを確認する
    if len(loc[0]):
        # 閾値,x座標,y座標の2次元リストに変換する
        points = list(zip(res[loc],*loc[::-1]))
        # 閾値の高い順に並び変える
        ret = sorted(points,reverse=True)
    return ret

あとは関数外で、リストの先頭の座標に元画像の高さと幅を足し合わせて、top,bottom,left,rightの座標を取得する。

これらを利用してダメージの値が表示されている矩形領域の座標を特定する。

この時に右端の座標は変わらないため、固定値とする。
※ ダメージのエリアの枠部分で取得しようとしたが、背景の誤検知が多いので使わないことにした。

これらを組み合わせて、ダメージのエリアを取得する関数を作成しておく。

def find_damge_area(img,symbol):
    matches =find_symbol(img,symbol,threshold=0.7)
    if matches == None or len(matches)<=0:
        return None
    [h, w] = symbol.shape[0: 2]
    top = matches[0][2]
    bottom = matches[0][2] + h
    left = matches[0][1] + w - 2
    [h, w] = img.shape[0: 2]

    # 右端は固定
    right = w-25 # -25としているが初めにキル,アシスト,ダメージの表示エリアをざっくりと切り抜いてある
    return [top,bottom,left,right]

これでダメージの表示エリアがわかった。

そもそも固定座標で切り抜けばよいのでは?と思うかもしれないが、実はダメージの桁数でダメージのシンボルの表示位置が変わる。

加えて、ダメージの数値のフォントは可変幅フォントであるため数字の1が含まれている場合は横幅が狭くなったりしてしまい。

OCRで存在しない数値を検知してしまうことがある。

OCRの精度と速度の向上のためにも、出来る限り表示領域を特定しておいた方が良い。

ただこれでも、別の問題が発生した。

残り部隊数や、キル、アシスト、ダメージのエリアは残り人数や部隊数が変動すると赤くなってしまう。

人数または部隊数が減った場合の画面

この状態も検知できるように画像等を調整することもできるとは思うが、白から赤へ、白から赤へは徐々に変化するため中間の状態も存在する。

これらをまとめて処理するため、次回は赤いものも白くするフィルターを作成する。

コメントする

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

CAPTCHA