【PaddleOCR】Pythonで簡単に日本語OCR_その2(exe化のおまけつき)

## 0. はじめに

私のQiita記事で今でも週間平均閲覧数が多いのが以下OCRの記事ということで、かなり強い需要があることに気づいたので、同じネタだがAIを搭載した`PaddleOCR`を扱うことにする。
※**同じようなレベルのOCRに「EasyOCR」があるが、個人的にはこちらの方が使い勝手がいいと感じてる**

https://qiita.com/ku_a_i/items/93fdbd75edacb34ec610


**本当は話題のexe化ライブラリである`Nutika`を使ってexe化させようと思っていたが、無茶苦茶コンパイル時間かかるし、後でわかると思うがPaddleOCRのexe化が超難しくて断念したのは内緒です...**


- 動作環境
  *  OS : Windows10 pro
  *  python： 3.9.6　
  *  PaddleOCR: 2.6.1.2
  *  paddlepaddle: 2.4.1
  *  pyinstaller: 5.6.2
  *  loguru: 0.6.0

:::note warn
<font color="Red">純粋にPaddleOCRに興味がある方は「2. PaddleOCRプログラム」まで読んでいただければOKです。</font>
:::

## 1. ライブラリの準備

poetryでもvenvでも何でもいいので、まずは仮想環境を適当に立ててから・・

### 1-1. PaddleOCR＋paddlepaddle

PaddleOCRとは、中国最大の検索エンジンの「Baidu(百度)」研究機関が開発したOSSであり、PaddlePadlleは、同じくBaiduが提供している深層学習フレームワークである。
今回導入は以下コマンドで行った。(下のpaddlepaddleは公式を確認して従った方がいい)

`pip install paddleocr`
`python -m pip install paddlepaddle==2.4.1 -i https://pypi.tuna.tsinghua.edu.cn/simple`

https://github.com/PaddlePaddle/PaddleOCR/tree/release/2.6

https://www.paddlepaddle.org.cn/en/install/quick?docurl=/documentation/docs/en/install/pip/windows-pip_en.html

:::note info
なおPaddleOCRのモデルは以下3種類から成り立っているが、**基本はインスタンス化する時の言語指定で最新が自動ダウンロードされる仕組みになっている**ので、あまり利用者としては気にする必要がない。もし直接扱いたい場合は以下リンクから選べばいい。

・Text Detection Model (det)　
　　⇒　文字の固まりを検出するモデル　※中国語、英語、その他Multi
・Text Angle Classification Model (cls)
　　⇒　傾き補正するモデル⇒通常は使わないことが多い ※全言語共通
・Text Recognition Model (rec)　
　　⇒　文字を推論するモデル ※中国語、英語、日本語・・etc種類が多い
:::

https://github.com/PaddlePaddle/PaddleOCR/blob/release/2.6/doc/doc_en/models_list_en.md

また、この事前学習済モデルは自動で以下に格納される仕様。↑で書いた()内と対応している
`C:\Users\***\.paddleocr\whl`

**実はこいつがexe化の時にもやっかいな動きをするのだが、それはまた後で・・**

![スクリーンショット 2023-01-21 172030.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/373060/e91acef2-93e3-32da-0033-68ee96322c61.png)

### 1-2. loguru

また、Nutikaをあきらめた代わりに**exe化に必須なloggingにくっそ便利**な`loguru`をついでに使用する
**<font color="Red">import loggingサヨナラ！</font>**

`pip install loguru`

https://github.com/Delgan/loguru#readme

**logger.add()でloggerを定義し、後はいつもと同じように書くだけ！らくちん！！**

## 2. PaddleOCRプログラム

### 2-1. ディレクトリ構成

最終的にexeの際に以下のような構成を考えている。
`今の段階ではまだexeはできてないので、exeの位置に.pyがあると思って読んでください。`

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/373060/17a15cd6-5def-39fe-7423-1176256ac474.png" width=80%>

### 2-2. OCR対象のサンプル

例にもれず前回記事と同じ[パブリックドメイン](https://ja.wikipedia.org/wiki/%E3%83%91%E3%83%96%E3%83%AA%E3%83%83%E3%82%AF%E3%83%89%E3%83%A1%E3%82%A4%E3%83%B3)である[青空文庫](https://www.aozora.gr.jp/)より[「宮沢賢治」雨ニモマケズ](https://www.aozora.gr.jp/cards/000081/files/45630_23908.html)を引用させていただく。
※この箇所をスクリーンショットで適当に保存して、dataフォルダに入れてます


<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/373060/f95e114c-b42e-bf8e-9514-9b0dbcb33f6e.jpeg" width=30%>

### 2-3. OCRプログラムと出力画像結果

:::note info
前回の記事との違いは
・OCRにtesseractを使用せずAIの力で性能の向上が見込める`PaddleOCR`を使用している
　⇒**英語は圧倒的に強いが、結論としては日本語はどっこいの性能かも？？**
・print出力ではなく、元画像に日本語を重ねて表示
・loggingとしてloguruを取り入れている
:::

:::note info
PaddleOCRの出力は
・result[0][0][0][0] 　#矩形左上座標
・result[0][0][0][1] 　#矩形右上座標
・result[0][0][0][2] 　#矩形右下座標
・result[0][0][0][3] 　#矩形左下座標
・result[0][1][0] 　　 #テキスト
・result[0][1][1] 　　     #confidence(自信度)
となっている
:::

なお、GPUを使用しなくても気にならないくらい処理は早いです！

```python: sample_ocr.py (OCRプログラム)
# coding: utf-8
from paddleocr import PaddleOCR
from PIL import Image, ImageEnhance, ImageFont, ImageDraw
import numpy as np
import cv2
from loguru import logger
# import logging ←バイバイ！

def japanese_puttext(img, text, position, font, fill = (255, 0, 0)):
    """ cv2.putTextが日本語対応してないので、自分で関数を定義する"""
    img_pil = Image.fromarray(img) # PIL Imageに変換。
    draw = ImageDraw.Draw(img_pil) # drawインスタンスを生成
    draw.text(position, text, font = font , fill = fill) # drawにテキストをのせる
    img = np.array(img_pil) # PILを配列に変換
    return img

def run_ocr(img_path):
    """ OCRメイン関数"""
    #画像読み込み＋前処理(適当)+PaddleOCR入力用にnpへ
    im = Image.open(img_path).convert('L')
    enhancer= ImageEnhance.Contrast(im) #コントラストを上げる
    im_con = enhancer.enhance(2.0) #コントラストを上げる
    np_img = np.asarray(im_con)
    logger.debug('画像読み込み完了') #logは今までと変わらない！

    #PaddleOCRを定義
    ocr = PaddleOCR(
        use_gpu=False, #GPUあるならTrue
        lang = "japan", #英語OCRならen
        det_limit_side_len=im_con.size[1], #画像サイズが960に圧縮されないように必須設定
        max_text_length = 30, #検証してないがテキスト最大長のパラメータ。今回は不要だが紹介
        )
    logger.debug('PaddleOCR設定完了')

    #PaddleOCRでOCR ※cls(傾き設定)は矩形全体での補正なので1文字1文字の補正ではない為不要
    result = ocr.ocr(img = np_img, det=True, rec=True, cls=False)
    logger.debug('PaddleOCR実行完了')

    #OCR結果転記用
    result_img = np_img.copy()
    result_img = cv2.cvtColor(result_img, cv2.COLOR_BGR2RGB)
    #画像に載せる日本語フォント設定　★Windows10だと C:\Windows\Fonts\以下にフォントがありまｓ
    fontpath ='C:\Windows\Fonts\HGRPP1.TTC'
    font = ImageFont.truetype(fontpath, 10) #サイズ指定

    #OCR結果を画像に転記
    for detection in result[0]:
        t_left = tuple([int(i) for i in detection[0][0]]) #左上
        # t_right = tuple([int(i) for i in detection[0][1]]) #右上
        b_right = tuple([int(i) for i in detection[0][2]]) #右下
        b_left = tuple([int(i) for i in detection[0][3]]) #左下
        ocr_text = detection[1][0] #テキスト(detection[1][1]なら自信度取得も可能)
        #画像に文字範囲の矩形を載せる(緑色)
        result_img = cv2.rectangle(result_img, t_left, b_right, (0, 255, 0), 3)
        """putTextだと日本語が??になってしまうので自作関数で処理。文字の位置は左下とした"""
        # result_img = cv2.putText(result_img, ocr_text, t_left, cv2.FONT_HERSHEY_SIMPLEX, 1, (255, 0, 0), 1, cv2.LINE_AA)
        result_img = japanese_puttext(result_img, ocr_text, (b_left[0], b_left[1]), font)

    logger.debug('画像にOCR結果記載完了')
    #保存する
    result_img = cv2.cvtColor(result_img, cv2.COLOR_RGB2BGR)
    cv2.imwrite('../out/ocr_result_picture.png', result_img)
    logger.debug('結果画像の保存完了')

if __name__ == '__main__':
    # loggerを定義
    logger.add("../out/samplelog.log", format="[{time:HH.mm:ss}] <lvl>{message}</lvl>", level='DEBUG', enqueue=True)
    img_path = "../data/miyazawa.png"
    run_ocr(img_path)
    # loggerを終了
    logger.remove()
```

これを実行すると、こんな画像がoutディレクトリに保存されます。
この例題に関しての`精度に関しては見学習の場合Tesseractより少し上・・か同じ程度？`という感想です。
`文字の判定時にこの文字の確立が一番高いだろう！というものを選定するようなAIアルゴ`なので、このような旧字体入りの文章を事前学習しているとは思えず、少し難易度高い課題でしたね。
日本語でももう少し一般的な文章だとTesseractより明らかに性能良くなると思ってます。
※英語のOCRだと無茶苦茶性能高いのでオススメ！

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/373060/962cf37e-6b4e-7805-ff59-53cf056d7ad5.png" width=50%>


## 3.(おまけ) PaddleOCR込みのプログラムをPyInstallerでexe化
**おまけという名の本編**です。
**<font color="Red">PaddleOCRだけ気になってた人は、後は蛇足なので「いいね！押していただいた後に(笑)」以下無視してブラウザバックしていただいて構いません。</font>**

**まさかexe化がこんなにシンドイなんて試すまではわかりませんでした** ので、本質ではないですがおまけで載せておきます。**実際にやったことある人いればわかりますが、githubのissueにも中国語で多数同様の質問が繰り返されており、どれもうまくいかないです。**
今ならNuitkaでもexe化できると思いますが、気力がないのでPyInstallerの方法を書いておきます。

`pip install pyinstaller`で導入を行う。(以下記事も参照)

https://qiita.com/ku_a_i/items/76f5fb34279ce3aa6e99

### 3-0.事前知識

`以下は私のにわか知識ですが、一応書いておきます。`

前回の上記リンクのexe化記事では書いてませんが、Pyinstallerには基本「onefile」と「onedir(one directory)」の2パターンのexe化の方法があります。だいたいは私の前回記事のようにonefileを選択するのですが、これだと「起動が遅い」「重い」等のデメリットや、なにより「依存ライブラリ」に弱い弱点があります。
今回のPaddleOCRもまさにこの「依存ライブラリ」をうまくPyinstallerが汲んでくれないため、エラー地獄が待ってます。
そんな時、onedirにしておけば後で依存関係を修復できるのである。

### 3-1.事前準備(プログラム改造)

まずは**subprocessでエラーを起こす**ので`shell=True`に修正しておく。(exe後に戻してもいいので)
なお、subprocessは標準ライブラリなのでPython本体側の修正が必要(私はpyenvなので以下の場所)
`"C:\Users\***\.pyenv\pyenv-win\versions\3.9.6\Lib\subprocess.py"`

```diff_python: subprocess.py:
class Popen(object):
    """略"""
    def __init__(self, args, bufsize=-1, executable=None,
                 stdin=None, stdout=None, stderr=None,
                 preexec_fn=None, close_fds=True,
-                 #shell=False, cwd=None, env=None, universal_newlines=None,
+                 shell=True, cwd=None, env=None, universal_newlines=None,
                 startupinfo=None, creationflags=0,
                 restore_signals=True, start_new_session=False,
                 pass_fds=(), *, user=None, group=None, extra_groups=None,
                 encoding=None, errors=None, text=None, umask=-1):
```

次に`paddleocr\ppocrの中にあるnetwork.py`でエラーを起こすので以下をコメントアウトしておく。

これはexe化して配布する際に、paddleOCRの3モデルを自動ダウンロードするのだが、`コンソールに進捗が出せなくてエラーになってしまう対応`である。`コメントアウトしてもバーが出なくなるだけでしっかりダウンロードされる。`
※これが1-1. PaddleOCR＋paddlepaddleで語っていた「exe化の時にもやっかいな動きをする」箇所である

```diff_python: .venv\Lib\site-packages\paddleocr\ppocr\utils\network.py:
def download_with_progressbar(url, save_path):
    logger = get_logger()
    response = requests.get(url, stream=True)
    if response.status_code == 200:
        total_size_in_bytes = int(response.headers.get('content-length', 1))
        block_size = 1024  # 1 Kibibyte
+        #progress_bar = tqdm(
+        #    total=total_size_in_bytes, unit='iB', unit_scale=True)
        with open(save_path, 'wb') as file:
            for data in response.iter_content(block_size):
+        #        progress_bar.update(len(data))
                file.write(data)
+        #progress_bar.close()
    else:
        logger.error("Something went wrong while downloading models")
        sys.exit(0)
```


### 3-2.　pyinstallerでexe化＋ライブラリを追加導入

さて、事前準備の後にはいよいよ実行なわけだが、**今回は「-D(--onedir)」オプションが必須となる。**
後は`--add-data`で強制的にexe後にも依存関係を残させている。
なお`;paddle/`はexe化前のライブラリpathを「paddle」という名前で強制的に依存させるという意味である。

```コマンドプロンプト:
pyinstaller -D -w --add-data "C:\Users\**(略)**\.venv\Lib\site-packages\paddle;paddle/" 
--add-data "C:\Users\**(略)**\.venv\Lib\site-packages\paddleocr;paddleocr/" 
sample_ocr.py
```
大事なのはonefileではなく、onedir(-D)にすること。
これをしないとexe化した後に「あれが足りない。これが足りない」とエラー地獄に陥り大変になる。
もちろん↑のコマンドに同様に`--add-data`していってもいいが、`数が多すぎたので私は以下をコピペした`

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/373060/1c10c9d8-a15a-4f5c-b78a-2bec2f849960.png" width=50%>

基本的には仮想環境化(`venv\Lib\site-packages\`)に入っている上記ライブラリを以下のように`exeの後にonedirとしてexeが入っているのと同じ階層にコピペする`ことになる。
`また1つだけ.pyがあるがこれはPython本体から持ってくる必要がある。`
`"C:\Users\***\.pyenv\pyenv-win\versions\3.9.6\Lib\imghdr.py"`

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/373060/39c373b2-5960-cacf-acdf-feefec711257.png" width=70%>

はい、おめでとうございます。
これでやっとexeが動くようになります。


## 4. おわりに

PaddleOCRは中国のOSSなので情報も中国語メインだし、中国版Qiita(Zenn)のCSDNを探してもどれもうまくいかないので情報として残しておく。私はこの方法で100%exe化を再現できたので、超マニアックな記事ですがぜひ活用してみてください。

OCRツールというのはDX化の中核にくるポテンシャルがあるのに、依頼元はさぞ簡単なんだろ？という態度で臨まれるのがいたたまれないですよね・・(頑張りましょう！)

**如果你在中国，并且发现它很有用，请按下喜欢（心）按钮！！！**

## 5. 追記

さらにDonutというOCRではなく、画像として文字生成する仕組みも書きましたのでご参考まで

https://qiita.com/ku_a_i/items/99bba7c7362285254c7d

## 参考

https://blog.csdn.net/u012254599/article/details/128012719?spm=1001.2101.3001.6650.2&utm_medium=distribute.pc_relevant.none-task-blog-2%7Edefault%7ECTRLIST%7ERate-2-128012719-blog-123146422.pc_relevant_default&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2%7Edefault%7ECTRLIST%7ERate-2-128012719-blog-123146422.pc_relevant_default