画像grepツールを作ってみた

# 経緯

ごく稀に、プロダクト内に書かれた文言の修正をすることってありますよね。
htmlやテンプレートファイルに文字列が記載されていれば、普通にgrepするなり、sedで一括置換できたりします。

問題は画像です・・・！

画像の中に置き換えなければいけない文字があることもあると思いますが、画像を目視で見ないと分からないですよね。
過去の経験的にも、あとから置き換えなければいけない文字を含む画像が見つかって、修正する・・・みたいなことを何度か経験したことがあります。

（本来は、画像内にあまり文字を書くのは良くないと思うのだけど・・・説明ページとかだと仕方ない場合もありますよね。）

画像内をgrepできたらいいのに、と思ったのでOCRを活用して画像内の文字列をテキスト化し、その中に調べたい文字列があるかをチェックするツールを作ってみたので紹介します。

# OCRとは

OCR（Optical Character Recognition：光学的文字認識）は、ざっくり言うと画像データのテキストを認識して、文字データに変換する機能のことです。

# 方針

pythonでいくつかライブラリがあるので、今回は `pytesseract` を使って日本語+英文字を画像からテキストに書き起こして、そのテキスト内に探したい文字列が含まれている場合に画像パスとテキストデータを出力するツールを作ってみようと思います。

だれでも使えるように、Dockerイメージを作成してコンテナ上で動かすようにしたいと思います。

## 用意するもの

- Dockerfile
- スクリプト（今回は main.py とします）

ディレクトリ構成はこんな感じにしてみようと思います。
```
.
|--Dockerfile
|--app
|  |--main.py
|--requirements.txt
```

### Dockerfile

- python:latestを最初は使っていたのですが、サイズが大きすぎるので `python:slim` にしました
- イメージ上に `tesseract-ocr` と `tesseract-ocr-jpn` をインストールします
- ライブラリは画像を扱うのでpillowと、今回の主役の pytessoract をpipで入れます

```Dockerfile:Dockerfile
FROM python:slim

RUN apt-get update
RUN apt-get -y install tesseract-ocr tesseract-ocr-jpn
RUN apt-get clean

COPY requirements.txt .
RUN pip3 install -r requirements.txt

COPY ./app /app

CMD [ "python", "/app/main.py" ]
```

```requirements.txt
pillow
pytesseract
```

### main.py

- main.pyはイメージ内にコピーしておくので、検索したいワードは環境変数で渡してみることにします
- globを使って、対象ディレクトリを再帰的にファイルを取得するようにしてみます
  - このとき拡張子は、とりあえずjpg/jpeg/pngを対象にしてみます
- あんまり小難しいことは考えず、ファイルパスを取得したらpytesseract.image_to_stringに投げてみます
- 返ってきたテキストは、半角スペースや改行が多く含まれるので、replaceでトリミングしてみます

```python:main.py
from pytesseract import pytesseract
from PIL import Image
import os
import glob

def main() -> None:
    word = os.getenv('word')
    if not word:
        print('wordを指定してください。')
        return

    files = get_files()
    for file in files:
        search_word(file, word)

def get_files() -> list:
    types = ('jpg', 'jpeg', 'png')
    files = []
    for type in types:
        files += glob.glob("/target/**/*." + type, recursive=True)

    return files

def search_word(file: str, word: str) -> None:
    text = get_image_text(file)
    if word in text:
        print(file + ': -> ' + text)

def get_image_text(filename: str) -> str:
    img = Image.open(filename);
    text = pytesseract.image_to_string(img, lang='eng+jpn')
    return text.replace(' ', '').replace('\n', '')

if __name__ == "__main__":
    main()
```

### 実行

まずイメージをビルドしてみます

```sh
docker build -t image-grep .
```

実行方法は以下のようなイメージ
```
docker run --rm -it -v {ここに検索対象のパスを指定}:/target -e word="検索ワード" image-grep:latest
```


## 検証

こんな画像を用意してみました。

![sample-image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64358/0c0045da-1e0e-eba9-4477-ff7adb9014ba.png)

期待通りに、文字の探索ができるかみてみようと思います。

```sh
$ docker run --rm -it -v ./sample_image:/target -e word="サウナ" image-grep:latest
/target/sample-image.png: -> 私の趣味はスノーボードです。ジムで筋トレレたり、仕事帰りにふらっとサウナに行くのも好きです。読書や映画鑑賞もよくします。

$ docker run --rm -it -v ./sample_image:/target -e word="プール" image-grep:latest

$ docker run --rm -it -v ./sample_image:/target -e word="スキー" image-grep:latest

$ docker run --rm -it -v ./sample_image:/target -e word="スノーボード" image-grep:latest
/target/sample-image.png: -> 私の趣味はスノーボードです。ジムで筋トレレたり、仕事帰りにふらっとサウナに行くのも好きです。読書や映画鑑賞もよくします。
```

`筋トレしたり` が `筋トレレたり`　になっていたりして、少し怪しいところもありそうですが、イイ感じです。

これで画像内に変更しなければいけない文字列があっても見つけられそうですね。
画像を目grepするよりは信頼できそうです :sunglasses: