1.新建Qt项目,选择MSVC64位编译器
2.添加opencv库,根据你自己的opencv安装位置来选择,详细可以参考Qt MSVC配置OpenCV教程,亲测已试过(详细版)_qt msvc opencv-CSDN博客
添加好后.pro文件会出现相应代码
win32:CONFIG(release, debug|release): LIBS += -LD:/MY_Software/opencv4.5.1/opencv4.5.1/opencv/build/x64/vc14/lib/ -lopencv_world451
else:win32:CONFIG(debug, debug|release): LIBS += -LD:/MY_Software/opencv4.5.1/opencv4.5.1/opencv/build/x64/vc14/lib/ -lopencv_world451d
INCLUDEPATH += D:/MY_Software/opencv4.5.1/opencv4.5.1/opencv/build/include
DEPENDPATH += D:/MY_Software/opencv4.5.1/opencv4.5.1/opencv/build/include
3.在main.cpp文件中进行头文件配置
#include "widget.h"
#include <QLabel>
#include <QVBoxLayout>
#include <QWidget>
#include <QImage>
#include <QDebug>
#include <iostream>
#include <opencv2/opencv.hpp>
#include <QApplication>
using namespace cv;
4.在主函数中读入图像,并进行图像预处理,可以对亮度,去噪参数进行调节。我使用的图片如下,一个变压器的铭牌。
// 图片文件路径,需要根据实际情况修改
std::string filePath = "C:/Users/Andy/Desktop/img4.jpg";
// 【1】载入图像
Mat src = imread(filePath, 1);
if (src.empty())
{
return -1;
}
// cv::namedWindow("src", cv::WINDOW_NORMAL);
// imshow("src", src);
// waitKey(0);
// 【图像预处理:增强对比度和亮度】
Mat enhancedSrc;
src.convertTo(enhancedSrc, -2, 1.4, 31.8); // 调整对比度和亮度
// 调整亮度
int brightness = 145; // 定义亮度增量,可根据需要调整
cv::Mat brightenedImage = src.clone(); // 复制原图像,避免修改原图像
// 遍历图像的每个像素
for (int y = 0; y < brightenedImage.rows; y++)
{
for (int x = 0; x < brightenedImage.cols; x++)
{
// 获取当前像素的BGR值
cv::Vec3b& pixel = brightenedImage.at<cv::Vec3b>(y, x);
// 增加每个通道的亮度值
for (int c = 0; c < 3; c++)
{
int value = pixel[c] + brightness;
// 确保值在0-255范围内
pixel[c] = cv::saturate_cast<uchar>(value);
}
}
}
// 【2】图像灰度化
Mat gray;
cvtColor(brightenedImage, gray, COLOR_BGR2GRAY);
// 【3】执行二值分割
threshold(gray, gray, 0, 255, THRESH_BINARY_INV | THRESH_OTSU);
// cv::namedWindow("threshold", cv::WINDOW_NORMAL);
// imshow("threshold", gray);
// waitKey(0);
// 【4】执行形态学开操作去除图像中的噪点
Mat kernel1 = getStructuringElement(MORPH_RECT, Size(3.2, 3.2), Point(-1, -1));
morphologyEx(gray, gray, MORPH_CLOSE, kernel1, Point(-1, -1), 3);
// cv::namedWindow("threshold", cv::WINDOW_NORMAL);
// imshow("morphologyEx", gray);
// waitKey(0); // 等待用户按键
5.轮廓发现
// 【5】轮廓发现
bitwise_not(gray, gray);
imshow("bitwise_not", gray);
waitKey(0); // 等待用户按键
std::vector<std::vector<Point>> contours;
std::vector<Vec4i> hier;
RNG rng(12345);
findContours(gray, contours, hier, RETR_TREE, CHAIN_APPROX_SIMPLE);
Mat colorImage = Mat::zeros(gray.size(), CV_8UC3);
for (size_t i = 0; i < contours.size(); i++)
{
Rect rect = boundingRect(contours[i]);
// 过滤目标轮廓
if (rect.width < src.cols - 5 && rect.height < src.rows - 5 && rect.width > src.cols / 2)
{
drawContours(colorImage, contours, i, Scalar(rng.uniform(0, 255), rng.uniform(0, 255), rng.uniform(0, 255)), 1);
}
}
// cv::namedWindow("findContours", cv::WINDOW_NORMAL);
// imshow("findContours", colorImage);
// waitKey(0); // 等待用户按键
6.使用霍夫直线检测,并绘制出直线
// 【6】使用霍夫直线检测
std::vector<Vec4i> lines;
cvtColor(colorImage, colorImage, COLOR_BGR2GRAY);
Mat kernel = getStructuringElement(MORPH_RECT, Size(3, 3), Point(-1, -1));
dilate(colorImage, colorImage, kernel, Point(-1, -1), 1);
cv::namedWindow("colorImage_gray", cv::WINDOW_NORMAL);
imshow("colorImage_gray", colorImage);
waitKey(0); // 等待用户按键
int accu = std::min(src.cols * 0.1, src.rows * 0.1);
HoughLinesP(colorImage, lines, 1, CV_PI / 180, accu, accu, 0);
// 【7】绘制出直线
Mat lineColorImage = Mat::zeros(gray.size(), CV_8UC3);
//qDebug() << "line count:" << lines.size();
for (size_t i = 0; i < lines.size(); i++)
{
Vec4i ll = lines[i];
line(lineColorImage, Point(ll[0], ll[1]), Point(ll[2], ll[3]), Scalar(rng.uniform(0, 255), rng.uniform(0, 255), rng.uniform(0, 255)), 2, LINE_8);
}
cv::namedWindow("lines", cv::WINDOW_NORMAL);
imshow("lines", lineColorImage);
waitKey(0); // 等待用户按键
绘制出来直线如下,可以看到铭牌的四条边都存在直线,可以对铭牌进行顶点定位
7.寻找铭牌上下左右四条边所对应的拟合直线,求出这四条直线方程,可以将拟合直线画在原图上面,可以看到铭牌四条边都有正确的拟合直线
// 【8】寻找与定位上下左右四条直线
int deltah = 0;
int width = src.cols;
int height = src.rows;
Vec4i topLine(0, 0, 0, 0), bottomLine(0, 0, 0, 0);
Vec4i leftLine(0, 0, 0, 0), rightLine(0, 0, 0, 0);
for (size_t i = 0; i < lines.size(); i++)
{
Vec4i ln = lines[i];
deltah = abs(ln[3] - ln[1]); // 直线高度
double slope = (double)(ln[3] - ln[1]) / (ln[2] - ln[0]); // 计算直线斜率
if (std::abs(slope) < 0.2)
{ // 水平直线
if (ln[1] < height / 2.0)
{
if (topLine[3] > ln[3] && topLine[3] > 0)
{
topLine = lines[i];
}
else
{
topLine = lines[i];
}
}
else
{
bottomLine = lines[i];
}
}
else if (std::abs(slope) > 5)
{ // 垂直直线
if (ln[0] < width / 2.0)
{
leftLine = lines[i];
}
else
{
rightLine = lines[i];
}
}
}
// 直线方程 y = kx + c
// 【9】拟合四条直线方程
float k1, c1;
k1 = float(topLine[3] - topLine[1]) / float(topLine[2] - topLine[0]);
c1 = topLine[1] - k1 * topLine[0];
float k2, c2;
k2 = float(bottomLine[3] - bottomLine[1]) / float(bottomLine[2] - bottomLine[0]);
c2 = bottomLine[1] - k2 * bottomLine[0];
float k3, c3;
k3 = float(leftLine[3] - leftLine[1]) / float(leftLine[2] - leftLine[0]);
c3 = leftLine[1] - k3 * leftLine[0];
float k4, c4;
k4 = float(rightLine[3] - rightLine[1]) / float(rightLine[2] - rightLine[0]);
c4 = rightLine[1] - k4 * rightLine[0];
// 将四条直线用红色标注在原图片 src 上
int lineThickness = 3; // 直线线条粗细,可以根据需要调整
if (topLine[2] - topLine[0] != 0)
{
line(src, Point(topLine[0], topLine[1]), Point(topLine[2], topLine[3]), Scalar(0, 0, 255), lineThickness, LINE_AA);
}
if (bottomLine[2] - bottomLine[0] != 0)
{
line(src, Point(bottomLine[0], bottomLine[1]), Point(bottomLine[2], bottomLine[3]), Scalar(0, 0, 255), lineThickness, LINE_AA);
}
if (leftLine[2] - leftLine[0] != 0)
{
line(src, Point(leftLine[0], leftLine[1]), Point(leftLine[2], leftLine[3]), Scalar(0, 0, 255), lineThickness, LINE_AA);
}
if (rightLine[2] - rightLine[0] != 0)
{
line(src, Point(rightLine[0], rightLine[1]), Point(rightLine[2], rightLine[3]), Scalar(0, 0, 255), lineThickness, LINE_AA);
}
cv::namedWindow("src with lines", cv::WINDOW_NORMAL);
imshow("src with lines", src);
waitKey(0);
8.求出这四条拟合直线的交点,即所求的四个顶点,将这四个顶点显示在原图片上面
// 【10】四条直线交点,其实最终的目的就是找这四条直线的交点
Point p1; // 左上角
p1.x = static_cast<int>((c1 - c3) / (k3 - k1));
p1.y = static_cast<int>(k1 * p1.x + c1);
Point p2; // 右上角
p2.x = static_cast<int>((c1 - c4) / (k4 - k1));
p2.y = static_cast<int>(k1 * p2.x + c1);
Point p3; // 左下角
p3.x = static_cast<int>((c2 - c3) / (k3 - k2));
p3.y = static_cast<int>(k2 * p3.x + c2);
Point p4; // 右下角
p4.x = static_cast<int>((c2 - c4) / (k4 - k2));
p4.y = static_cast<int>(k2 * p4.x + c2);
// 调试输出交点坐标
qDebug() << "p1: (" << p1.x << ", " << p1.y << ")";
qDebug() << "p2: (" << p2.x << ", " << p2.y << ")";
qDebug() << "p3: (" << p3.x << ", " << p3.y << ")";
qDebug() << "p4: (" << p4.x << ", " << p4.y << ")";
// 显示四个点坐标
int circleRadius = 10; // 圆圈半径,可以根据需要调整
int circleThickness = 3; // 圆圈线条粗细,可以根据需要调整
circle(src, p1, circleRadius, Scalar(0, 0, 255), circleThickness, LINE_AA);
circle(src, p2, circleRadius, Scalar(0, 0, 255), circleThickness, LINE_AA);
circle(src, p3, circleRadius, Scalar(0, 0, 255), circleThickness, LINE_AA);
circle(src, p4, circleRadius, Scalar(0, 0, 255), circleThickness, LINE_AA);
cv::namedWindow("src with corners", cv::WINDOW_NORMAL);
imshow("src with corners", src);
waitKey(0);
四个顶点的坐标如下:
9.透视变换
// 【11】透视变换
std::vector<Point2f> src_corners(4);
src_corners[0] = p1;
src_corners[1] = p2;
src_corners[2] = p3;
src_corners[3] = p4;
std::vector<Point2f> dst_corners(4);
dst_corners[0] = Point(0, 0);
dst_corners[1] = Point(width, 0);
dst_corners[2] = Point(0, height);
dst_corners[3] = Point(width, height);
// 【12】获取透视变换矩阵,并最终显示变换后的结果
Mat resultImage;
Mat warpmatrix = getPerspectiveTransform(src_corners, dst_corners);
//warpPerspective(src, resultImage, warpmatrix, resultImage.size(), INTER_LINEAR);
// 使用 INTER_CUBIC 插值方法
warpPerspective(enhancedSrc, resultImage, warpmatrix, resultImage.size(), INTER_CUBIC);
// 调整输出图像的尺寸
Size newSize(width, height);
resize(resultImage, resultImage, newSize);
cv::namedWindow("Final Result", cv::WINDOW_NORMAL);
imshow("Final Result", resultImage);
waitKey(0); // 等待用户按键
结果如下:
我将资源上传了,代码里面的逻辑和这个文章里面进行了一定的优化,识别的效果以及识别的成功率更好。
10.可能出现的问题
一、第六步使用霍夫直线检测,并绘制出直线,结果可能只有几条边有拟合直线,没有四条边,可能的解决方法:可以试着增强图片的亮度参数,或者改变去除图像中的噪点的去噪参数。
二、四条拟合直线只显示出来了几条,导致最后只显示了1,2个点,可能解决方法:修改霍夫直线检测的参数;修改直线斜率判断阈值。
三、由于是对铭牌进行透视变换,可能会有铭牌内部的直线被选取代替边缘的直线,导致结果只是铭牌的一部分,可以在相关的代码逻辑里面改成计算所有直线对于上下左右边缘的距离,选择离边缘最近的距离为该边界。