ソシャゲのストーリーをテキスト化して布教したい！【画像認識】

# 初めに

昨今のソシャゲはゲーム内容だけでなくストーリーや世界観なども凝ったものが多くなってきた印象があります。かくいう私も、プロジェクトセカイ(以下プロセカ)というゲームのストーリーにハマったオタクです。

https://pjsekai.sega.jp/

面白いコンテンツがあったら友達に布教したくなるのがオタクのサガ。さっそく友達に布教しようと思ったところ、

「ゲームや動画でストーリーを読むのは時間がかかる。文字媒体なら読むかも。」

と言われましたが、公式から台本なども特に提供していなかったためゲーム画面からセリフをテキスト化するコードを作成することにしました。

# 動作解説

![遥杏.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2916360/daef0084-f238-1c9e-511c-19dc9f83a523.gif)

## 動作手順

1. 動画を読み込ませる
1. 「キャラ名ウィンドウ」と「セリフウィンドウ」のそれぞれの領域をマウスで選択する
1. 一定フレーム毎に選択した領域内の文字を読み込む
1. セリフが次に移ったタイミングで「キャラ名」と「セリフ」を出力する

# コードの解説

コード全文は[こちら](https://github.com/s-tamazoo/MakeStoryScript)で公開しています。

## 読み取り範囲領域の選択

```python:main.py
import cv2

x0, y0, w, h = cv2.selectROI(frame)
```

読み取り範囲の選択には、OpenCVライブラリの`selectROI()`を用いました。
`selectROI()`の引数に画像データを渡すとGUI上で画面領域が選択でき、選択した領域のx座標、y座標、幅、高さが引数として返されます。

これを用いて"キャラ名ウィンドウ"と"セリフウィンドウ"のそれぞれの読み取り範囲を指定します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2916360/48542695-1736-8477-57f4-c15494d7b353.png)

## 画像加工

```python:main.py
# 二値変換
img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
threshold = 200
cv2.threshold(img, threshold, 255, cv2.THRESH_BINARY, dst=img)

# ビット反転処理
if reverse_flag:
    img = cv2.bitwise_not(img)

# 拡大処理
h, w = img.shape[:2]
img = cv2.resize(img, dsize=(w * scale, h * scale))
```

セリフウィンドウはそのままでもうまく画像認識できたのですが、キャラ名ウィンドウはなかなか読み取りができなかったため事前に画像に加工をかけることにしました。

tesseract-ocrは白背景に黒文字でないと読み取り精度が下がるため、指定した画像領域を二値変換した後ビット反転処理をかけてあげる必要があります。

**オリジナル > 二値変換 > ビット反転処理**
<img width="100" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2916360/744aad35-2517-a1e2-16a1-95e2693c955b.jpeg">  <img width="100" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2916360/eeb79be6-fa9a-3c07-897e-d78cb0437342.jpeg"> <img width="100" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2916360/11e265d5-f94d-3caf-4154-ad09233a8b11.jpeg">

他にも、キャラ名ウィンドウは小さすぎてうまく画像認識が出来なかったため元の画像を10倍に引き伸ばす処理も入れました。

## 画像認識

### pytesseract

```python:main.py
import pytesseract

text_img = frame[text_y:text_y + text_h, text_x:text_x + text_w]
text = pytesseract.image_to_string(text_img, lang="jpn", config=f"--psm 6, --oem 3, name_whitelist.txt")
```

今回は、TesseractのPythonラッパーである`pytesseract`を用いて画像認識を行いました。
これを用いるにはTesseractのインストールをする必要があります。
https://github.com/tesseract-ocr/tesseract

基本的な使い方は、`image_to_string()`に`ndarray`型の画像データと言語とオプションを指定してあげると画像から認識した文字を返してくれます。
オプションではそれぞれ以下の指定を行っています。
- `psm`:読み込ませるテキストの形式を指定(縦書きか横書きか、複数行なのか、等)
- `oem`:学習方法を指定(認識速度や精度に影響あり)
- `name_whitelist.txt`:今回はホワイトリストを読み込ませるために使用

### ホワイトリスト

```name_whitelist.txt
tessedit_char_whitelist みのり遥雫愛莉杏
```

登場するキャラはある程度決まっているので、ホワイトリスト機能を用いて主要キャラの名前に含まれる文字をそれぞれ指定しました。
これにより、キャラ名を誤認識することがほとんどなくなりました。

## テキスト起こしのタイミング

```python:main.py
from difflib import SequenceMatcher

# 前のセリフとの一致率が低くなったタイミングで書き出し処理
if SequenceMatcher(None, cur_text, prev_text).ratio() < 0.3:
    text += f'{prev_name}{prev_text}\n'
    if TEST_FLAG:
        print(f'{prev_name}{prev_text}\n')
```

一定フレームごとに読み取ったテキストを読み込んでいては、セリフが流れている途中のテキストも読み込んでしまいます。ただ、セリフが最後まで表示されたときのサインが見当たらなかったため、一定フレーム前の画像とテキストの比較をして類似度が低かった場合にテキストとして書き出す方針にしました。

例として以下のような場合を考えてみます。


```plaintext:セリフ途中のテキスト例
前：「これ」
今：「これはペンで」
```

これはセリフが流れている最中のテキストを受け取った例です。
この場合は類似度が高くなるため、テキスト書き出しは行いません。

書き出し対象となるのは以下のような場合です。

```plaintext:セリフが切り替わったテキスト例
前：「これはペンですか？」
今：「はい。それは」
```

セリフが切り替わった際はテキストの一致度が大幅に下がるため、これをトリガーとしてテキスト書き出しを行います。


この類似度の算出には`difflib`ライブラリの`SequenceMatcher()`を使用しました。
取得するフレームの間隔やゲーム内のセリフの流れる速度によると思いますが、今回は30%あたりを閾値にすると良い感じにセリフの切り替わりを感知できました。

# 出力結果

```plaintext:output.txt
杏
遥はこのへんじゃ服とか買わなそうだよね。
意外とちゃんと女の子っぽい服、着てるし

遥
意外と?

杏
あはは。だって小学校とか中学校の途中までは、
どっちかが足速いとか、どっちが先に給食食べ終わるとか
ぐくだらないことでいろいろ競い合ってきだただじゃん?

遥
そうね。勉強以外はいい勝負だったね

杏
うっ……やり返したつもりう?

8a_iNU
ーー抽に7本兆のことを言っ誠記けだだよav
*:j電A、'||
4導|「論、|.
```

なかなかの精度で読み取ることが出来ました。
ただ、実用化するには以下のような点が課題だと考えています。

- そのまま台本として使うには、テキストの読み取り精度に不安が残る
    - 「"ぐく"だらないことで」 「合ってき"だただ"じゃん?」など
- 場面の切り替えなどのときに背景絵を文字として認識してしまう
    - 文字化けを起こしているような箇所はこれのせい

なので、現状としては画像認識で読み取ったテキストをベースに目視で修正といった運用になるかと思います。誤っている箇所の自動検知などもできれば面白そうですね。

# 終わりに

画像認識系の技術に少し興味があったので、趣味と実益を兼ねたコードが書けて楽しかったです。この記事を通して、コンテンツの布教に取り憑かれたオタクの熱意を感じてもらえれば幸いです。

また、友人への布教結果は後に追記しようと思っています。