圆形标签内旋转文本方向矫正
·
在工业现场的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;
}
更多推荐


所有评论(0)