しまぞうブログ

プログラミングと資産運用

文字認識アプリをPythonで作ってみた【OCR,anaconda】

読んだ本の内容をメモして残しておきたい
紙の資料に書かれた文章をパソコンに取り込みたい

こんな風に思ったことはありませんか?

我が家は夫婦そろって、読書して気になった部分をWordにメモしています。
この作業は少量なら何てことないのですが、良本になるとメモしたい部分が多くなり、時間がかかってしまうんですよね…

これをもっとラクにしたいと思い少し調べたところ、Pythonで文字認識が簡単にできそうだったので、試しにGUIアプリを作成することにしました。

作業環境

  • Windows10
  • Anaconda 4.10(scoopでインストール)
  • Python 3.8

要求仕様

最終的に奥さんでも使えるように、以下のような仕様に仕上げます。

  • GUIで操作可能にする
  • 複数の画像ファイルを選択し、一括してtxtファイルに変換する
  • ファイルはexeにする

文字認識【OCR, tesseract】

まずはPythonで文字認識するプログラムを作成します。
これについては以下の記事にまとめています。

shimazoh.hatenablog.com

今回はGUIアプリということなので、文字認識用のpyファイル「OcrTool.py」を作成して、それをメインのpyファイルにインポートして使用します。

コード(OcrTool.py)は以下のようになります。

from PIL import Image
import sys
import pyocr

class OcrConverter():

    def __init__(self, lang='jpn_vert', layout=5):
        self.tool = self.get_ocr_tool()
        self.lang = lang
        self.layout = layout

    def get_ocr_tool(self):
        tools = pyocr.get_available_tools()
        if len(tools) == 0:
            print("No OCR tool found.")
            sys.exit(1)

        return tools[0]

    def get_text(self, src):

        if not (src.endswith(('.png', '.jpg', '.PNG', '.JPG'))):
            print("File type is not image.")
            exit()

        txt = self.tool.image_to_string(
            Image.open(src),
            lang=self.lang,
            builder=pyocr.builders.TextBuilder(tesseract_layout=self.layout))

        txt = txt.replace(' ', '')

        return txt

GUIPyQt, PySide, QtDesigner】

次にPySideを使用してGUIを作成します。
PySideを使用したGUIの作成方法については、以下の記事にまとめています。

shimazoh.hatenablog.com

フォームの作成

今回は画像ファイルを選択して、テキストファイルに変換するツールということで、以下のような外観にしました。

各オブジェクトは以下のような役割となっています。

「Add」ボタン画像ファイルを追加
「Remove」ボタン選択した画像ファイルを対象から除外
「Clear all」ボタン全画像ファイルを対象から除外
ドロップダウンリスト認識する文字列の言語
「…」ボタン拡張設定用(未使用)
「Convert!」ボタン画像ファイルをtxtファイルに変換
右側の領域変換対象の画像ファイルを表示

コード(OcrForm.py)は以下の通りです。

from PySide2 import QtCore, QtWidgets

class Ui_form_OCR(object):
    def setupUi(self, form_OCR):
        form_OCR.setObjectName("form_OCR")
        form_OCR.resize(362, 240)
        self.horizontalLayout_2 = QtWidgets.QHBoxLayout(form_OCR)
        self.horizontalLayout_2.setObjectName("horizontalLayout_2")
        self.verticalLayout = QtWidgets.QVBoxLayout()
        self.verticalLayout.setObjectName("verticalLayout")
        self.pushButton_add = QtWidgets.QPushButton(form_OCR)
        self.pushButton_add.setObjectName("pushButton_add")
        self.verticalLayout.addWidget(self.pushButton_add)
        self.pushButton_remove = QtWidgets.QPushButton(form_OCR)
        self.pushButton_remove.setObjectName("pushButton_remove")
        self.verticalLayout.addWidget(self.pushButton_remove)
        self.pushButton_clearAll = QtWidgets.QPushButton(form_OCR)
        self.pushButton_clearAll.setObjectName("pushButton_clearAll")
        self.verticalLayout.addWidget(self.pushButton_clearAll)
        spacerItem = QtWidgets.QSpacerItem(
            20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)
        self.verticalLayout.addItem(spacerItem)
        self.horizontalLayout = QtWidgets.QHBoxLayout()
        self.horizontalLayout.setObjectName("horizontalLayout")
        self.comboBox_config = QtWidgets.QComboBox(form_OCR)
        self.comboBox_config.setObjectName("comboBox_config")
        self.comboBox_config.addItem("")
        self.comboBox_config.addItem("")
        self.comboBox_config.addItem("")
        self.comboBox_config.addItem("")
        self.horizontalLayout.addWidget(self.comboBox_config)
        self.toolButton_config = QtWidgets.QToolButton(form_OCR)
        self.toolButton_config.setEnabled(True)
        self.toolButton_config.setLayoutDirection(QtCore.Qt.LeftToRight)
        self.toolButton_config.setObjectName("toolButton_config")
        self.horizontalLayout.addWidget(self.toolButton_config)
        self.verticalLayout.addLayout(self.horizontalLayout)
        spacerItem1 = QtWidgets.QSpacerItem(
            20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)
        self.verticalLayout.addItem(spacerItem1)
        self.pushButton_convert = QtWidgets.QPushButton(form_OCR)
        sizePolicy = QtWidgets.QSizePolicy(
            QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Preferred)
        sizePolicy.setHorizontalStretch(0)
        sizePolicy.setVerticalStretch(0)
        sizePolicy.setHeightForWidth(
            self.pushButton_convert.sizePolicy().hasHeightForWidth())
        self.pushButton_convert.setSizePolicy(sizePolicy)
        self.pushButton_convert.setObjectName("pushButton_convert")
        self.verticalLayout.addWidget(self.pushButton_convert)
        self.horizontalLayout_2.addLayout(self.verticalLayout)
        self.verticalLayout_2 = QtWidgets.QVBoxLayout()
        self.verticalLayout_2.setObjectName("verticalLayout_2")
        self.listWidget = QtWidgets.QListWidget(form_OCR)
        sizePolicy = QtWidgets.QSizePolicy(
            QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding)
        sizePolicy.setHorizontalStretch(0)
        sizePolicy.setVerticalStretch(0)
        sizePolicy.setHeightForWidth(
            self.listWidget.sizePolicy().hasHeightForWidth())
        self.listWidget.setSizePolicy(sizePolicy)
        self.listWidget.setMinimumSize(QtCore.QSize(40, 40))
        self.listWidget.setAutoFillBackground(False)
        self.listWidget.setDragDropMode(QtWidgets.QAbstractItemView.NoDragDrop)
        self.listWidget.setResizeMode(QtWidgets.QListView.Fixed)
        self.listWidget.setObjectName("listWidget")
        self.verticalLayout_2.addWidget(self.listWidget)
        self.horizontalLayout_2.addLayout(self.verticalLayout_2)

        self.retranslateUi(form_OCR)
        self.pushButton_convert.clicked['bool'].connect(form_OCR.convert)
        self.pushButton_add.clicked.connect(form_OCR.add)
        self.pushButton_remove.clicked.connect(form_OCR.remove)
        self.pushButton_clearAll.clicked.connect(form_OCR.clearAll)
        QtCore.QMetaObject.connectSlotsByName(form_OCR)
        form_OCR.setTabOrder(self.pushButton_add, self.pushButton_remove)
        form_OCR.setTabOrder(self.pushButton_remove, self.comboBox_config)
        form_OCR.setTabOrder(self.comboBox_config, self.toolButton_config)
        form_OCR.setTabOrder(self.toolButton_config, self.pushButton_convert)
        form_OCR.setTabOrder(self.pushButton_convert, self.listWidget)

    def retranslateUi(self, form_OCR):
        _translate = QtCore.QCoreApplication.translate
        form_OCR.setWindowTitle(_translate("form_OCR", "OCR"))
        self.pushButton_add.setText(_translate("form_OCR", "Add"))
        self.pushButton_remove.setText(_translate("form_OCR", "Remove"))
        self.pushButton_clearAll.setText(_translate("form_OCR", "Clear all"))
        self.comboBox_config.setItemText(0, _translate("form_OCR", "JP vert"))
        self.comboBox_config.setItemText(1, _translate("form_OCR", "JP hor"))
        self.comboBox_config.setItemText(2, _translate("form_OCR", "ENG"))
        self.comboBox_config.setItemText(3, _translate("form_OCR", "Custom"))
        self.toolButton_config.setText(_translate("form_OCR", "..."))
        self.pushButton_convert.setText(_translate("form_OCR", "Convert!"))

フォームを表示するには以下のようなプログラムを実行します。

import sys
from PySide2 import QtWidgets
from OcrForm import Ui_form_OCR
import os

class OcrUi(QtWidgets.QDialog):
    def __init__(self, parent=None):
        super(OcrUi, self).__init__(parent)
        self.ui = Ui_form_OCR()
        self.ui.setupUi(self)

if __name__ == '__main__':
    app = QtWidgets.QApplication(sys.argv)
    window = OcrUi()
    window.show()
    sys.exit(app.exec_())

ボタンクリック時の動作を定義する

GUIのボタンを押した際に実行される関数を定義します。詳細は省きます。

なお、各フォームのメソッドについては、公式ページ(英語)に記載があります。

最終的に以下のようなコードになりました。

import sys
from PySide2 import QtWidgets
from OcrForm import Ui_form_OCR
from OcrTool import OcrConverter
import os

class OcrUi(QtWidgets.QDialog):
    def __init__(self, parent=None):
        super(OcrUi, self).__init__(parent)
        self.ui = Ui_form_OCR()
        self.ui.setupUi(self)

    def add(self):
        fileNames = self.get_filenames()
        self.ui.listWidget.addItems(fileNames)

    def get_filenames(self):
        fileInfos = QtWidgets.QFileDialog.getOpenFileNames(
            filter="Images (*.jpg *.png)")
        return fileInfos[0]

    def get_directory(self):
        dirName = QtWidgets.QFileDialog.getExistingDirectory(
            options=QtWidgets.QFileDialog.ShowDirsOnly)
        return dirName

    def remove(self):
        self.ui.listWidget.takeItem(self.ui.listWidget.currentRow())

    def clearAll(self):
        self.ui.listWidget.clear()

    def get_list(self):
        lst = []
        for index in range(self.ui.listWidget.count()):
            lst.append(self.ui.listWidget.item(index).text())
        return lst

    def convert(self):
        
        dirName = self.get_directory()

        lang = self.get_ocr_lang(self.ui.comboBox_config.currentText())
        builder = self.get_ocr_builder(self.ui.comboBox_config.currentText())
        image_recog = OcrConverter(lang, builder)

        image_filter = ('.png', '.jpg', '.PNG', '.JPG')
        to_txt_mode = 'w'
        to_txt_encode = 'cp932'

        for src in self.get_list():

            if not (src.endswith(image_filter)):
                continue

            print("Converting... "+format(src))
            txt = image_recog.get_text(src)
            print(txt+'\n')

            filename = dirName+"\\"+os.path.splitext(os.path.basename(src))[0]+".txt"
            with open(filename, mode=to_txt_mode, encoding=to_txt_encode) as f:
                f.write(txt)

        self.clearAll()
        print("All images were converted.")

    def get_ocr_lang(self, method):
        if method == "JP vert":
            return 'jpn_vert'
        elif method == 'JP hor':
            return 'jpn'
        elif method == 'ENG':
            return 'eng'
        else:
            print("This OCR language is not available.")
            exit()

    def get_ocr_builder(self, method):
        if method == "JP vert":
            return 5
        elif method == 'JP hor':
            return 6
        elif method == 'ENG':
            return 6
        else:
            print("This OCR builder is not available.")
            exit()


if __name__ == '__main__':
    app = QtWidgets.QApplication(sys.argv)
    window = OcrUi()
    window.show()
    sys.exit(app.exec_())

実行ファイル(.exe)への変換

ここまででアプリとして使用できるのですが、うちの奥さんはPCオンチなので、作ったpyファイルを実行してもらうのはちょっと厳しいです…
このため、exe化して気軽に使えるようにします。

exe化については以下の記事にて説明しています。

shimazoh.hatenablog.com

動作確認

完成したツールを起動すると、以下のような画面が表示されます。

まずは「Add」ボタンで画像ファイルを追加してみます。

複数ファイルの同時選択も可能

追加したファイル名が右側のリストに表示されました。

リスト中のファイルを選択し、「Remove」ボタンを押すとファイルが除外されます。

sample2.pngを選択して「Remove」を押下
sample2.pngが除外された

「Clear all」ボタンを押すとすべてのファイルがリストから除外されます。

リストに画像ファイルが追加されている状態で「Convert!」ボタンを押すとテキストファイルへの変換が始まります。

変換が完了すると、テキストファイルが生成されています。

sample1.txtとsample2.txtが生成されている

まとめ

画像ファイルを文字認識して、テキストファイルを生成するPythonGUIアプリを作成しました。

アプリは実際に奥さんにも使ってもらえるぐらい簡単に使用できるのですが、実は文字認識の精度が微妙です…
実際の使用場面では、変換後のテキストファイルを人の目で確認して細かく修正を入れる必要があるのが少し残念です。

私個人としては色々と勉強になる部分が多かったです。また、何かのアプリ制作を通して、プログラミング学習ができればと思っています。