🐹

【Python】OpenCVでドラクエモンスター判別

2024/12/27に公開

はじめに

こんにちは、Yusei です。今回は第1稿の続編です。

前回の記事はコチラ。これからOpenCVで物体検出を考えている方の一助になれば嬉しいです。

今回のネタ

前回はドラクエの自動レベル上げシステムを紹介しました。コードの中に以下のマジックナンバーがあります。

threshold=0.64

なんだこれは。どこから出てきたんであろうか。

ということで、この閾値設定の裏話と、OpenCVのテンプレートマッチングを使って調査したこと、試行錯誤したことをお伝えできれば良いなと思っています。

テンプレートマッチング

前記事で結構さらっと流してしまっていますが、画像の判別時に具体的に内部でどんな処理がおこなわれているのか気になりませんか?

「ドキュメント読んでください」で終わらせれば話は早いですが、何も知らない人でも全容をつかめるほど、わかりやすくまとめることもエンジニア素養の一つだと考えています。

テンプレートマッチングは、大きな画像(入力画像)の中から、小さな画像(テンプレート画像)と最も類似している部分を見つけ出す手法です。OpenCVでは、cv2.matchTemplate()関数を使用してこの処理を行います。

処理の流れ

  1. テンプレート画像を入力画像上で左上から右下へスライドさせていきます。
  2. 各位置で、テンプレート画像と入力画像の重なっている部分の類似度を計算します。
  3. 類似度を計算した結果を保存した2次元配列(類似度マップ)を作成します。
  4. 類似度マップから最も高い(または低い)値を持つ位置を特定します。

類似度の種類

OpenCVでは、類似度の計算に複数の手法を提供しています。主な手法は以下の通りです。

  1. SSD (Sum of Squared Difference): cv2.TM_SQDIFF
  2. SAD (Sum of Absolute Difference): cv2.TM_SQDIFF_NORMED
  3. 相関係数: cv2.TM_CCORR_NORMED
  4. 正規化相関係数: cv2.TM_CCOEFF_NORMED

そして、各手法の特徴とよく使われる場面をまとめました。

手法 特徴 使われる場面
SSD 差分を2乗して類似度を計算する。値が小さいほど類似。明るさやコントラストに影響を受けやすい。 基本的な類似度計算や明るさが一定の画像間で使用
SAD 差分の絶対値を使う手法。SSD より計算が軽いが、同様に明るさやコントラストに影響される。 軽量な計算が必要な場合
相関係数 ピクセル値の単純な相関を計算。明るさやコントラストの影響を受けやすい。 予備的な分析や高速な類似度判定に使用
正規化相関係数 明るさやコントラストの影響を軽減。安定した結果を提供。値が -1 から 1 に正規化される。 検出の精度が重要な場面

一般的な用途では、cv2.TM_CCOEFF_NORMED(正規化相関係数)が最もバランスが良く、今回のプロジェクトでも使用しています。よって、今回は正規化相関係数について詳細な計算法を紹介します。

正規化相関係数の理論

入力画像を I、テンプレート画像を T、出力結果を ccoeff とします。

検索窓が位置 (x, y) にあり、入力画像の検索窓領域と、テンプレート画像の領域を 1 次元配列としたものを \boldsymbol{i}_{xy}, \boldsymbol{t}_{xy} としたとき、類似度の計算はこの 2 つのベクトルの類似度を計算することなのです。

正規化相関係数 の定義は以下の通りです。出力値は [-1, 1] の範囲になります。

\begin{aligned} ccoeff(x,y) &= \frac{\sum_{i,j}(T'(i,j) \cdot T'(x+i,y+j))}{\sqrt{\sum_{i,j}T'(i,j)^2 \cdot \sum_{i,j}T'(x+i,y+j)^2}} \\ &= \frac{(\boldsymbol{i}_{xy}-\bar{\boldsymbol{i}}_{xy}) \cdot (\boldsymbol{t}_{xy}-\bar{\boldsymbol{t}}_{xy})}{\|\boldsymbol{i}_{xy}-\bar{\boldsymbol{i}}_{xy}\| \|\boldsymbol{t}_{xy}-\bar{\boldsymbol{t}}_{xy}\|} \end{aligned}

ただし、 T'I' の定義は以下の通りです。

T'(i,j) = T(i,j) - \frac{1}{wh} \sum_{i',j'} T(i',j')
I'(x+i,y+j) = I(x+i,y+j) - \frac{1}{wh} \sum_{i',j'} I(x+i',y+j')

wh はwidthとheight、つまりテンプレート画像の横幅と高さを表しています。

結局のところ何をしているかといえば、全ピクセルの値の平均値を基準として、そこからどれだけズレているかというのを計算しているだけなのです。例を見れば一目瞭然でしょう。

検出閾値

それでは実際にはぐメタ検出を詳しく見ていきましょう。以下に検出コード全文を載せておきます。閾値を適当に設定して、それ以上の値をもつ部分を検出していきます。

全てのコードを見る
# 画像とテンプレートの読み込み
base = cv2.imread('../img/base.png', cv2.IMREAD_COLOR)
template = cv2.imread('../img/lost_metal.png', cv2.IMREAD_COLOR)

# テンプレートを34x26に縮小
template = cv2.resize(template, (34, 26), interpolation=cv2.INTER_AREA)

# テンプレートマッチングの実行
result = cv2.matchTemplate(base, template, cv2.TM_CCOEFF_NORMED)

# 閾値を設定
threshold = 0.65  # 類似度がこの値以上の場所に赤枠を描画

# 閾値以上の位置を取得
locations = np.where(result >= threshold)

# ヒートマップの生成
heatmap = cv2.normalize(result, None, alpha=0, beta=255, norm_type=cv2.NORM_MINMAX, dtype=cv2.CV_8U)
heatmap = cv2.applyColorMap(heatmap, cv2.COLORMAP_HOT)

# 赤枠を元画像に描画
base_with_rectangle = base.copy()

# 閾値を超えた位置に赤枠を描画
for pt in zip(*locations[::-1]):  # locations は (y, x) のタプルなので反転して (x, y) に
    top_left = pt
    bottom_right = (top_left[0] + template.shape[1], top_left[1] + template.shape[0])
    cv2.rectangle(base_with_rectangle, top_left, bottom_right, (0, 0, 255), 2)

# 結果を表示
plt.figure(figsize=(12, 6))

# ヒートマップの表示
plt.subplot(1, 2, 1)
plt.title('Similarity Heatmap')
plt.imshow(heatmap)
plt.axis('off')

# 赤枠を描画した画像の表示
plt.subplot(1, 2, 2)
plt.title('Detected Area')
plt.imshow(cv2.cvtColor(base_with_rectangle, cv2.COLOR_BGR2RGB))
plt.axis('off')

plt.show()

閾値が0.4, 0.6, 0.8のときの検出精度を見てみましょう。赤枠で囲まれた部分が閾値以上の類似度をもつ検出領域です。これを見ると、だいたい0.6くらいが良さそうな気がしますね。

はぐメタの検出だけに注目すれば、閾値を0.6程度に設定しておけば特に問題はないでしょう。

しかし、今作でははぐメタと非常に見た目が似たモンスター、その名も「バブルスライム」が登場するのです。色が緑色ですが、それ以外の要素はもう完璧にはぐメタと一致しています。

openCVでは画像を最初にグレースケール化するため、色の違いの判別はかなりシビアです。そのため、バブルスライムとの識別をしっかりこなしてこそ、完璧なシステムと言えるでしょう。

したがって、今度はバブルスライムをテンプレート画像に設定して、全く同じ検出コードを走らせます。

閾値が0.62, 0.63, 0.64のときの検出精度を見てみましょう。バブルスライムが検出されないギリギリの閾値は、0.64以上だということが経験則的に言えそうですよね。

最終確認として、閾値を0.64に設定してテンプレート画像をはぐメタとバブルスライムで変えた場合の比較をしてみましょう。

はぐメタには赤枠がたくさんありますが、バブルスライムには一つもありません。これで、しっかりと2つのモンスターを区別しながらはぐメタだけ検出できるようになりましたね!!👏

終わりに

いかがでしたでしょうか?今回はOpenCVの画像認識について少し踏み入った話を交えながら、マジックナンバーである「0.64」のナゾを解明しました。

画像認識と聞くと、普通は機械学習のニューラルネットワークモデルを使わないといけないイメージがあり、非情報系の人間にとってはかなり勉強やコーディングのハードルが高いです😵‍💫

しかし、openCVのメソッドは使い方が非常に明快なものが多く、簡単に画像を識別したいだけなら非常に有用なライブラリとなっています。

ギャンブル好きの僕がパッと思いついたネタは、ネット麻雀の牌認識などに応用してみて、最近の麻雀AIと組み合わせることです。なかなか面白そうな結果が待っていそうですね。いつかやりたいなぁ。。。

よかったらZennとGithubもフォローしてくださると嬉しいです。新しいアイデアを記事として残すモチベーションがとても上がります。

ではまた🤗

Discussion