光学字符识别
void MainWindow::extractText()
{
//在方法主体的开头,我们检查currentImage成员字段是否为空
//如果为null,则在我们的应用中没有打开任何图像,因此我们在显示消息框后立即返回。
if (currentImage == nullptr) {
QMessageBox::information(this, "Information", "No opened image.");
return;
}
//如果它不为null,那么我们将创建 Tesseract API 实例
//Tesseract 要求我们必须将语言环境设置为C,
//首先,我们使用LC_ALL类别和空值调用setlocale函数,以获取并保存当前的语言环境设置,
//然后再次使用相同的类别和值C来设置 Tesseract 所需的语言环境
char *old_ctype = strdup(setlocale(LC_ALL, NULL));
setlocale(LC_ALL, "C");
if (tesseractAPI == nullptr) {
//在将区域设置设置为C之后,现在,我们可以创建 Tesseract API 实例
//我们使用表达式new tesseract::TessBaseAPI()创建它。
//新创建的 API 实例必须在使用前进行初始化
tesseractAPI = new tesseract::TessBaseAPI();
// Initialize tesseract-ocr with English, with specifying tessdata path
if (tesseractAPI->Init(TESSDATA_PREFIX, "chi_sim")) {
QMessageBox::information(this, "Error", "Could not initialize tesseract.");
return;
}
}
//准备好 Tesseract API 实例后,我们将获得当前打开的图像,并将其转换为QImage::Format_RGB888格式的图像,就像在先前项目中所做的那样。
QPixmap pixmap = currentImage->pixmap();
QImage image = pixmap.toImage();
image = image.convertToFormat(QImage::Format_RGB888);
//获得具有RGB888格式的图像后,可以通过调用其SetImage方法将其提供给 Tesseract API 实例。
//它需要的所有信息都可以从QImage实例中检索到,该实例具有 3 个通道,深度为 8 位。 因此,它为每个像素使用3字节:
tesseractAPI->SetImage(image.bits(), image.width(), image.height(),
3, image.bytesPerLine());
if (detectAreaCheckBox->checkState() == Qt::Checked) {
std::vector<cv::Rect> areas;
cv::Mat newImage = detectTextAreas(image, areas);
showImage(newImage);
editor->setPlainText("");
for(cv::Rect &rect : areas) {
tesseractAPI->SetRectangle(rect.x, rect.y, rect.width, rect.height);
//Tesseract API 实例获取图像后,我们可以调用其GetUTF8Text()方法来获取其从图像中识别的文本。 在这里值得注意的是,调用者有责任释放此方法的结果数据缓冲区。
char *outText = tesseractAPI->GetUTF8Text();
editor->setPlainText(editor->toPlainText() + outText);
delete [] outText;
}
} else {
char *outText = tesseractAPI->GetUTF8Text();
editor->setPlainText(outText);
delete [] outText;
}
//OCR 工作完成后,我们将使用保存的LC_ALL值恢复语言环境设置。
setlocale(LC_ALL, old_ctype);
free(old_ctype);
}
使用 OpenCV 检测文本区域
//我们将使用带有 OpenCV 的 EAST 文本检测器来检测图像中是否存在文本。 EAST 是有效且准确的场景文本检测器的缩写,它是基于神经网络的算法,但是其神经网络模型的架构和训练过程不在本章范围之内。 在本节中,我们将重点介绍如何使用 OpenCV 的 EAST 文本检测器的预训练模型。
// 按照预期,此方法将QImage对象作为输入图像作为其第一个参数
//它的第二个参数是对cv::Rect向量的引用,该向量用于保存检测到的文本区域
//该方法的返回值为cv::Mat,它表示在其上绘制了检测到的矩形的输入图像
cv::Mat MainWindow::detectTextAreas(QImage &image, std::vector<cv::Rect> &areas)
{
float confThreshold = 0.5;
float nmsThreshold = 0.4;
int inputWidth = 320;
int inputHeight = 320;
std::string model = "/home/xz/study/qt_collect_pro/qt_5_opencv/data/frozen_east_text_detection.pb";
// Load DNN network.
if (net.empty()) {
net = cv::dnn::readNet(model);
}
//将加载 DNN 模型。 现在,让我们将输入图像发送到模型以执行文本检测:
//我们定义了一个cv::Mat向量,以保存模型的输出层
std::vector<cv::Mat> outs;
//然后,将需要从 DNN 模型中提取的两层的名称放入字符串向量,即layerNames变量。 这两个层包含我们想要的信息
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,然后将矩阵转换为另一个矩阵,该矩阵是一个 4 维 BLOB,可以用作 DNN 模型的输入,换句话说, 输入层。 后一种转换是通过在 OpenCV 库的cv::dnn名称空间中调用blobFromImage函数来实现的。 在此转换中执行许多操作,例如从中心调整大小和裁剪图像,减去平均值,通过比例因子缩放值以及交换 R 和 B 通道。 在对blobFromImage函数的调用中,我们有很多参数。 现在让我们一一解释:
cv::Mat frame = cv::Mat(
image.height(),
image.width(),
CV_8UC3,
image.bits(),
image.bytesPerLine()).clone();
cv::Mat blob;
//第一个参数是输入图像
//第二个参数是输出图像
//第三个参数是每个像素值的比例因子。 我们使用 1.0,因为我们不需要在此处缩放像素
//第四个参数是输出图像的空间大小。 我们说过,此尺寸的宽度和高度必须是 32 的倍数,此处我们将320 x 320与我们定义的变量一起使用。
//第五个参数是应该从每个图像中减去的平均值,因为在训练模型时已使用了该平均值。 在此,使用的平均值为(123.68, 116.78, 103.94)。
//下一个参数是我们是否要交换 R 和 B 通道。 这是必需的,因为 OpenCV 使用 BGR 格式,而 TensorFlow 使用 RGB 格式
//最后一个参数是我们是否要裁剪图像并进行中心裁剪。 在这种情况下,我们指定false。
cv::dnn::blobFromImage(
frame, blob,
1.0, cv::Size(inputWidth, inputHeight),
cv::Scalar(123.68, 116.78, 103.94), true, false
);
//在该调用返回之后,我们得到了可用作 DNN 模型输入的 Blob。 然后,将其传递给神经网络,并通过调用模型的setInput方法和forward方法执行一轮转发以获取输出层。 转发完成后,我们想要的两个输出层将存储在我们定义的outs向量中。
net.setInput(blob);
net.forward(outs, layerNames);
//下一步是处理这些输出层以获取文本区域:
//outs向量的第一个元素是得分
cv::Mat scores = outs[0];
//而第二个元素是几何形状
cv::Mat geometry = outs[1];
//然后,我们调用MainWindow类的另一种方法decode,以解码文本框的位置及其方向。
std::vector<cv::RotatedRect> boxes;
std::vector<float> confidences;
//通过此解码过程,我们将候选文本区域作为cv::RotatedRect并将其存储在boxes变量中。 这些框的相应置信度存储在confidences变量中。
decode(scores, geometry, confThreshold, boxes, confidences);
//由于我们可能会为文本框找到许多候选对象,因此我们需要过滤掉外观最好的文本框。 这是使用非最大抑制来完成的,即对NMSBoxes方法的调用。 在此调用中,我们给出解码后的框,置信度以及置信度和非最大值抑制的阈值,未消除的框的索引将存储在最后一个参数indices中。
std::vector<int> indices;
cv::dnn::NMSBoxes(boxes, confidences, confThreshold, nmsThreshold, indices);
// Render detections.
//现在,我们将所有文本区域作为cv::RotatedRect的实例,并且这些区域用于调整大小的图像,因此我们应该将它们映射到原始输入图像上:
cv::Point2f ratio((float)frame.cols / inputWidth, (float)frame.rows / inputHeight);
cv::Scalar green = cv::Scalar(0, 255, 0);
//为了将文本区域映射到原始图像,我们应该知道在将图像发送到 DNN 模型之前如何调整图像大小,然后逆转文本区域的大小调整过程。
// 因此,我们根据宽度和高度方面计算尺寸调整率,然后将它们保存到cv::Point2f ratio中。
//然后,我们迭代保留的索引,并获得每个索引指示的每个cv::RotatedRect对象。
for (size_t i = 0; i < indices.size(); ++i) {
cv::RotatedRect& box = boxes[indices[i]];
cv::Rect area = box.boundingRect();
//为了降低代码的复杂性,我们无需将cv::RotatedRect及其内容旋转为规则矩形,而是简单地获取其边界矩形。
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;
}
//decode方法用于从输出层提取置信度和框信息。 可以在这个页面中找到其实现。 要理解它,您应该了解 DNN 模型中的数据结构,尤其是输出层中的数据结构。 但是,这超出了本书的范围。 如果您对此感兴趣,可以在这个页面上参阅与 EAST 有关的论文,并在这个页面上使用 Tensorflow 来实现它的一种实现。
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(geometry.dims == 4);
CV_Assert(scores.size[0] == 1); CV_Assert(scores.size[1] == 1);
CV_Assert(geometry.size[0] == 1); CV_Assert(geometry.size[1] == 5);
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);
}
}
}