文字認識アプリをPythonで作ってみた【OCR,anaconda】
読んだ本の内容をメモして残しておきたい
紙の資料に書かれた文章をパソコンに取り込みたい
こんな風に思ったことはありませんか?
我が家は夫婦そろって、読書して気になった部分をWordにメモしています。
この作業は少量なら何てことないのですが、良本になるとメモしたい部分が多くなり、時間がかかってしまうんですよね…
これをもっとラクにしたいと思い少し調べたところ、Pythonで文字認識が簡単にできそうだったので、試しにGUIアプリを作成することにしました。
作業環境
- Windows10
- Anaconda 4.10(scoopでインストール)
- Python 3.8
要求仕様
最終的に奥さんでも使えるように、以下のような仕様に仕上げます。
- GUIで操作可能にする
- 複数の画像ファイルを選択し、一括してtxtファイルに変換する
- ファイルはexeにする
文字認識【OCR, tesseract】
まずはPythonで文字認識するプログラムを作成します。
これについては以下の記事にまとめています。
今回は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
GUI【PyQt, PySide, QtDesigner】
次にPySideを使用してGUIを作成します。
PySideを使用したGUIの作成方法については、以下の記事にまとめています。
フォームの作成
今回は画像ファイルを選択して、テキストファイルに変換するツールということで、以下のような外観にしました。
各オブジェクトは以下のような役割となっています。
「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化については以下の記事にて説明しています。
動作確認
完成したツールを起動すると、以下のような画面が表示されます。
まずは「Add」ボタンで画像ファイルを追加してみます。
追加したファイル名が右側のリストに表示されました。
リスト中のファイルを選択し、「Remove」ボタンを押すとファイルが除外されます。
「Clear all」ボタンを押すとすべてのファイルがリストから除外されます。
リストに画像ファイルが追加されている状態で「Convert!」ボタンを押すとテキストファイルへの変換が始まります。
変換が完了すると、テキストファイルが生成されています。
まとめ
画像ファイルを文字認識して、テキストファイルを生成するPythonのGUIアプリを作成しました。
アプリは実際に奥さんにも使ってもらえるぐらい簡単に使用できるのですが、実は文字認識の精度が微妙です…
実際の使用場面では、変換後のテキストファイルを人の目で確認して細かく修正を入れる必要があるのが少し残念です。
私個人としては色々と勉強になる部分が多かったです。また、何かのアプリ制作を通して、プログラミング学習ができればと思っています。