简介

在OCR(光学字符识别)系统中,为了提高OCR系统的性能,确保准确识别文本内容。图像预处理是一个关键的组成部分。其中,一个重要的任务是矫正文本方向。例如,在进行文字识别时,不仅需要有效地提取和识别文字,还应确保文本以正确的方向呈现,以提高准确性。这意味着在识别文本之前,必须对图像进行预处理,以使文本在水平或垂直方向上对齐。在传统数字图像处理中常用投影分析、Hough变换、方向梯度直方图(HOG)等,来检测并调整文本的方向。
但在实现过程中,发现传统的数字图像处理撸棒性并不是很高,所以选用了基于深度学习的方法,实现的步骤是使用先对文档进行边缘检测,关于边缘检测,可以看我之前的博客。然后对剪切出来的文档使用DBNet进行文本检测,之后对检测的行做文字方向检测。

安卓实现效果视频:

文本方向检测与校正

文本检测

常用的基于深度学习的文字检测方法一般可以分为基于回归的、基于分割的两大类,DBNet把两者进行结合的方法。

常用的基于回归的方法有:

  1. CTPN(Connectionist Text Proposal Network): CTPN是一种基于回归的文本检测方法,主要通过在图像中生成文本线的候选区域,并通过回归来精细调整这些区域。

  2. Textbox系列: Textbox是一系列基于回归的算法,主要关注在生成文本框的同时,对文本的旋转和形变进行建模,以适应各种文本形状。

  3. EAST(Efficient and Accurate Scene Text Detector): EAST是一种基于回归的文本检测方法,采用全卷积网络,通过预测文本框的四个角点坐标实现文本检测。

  4. CRAFT(Character Region Awareness for Text Detection): CRAFT是一种采用像素值回归的方法,通过在字符级别上实现像素级别的回归,能够有效地处理曲线形状的文本。

  5. SA-Text(Structure-Aware Text Detector): SA-Text是另一种基于像素值回归的方法,通过捕获文本结构信息,能够对小文本和曲线文本进行有效检测。

**基于分割的方法和结合回归和分割的方法 **:

  1. PSENet(Shape Robust Text Detection with Progressive Scale Expansion Network): PSENet是一种基于分割的文本检测方法,通过逐步扩展文本区域的尺度来实现文本实例的检测。

  2. DBNet(Dilated Bi-directional Network): DBNet是一种将回归和分割结合的文本检测方法,采用了膨胀卷积和双向上下文信息,使其能够在不同尺度上捕获文本信息,同时通过联合训练提高检测性能。

DBNet

DBNet算法在传统的基于0,1黑白像素阈值进行二值化的基础上,提出了threshold map陪练probability map生成DB(Differentiable Binarization,可微二值化)函数,从而优化反向传播梯度更新的图像文本检测方法
DBNet的最大创新点。在基于分割的文本检测网络中,最终的二值化map都是使用的固定阈值来获取,并且阈值不同对性能影响较大。在DBNet,对每一个像素点进行自适应二值化,二值化阈值由网络学习得到,彻底将二值化这一步骤加入到网络里一起训练,这样最终的输出图对于阈值就会非常鲁棒。
安卓拍照扫描APP解决方案——基于深度学习的文本方向检测与校正-LMLPHP
更多关于算法原理,可以转到DBNet的git:https://github.com/WenmuZhou/DBNet.pytorch?tab=readme-ov-file

检测效果:
安卓拍照扫描APP解决方案——基于深度学习的文本方向检测与校正-LMLPHP
安卓拍照扫描APP解决方案——基于深度学习的文本方向检测与校正-LMLPHP

文本方向分类

在文档拍摄过程中,由于拍摄设备旋转,生成的图片可能存在不同方向。要对这些方向进行分类,这里采用了基于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;
    }
} 
01-06 15:42