简介
在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
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;}
}