当前位置: 首页 > 工具软件 > darknet-ocr > 使用案例 >

Qt6 tesseract-ocr 截图识字

严曜文
2023-12-01

ScreenCapturer.h

#ifndef SCREENCAPTURER_H
#define SCREENCAPTURER_H

#include "mainwindow.h"

// 它是QWidget的子类
class ScreenCapturer : public QWidget
{
    //并且在类主体的开头具有Q_OBJECT宏
    Q_OBJECT
public:
    explicit ScreenCapturer(MainWindow *w);
    ~ScreenCapturer();
protected:
    void paintEvent(QPaintEvent *event) override;
    void mouseMoveEvent(QMouseEvent *event) override;
    void mousePressEvent(QMouseEvent *event) override;
    void mouseReleaseEvent(QMouseEvent *event) override;
private slots:
    void closeMe();
    void confirmCapture();
private:
    void initShortcuts();
    QPixmap captureDesktop();
private:
    //我们在此类中定义了许多成员字段
    MainWindow *window; //是一个指向我们应用主窗口对象的指针, 抓取图像时,我们将调用其showImage方法以显示抓取的图像
    QPixmap screen; //用于存储整个屏幕或多个屏幕的图像。
    QPoint p1,p2;//是所选矩形的右上角和左下角点
    bool mouseDown;//是用于指示是否按下鼠标按钮的标志,也就是说,如果用户正在拖动或移动鼠标
};

#endif // SCREENCAPTURER_H

ScreenCapturer.cpp

#include "ScreenCapturer.h"
#include <QGuiApplication>
#include <QApplication>
#include <QScreen>
#include <QPainter>
#include <QRegion>
#include <QMouseEvent>
#include <QShortcut>

//在构造器的实现中,我们使用空指针调用其父类构造器
//并使用唯一参数初始化window成员
ScreenCapturer::ScreenCapturer(MainWindow *w)
    :window(w),QWidget(nullptr)
{
    //在方法主体中,我们为小部件设置了许多标志
    setWindowFlags(Qt::BypassWindowManagerHint //告诉它忽略窗口管理器的布置
                  | Qt::WindowStaysOnTopHint //告诉它保持在桌面的最顶层
                  | Qt::FramelessWindowHint //使窗口小部件没有标题栏或窗口边框。
                  | Qt::Tool);//指示窗口小部件是工具窗口
    //有了这些标志,我们的小部件将成为一个无边界的工具窗口,始终位于桌面的顶层

    //Qt::WA_DeleteOnClose属性可确保在关闭小部件实例后将其删除。
    setAttribute(Qt::WA_DeleteOnClose);

    //设置完所有标志和属性后,我们调用captureDesktop()方法将整个桌面作为一个大图像捕获
    //并将其分配给screen成员字段
    //然后,我们将小部件的大小调整为大图像的大小
    screen = captureDesktop();
    resize(screen.size());
    //并调用initShortcuts设置一些热键
    initShortcuts();
}


ScreenCapturer::~ScreenCapturer()
{}

QPixmap ScreenCapturer::captureDesktop()
{
    QRect geometry;
    //一个桌面可能有多个屏幕
    //因此我们可以通过QGuiApplication::screens()获得所有这些屏幕
    for(QScreen *const screen : QGuiApplication::screens())
    {
        //并将它们的几何形状组合成一个大矩形
        geometry = geometry.united(screen->geometry());
    }

    //并将桌面根窗口作为QPixmap的实例
    //由于我们将组合矩形的位置和大小传递给grabWIndow函数
    //因此将抓取包括所有屏幕在内的整个桌面
    //最后,我们将图像的设备像素比率设置为适合本地设备的像素比率
    //然后将其返回
    //现在我们知道了小部件的构造方式以及在构造过程中如何抓取桌面
    //接下来的事情是在小部件上显示抓取的图像
    QPixmap pixmap(QApplication::primaryScreen()->grabWindow(
                       //QApplication::desktop()->winId(),
                       QWidget::winId(),
                       geometry.x(),
                       geometry.y(),
                       geometry.width(),
                       geometry.height()));
    //pixmap.setDevicePixelRatio(QScreen::devicePixelRatio());//QApplication::desktop()->devicePixelRatio());
    return pixmap;
}

//接下来的事情是在小部件上显示抓取的图像
//这是通过覆盖其paintEvent方法来完成的
//每当小部件需要更新自身时,都会调用此方法paintEvent
//例如,当它打开,调整大小或移动时.在此方法中,我们定义了QPainter方法
void ScreenCapturer::paintEvent(QPaintEvent *event)
{
    QPainter painter(this);
    painter.drawPixmap(0,0,screen);

    QRegion grey(rect());
    //如您所见,在方法主体中,我们添加了四行代码。 我们检查p1和p2是否相同
    //如果不是,则绘制由p1和p2确定的矩形的边界
    //并从绘制半透明灰色叠加层的区域中减去该矩形
    if(p1.x() != p2.x() && p1.y() != p2.y())
    {
        painter.setPen(QColor(200, 100, 50, 255));
        painter.drawRect(QRect(p1,p2));
        grey = grey.subtracted(QRect(p1,p2));
    }
    painter.setClipRegion(grey);
    QColor overlayColor(20,20,20,50);
    painter.fillRect(rect(),overlayColor);
    painter.setClipRect(rect());

    //使用这三个事件处理器,当用户拖动鼠标时,我们可以得到一个由连续更新的点p1和p2确定的矩形
}

//然后使用它在小部件上绘制抓取的图像,由于抓取的图像看起来与桌面完全相同
//因此用户可能没有意识到我们正在显示抓取的图像
//了告诉用户他们面对的只是一个抓取的图像而不是桌面
//我们在抓取的图像的顶部绘制了一个半透明的灰色覆盖层

//然后,我们应该找出一种方法,允许用户选择所抓取图像的区域。 这是通过覆盖三个鼠标事件处理器来完成的
//mousePressEvent,当按下鼠标按钮时调用
//mouseMoveEvent,当鼠标移动时调用
//mouseReleaseEvent,当释放按下的鼠标按钮时调用
//当按下鼠标按钮时,我们会将其按下的位置保存到成员p1和p2中
//将mouseDown标志标记为true,然后调用update告诉小部件重新绘制自身
void ScreenCapturer::mousePressEvent(QMouseEvent *event)
{
    mouseDown = true;
    p1 = event->pos();
    p2 = event->pos();
    update();
}

//当鼠标移动时,我们检查mouseDown标志
//如果为true,则用户正在拖动鼠标
//因此我们将鼠标的当前位置更新为成员字段p2
//然后调用update重新绘制小部件
void ScreenCapturer::mouseMoveEvent(QMouseEvent *event)
{
    if(!mouseDown) return;
    p2 = event->pos();
    update();
}

//释放按下的鼠标按钮时
void ScreenCapturer::mouseReleaseEvent(QMouseEvent *event)
{
    mouseDown = false;
    p2 = event->pos();
    update();
}



//现在,如果用户拖动鼠标,他们将看到选定的矩形
//在confirmCapture插槽中,
//我们将选定的矩形复制到整个桌面的抓取图像中作为新图像
//然后使用它调用主窗口的showImage方法来显示并使用它
//最后,我们调用closeMe关闭窗口小部件窗口
void ScreenCapturer::confirmCapture()
{
    QPixmap image = screen.copy(QRect(p1,p2));
    window->showImage(image);
    closeMe();
}

//在closeMe插槽中,除了关闭当前窗口小部件窗口并恢复主窗口的状态外.我们什么也不做。
void ScreenCapturer::closeMe()
{
    this->close();
    window->showNormal();
    window->activateWindow();
}

//我们将这些插槽连接到initShortcuts方法中的一些热键
void ScreenCapturer::initShortcuts()
{
    new QShortcut(Qt::Key_Escape,this,SLOT(closeMe()));
    new QShortcut(Qt::Key_Return, this, SLOT(confirmCapture()));
}

mainwindow.h

#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QMainWindow>
#include <QGraphicsScene>
#include <QTextEdit>
#include <QLabel>
#include <QCheckBox>
#include "tesseract/baseapi.h"

#include <opencv2/opencv.hpp>
#include <opencv2/dnn.hpp>
//在此变更集中,我们首先添加include指令以包含 Tesseract 库的基本 API 头文件


//它是QMainWindow的子类
//最重要的方面是我们在专用部分声明的小部件
class MainWindow : public QMainWindow
{
    //因此,它的主体开头具有Q_OBJECT宏
    Q_OBJECT
public:
    MainWindow(QWidget *parent = nullptr);
    ~MainWindow();
    void showImage(QPixmap);
private:
    //我们还提供了三种私有方法来实例化这些小部件并将它们安排在我们设计的主窗口中
    void initUI(); //实例化除动作以外的所有小部件
    void createActions(); //创建所有动作; 这由initUI方法调用。
    void setupShortcuts(); //设置一些热键,使我们的应用更易于使用。 这由createActions方法调用

    //接下来
    //从本地磁盘打开图像
    //将当前图像作为文件保存到本地磁盘上
    //将编辑器小部件中的文本另存为文本文件到本地磁盘上
    //showImage方法和openImage插槽的实现与我们在ImageViewer应用中编写
    //的MainWindow::showImage和MainWindow::openImage方法的实现相同
    void showImage(QString);


    //所有这些步骤都是使用 OpenCV 完成的。 因此,该图像将表示为cv::Mat的实例,
    //因此我们重载了showImage方法的另一个版本,该方法将cv::Mat的实例作为其唯一参数,以在 UI 上显示更新的图像。
    void showImage(cv::Mat);

    void decode(const cv::Mat& scores, const cv::Mat& geometry, float scoreThresh,
                std::vector<cv::RotatedRect> &detections,std::vector<float>& confidences);
    //detectTextAreas方法用于通过 OpenCV 检测区域
    //而decode方法构成辅助方法
    //在检测到图像上的文本区域后,我们将为该图像上的每个文本区域绘制一个矩形。
    //所有这些步骤都是使用 OpenCV 完成的
    //因此,该图像将表示为cv::Mat的实例
    //因此我们重载了showImage方法的另一个版本
    cv::Mat detectTextAreas(QImage &image,std::vector<cv::Rect>&);
private slots:
    void openImage();
    //同样,saveImageAs插槽与该ImageViewer应用中的MainWindow::saveAs方法具有完全相同的实现
    // 由于我们应该在新应用中保存图像和文本
    //因此我们在此处仅使用一个不同的名称
    //并且此方法仅用于保存图像
    void saveImageAs();
    void saveTextAs();

    //然后向MainWindow类添加一个插槽和两个成员
    //提取文字
    void extractText();

    void captureScreen();
    void startCapture();

private:
    //包括文件菜单fileMenu
    QMenu *fileMenu;
    //工具栏fileToolBar
    QToolBar *fileToolBar;
    //QGraphicsScene和QGraphicsView显示目标图像
    QGraphicsScene *imageScene;
    QGraphicsView *imageView;

    // QTextEditor在其上放置识别的文本
    QTextEdit *editor;

    //状态栏和标签
    QStatusBar *mainStatusBar;
    QLabel *mainStatusLabel;

    //最后是四个QAction指针
    QAction *openAction;
    QAction *saveImageAsAction;
    QAction *saveTextAsAtion;
    QAction *exitAtion;
    QAction *captureAction;


    QString currentImagePath;
    QGraphicsPixmapItem *currentImage;

    //QAction *ocrAction成员将出现在主窗口的工具栏上
    // 触发此操作后,将调用新添加的插槽extractText,
    QAction *ocrAction;
    //该插槽将使用tesseract::TessBaseAPI *tesseractAPI成员来识别已打开的图像中的字符。
    tesseract::TessBaseAPI *tesseractAPI;
    //detectAreaCheckBox字段是一个出现在工具栏上的复选框
    //允许用户向我们提供一个指示器
    //以确定在执行 Tesseract 的 OCR 工作之前是否应该检测文本区域
    QCheckBox *detectAreaCheckBox;
    // 成员字段cv::dnn::Net net是一个深层神经网络实例
    //将用于检测文本区域
    cv::dnn::Net net;
};
#endif // MAINWINDOW_H

mainwindow.cpp

#include "mainwindow.h"
#include <QMenuBar>
#include <QSplitter>
#include <QGraphicsView>
#include <QLabel>
#include <QStatusBar>
#include <QToolBar>
#include <QFileDialog>
#include <QMessageBox>
#include <QTextStream>
#include <QApplication>
#include <QGraphicsPixmapItem>
#include <QRegExp>

#include <QTimer>
#include "ScreenCapturer.h"

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
    ,currentImage(nullptr)
    ,tesseractAPI(nullptr) //将成员字段tesseractAPI初始化为nullptr

{
    initUI();
}

MainWindow::~MainWindow()
{
    // Destroy used object and release memory

    if(tesseractAPI != nullptr)
    {
        tesseractAPI->End();
        delete tesseractAPI;
    }
}

//现在是时候实现这些方法了
//我们在项目的根目录中创建一个名为mainwindow.cpp的新源文件
//以适应这些实现。 首先,让我们看一下initUI方法
void MainWindow::initUI()
{
    //所有这些工作与我们在先前项目中所做的相同
    //在这种方法中,我们首先设置窗口大小
    this->resize(800,600);

    //创建文件菜单,然后将其添加到菜单栏
    //setup menubar
    fileMenu = menuBar()->addMenu("&File");


    //setup toolbar
    fileToolBar = addToolBar("File");

    //创建文件工具栏,最后,我们创建状态栏
    //main area
    QSplitter *splitter = new QSplitter(Qt::Horizontal,this);

    //与先前项目不同的重要部分是主区域的创建
    //该主区域是方法主体的中间部分
    imageScene = new QGraphicsScene(this);
    imageView = new QGraphicsView(imageScene);
    splitter->addWidget(imageView);

    // 在本部分中,我们将创建一个水平方向的QSplitter对象
    //而不是一个QGridLayout实例来容纳图形视图和编辑器
    editor = new QTextEdit(this);
    splitter->addWidget(editor);

    //使用QSplitter使我们能够通过拖动其分隔条自由地更改其子窗口小部件的宽度
    //这是QGridLayout无法实现的
    //此后,我们创建图形场景以及图形视图,然后编辑器将它们有序地添加到拆分器中
    QList<int> sizes = {400, 400};
    // 通过使用列表int调用setSizes方法来设置拆分器的子级的宽度
    //们让每个子项占据 400 像素的相等宽度。 最后,我们将分割器设置为主窗口的中央小部件。
    splitter->setSizes(sizes);

    setCentralWidget(splitter);

    //然后在其上放置标签
    //setup status bar
    mainStatusBar = statusBar();
    mainStatusLabel = new QLabel(mainStatusBar);
    mainStatusBar->addPermanentWidget(mainStatusLabel);
    mainStatusLabel->setText("Application information will be here!");

    detectAreaCheckBox = new QCheckBox("Detect Text Areas",this);
    fileToolBar->addWidget(detectAreaCheckBox);

    createActions();
}

void MainWindow::createActions()
{
    //在这里,我们创建所有声明的动作
    //并将它们添加到文件菜单和工具栏
    // create actions, add them to menus
    openAction = new QAction("&Open",this);
    fileMenu->addAction(openAction);
    saveImageAsAction = new QAction("Save &Image as",this);
    fileMenu->addAction(saveImageAsAction);
    saveTextAsAtion = new QAction("Save &Text As",this);
    fileMenu->addAction(saveTextAsAtion);
    exitAtion = new QAction("E&xit",this);
    fileMenu->addAction(exitAtion);

    ocrAction = new QAction("OCR",this);
    fileToolBar->addAction(ocrAction);

    captureAction = new QAction("Capture screen",this);
    fileToolBar->addAction(captureAction);



    //add action to toolbars
    fileToolBar->addAction(openAction);

    // connect the signals and slots
    connect(exitAtion,SIGNAL(triggered(bool)),QApplication::instance(),SLOT(quit()));
    connect(openAction,SIGNAL(triggered(bool)),this,SLOT(openImage()));
    connect(saveImageAsAction,SIGNAL(triggered(bool)),this,SLOT(saveImageAs()));
    connect(saveTextAsAtion,SIGNAL(triggered(bool)),this,SLOT(saveTextAs()));
    connect(ocrAction,SIGNAL(triggered(bool)),this,SLOT(extractText()));
    connect(captureAction,SIGNAL(triggered(bool)), this, SLOT(captureScreen()));
    //在此方法的结尾,我们调用setupShortcuts
    setupShortcuts();
}


//现在,让我们看看我们在其中设置了哪些快捷方式
void MainWindow::setupShortcuts()
{
    //如您所见,我们使用Ctrl-O触发openAction
    QList<QKeySequence> shortcuts;
    shortcuts<<(Qt::CTRL + Qt::Key_O);
    openAction->setShortcuts(shortcuts);

    //并使用Ctrl-Q触发exitAction
    shortcuts.clear();
    shortcuts<<(Qt::CTRL + Qt::Key_Q);
    exitAtion->setShortcuts(shortcuts);
}

//最后,有构造器和析构器
//因此,我们设置了没有任何交互功能的完整 UI。 接下来,我们将向我们的应用添加许多交互式功能,包括以下内容

//它与saveImageAs方法非常相似。 区别如下:
void MainWindow::saveTextAs()
{
    QFileDialog dialog(this);
    dialog.setWindowTitle("Save Text As ...");
    dialog.setFileMode(QFileDialog::AnyFile);
    dialog.setAcceptMode(QFileDialog::AcceptSave);
    //在文件对话框中,我们使用扩展名txt设置名称过滤器,以确保只能选择文本文件
    dialog.setNameFilter(tr("Text files (*.txt)"));
    QStringList fileNames;
    if(dialog.exec())
    {
        fileNames = dialog.selectedFiles();
        if(QRegExp(".+\\.(txt)").exactMatch(fileNames.at(0)))
        {
            //保存文本时,我们使用所选文件名创建一个QFile实例
            QFile file(fileNames.at(0));
            if(!file.open(QIODevice::WriteOnly | QIODevice::Text))
            {
                QMessageBox::information(this,"Error","Can't save text");
                return ;
            }
            //然后使用可以写入的QFile实例创建一个QTextStream实例
            //最后,我们通过调用文本编辑器的toPlainText()方法获取文本编辑器的内容
            //并将其写入刚刚创建的流中
            QTextStream out(&file);
            out<<editor->toPlainText()<<"\n";
        }
        else
        {
            QMessageBox::information(this,"Error","Save Error:bad format or filename");
        }
    }
}

void MainWindow::openImage()
{
    QFileDialog dialog(this);
    dialog.setWindowTitle("Open Image");
    dialog.setFileMode(QFileDialog::ExistingFile);
    dialog.setNameFilter(tr("Images (*.png *.bmp *.jpg)"));
    QStringList filePaths;
    if(dialog.exec())
    {
        filePaths = dialog.selectedFiles();
        showImage(filePaths.at(0));
    }
}

void MainWindow::saveImageAs()
{
    if(currentImage == nullptr)
    {
        QMessageBox::information(this,"Information","Nothing to save.");
        return;
    }
    QFileDialog dialog(this);
    dialog.setWindowTitle("Save Image As ...");
    dialog.setFileMode(QFileDialog::AnyFile);
    dialog.setAcceptMode(QFileDialog::AcceptSave);
    dialog.setNameFilter(tr("Images (*.png *.bmp *.jpg)"));
    QStringList fileNames;
    if(dialog.exec())
    {
        fileNames = dialog.selectedFiles();
        if(QRegExp(".+\\.(png | bmp | jpg)").exactMatch(fileNames.at(0)))
        {
            currentImage->pixmap().save(fileNames.at(0));
        }
        else
        {
            QMessageBox::information(this,"Error","Save error: bad format or filename.");
        }
    }
}


void MainWindow::showImage(QString path)
{
    QPixmap image(path);
    showImage(image);
    currentImagePath = path;
    QString status = QString("%1, %2x%3, %4 Bytes").arg(path).arg(image.width())
            .arg(image.height()).arg(QFile(path).size());
    mainStatusLabel->setText(status);
}

void MainWindow::showImage(QPixmap image)
{
    imageScene->clear();
    imageView->resetTransform();
    currentImage = imageScene->addPixmap(image);
    imageScene->update();
    imageView->setSceneRect(image.rect());
}


void MainWindow::extractText()
{
    //在方法主体的开头,我们检查currentImage成员字段是否为空
    //如果为null,则在我们的应用中没有打开任何图像
    if(currentImage == nullptr)
    {
        //因此我们在显示消息框后立即返回。
        QMessageBox::information(this,"Information","No opened image");
        return;
    }

    //如果它不为null,那么我们将创建 Tesseract API 实例
    //因此,首先,我们使用LC_ALL类别和空值调用setlocale函数
    //以获取并保存当前的语言环境设置
    char *old_ctype = strdup(setlocale(LC_ALL,NULL));
    //然后再次使用相同的类别和值C来设置 Tesseract 所需的语言环境
    setlocale(LC_ALL,"C");
    //由于 Tesseract API 实例可以重复使用,因此我们只需创建和初始化一次即可
    if(tesseractAPI == nullptr)
    {
        //Tesseract 要求我们必须将语言环境设置为C
        //在将区域设置设置为C之后
        //我们可以创建 Tesseract API 实例
        //我们使用表达式new tesseract::TessBaseAPI()创建它
        //新创建的 API 实例必须在使用前进行初始化
        tesseractAPI = new tesseract::TessBaseAPI();
        // Initialize tesseract-ocr with English, with specifying tessdata path
        // 通过调用Init方法执行初始化。 Init方法有很多版本
        //我们可以指定预训练的数据路径,语言名称,OCR 引擎模式,页面分段模式以及许多其他配置
        //为了简化代码,我们使用此方法的最简单版本(第三个版本)来初始化 API 实例
        //在此调用中,我们仅传递数据路径和语言名称
        //值得注意的是,数据路径由我们在项目文件中定义的宏表示
        //初始化过程可能会失败,因此如果初始化失败.我们会在显示简短消息后检查其结果并立即返回。
        if(tesseractAPI->Init(TESSDATA_PREFIX,"chi_sim"))
        {
            QMessageBox::information(this,"Error","Could not initialize tesserat.");
            return;
        }
    }
    //准备好 Tesseract API 实例后,我们将获得当前打开的图像
    QPixmap pixmap = currentImage->pixmap();
    QImage image = pixmap.toImage();
    //并将其转换为QImage::Format_RGB888格式的图像
    //就像在先前项目中所做的那样
    image = image.convertToFormat(QImage::Format_RGB888);
    //获得具有RGB888格式的图像后,可以通过调用其SetImage方法将其提供给 Tesseract API 实例
    tesseractAPI->SetImage(image.bits(),image.width(),image.height(),
                           3,image.bytesPerLine());

    //将整个图像设置为 Tesseract API 之后,我们检查该复选框的状态
    //如您所见,如果选中此复选框
    if(detectAreaCheckBox->checkState() == Qt::Checked)
    {
        std::vector<cv::Rect> areas;
        //我们将调用detextTextAreas方法来检测文本区域
        //调用返回该图像作为cv::Mat的实例
        cv::Mat newImage = detectTextAreas(image,areas);
        //并在其上绘制了文本区域和索引
        //然后调用带有该图像的showImage方法以将其显示在窗口上
        showImage(newImage);
        editor->setPlainText("");
        for(cv::Rect &rect : areas)
        {
            //然后,我们遍历文本区域,并通过调用其SetRectangle方法将其发送到 Tesseract API
            //以告知它仅尝试识别此矩形内的字符
            tesseractAPI->SetRectangle(rect.x, rect.y, rect.width, rect.height);
            char *outText = tesseractAPI->GetUTF8Text();
            // 然后,我们获得识别的文本,将其添加到编辑器中,并释放文本的存储空间
            editor->setPlainText(editor->toPlainText() + outText);
            delete [] outText;
        }
    }
    else  //如果未选中该复选框,则我们将应用长期存在的逻辑.让 Tesseract 识别整个图像中的文本。
    {
        // SetImage方法还具有许多不同的重载版本
        //可以使用给定图像的数据格式信息调用第一个
        //它不限于任何库定义的类
        //例如 Qt 中的QImage或 OpenCV 中的Mat
        // 第二个版本接受Pix指针作为输入图像,Pix类由图像处理库 Leptonica 定义
        // 显然,这是最适合这种情况的第一个版本
        //它需要的所有信息都可以从QImage实例中检索到,实例具有 3 个通道,深度为 8 位。 因此,它为每个像素使用3字节
        //tesseractAPI->SetImage(image.bits(),image.width(),image.height(),3,image.bytesPerLine());

        //Tesseract API 实例获取图像后,我们可以调用其GetUTF8Text()方法来获取其从图像中识别的文本
        //在这里值得注意的是,调用者有责任释放此方法的结果数据缓冲区
        char *outText = tesseractAPI->GetUTF8Text();
        //剩下的任务是将提取的文本设置到编辑器小部件
        //销毁 Tesseract API 实例,删除GetUTF8Text调用的结果数据缓冲区
        //并恢复语言环境设置
        editor->setPlainText(outText);
        delete[] outText;
        setlocale(LC_ALL,old_ctype);
    }
    free(old_ctype);
    // OCR 工作完成后,我们将使用保存的LC_ALL值恢复语言环境设置
}

cv::Mat MainWindow::detectTextAreas(QImage &image, std::vector<cv::Rect> &areas)
{
    //我们定义许多变量并创建一个深度神经网络
    //前两个阈值用于置信度和非最大抑制
    //我们将使用它们来过滤 AI 模型的检测结果
    //EAST 模型要求图像的宽度和高度必须是 32 的倍数
    //因此我们定义了两个int变量,其值均为320
    //在将输入图像发送到 DNN 模型之前
    //我们将其调整为这两个变量描述的尺寸
    //在这种情况下为320x320
    //使用已下载的预训练模型数据文件的路径定义一个字符串
    //并在类成员net为空的情况下调用cv::dnn::readNet函数来加载它
    //OpenCV 的 DNN 支持多种预训练的模型数据文件
    //*.caffemodel(Caffe)
    //*.pb(TensorFlow)
    //*.t7或*.net(Torch)
    //*.weights(Darknet)
    //*.bin(DLDT)
//从前面的列表中,您可以确定我们使用的预训练模型是使用 TensorFlow 框架构建和训练的
    float confThreshold = 0.5;
    float nmsThreshold = 0.4;
    int inputWidth = 320;
    int inputHeight = 320;
    std::string model = "./frozen_east_text_detection.pb";
    //Load DNN network
    if(net.empty())
    {
        net = cv::dnn::readNet(model);
    }
    //我们定义了一个cv::Mat向量
    //以保存模型的输出层
    //然后,将需要从 DNN 模型中提取的两层的名称放入字符串向量
    //即layerNames变量。 这两个层包含我们想要的信息
    std::vector<cv::Mat> outs;
    std::vector<std::string>layerNames(2);
    //第一层feature_fusion/Conv_7/Sigmoid是 Sigmoid 激活的输出层
    //该层中的数据包含给定区域是否包含文本的概率。
    layerNames[0] = "feature_fusion/Conv_7/Sigmoid";
    //第二层feature_fusion/concat_3是特征映射的输出层
    //该层中的数据包含图像的几何形状,通过稍后在此层中解码数据,我们将获得许多边界框
    layerNames[1] = "feature_fusion/concat_3";

    //我们将输入图像从QImage转换为cv::Mat

    cv::Mat frame = cv::Mat(
                image.height(),
                image.width(),
                CV_8UC3,
                image.bits(),
                image.bytesPerLine()).clone();
    cv::Mat blob;
    //然后将矩阵转换为另一个矩阵,该矩阵是一个 4 维 BLOB
    //可以用作DNN模型的输入,换句话说, 输入层。
    // 后一种转换是通过在OpenCV库的cv::dnn名称空间中调用blobFromImage函数来实现的
    //在此转换中执行许多操作,例如从中心调整大小和裁剪图像,减去平均值,通过比例因子缩放值以及交换 R 和 B 通道。
    //在对blobFromImage函数的调用中
    //我们有很多参数
    cv::dnn::blobFromImage(
                frame,//第一个参数是输入图像。
                blob,//第二个参数是输出图像
                1.0,//第三个参数是每个像素值的比例因子,我们使用 1.0,因为我们不需要在此处缩放像素
                //第四个参数是输出图像的空间大小,我们说过,此尺寸的宽度和高度必须是 32 的倍数,此处我们将320 x 320与我们定义的变量一起使用
                cv::Size(inputWidth,inputHeight),
                //第五个参数是应该从每个图像中减去的平均值,因为在训练模型时已使用了该平均值, 在此,使用的平均值为(123.68, 116.78, 103.94)。
                cv::Scalar(123.68, 116.78, 103.94),
                //下一个参数是我们是否要交换R和B通道,这是必需的,因为 OpenCV 使用 BGR 格式,而 TensorFlow 使用 RGB 格式
                true,
                //最后一个参数是我们是否要裁剪图像并进行中心裁剪, 在这种情况下,我们指定false。
                false);
    //在该调用返回之后,我们得到了可用作 DNN 模型输入的 Blob
    //然后,将其传递给神经网络,并通过调用模型的setInput方法和forward方法执行一轮转发以获取输出层
    //转发完成后,我们想要的两个输出层将存储在我们定义的outs向量中
    net.setInput(blob);
    net.forward(outs,layerNames);

    //outs向量的第一个元素是得分
    cv::Mat scores = outs[0];
    //而第二个元素是几何形状
    cv::Mat geometry = outs[1];

    //通过此解码过程,我们将候选文本区域作为cv::RotatedRect并将其存储在boxes变量中
    std::vector<cv::RotatedRect> boxes;
    //这些框的相应置信度存储在confidences变量中
    std::vector<float> confidences;
    //然后,我们调用MainWindow类的另一种方法decode
    //以解码文本框的位置及其方向

    //因此我们需要过滤掉外观最好的文本框
    //decode方法用于从输出层提取置信度和框信息
    //可以在这个页面中找到其实现
    //理解它,您应该了解 DNN 模型中的数据结构,尤其是输出层中的数据结构
    decode(scores, geometry, confThreshold, boxes, confidences);

    std::vector<int> indices;
    //由于我们可能会为文本框找到许多候选对象
    //这是使用非最大抑制来完成的,即对NMSBoxes方法的调用。
    //在此调用中,我们给出解码后的框
    //置信度以及置信度和非最大值抑制的阈值
    //未消除的框的索引将存储在最后一个参数indices中
    cv::dnn::NMSBoxes(boxes, confidences, confThreshold, nmsThreshold, indices);

    //我们将所有文本区域作为cv::RotatedRect的实例,
    //并且这些区域用于调整大小的图像
    //因此我们应该将它们映射到原始输入图像上

    //为了将文本区域映射到原始图像
    //我们应该知道在将图像发送到 DNN 模型之前如何调整图像大小
    //然后逆转文本区域的大小调整过程
    //因此,我们根据宽度和高度方面计算尺寸调整率
    //然后将它们保存到cv::Point2f ratio中
    cv::Point2f ratio((float)frame.cols / inputWidth,(float)frame.rows / inputHeight);
    cv::Scalar green = cv::Scalar(0, 255, 0);

    //然后,我们迭代保留的索引
    for(size_t i = 0;i < indices.size();++i)
    {
         //并获得每个索引指示的每个cv::RotatedRect对象
         //为了降低代码的复杂性
         cv::RotatedRect& box = boxes[indices[i]];
         //我们无需将cv::RotatedRect及其内容旋转为规则矩形
         //而是简单地获取其边界矩形
         //然后,我们对矩形进行反向调整大小
         cv::Rect area = box.boundingRect();
         area.x *= ratio.x;
         area.width *= ratio.x;
         area.y *= ratio.y;
         area.height *= ratio.y;
         //然后将其推入areas向量
         //为了演示这些区域的显示方式
         areas.push_back(area);
         cv::rectangle(frame,area,green,1);
         QString index = QString("%1").arg(i);
         //我们还将它们绘制在原始图像上
         //并在每个矩形的右上角插入一个数字
         //以指示它们将被处理的顺序
         //在方法的最后,我们返回更新的原始图像
         //现在我们已经完成了文本区域检测方法,让我们将其集成到我们的应用中
         cv::putText(
                     frame,index.toStdString(),cv::Point2f(area.x,area.y - 2),
                     cv::FONT_HERSHEY_SIMPLEX,0.5,green,1);
    }
    return frame;
}


void MainWindow::showImage(cv::Mat mat)
{
    QImage image(
        mat.data,
        mat.cols,
        mat.rows,
        mat.step,
        QImage::Format_RGB888);
    QPixmap pixmap = QPixmap::fromImage(image);
    imageScene->clear();
    imageView->resetTransform();
    currentImage = imageScene->addPixmap(pixmap);
    imageScene->update();
    imageView->setSceneRect(pixmap.rect());
}


void MainWindow::decode(const cv::Mat &scores, const cv::Mat &geometry,
                        float scoreThresh, std::vector<cv::RotatedRect> &detections, std::vector<float> &confidences)
{
    CV_Assert(scores.dims == 4);
    CV_Assert(scores.size[0] == 1);
    CV_Assert(geometry.size[0] == 1);
    CV_Assert(scores.size[2] == geometry.size[2]);
    CV_Assert(scores.size[3] == geometry.size[3]);

    detections.clear();
    const int height = scores.size[2];
    const int width = scores.size[3];
    for(int y = 0;y < height; ++y)
    {
        const float* scoresData = scores.ptr<float>(0, 0, y);
        const float* x0_data = geometry.ptr<float>(0, 0, y);
        const float* x1_data = geometry.ptr<float>(0, 1, y);
        const float* x2_data = geometry.ptr<float>(0, 2, y);
        const float* x3_data = geometry.ptr<float>(0, 3, y);
        const float* anglesData = geometry.ptr<float>(0, 4, y);
        for(int x = 0; x < width; ++x)
        {
            float score = scoresData[x];
            if(score < scoreThresh)
                continue;

            // Decode a prediction.
            // Multiple by 4 because feature maps are 4 time less than input image.
            float offsetX =  x * 4.0f, offsetY = y * 4.0f;
            float angle = anglesData[x];
            float cosA = std::cos(angle);
            float sinA = std::sin(angle);
            float h = x0_data[x] + x2_data[x];
            float w = x1_data[x] + x3_data[x];

            cv::Point2f offset(offsetX + cosA * x1_data[x] + sinA * x2_data[x],
                               offsetY - sinA * x1_data[x] + cosA * x2_data[x]);
            cv::Point2f p1 = cv::Point2f(-sinA * h, -cosA * h) + offset;
            cv::Point2f p3 = cv::Point2f(-cosA * w, sinA * w) + offset;
            cv::RotatedRect r(0.5f * (p1 + p3), cv::Size2f(w, h), -angle * 180.0f / (float)CV_PI);
            detections.push_back(r);
            confidences.push_back(score);
        }
    }
}


//在此时隙中,我们最小化主窗口,然后在 0.5 秒后安排对startCapture时隙的调用
void MainWindow::captureScreen()
{
    this->setWindowState(this->windowState() | Qt::WindowMinimized);
    QTimer::singleShot(500, this, SLOT(startCapture()));
}


//在startCapture插槽中,我们将创建一个屏幕截图小部件实例,将其打开,然后将其激活
//在这里,我们使用另一个插槽并将其安排在较短的时间后
//因为如果立即执行此操作,则在捕获屏幕时不会完成主窗口的最小化。
void MainWindow::startCapture()
{
    ScreenCapturer *cap = new ScreenCapturer(this);
    cap->show();
    cap->activateWindow();
}

main.cpp

#include "mainwindow.h"
#include <QApplication>

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    MainWindow w;
    w.setWindowTitle("Literacy");
    w.show();
    return a.exec();
}

pro

QT       += core gui
TEMPLATE = app
QT += core5compat
greaterThan(QT_MAJOR_VERSION, 4): QT += widgets

CONFIG += c++11
INCLUDEPATH += /usr/include/opencv4
#我们将opencv_dnn模块添加到LIBS设置中。 因此,该模块中实际上是一个深度神经网络的 EAST 算法的实现。
LIBS += -ltesseract -lopencv_dnn -lopencv_core -lopencv_imgproc
#在前面的变更集中,我们为不同平台添加了 Tesseract 库的include路径和库路径
#然后定义了一个宏TESSDATA_PREFIX,
#其值是 Tesseract 库的数据路径的路径, 稍后,我们将使用此宏将预训练的数据加载到我们的代码中。
DEFINES += TESSDATA_PREFIX=\\\"/usr/share/tesseract-ocr/4.00/tessdata/\\\"

DEFINES += QT_DEPRECATED_WARNINGS

SOURCES += \
    ScreenCapturer.cpp \
    main.cpp \
    mainwindow.cpp

HEADERS += \
    ScreenCapturer.h \
    mainwindow.h

FORMS +=

# Default rules for deployment.
qnx: target.path = /tmp/$${TARGET}/bin
else: unix:!android: target.path = /opt/$${TARGET}/bin
!isEmpty(target.path): INSTALLS += target
 类似资料: