在工业现场的OCR识别中,由于cls倒置文本分类器的存在,OCR识别对完全正向或者倒置的文本识别成功率差不多,但是一旦出现文本倾斜,甚至倾斜接近90°,识别成功率会大幅度下降,这时就需要在图片提交OCR任务之前对圆形标签内旋转文本的方向进行矫正,将图片矫正为完全正向或者倒置的状态。

首先在项目里面添加opencv的库,是该项目可以调用opencv进行图像处理。再添加相应的头文件,并定义圆周率的值。

#include <opencv2/opencv.hpp>
#include <QApplication>
#include <opencv2\imgcodecs.hpp>
#include <opencv2\core.hpp>
#include <opencv2\imgproc.hpp>
#include <opencv2\highgui.hpp>
#include <opencv2\features2d.hpp>
#include <vector>
#include <QCoreApplication>
#include <algorithm>
#include <cmath>
#include <QDebug>
#include <string>

// 添加在文件最开始的宏定义
#ifndef M_PI
#define M_PI 3.14159265358979323846
#endif

之后导入图像,并对图像进行预处理,包括灰度化,去噪以及二值化,方便接下寻找轮廓。

    // 1. 加载图像
    std::string path = "C:/Users/Andy/Desktop/pic/pic30.png"; // 替换为你的图片路径
    cv::Mat src = cv::imread(path);
    if (src.empty()) {
        qDebug() << "Error: Image not found!";
        return -1;
    }

    // 2. 椭圆检测预处理
    cv::Mat gray, blurred, binary;
    cv::cvtColor(src, gray, cv::COLOR_BGR2GRAY);
    cv::GaussianBlur(gray, blurred, cv::Size(0, 0), 2.0);
    cv::threshold(blurred, binary, 0, 255, cv::THRESH_BINARY | cv::THRESH_OTSU);

之后对经过预处理后的图片寻找轮廓,由于我要处理的标签都是圆形标签,所以寻找椭圆轮廓来定位标签位置。

通过5点拟合椭圆,并筛选出面积最大的椭圆当作标签轮廓的拟合结果。

    // 3. 轮廓检测
    std::vector<std::vector<cv::Point>> contours;
    cv::findContours(binary, contours, cv::RETR_EXTERNAL, cv::CHAIN_APPROX_SIMPLE);

    cv::RotatedRect maxEllipse;
    double maxArea = 0;
    bool ellipseFound = false;
    cv::Mat mask = cv::Mat::zeros(src.size(), CV_8UC1); // 创建椭圆掩码

    // 4. 检测最大椭圆
    for (const auto& contour : contours) {
        if (contour.size() < 5) continue;
        double contourArea = cv::contourArea(contour);
        cv::RotatedRect ellipse = cv::fitEllipse(contour);
        double ellipseArea = CV_PI * ellipse.size.width * ellipse.size.height / 4;

        if (contourArea / ellipseArea > 0.7 && contourArea > maxArea) {
            maxArea = contourArea;
            maxEllipse = ellipse;
            ellipseFound = true;
        }
    }

之后根据椭圆检测结果生成掩码mask,掩码可用于提取椭圆区域内的像素(例如抠出椭圆中的物体,或屏蔽外部背景),用于后续图像处理。

    // 5. 创建椭圆ROI掩码
    if (ellipseFound) {
        cv::ellipse(mask, maxEllipse, cv::Scalar(255), cv::FILLED);
    } else {
        qDebug() << "No ellipse found! Using full image.";
        mask.setTo(255); // 没有椭圆时使用整个图像
    }

之后进行直线检测预处理,​在灰度图像上提取椭圆ROI区域的边缘,并通过掩码(mask)确保只保留椭圆内的边缘信息,之后通过霍夫变换在椭圆轮廓内检测直线,并进行可视化,在原图上绘制椭圆和直线。

    // 6. 直线检测预处理(在椭圆区域内)
    cv::Mat roiGray, roiEdges;
    cv::cvtColor(src, roiGray, cv::COLOR_BGR2GRAY);
    cv::Canny(roiGray, roiEdges, 50, 200);
    cv::bitwise_and(roiEdges, mask, roiEdges); // 应用椭圆掩码

    // 7. 霍夫变换检测直线(只检测椭圆区域内)
    std::vector<cv::Vec4i> lines;
    cv::HoughLinesP(
        roiEdges, lines,
        1,              // rho分辨率
        CV_PI / 180,    // theta分辨率
        80,             // 累加器阈值
        40,             // 最小线段长度
        10              // 最大间隙
        );

    // 8. 在原图上绘制椭圆和直线
    cv::Mat result = src.clone();
    if (ellipseFound) {
        cv::ellipse(result, maxEllipse, cv::Scalar(0, 255, 0), 2); // 绿色椭圆
    }

之后收集所有检测到的直线角度,并对直线角度排序,取中位数,作为图像目标矫正角度。

    // 1. 收集所有检测到的直线角度
    std::vector<double> allAngles;
    for (const auto& line : lines) {
        cv::line(result,
                 cv::Point(line[0], line[1]),
                 cv::Point(line[2], line[3]),
                 cv::Scalar(0, 0, 255), 2);

        double dx = line[2] - line[0];
        double dy = line[3] - line[1];
        double angle = std::atan2(dy, dx) * 180 / M_PI;

        // 收集所有角度
        allAngles.push_back(angle);
    }

    // 2. 输出所有原始角度(未排序)
    if (!allAngles.empty()) {
        qDebug() << "===== 原始检测到的所有直线角度 =====";
        for (size_t i = 0; i < allAngles.size(); ++i) {
            qDebug() << "直线" << i+1 << ": " << allAngles[i] << "°";
        }
    }

    // 3. 计算中位数角度
    double medianAngle = 0.0;
    if (!allAngles.empty()) {
        // 3.1 对角度进行排序(从小到大)
        std::sort(allAngles.begin(), allAngles.end());

        // 3.2 输出排序后的角度
        qDebug() << "===== 排序后的所有直线角度 =====";
        for (size_t i = 0; i < allAngles.size(); ++i) {
            qDebug() << "角度" << i+1 << ": " << allAngles[i] << "°";
        }

        // 3.3 计算中位数
        size_t n = allAngles.size();
        if (n % 2 == 0) {
            // 偶数个元素:取中间两个值的平均
            medianAngle = (allAngles[n/2 - 1] + allAngles[n/2]) / 2.0;
        } else {
            // 奇数个元素:取中间值
            medianAngle = allAngles[n/2];
        }

        qDebug() << "图片矫正角度:" << medianAngle << "°";
    } else {
        qDebug() << "警告:未检测到任何直线!使用默认角度0°";
        medianAngle = 0.0;
    }

    // 9. 图像旋转校正(使用中位数角度)
    cv::Point2f center(src.cols / 2.0, src.rows / 2.0);

    // 选择最小旋转角度(避免大角度旋转)
    double rotationAngle = medianAngle;
    if (medianAngle > 90) {
        rotationAngle = medianAngle - 180; // 旋转小角度
    }

采用插值法旋转图像,保证图像处理后的质量。

    // 旋转校正
    cv::Mat rotationMat = cv::getRotationMatrix2D(center, rotationAngle, 1.0);
    cv::Mat corrected;
    cv::warpAffine(src, corrected, rotationMat, src.size(),
                   cv::INTER_LANCZOS4, cv::BORDER_CONSTANT, cv::Scalar(255, 255, 255));

旋转完后,对结果进行后处理,提升图像质量。

    // +++ 改进的旋转后处理(降低强度+去噪)+++
    {
        // 1. 轻度锐化(强度降低50%)
        cv::Mat blurred;
        cv::GaussianBlur(corrected, blurred, cv::Size(3, 3), 1.2);
        cv::addWeighted(corrected, 1.15, blurred, -0.15, 0, corrected);

        // 2. 消除小黑点 - 选择性去噪
        cv::Mat denoised;
        
        // 只对明亮区域去噪(避免影响文字)
        cv::Mat mask;
        cv::cvtColor(corrected, mask, cv::COLOR_BGR2GRAY);
        cv::threshold(mask, mask, 200, 255, cv::THRESH_BINARY);

        // 对高亮区域轻度去噪
        cv::fastNlMeansDenoisingColored(corrected, denoised, 5, 5, 7, 15);
        denoised.copyTo(corrected, mask);
    }
    // --- 结束修改 ---

处理结果示例如下:

完整代码:

#include <opencv2/opencv.hpp>
#include <QApplication>
#include <opencv2\imgcodecs.hpp>
#include <opencv2\core.hpp>
#include <opencv2\imgproc.hpp>
#include <opencv2\highgui.hpp>
#include <opencv2\features2d.hpp>
#include <vector>
#include <QCoreApplication>
#include <algorithm>
#include <cmath>
#include <QDebug>
#include <string>

// 添加在文件最开始的宏定义
#ifndef M_PI
#define M_PI 3.14159265358979323846
#endif

int main(int argc, char *argv[])
{
    QApplication app(argc, argv);
    // 1. 加载图像
    std::string path = "C:/Users/Andy/Desktop/pic/pic9.png"; // 替换为你的图片路径
    cv::Mat src = cv::imread(path);
    if (src.empty()) {
        qDebug() << "Error: Image not found!";
        return -1;
    }

    // 2. 椭圆检测预处理
    cv::Mat gray, blurred, binary;
    cv::cvtColor(src, gray, cv::COLOR_BGR2GRAY);
    cv::GaussianBlur(gray, blurred, cv::Size(0, 0), 2.0);
    cv::threshold(blurred, binary, 0, 255, cv::THRESH_BINARY | cv::THRESH_OTSU);

    // 3. 轮廓检测
    std::vector<std::vector<cv::Point>> contours;
    cv::findContours(binary, contours, cv::RETR_EXTERNAL, cv::CHAIN_APPROX_SIMPLE);

    cv::RotatedRect maxEllipse;
    double maxArea = 0;
    bool ellipseFound = false;
    cv::Mat mask = cv::Mat::zeros(src.size(), CV_8UC1); // 创建椭圆掩码

    // 4. 检测最大椭圆
    for (const auto& contour : contours) {
        if (contour.size() < 5) continue;
        double contourArea = cv::contourArea(contour);
        cv::RotatedRect ellipse = cv::fitEllipse(contour);
        double ellipseArea = CV_PI * ellipse.size.width * ellipse.size.height / 4;

        if (contourArea / ellipseArea > 0.7 && contourArea > maxArea) {
            maxArea = contourArea;
            maxEllipse = ellipse;
            ellipseFound = true;
        }
    }

    // 5. 创建椭圆ROI掩码
    if (ellipseFound) {
        cv::ellipse(mask, maxEllipse, cv::Scalar(255), cv::FILLED);
    } else {
        qDebug() << "No ellipse found! Using full image.";
        mask.setTo(255); // 没有椭圆时使用整个图像
    }

    // 6. 直线检测预处理(在椭圆区域内)
    cv::Mat roiGray, roiEdges;
    cv::cvtColor(src, roiGray, cv::COLOR_BGR2GRAY);
    cv::Canny(roiGray, roiEdges, 50, 200);
    cv::bitwise_and(roiEdges, mask, roiEdges); // 应用椭圆掩码

    // 7. 霍夫变换检测直线(只检测椭圆区域内)
    std::vector<cv::Vec4i> lines;
    cv::HoughLinesP(
        roiEdges, lines,
        1,              // rho分辨率
        CV_PI / 180,    // theta分辨率
        80,             // 累加器阈值
        40,             // 最小线段长度
        10              // 最大间隙
        );

    // 8. 在原图上绘制椭圆和直线
    cv::Mat result = src.clone();
    if (ellipseFound) {
        cv::ellipse(result, maxEllipse, cv::Scalar(0, 255, 0), 2); // 绿色椭圆
    }

    // 1. 收集所有检测到的直线角度
    std::vector<double> allAngles;
    for (const auto& line : lines) {
        cv::line(result,
                 cv::Point(line[0], line[1]),
                 cv::Point(line[2], line[3]),
                 cv::Scalar(0, 0, 255), 2);

        double dx = line[2] - line[0];
        double dy = line[3] - line[1];
        double angle = std::atan2(dy, dx) * 180 / M_PI;

        // 收集所有角度
        allAngles.push_back(angle);
    }

    // 2. 输出所有原始角度(未排序)
    if (!allAngles.empty()) {
        qDebug() << "===== 原始检测到的所有直线角度 =====";
        for (size_t i = 0; i < allAngles.size(); ++i) {
            qDebug() << "直线" << i+1 << ": " << allAngles[i] << "°";
        }
    }

    // 3. 计算中位数角度
    double medianAngle = 0.0;
    if (!allAngles.empty()) {
        // 3.1 对角度进行排序(从小到大)
        std::sort(allAngles.begin(), allAngles.end());

        // 3.2 输出排序后的角度
        qDebug() << "===== 排序后的所有直线角度 =====";
        for (size_t i = 0; i < allAngles.size(); ++i) {
            qDebug() << "角度" << i+1 << ": " << allAngles[i] << "°";
        }

        // 3.3 计算中位数
        size_t n = allAngles.size();
        if (n % 2 == 0) {
            // 偶数个元素:取中间两个值的平均
            medianAngle = (allAngles[n/2 - 1] + allAngles[n/2]) / 2.0;
        } else {
            // 奇数个元素:取中间值
            medianAngle = allAngles[n/2];
        }

        qDebug() << "图片矫正角度:" << medianAngle << "°";
    } else {
        qDebug() << "警告:未检测到任何直线!使用默认角度0°";
        medianAngle = 0.0;
    }

    // 9. 图像旋转校正(使用中位数角度)
    cv::Point2f center(src.cols / 2.0, src.rows / 2.0);

    // 选择最小旋转角度(避免大角度旋转)
    double rotationAngle = medianAngle;
    if (medianAngle > 90) {
        rotationAngle = medianAngle - 180; // 旋转小角度
    }

    // 旋转校正
    cv::Mat rotationMat = cv::getRotationMatrix2D(center, rotationAngle, 1.0);
    cv::Mat corrected;
    cv::warpAffine(src, corrected, rotationMat, src.size(),
                   cv::INTER_LANCZOS4, cv::BORDER_CONSTANT, cv::Scalar(255, 255, 255));

    // +++ 改进的旋转后处理(降低强度+去噪)+++
    {
        // 1. 轻度锐化(强度降低50%)
        cv::Mat blurred;
        cv::GaussianBlur(corrected, blurred, cv::Size(3, 3), 1.2);
        cv::addWeighted(corrected, 1.15, blurred, -0.15, 0, corrected);

        // 2. 消除小黑点 - 选择性去噪
        cv::Mat denoised;

        // 只对明亮区域去噪(避免影响文字)
        cv::Mat mask;
        cv::cvtColor(corrected, mask, cv::COLOR_BGR2GRAY);
        cv::threshold(mask, mask, 200, 255, cv::THRESH_BINARY);

        // 对高亮区域轻度去噪
        cv::fastNlMeansDenoisingColored(corrected, denoised, 5, 5, 7, 15);
        denoised.copyTo(corrected, mask);
    }
    // --- 结束修改 ---

    // 10. 显示结果
    cv::imshow("Original", src);
    cv::imshow("Ellipse with Lines", result);
    cv::imshow("Corrected", corrected);
    cv::waitKey(0);

    return 0;
}

Logo

中国智能体开发者社区,聚焦智能体与大模型开发,提供前沿资讯、实用工具链、开源项目及行业案例。通过技术沙龙、开发者大赛等活动,促进经验交流与协作,助力开发者快速构建创新智能应用。

更多推荐