OCR TOOL rev1 made by OpenCV and Tesseract-OCR

2021/03/27 categories:OpenCV| tags:OpenCV|Python|Tesseract-OCR|

Updated the roots created in Table OCR Tool Created with OpenCV and Tesseract-OCR. The main changes are rewriting the structure of the code and recreating the GUI. I changed it because I got used to writing PyQt5 code and wanted to rewrite the code I wrote before. Also, I thought the GUI was difficult to use, so I changed it.

Add and view files

Click the Open button and select the PDF file, the file name will be displayed in the file list. Click the file name to convert the PDF to an image and display it. The image-converted file is displayed as image in the second column. If you click the file displayed as image, the created image will be displayed without converting the image again. You can show or hide the image by switching the check box.

Cell recognition

Click Recognize all to recognize the cells in the table. The recognition result is added to the list as a child of the file name. Rect is the cell recognition result displayed in red, and Crop is the cropped margin in the cell displayed in green. The cropped image based on the recognition result is displayed in the second column of the recognition result.

If you click on an item that is displayed as a child of the file name, it will be displayed in blue so that you can check where that item is.

Delete and add cell recognition area

You can delete the selected item by clicking the Delete rect button while clicking the item displayed as a child of the file name. Also, if you click Draw rect in that state and then drag in the image to specify the range, the range will be cropped and added to the file item.

Cell OCR

Click the OCR button to convert all the images in the cells displayed in the second column to character strings.

Save CSV file

The character string can be saved as a CSV file by clicking the Save button. The CSV file created is as follows.

Source code

main.py

Click here to expand
# -*- 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

Click here to expand
# -*- 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

Click here to expand
# -*- 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

Click here to expand
# -*- 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

Click here to expand
# -*- 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

Click here to expand
# -*- 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

Click here to expand
# -*- 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()

Share post

Related Posts

コメント