OpenCVとTesseract-OCRで作る表のOCRツール rev1

 OpenCVとTesseract-OCRで作る表のOCRツールで作成したルーツを更新しました。主な変更点はコードの構造を書き換えたり、GUIを作り変えたといった内容です。PyQt5のコード作成に慣れてきて以前書いたコードを書き換えたくなったので変更しました。また、GUIが使いにくいと思っていたので変更をしました。

ファイルの追加と表示

 OpenボタンをクリックしてPDFファイルを選択するとファイルリストにファイル名が表示されます。ファイル名をクリックするとPDFが画像に変換されて表示します。画像変換済みのファイルは2列目にimageと表示されます。imageと表示されているファイルをクリックした場合、再度画像変換することはなく、作成済みの画像を表示します。チェックボックスを切り替えると画像の表示と非表示が切り替えられます。

セルの認識

 Recognize allをクリックして表のセルを認識します。認識結果はファイル名の子としてリストに追加されます。Rectはセルの認識結果で赤色で表示しているもの、Cropはセル内の余白をクロップした緑色で表示しているものです。その認識結果の2列目には認識結果に基づいてクロップした画像が表示されます。

 ファイル名の子として表示されているアイテムをクリックすると、そのアイテムがどの場所であるか確認できるように青色で表示します。

セルの認識エリアの削除と追加

 ファイル名の子として表示されているアイテムをクリックした状態で、Delete rectボタンをクリックすると、選択したアイテムを削除できます。また、その状態でDraw rectをクリックしてから画像内をドラッグして範囲指定すると、その範囲をクロップしてファイルアイテムに追加します。

セルのOCR

 OCRボタンをクリックして2列目に表示されているセルの画像をすべて文字列に変換します。

CSVファイルの保存

 文字列はSaveボタンをクリックするとCSVファイルとして保存できます。作成されるCSVファイルは以下の通りです。

ソースコード

main.py

ここをクリックして展開
# -*- coding: utf-8 -*-
import csv
import sys
from pathlib import Path
from poppler import Poppler
from PyQt5 import QtWidgets, QtCore, QtGui

from table_recognition_view import TableRecognitionView
from toolbar import ToolBar
from filelist import FileList
from tesseract_ocr import TesseractOCR
from iamge_processing import ImageProcessing

class MainWindow(QtWidgets.QMainWindow):
    def __init__(self):
        super().__init__()
        self.poppler = Poppler('./poppler/')
        self.tesseractOCR = TesseractOCR('./Tesseract-OCR/tesseract.exe')
        self.resize(800, 600)
        self.setCentralWidget( QtWidgets.QWidget(self) )
        self.recogView = TableRecognitionView( self.centralWidget() )
        
        self.layout = QtWidgets.QVBoxLayout( self.centralWidget() )
        self.layout.setContentsMargins(0, 0, 0, 0)
        self.layout.addWidget(self.recogView)

        self.toolBar = ToolBar( self.centralWidget() )
        self.toolBarDock = QtWidgets.QDockWidget('', self)
        self.toolBarDock.setWidget(self.toolBar)
        self.toolBarDock.setFloating(False)
        self.toolBarDock.setFeatures(QtWidgets.QDockWidget.NoDockWidgetFeatures)
        self.toolBarDock.setTitleBarWidget( QtWidgets.QWidget() )
        self.addDockWidget(QtCore.Qt.TopDockWidgetArea, self.toolBarDock)

        self.fileList = FileList(self)
        self.fileListDock = QtWidgets.QDockWidget('File list', self)
        self.fileListDock.setWidget(self.fileList)
        self.fileListDock.setFeatures(QtWidgets.QDockWidget.NoDockWidgetFeatures)
        self.fileListDock.setTitleBarWidget( QtWidgets.QWidget() )
        self.addDockWidget(QtCore.Qt.LeftDockWidgetArea, self.fileListDock)
        
        self.toolBar.drawMenu.drawRectButton.clicked.connect(lambda flag : self.recogView.setDrawRectFlag(flag))
        self.toolBar.drawMenu.deleteRectButton.clicked.connect(self.removeSelectedRects)
        self.toolBar.drawMenu.drawRectButton.setEnabled(False)
        self.toolBar.fileMenu.openButton.clicked.connect(self.fileOpen)
        self.toolBar.fileMenu.saveButton.clicked.connect(self.saveFiles)
        self.toolBar.imageMenu.recognizeButton.clicked.connect(self.recognizeAll)
        self.toolBar.viewMenu.rowHeight.valueChanged.connect(self.rowHeightChanged)
        self.toolBar.viewMenu.addToggleViewButton(self.fileListDock.toggleViewAction(), 0, 0, 1, 1)
        self.toolBar.ocrMenu.ocrButton.clicked.connect(self.ocrChildren)
        self.recogView.mouseLeftReleasedSignal.connect(self.recogViewClicked)
        self.fileList.tableClicked.connect(self.filelistClicked)

        self.rowHeightChanged( self.toolBar.viewMenu.rowHeight.value() )

    def filelistClicked(self, clickedIndex):
        if not clickedIndex.parent().isValid():
            self.recogView.drawRectFlag = self.toolBar.drawMenu.drawRectButton.isChecked() and True

            pixmap = self.fileList.item(clickedIndex.row(), 1).data()
            if pixmap is None:
                filePath = self.fileList.filePath( clickedIndex.row() )
                pixmap = self.pathToPixmap( clickedIndex, filePath )
                pixmapIndex = self.fileList.index(clickedIndex.row(), 1)
                self.fileList.setData( pixmapIndex, 'image', pixmap )

            parentIndex = clickedIndex
            pixmap = self.fileList.getImage( parentIndex.row() )
            self.fileList.view.setCurrentIndex(clickedIndex)
            self.toolBar.drawMenu.drawRectButton.setEnabled(True)
        else:
            self.recogView.drawRectFlag = False
            parentIndex = clickedIndex.parent()
            pixmap = self.fileList.getImage( parentIndex.row() )
            self.toolBar.drawMenu.drawRectButton.setEnabled(False)

        self.refreshView(pixmap, parentIndex)

    def fileOpen(self):
        files, _ = QtWidgets.QFileDialog.getOpenFileNames(None, 'Open PDF files', '')
        for path in [ Path(f) for f in files]:
            self.fileList.appendFile(path)
        self.fileList.setViewSize()

    def ocrChildren(self):
        count = 0
        for row in range(self.fileList.model.rowCount()):
            for rectRow in range(self.fileList.model.item(row).rowCount()):
                count = count + 1
        progressBar = QtWidgets.QProgressBar()
        progressBar.setMaximum(count)
        self.statusBar().addPermanentWidget(progressBar)

        count = 0
        for row in range(self.fileList.model.rowCount()):
            fileItem = self.fileList.model.item(row)
            for rectRow in range(fileItem.rowCount()):
                rectPixmap = fileItem.child(rectRow, 1).data()
                text = self.tesseractOCR.OCR(rectPixmap)
                fileItem.child(rectRow, 2).setText(text)
                count = count + 1
                progressBar.setValue(count)
                QtWidgets.QApplication.processEvents()

        self.statusBar().removeWidget(progressBar)

    def pathToPixmap(self, index, pdfpath):
        paths = self.poppler.pdftocairo(pdfpath, Path('pdftocairo_temp.png'), 300)
        if len(paths) > 0:
            pixmap = QtGui.QPixmap( str(paths[0]) )
            for path in paths:
                path.unlink()
            return pixmap
        return None

    def refreshView(self, pixmap=None, parentIndex=None):
        self.recogView.clear()

        if not pixmap is None:
            self.recogView.addPixmap(pixmap)
        
        if not parentIndex is None:
            for item in self.fileList.childrenWithText('Rect', 0, parentIndex):
                self.recogView.appendRect(item.data())

            for item in self.fileList.childrenWithText('Crop', 0, parentIndex):
                self.recogView.appendCrop(item.data())

            for item in self.fileList.selectedChildren(0, parentIndex):
                self.recogView.appendSelectedRect(item.data())
        
        self.recogView.update()

    def recognizeAll(self):
        self.fileList.removeAllRect()
        
        for row in range( self.fileList.model.rowCount() ):
            parentIndex = self.fileList.index(row, 0)
            pixmap = self.fileList.item(row, 1).data()
            if pixmap is None:
                filePath = self.fileList.filePath(row)
                pixmap = self.pathToPixmap( parentIndex, filePath )
                self.fileList.setFileData(row, 1, 'image', pixmap)

            area_range = ( self.toolBar.imageMenu.contourAreaMin.value(), self.toolBar.imageMenu.contourAreaMax.value() )
            dilate_size = ( self.toolBar.imageMenu.dilate.value(), self.toolBar.imageMenu.dilate.value() )
            image_process = ImageProcessing(pixmap)
            edge, rects, crops = image_process.recognize_table(area_range, dilate_size)

            self.fileList.setFileData(row, 2, 'edge image', edge, QtCore.Qt.Unchecked)

            for text, datas in zip(['Rect', 'Crop'], [rects, crops]):
                for data in datas:
                    rectItem = QtGui.QStandardItem(text)
                    rectItem.setData(QtCore.QRectF(data[0], data[1], data[2], data[3]))
                    rectItem.setCheckable(True)
                    rectItem.setCheckState(QtCore.Qt.Checked)
                    rectItem.setEditable(False)

                    cropPixmap = pixmap.copy(QtCore.QRect(data[0], data[1], data[2], data[3]))
                    pixmapItem = QtGui.QStandardItem()
                    pixmapItem.setData(cropPixmap)
                    pixmapItem.setData(cropPixmap, QtCore.Qt.DecorationRole)
                    pixmapItem.setEditable(False)

                    self.fileList.appendChild( parentIndex, [rectItem, pixmapItem, QtGui.QStandardItem()] )
                
        self.refreshView(None, None)

    def recogViewClicked(self, rect):
        if self.recogView.drawRectFlag:
            self.fileList.addRect(rect)
            self.recogView.appendRect(rect)

    def removeSelectedRects(self):
        removedRects, parentIndex = self.fileList.removeSelectedRects()
        for rect in removedRects:
            self.recogView.removeRect(rect)
        pixmap = self.fileList.getImage( parentIndex.row() )
        self.refreshView(pixmap, parentIndex)
        
    def rowHeightChanged(self, value):
        self.fileList.view.setStyleSheet('QTreeView::item { padding: ' + str(value) + 'px }')

    def saveFiles(self):
        csvDatas = []
        for row in range(self.fileList.model.rowCount()):
            fileItem = self.fileList.model.item(row)
            for rectRow in range(fileItem.rowCount()):
                csvDatas.append([
                    fileItem.child(rectRow, 0).text().strip(),
                    fileItem.child(rectRow, 2).text().strip()
                ])
        with open('output.csv', 'w', newline='') as f:
            writer = csv.writer(f)
            writer.writerows(csvDatas)

def main():
    app = QtWidgets.QApplication(sys.argv)
    window = MainWindow()
    window.show()
    app.exec()

if __name__ == '__main__':
    main()

filelist.py

ここをクリックして展開
# -*- coding: utf-8 -*-
from pathlib import Path
from PyQt5 import QtWidgets, QtCore, QtGui

class FileList(QtWidgets.QWidget):
    tableClicked = QtCore.pyqtSignal(QtCore.QModelIndex)
    def __init__(self, parent=None, path=None):
        super(FileList, self).__init__(parent)
        self.setLayout( QtWidgets.QVBoxLayout(self) )
        self.model = QtGui.QStandardItemModel(self)
        self.model.setHorizontalHeaderLabels(['', '', ''])
        self.view = QtWidgets.QTreeView(self)
        self.view.setModel(self.model)
        self.view.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)
        self.view.setItemDelegate(Delegate())

        self.layout().setContentsMargins(0, 0, 0, 0)
        self.layout().addWidget(self.view)

        self.view.clicked.connect( lambda index : self.tableClicked.emit(index) )

    def addRect(self, rect):
        rows = list(set([ index.row() for index in self.view.selectedIndexes() ]))
        for row in rows:
            parentIndex = self.model.index(row, 0)
            parentItem = self.model.item( parentIndex.row(), 1 )
            pixmap = parentItem.data()

            rectItem = QtGui.QStandardItem('Rect')
            rectItem.setData(rect)
            rectItem.setCheckable(True)
            rectItem.setCheckState(QtCore.Qt.Checked)
            rectItem.setEditable(False)

            cropPixmap = pixmap.copy(QtCore.QRect( rect.x(), rect.y(), rect.width(), rect.height() ))
            pixmapItem = QtGui.QStandardItem()
            pixmapItem.setData(cropPixmap)
            pixmapItem.setData(cropPixmap, QtCore.Qt.DecorationRole)
            pixmapItem.setEditable(False)

            self.appendChild( parentIndex, [rectItem, pixmapItem, QtGui.QStandardItem()] )
            
    def appendChild(self, parentIndex, items):
        parentItem = self.model.item( parentIndex.row() )
        parentItem.appendRow(items)

    def appendFile(self, path):
        filename = QtGui.QStandardItem(str(path.name))
        filename.setData(path)
        image = QtGui.QStandardItem()
        edge = QtGui.QStandardItem()
        self.model.appendRow([ filename, image, edge ])

    def childrenWithText(self, text, column=0, parentIndex=QtCore.QModelIndex()):
        children = []
        selectedIndexes = self.view.selectedIndexes()
        for item in self.items(column, parentIndex):
            if not item.checkState() == QtCore.Qt.Checked:
                continue
            if not item.text() == text:
                continue
            if self.model.index(item.row(), column) in selectedIndexes:
                continue
            children.append(item)
        return children

    def filePath(self, row):
        return self.model.item(row).data()

    def getImage(self, row):
        if self.item(row, 1).checkState() == QtCore.Qt.Checked:
            return self.item(row, 1).data()
        if self.item(row, 2).checkState() == QtCore.Qt.Checked:
            return self.item(row, 2).data()
        return None

    def index(self, row, column, parentIndex=QtCore.QModelIndex()):
        return self.model.index(row, column, parentIndex)

    def item(self, row, column=0):
        return self.model.item(row, column)

    def items(self, column=0, parentIndex=QtCore.QModelIndex()):
        if parentIndex.isValid():
            item = self.model.item( parentIndex.row() )
            return [ item.child(row, column) for row in range( item.rowCount() ) ]
        else:
            return [ self.model.item(row, column) for row in range( self.model.rowCount() ) ]
        
    def removeSelectedRects(self):
        removeList = {}
        removedRects = []
        for index in self.view.selectedIndexes():
            parentIndex = index.parent()
            if not parentIndex.isValid():
                continue
            if not parentIndex in removeList:
                removeList[parentIndex] = {}
            parentItem = self.model.item(parentIndex.row())
            item = parentItem.child(index.row())
            removeList[parentIndex][index.row()] = item.data()
        
        for parentIndex in removeList:
            deleteRowAndRects = sorted( removeList[parentIndex].items(), reverse=True )
            deleteRowAndRects = { key : data for key, data in deleteRowAndRects }
            for deleteRow in deleteRowAndRects:
                rect = deleteRowAndRects[deleteRow]
                self.model.removeRow(deleteRow, parentIndex)
                removedRects.append(rect)

        parentIndex = list(removeList.keys())[0]
        return removedRects, parentIndex

    def removeAllRect(self):
        for row in range( self.model.rowCount() ):
            parentItem = self.item(row)
            for childRow in list( range(parentItem.rowCount()) )[::-1]:
                parentItem.removeRow(childRow)

    def selectedChildren(self, column=0, parentIndex=QtCore.QModelIndex()):
        children = []
        selectedIndexes = [ [i.row(), i.column(), i.parent()] for i in self.view.selectedIndexes() ]
        for item in self.items(column, parentIndex):
            if not item.checkState() == QtCore.Qt.Checked:
                continue
            if not [item.row(), 0, parentIndex] in selectedIndexes:
                continue
            children.append(item)
        return children

    def selectedIndexes(self):
        return self.view.selectedIndexes()

    def setData(self, index, text, data, state=QtCore.Qt.Checked):
        item = QtGui.QStandardItem(text)
        item.setData(data)
        item.setCheckable(True)
        item.setCheckState(state)
        self.model.setItem( index.row(), index.column(), item )
        return item

    def setFileData(self, row, column, text, data, state=QtCore.Qt.Checked):
        item = QtGui.QStandardItem(text)
        item.setData(data)
        item.setCheckable(True)
        item.setCheckState(state)
        self.model.setItem( row, column, item )
        return item

    def setViewSize(self):
        self.view.setColumnWidth(0, 150)
        self.view.setColumnWidth(1, 80)
        self.view.setColumnWidth(2, 80)
        
class Delegate(QtWidgets.QStyledItemDelegate):
    def __init__(self, parent=None):
        super(Delegate, self).__init__(parent)

    def paint(self, painter, option, index):
        item = index.model().itemFromIndex(index)
        if type( item.data() ) is QtGui.QPixmap and not item.parent() is None:
            pixmapWidth, pixmapHeight = item.data().size().width(), item.data().size().height()
            cellWidth, cellHeight = option.rect.width(), option.rect.height()
            if cellWidth / cellHeight < pixmapWidth / pixmapHeight:
                scaledPixmap = item.data().scaledToWidth(cellWidth)
            else:
                scaledPixmap = item.data().scaledToHeight(cellHeight)
            item.setData(scaledPixmap, QtCore.Qt.DecorationRole)

        super(Delegate, self).paint(painter, option, index)

if __name__ == '__main__':
    import sys
    
    def item(file):
        pixmapItem = QtGui.QStandardItem()
        pixmapItem.setData(QtGui.QPixmap(file))
        pixmapItem.setData(QtGui.QPixmap(file), QtCore.Qt.DecorationRole)
        return pixmapItem

    app = QtWidgets.QApplication(sys.argv)
    view = FileList()
    view.model.setHorizontalHeaderLabels([ 'column' + str(i) for i in range(3) ])
    #view.model.appendRow([ QtGui.QStandardItem(str(i)) for i in range(3) ])
    
    view.model.appendRow([ item('cell00'), item('cell01'), QtGui.QStandardItem() ])
    view.model.appendRow([ item('cell10'), item('cell11'), item('cell12') ])
    view.model.appendRow([ item('cell20'), item('cell21'), QtGui.QStandardItem() ])

    view.resize(600, 300)
    view.show()
    app.exec()

iamge_processing.py

ここをクリックして展開
# -*- coding: utf-8 -*-
import cv2
import numpy as np
from pathlib import Path, WindowsPath
from PyQt5 import QtGui

class ImageProcessing(object):
    def __init__(self, data):
        self.data = self.load_data(data)
        
    def cv_to_pixmap(self, cv_image):
        shape_size = len(cv_image.shape)
        if shape_size == 2:
            rgb = cv2.cvtColor(cv_image, cv2.COLOR_GRAY2RGB)
        elif shape_size == 3:
            rgb = cv2.cvtColor(cv_image, cv2.COLOR_BGR2RGB)
        height, width, bytesPerComponent = rgb.shape
        bytesPerLine = bytesPerComponent * width
        image = QtGui.QImage(rgb.data, width, height, bytesPerLine, QtGui.QImage.Format_RGB888)
        qpixmap = QtGui.QPixmap.fromImage(image)
        return qpixmap

    def edge_image(self, size):
        gray = cv2.cvtColor(self.data, cv2.COLOR_BGR2GRAY)
        edge = cv2.Canny(gray, 1, 100, apertureSize=3)
        kernel = cv2.getStructuringElement(cv2.MORPH_RECT, size)
        dilate = cv2.dilate(edge, kernel)
        return dilate

    def edge_to_rects(self, edge, area_range):
        contours, hierarchy = cv2.findContours(edge, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)

        rects = []
        for contour, hierarchy in zip(contours, hierarchy[0]):

            if not area_range[0] < cv2.contourArea(contour) < area_range[1]:
                continue

            curve = cv2.approxPolyDP(contour, 0.01 * cv2.arcLength(contour, True), True)
            if len(curve) == 4:
                p1, p3 = curve[0][0], curve[2][0]
                x, y, w, h = p1[0], p1[1], p3[0] - p1[0], p3[1] - p1[1]
                rect =  [x, y, w, h]

                if False in [ False for r in rect if r < 1 ]:
                    continue
                
                if self.same_rect_is_in_rects(rect, rects, 10):
                    continue

                rects.append(rect)
        
        rects = sorted( rects, key=lambda x: (x[1], x[0]) )

        return rects

    def load_data(self, data):
        data_type = type(data)

        if data_type is str or data_type is WindowsPath:
            return cv2.imread( str(data) )

        if data_type is QtGui.QPixmap:
            return self.qimage_to_cv( data.toImage() )

    def qimage_to_cv(self, qimage):
        w, h, d = qimage.size().width(), qimage.size().height(), qimage.depth()
        bytes_ = qimage.bits().asstring(w * h * d // 8)
        arr = np.frombuffer(bytes_, dtype=np.uint8).reshape((h, w, d // 8))
        return arr

    def recognize_table(self, area_range=(10, 1000), dilate_size=(6, 6)):
        edge = self.edge_image(dilate_size)
        rects = self.edge_to_rects(edge, area_range)
        crops = self.rects_to_crops(rects)
        edge = self.cv_to_pixmap(edge)
        return edge, rects, crops

    def rects_to_crops(self, rects, margin=10):
        crops = []
        for rect in rects:
            x, y, w, h = rect[0], rect[1], rect[2], rect[3]
            cropped = self.data[ y : y + h, x : x + w ]

            gray = cv2.cvtColor(cropped, cv2.COLOR_BGR2GRAY)
            threshold = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY)[1]
            contours = cv2.findContours(threshold, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)[0]

            rects_in_cropped = [ cv2.boundingRect(contour) for contour in contours[1:] ]

            if len(rects_in_cropped) == 0:
                continue

            x1 = min([ r[0] for r in rects_in_cropped ]) - margin
            y1 = min([ r[1] for r in rects_in_cropped ]) - margin
            x2 = max([ r[0] + r[2] for r in rects_in_cropped ]) + margin
            y2 = max([ r[1] + r[3] for r in rects_in_cropped ]) + margin

            if x1 < 0:
                x1 = 0
            if y1 < 0:
                y1 = 0
            if x2 > cropped.shape[1]:
                x2 = cropped.shape[1]
            if y2 > cropped.shape[0]:
                y2 = cropped.shape[0]

            crops.append([x + x1, y + y1, x2 - x1, y2 - y1])

        return crops

    def same_rect_is_in_rects(self, rect1, rects, tolerance=5):
        for rect2 in rects:
            frag = True
            for r1, r2 in zip(rect1, rect2):
                if not r2 - tolerance < r1 < r2 + tolerance:
                    frag = False
                    break
            if frag:
                return True
        return False

poppler.py

ここをクリックして展開
# -*- coding: utf-8 -*-
import subprocess
from pathlib import Path
import chardet

class Poppler():
    def __init__(self, popplerPath):
        pdftocairo = list(Path(popplerPath).glob('**/pdftocairo.exe'))
        if len(pdftocairo) > 0:
            self.pdftocairo_path = pdftocairo[0]
        else:
            self.pdftocairo_path = None
        
    def pdftocairo(self, input_path, output_path, resolution):
        suffix = '-'+str(output_path.suffix).replace('.', '')
        cmd = [str(self.pdftocairo_path), suffix, '-r', str(resolution), str(input_path), str(output_path.stem)]
        return_value = self.subprocess_run(cmd)
        paths = []
        count = 1
        p = Path(output_path.stem + '-' + str(count) + '.png')
        while p.exists():
            paths.append(p)
            count = count + 1
            p = Path(output_path.stem + '-' + str(count) + '.png')
        return paths

    def subprocess_run(self, cmd):
        startupinfo = subprocess.STARTUPINFO()
        startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
        startupinfo.wShowWindow = subprocess.SW_HIDE
        
        returncode = subprocess.Popen(cmd, startupinfo=startupinfo)
        returncode.wait()
        
        return returncode

table_recognition_view.py

ここをクリックして展開
# -*- coding: utf-8 -*-
from PyQt5 import QtWidgets, QtCore, QtGui

class TableRecognitionView(QtWidgets.QWidget):
    mouseLeftReleasedSignal = QtCore.pyqtSignal(QtCore.QRectF)
    def __init__(self, parent=None):
        super().__init__(parent)
        self.drawRectFlag = False
        self.rectPen     = QtGui.QPen( QtGui.QBrush( QtGui.QColor(255,   0,   0, 100) ), 6 )
        self.cropPen     = QtGui.QPen( QtGui.QBrush( QtGui.QColor(  0, 255,   0, 100) ), 6 )
        self.freeHandPen = QtGui.QPen( QtGui.QBrush( QtGui.QColor(100, 100,   0, 150) ), 6 )
        self.selectedPen = QtGui.QPen( QtGui.QBrush( QtGui.QColor(  0,   0, 255, 150) ), 20 )
        self.scene = QtWidgets.QGraphicsScene()
        self.graphicsView = GraphicsView(self)
        self.graphicsView.setScene(self.scene)
        self.graphicsView.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding)
        self.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding)

        self.setLayout( QtWidgets.QVBoxLayout(self) )
        self.layout().setContentsMargins(0, 0, 0, 0)
        self.layout().addWidget(self.graphicsView)

        self.graphicsView.mouseLeftReleased.connect(self.mouseLeftReleased)

    def appendCrop(self, rect):
        rectItem = QtWidgets.QGraphicsRectItem(rect)
        rectItem.setPen(self.cropPen)
        self.scene.addItem(rectItem)

    def appendRect(self, rect):
        rectItem = QtWidgets.QGraphicsRectItem(rect)
        rectItem.setPen(self.rectPen)
        self.scene.addItem(rectItem)

    def appendSelectedRect(self, rect):
        rectItem = QtWidgets.QGraphicsRectItem(rect)
        rectItem.setPen(self.selectedPen)
        self.scene.addItem(rectItem)

    def mouseLeftReleased(self, rect):
        if self.drawRectFlag:
            rectItem = QtWidgets.QGraphicsRectItem(rect)
            rectItem.setPen(self.freeHandPen)
            self.scene.addItem(rectItem)
            self.mouseLeftReleasedSignal.emit(rect)

    def resetPixmap(self, pixmap):
        self.scene.clear()
        if not pixmap is None:
            self.scene.addPixmap(pixmap)

    def removeRect(self, rect):
        for rectItem in self.scene.items():
            if type(rectItem) is QtWidgets.QGraphicsRectItem:
                if rect == rectItem.rect():
                    self.scene.removeItem(rectItem)

    def removeAllRect(self):
        for rectItem in self.scene.items():
            if type(rectItem) is QtWidgets.QGraphicsRectItem:
                self.scene.removeItem(rectItem)

    def setDrawRectFlag(self, flag):
        self.drawRectFlag = flag

    def setRectVisible(self, rect, isVisible):
        for rectItem in self.scene.items():
            if type(rectItem) is QtWidgets.QGraphicsRectItem:
                if rect is rectItem.rect():
                    rectItem.setVisible(isVisible)

    def addPixmap(self, pixmap):
        self.scene.addPixmap(pixmap)

    def clear(self):
        self.scene.clear()

class GraphicsView(QtWidgets.QGraphicsView):
    mouseLeftReleased = QtCore.pyqtSignal(QtCore.QRectF)
    def __init__(self, parent=None):
        super(GraphicsView, self).__init__(parent)
        self.numScheduledScalings = 0
        self.rect = QtCore.QRectF(0.0, 0.0, 0.0, 0.0)

    def animation_finished(self):
        if self.numScheduledScalings > 0:
            self.numScheduledScalings -= 1
        else:
            self.numScheduledScalings += 1

    def wheelEvent(self, event):
        numDegrees = event.angleDelta().y() / 8
        numSteps = numDegrees / 15
        self.numScheduledScalings += numSteps
        if self.numScheduledScalings * numSteps < 0:
            self.numScheduledScalings = numSteps
        anim = QtCore.QTimeLine(350, self)
        anim.setUpdateInterval(20)
        anim.valueChanged.connect(self.scaling_time)
        anim.finished.connect(self.animation_finished)
        anim.start()

    def mousePressEvent(self, event):
        if event.button() == QtCore.Qt.MidButton:
            self.setDragMode(QtWidgets.QGraphicsView.ScrollHandDrag)

            event = QtGui.QMouseEvent(
                QtCore.QEvent.GraphicsSceneDragMove, 
                event.pos(), 
                QtCore.Qt.MouseButton.LeftButton, 
                QtCore.Qt.MouseButton.LeftButton, 
                QtCore.Qt.KeyboardModifier.NoModifier
            )

        elif event.button() == QtCore.Qt.LeftButton:
            self.setDragMode(QtWidgets.QGraphicsView.RubberBandDrag)
            point = self.mapToScene( event.pos() )
            self.rect = QtCore.QRectF(point, point)

        super().mousePressEvent(event)
        
    def mouseReleaseEvent(self, event):
        super().mouseReleaseEvent(event)

        self.setDragMode(QtWidgets.QGraphicsView.NoDrag)

        if event.button() == QtCore.Qt.LeftButton:
            p2 = self.mapToScene( event.pos() )
            self.rect.setBottomRight(p2)
            self.mouseLeftReleased.emit(self.rect)

    def scaling_time(self, x):
        factor = 1.0 + float(self.numScheduledScalings) / 300.0
        self.scale(factor, factor)

if __name__ == '__main__':
    import sys
    app = QtWidgets.QApplication(sys.argv)
    view = TableRecognitionView()
    view.setPixmap( QtGui.QPixmap('table1.png') )
    view.resize(600, 300)
    view.show()
    app.exec()

tesseract_ocr.py

ここをクリックして展開
# -*- coding: utf-8 -*-
import subprocess
from pathlib import Path, WindowsPath
from PyQt5 import QtGui

class TesseractOCR():
    def __init__(self, tesseractPath):
        self.tesseract = tesseractPath

    def command(self, filename, output_file):
        return [self.tesseract, str(filename), output_file.stem]

    def OCR(self, data):
        if type(data) is str or type(data) is WindowsPath:
            return self.OCR_file(data)
        if type(data) is QtGui.QPixmap:
            imagefile = Path('__temp__.png')
            data.save(str(imagefile))
            output = self.OCR_file(imagefile)
            imagefile.unlink()
            return output

    def OCR_file(self, filename):
        output_file = Path('__temp__.txt')
        cmd = [self.tesseract, str(filename), output_file.stem]

        startupinfo = subprocess.STARTUPINFO()
        startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
        startupinfo.wShowWindow = subprocess.SW_HIDE
        
        returncode = subprocess.Popen(cmd, startupinfo=startupinfo)
        returncode.wait()
        
        try:
            with open(output_file, 'r', encoding='utf-8') as file:
                output = file.readline()
            output_file.unlink()
            return output
        except:
            return ''

toolbar.py

ここをクリックして展開
# -*- coding: utf-8 -*-
from PyQt5 import QtWidgets, QtCore, QtGui

class ToolBar(QtWidgets.QWidget):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.tab = QtWidgets.QTabWidget(self)
        self.setLayout( QtWidgets.QGridLayout(self) )
        self.layout().setContentsMargins(0, 0, 0, 0)
        self.layout().setHorizontalSpacing(0)
        self.layout().setVerticalSpacing(0)
        self.layout().addWidget(self.tab)
        self.setSizePolicy(QtWidgets.QSizePolicy.MinimumExpanding, QtWidgets.QSizePolicy.Maximum)

        self.homeTab, homeTabLayout = self.addTab('Home')
        self.fileMenu = FileMenu(self.homeTab, 'File')
        self.viewMenu = ViewMenu(self.homeTab, 'View')
        self.imageMenu = ImageMenu(self.homeTab, 'Image')
        self.ocrMenu = OcrMenu(self.homeTab, 'OCR')
        self.drawMenu = DrawMenu(self.homeTab, 'Draw')

        homeTabLayout.addWidget(self.fileMenu)
        homeTabLayout.addWidget(self.viewMenu)
        homeTabLayout.addWidget(self.imageMenu)
        homeTabLayout.addWidget(self.ocrMenu)
        homeTabLayout.addWidget(self.drawMenu)
        homeTabLayout.addSpacerItem( QtWidgets.QSpacerItem(0, 0, QtWidgets.QSizePolicy.MinimumExpanding, QtWidgets.QSizePolicy.MinimumExpanding) )

    def addTab(self, title):
        tabWidget = QtWidgets.QWidget(self)
        tabWidget.setLayout( QtWidgets.QHBoxLayout(tabWidget) )
        tabWidget.layout().setContentsMargins(2, 2, 2, 2)
        self.tab.addTab(tabWidget, title)
        return tabWidget, tabWidget.layout()

class MenuGroup(QtWidgets.QGroupBox):
    def __init__(self, parent, title):
        super().__init__(parent)
        self.toggleViewButton = {}
        self.setLayout( QtWidgets.QVBoxLayout(self) )
        self.layout().setContentsMargins(2, 2, 2, 2)
        self.setSizePolicy(QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.MinimumExpanding)

        self.gridWidget = QtWidgets.QWidget(self)
        self.layout().addWidget(self.gridWidget)
        self.layout().addSpacerItem( QtWidgets.QSpacerItem(0, 0, QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.MinimumExpanding) )
        label = QtWidgets.QLabel(title, self)
        label.setAlignment(QtCore.Qt.AlignHCenter)
        self.layout().addWidget(label)
        self.gridWidgetLayout = QtWidgets.QGridLayout(self.gridWidget)
        self.gridWidgetLayout.setContentsMargins(0, 0, 0, 0)
        self.gridWidgetLayout.setHorizontalSpacing(0)
        self.gridWidgetLayout.setVerticalSpacing(0)

    def addWidget(self, widget, fromRow, fromColumn, rowSpan, columnSpan):
        if widget is None:
            return widget
        widget.setParent(self.gridWidget)
        self.gridWidgetLayout.addWidget(widget, fromRow, fromColumn, rowSpan, columnSpan)
        return widget

    def createButton(self, parent=None, text='', offIconPath=None, onIconPath=None, checkable=False, iconSize=24):
        button = QtWidgets.QToolButton(parent)
        button.setText(text)
        icon = QtGui.QIcon()
        icon.addPixmap(QtGui.QPixmap(offIconPath), QtGui.QIcon.Normal, QtGui.QIcon.Off)
        icon.addPixmap(QtGui.QPixmap(onIconPath), QtGui.QIcon.Normal, QtGui.QIcon.On)
        button.setIcon(icon)
        button.setIconSize( QtCore.QSize(iconSize, iconSize) )
        button.setToolButtonStyle(QtCore.Qt.ToolButtonTextBesideIcon)
        button.setAutoRaise(True)
        button.setArrowType(QtCore.Qt.NoArrow)
        button.setCheckable(checkable)
        button.setSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Minimum)
        return button

    def createSlider(self, parent, orientation, minimum, maximum, maximumWidth=100):
        slider = QtWidgets.QSlider(orientation)
        slider.setMinimum(minimum)
        slider.setMaximum(maximum)
        slider.setMaximumWidth(maximumWidth)
        return slider
        
    def addToggleViewButton(self, action, fromRow, fromColumn, rowSpan, columnSpan):
        toolButton = QtWidgets.QToolButton(self)
        icon = QtGui.QIcon()
        icon.addPixmap(QtGui.QPixmap('./icons/task-line.svg'), QtGui.QIcon.Normal, QtGui.QIcon.Off)
        icon.addPixmap(QtGui.QPixmap('./icons/task-fill.svg'), QtGui.QIcon.Normal, QtGui.QIcon.On)
        toolButton.setArrowType(QtCore.Qt.NoArrow)
        toolButton.setAutoRaise(True)
        toolButton.setCheckable(True)
        toolButton.setIcon(icon)
        toolButton.setIconSize( QtCore.QSize(24, 24) )
        toolButton.setText( action.text() )
        toolButton.setToolButtonStyle(QtCore.Qt.ToolButtonTextBesideIcon)
        toolButton.click()
        toolButton.toggled.connect( lambda : action.trigger() )
        toolButton.setSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Minimum)
        self.toggleViewButton[toolButton.text()] = toolButton
        self.addWidget(toolButton,  fromRow, fromColumn, rowSpan, columnSpan)

class FileMenu(MenuGroup):
    def __init__(self, parent, title):
        super().__init__(parent, title)
        self.openButton = self.createButton(self, 'Open', './icons/file-line.svg')
        self.saveButton = self.createButton(self, 'Save', './icons/save-3-line.svg')
        self.openButton = self.addWidget(self.openButton, 0, 0, 1, 1)
        self.saveButton = self.addWidget(self.saveButton, 1, 0, 1, 1)

class ViewMenu(MenuGroup):
    def __init__(self, parent, title):
        super().__init__(parent, title)
        self.rowHeight = SpinBox(self, 'Row height ', 1, 100, 5, 40)
        self.addWidget(self.rowHeight, 1, 0, 1, 1)

class ImageMenu(MenuGroup):
    def __init__(self, parent, title):
        super().__init__(parent, title)
        self.recognizeButton    = self.createButton(self, 'Recognize all',  './icons/image-line.svg')
        self.contourAreaMin = SpinBox(self, 'Contour area min ', 1, 999999999, 5000)
        self.contourAreaMax = SpinBox(self, 'Contour area max ', 1, 999999999, 1000000)
        self.dilate         = SpinBox(self, 'Dilate ',           1, 999999999, 3)
        self.addWidget(self.recognizeButton,    0, 0, 1, 1)
        self.addWidget(self.contourAreaMin,     0, 1, 1, 1)
        self.addWidget(self.contourAreaMax,     1, 1, 1, 1)
        self.addWidget(self.dilate,             2, 1, 1, 1)

class OcrMenu(MenuGroup):
    def __init__(self, parent, title):
        super().__init__(parent, title)
        self.ocrButton = self.createButton(self, 'OCR',  './icons/search-line.svg')
        self.addWidget(self.ocrButton, 0, 0, 1, 1)

class DrawMenu(MenuGroup):
    def __init__(self, parent, title):
        super().__init__(parent, title)
        self.drawRectButton   = self.createButton(self, 'Draw rect', './icons/edit-box-line.svg', './icons/edit-box-line.svg', True)
        self.deleteRectButton = self.createButton(self, 'Delete rect', './icons/delete-bin-line.svg')
        self.addWidget(self.drawRectButton,   0, 0, 1, 1)
        self.addWidget(self.deleteRectButton, 1, 0, 1, 1)

class SpinBox(QtWidgets.QWidget):
    valueChanged = QtCore.pyqtSignal(int)
    def __init__(self, parent, text, minimum, maximum, value, width=80):
        super().__init__(parent)
        self.setLayout( QtWidgets.QHBoxLayout(self) )
        self.layout().addWidget( QtWidgets.QLabel(text, self) )
        self.spinBox = QtWidgets.QSpinBox(self)
        self.spinBox.setMinimum(minimum)
        self.spinBox.setMaximum(maximum)
        self.spinBox.setMinimumWidth(width)
        self.spinBox.setMaximumWidth(width)
        self.spinBox.setValue(value)
        self.layout().addWidget(self.spinBox)
        self.layout().setContentsMargins(0, 0, 0, 0)
        self.spinBox.valueChanged.connect(lambda value : self.valueChanged.emit(value))

    def value(self):
        return self.spinBox.value()

if __name__ == '__main__':
    import sys
    app = QtWidgets.QApplication(sys.argv)
    view = ToolBar()
    view.show()
    app.exec()

記事の共有

関連記事

コメント

comments powered by Disqus