在OCR(光学字符识别)系统中,为了提高OCR系统的性能,确保准确识别文本内容。图像预处理是一个关键的组成部分。其中,一个重要的任务是矫正文本方向。例如,在进行文字识别时,不仅需要有效地提取和识别文字,还应确保文本以正确的方向呈现,以提高准确性。这意味着在识别文本之前,必须对图像进行预处理,以使文本在水平或垂直方向上对齐。在传统数字图像处理中常用投影分析、Hough变换、方向梯度直方图(HOG)等,来检测并调整文本的方向。
但在实现过程中,发现传统的数字图像处理撸棒性并不是很高,所以选用了基于深度学习的方法,实现的步骤是使用先对文档进行边缘检测,关于边缘检测,可以看我之前的博客。然后对剪切出来的文档使用DBNet进行文本检测,之后对检测的行做文字方向检测。
安卓实现效果视频:
文本方向检测与校正
常用的基于深度学习的文字检测方法一般可以分为基于回归的、基于分割的两大类,DBNet把两者进行结合的方法。
常用的基于回归的方法有:
CTPN(Connectionist Text Proposal Network): CTPN是一种基于回归的文本检测方法,主要通过在图像中生成文本线的候选区域,并通过回归来精细调整这些区域。
Textbox系列: Textbox是一系列基于回归的算法,主要关注在生成文本框的同时,对文本的旋转和形变进行建模,以适应各种文本形状。
EAST(Efficient and Accurate Scene Text Detector): EAST是一种基于回归的文本检测方法,采用全卷积网络,通过预测文本框的四个角点坐标实现文本检测。
CRAFT(Character Region Awareness for Text Detection): CRAFT是一种采用像素值回归的方法,通过在字符级别上实现像素级别的回归,能够有效地处理曲线形状的文本。
SA-Text(Structure-Aware Text Detector): SA-Text是另一种基于像素值回归的方法,通过捕获文本结构信息,能够对小文本和曲线文本进行有效检测。
**基于分割的方法和结合回归和分割的方法 **:
PSENet(Shape Robust Text Detection with Progressive Scale Expansion Network): PSENet是一种基于分割的文本检测方法,通过逐步扩展文本区域的尺度来实现文本实例的检测。
DBNet(Dilated Bi-directional Network): DBNet是一种将回归和分割结合的文本检测方法,采用了膨胀卷积和双向上下文信息,使其能够在不同尺度上捕获文本信息,同时通过联合训练提高检测性能。
DBNet算法在传统的基于0,1黑白像素阈值进行二值化的基础上,提出了threshold map陪练probability map生成DB(Differentiable Binarization,可微二值化)函数,从而优化反向传播梯度更新的图像文本检测方法
DBNet的最大创新点。在基于分割的文本检测网络中,最终的二值化map都是使用的固定阈值来获取,并且阈值不同对性能影响较大。在DBNet,对每一个像素点进行自适应二值化,二值化阈值由网络学习得到,彻底将二值化这一步骤加入到网络里一起训练,这样最终的输出图对于阈值就会非常鲁棒。
更多关于算法原理,可以转到DBNet的git:https://github.com/WenmuZhou/DBNet.pytorch?tab=readme-ov-file 。
在文档拍摄过程中,由于拍摄设备旋转,生成的图片可能存在不同方向。要对这些方向进行分类,这里采用了基于PaddleClas的超轻量图像分类方案(PULC)算法。该算法旨在快速构建轻量级、高精度、可实际应用的文字图像方向分类模型。
关于文字方向分类具体优化与如何训练自己的数据可以参考Paddle的官方文档:https://github.com/PaddlePaddle/PaddleClas/blob/release/2.5/docs/zh_CN/models/PULC/PULC_text_image_orientation.md
我的开发环境是Android Studio 北极狐,真机是华为mate 30 pro,系统是HarmonyOS 4.0.0, NDK 是21.1.6352462这个版本,可实现CPU与GPU、NPU推理,推理速度与精度可以按真机去匹配。使用的推理库是onnxruntime。
#pragma once
#include "../onnxocr/DbNet.h"
#include "../onnxocr/AngleNet.h"
#include "../onnxocr/OcrUtils.h"
namespace SCAN
{
class TextDirection
{
public:
TextDirection();
~TextDirection();
int read_model(std::string _db_model_path = "ch_PP-OCRv3_det_infer.onnx",
std::string _angle_model_path = "ch_ppocr_mobile_v2.0_cls_infer.onnx",
int _thread_num = 4, int _gpu_index = 0);
void set_thread_num(int _thread_num);
void set_gpu_index(int _gpu_index);
int direction(cv::Mat& cv_src, cv::Mat& cv_dst);
private:
ONNXOCR::DbNet db_net;
ONNXOCR::AngleNet angle_net;
int thread_num;
int gpu_index;
const int angle_w = 192;
const int angle_h = 48;
public:
int padding = 10;
int maxSideLen = 1024;
float boxScoreThresh = 0.4f;
float boxThresh = 0.2f;
float unClipRatio = 1.6f;
std::string db_model_path;
std::string angle_model_path;
};
}
#include "TextDirection.h"
namespace SCAN
{
TextDirection::TextDirection()
{
}
TextDirection::~TextDirection()
{
}
int TextDirection::read_model(std::string _db_model_path, std::string _angle_model_path, int _thread_num, int _gpu_index)
{
db_model_path = _db_model_path;
angle_model_path = _angle_model_path;
thread_num = _thread_num;
gpu_index = _gpu_index;
db_net.set_thread_num(thread_num);
angle_net.set_thread_num(thread_num);
db_net.set_gpu_index(gpu_index);
angle_net.set_gpu_index(-1);
db_net.read_model(db_model_path);
angle_net.read_model(angle_model_path);
return 0;
}
void TextDirection::set_gpu_index(int _gpu_index)
{
gpu_index = _gpu_index;
db_net.set_gpu_index(gpu_index);
angle_net.set_gpu_index(-1);
}
void TextDirection::set_thread_num(int _thread_num)
{
thread_num = _thread_num;
db_net.set_thread_num(thread_num);
angle_net.set_thread_num(thread_num);
}
cv::Mat make_padding(cv::Mat& src, const int padding)
{
if (padding <= 0) return src;
cv::Scalar paddingScalar = { 255, 255, 255 };
cv::Mat paddingSrc;
cv::copyMakeBorder(src, paddingSrc, padding, padding, padding, padding, cv::BORDER_ISOLATED, paddingScalar);
return paddingSrc;
}
/// -1 - 180度
/// 0 - 90度
/// 1 - 270度
cv::Mat rotateMat(cv::Mat& cv_src, int angle_index)
{
cv::Mat cv_copy = cv_src.clone();
cv::Mat cv_dst;
if (angle_index == -1)
{
flip(cv_copy, cv_dst, angle_index);
return cv_dst;
}
transpose(cv_copy, cv_copy);
flip(cv_copy, cv_dst, angle_index);
return cv_dst;
}
int TextDirection::direction(cv::Mat& cv_src, cv::Mat& cv_dst)
{
cv::Mat originSrc = cv_src;
int originMaxSide = (std::max)(originSrc.cols, originSrc.rows);
int resize;
if (maxSideLen <= 0 || maxSideLen > originMaxSide)
{
resize = originMaxSide;
}
else
{
resize = maxSideLen;
}
resize += 2 * padding;
cv::Rect paddingRect(padding, padding, originSrc.cols, originSrc.rows);
cv::Mat cv_padding = make_padding(originSrc, padding);
ScaleParam scale = ONNXOCR::getScaleParam(cv_padding, resize);
std::vector<TextBox> textBoxes = db_net.get_text_boxes(cv_padding, scale, boxScoreThresh, boxThresh, unClipRatio);
std::vector<int> angle_index = { 0, 0, 0, 0};
for (size_t i = 0; i < textBoxes.size(); ++i)
{
cv::Mat cv_part = ONNXOCR::get_crop_image(cv_padding, textBoxes[i].boxPoint);
if (float(cv_part.rows) >= float(cv_part.cols) * 1.5)
{
cv::Mat cv_copy = cv::Mat(cv_part.rows, cv_part.cols, cv_part.depth());
cv::transpose(cv_part, cv_copy);
cv::flip(cv_copy, cv_copy, 0);
cv::Mat cv_angle;
cv::resize(cv_copy, cv_angle, cv::Size(angle_w, angle_h));
Angle angle = angle_net.get_angle(cv_angle);
if (angle.index == 0)
{
angle_index[0] ++;
}
else if(angle.index == 1)
{
angle_index[1] ++;
}
}
else
{
cv::Mat cv_angle;
cv::resize(cv_part, cv_angle, cv::Size(angle_w, angle_h));
Angle angle = angle_net.get_angle(cv_angle);
if (angle.index == 0)
{
angle_index[2] ++;
}
else if(angle.index == 1)
{
angle_index[3] ++;
}
}
}
auto maxElement = std::max_element(angle_index.begin(), angle_index.end());
int maxIndex = std::distance(angle_index.begin(), maxElement);
switch (maxIndex)
{
case 0:
cv_dst = rotateMat(cv_src, 0);
break;
case 1:
cv_dst = rotateMat(cv_src, 1);
break;
case 2:
cv_dst = cv_src.clone();
break;
case 3:
cv_dst = rotateMat(cv_src, -1);
break;
default:
break;
}
return maxIndex;
}
}