轮廓分析拟合方面我现在只考虑矩形拟合和圆形拟合
细分的话,椭圆拟合,矩形拟合,最小外接矩形,最小外接圆。
对于一张图像可能有不同的图形,不同的圆,不同的矩形,我需要对其进行筛选,也需要对检测的目标对针对性计算,例如面积,周长,圆心点坐标。
也需要对筛选的模块进行进一步检索,例如都是圆,两个大一个小,我就需要通过筛选他们的面积来确定我具体要哪一个圆。
那最后就是显示功能了,因为轮廓分析往往是基于图像处理后显示的,这样不够直观也不够美观,我们需要让他显示在原图上,我们可以选择它显示的方式,最小圆,最小矩形,十字线,中心点这样会好看很多。
我们整体的页面思路已经设计好了下面看一下实现
一.界面方面
从头再过一下加强Qt的控件记忆
设计轮廓分析的主界面设计,采用垂直排序的方式,
setContentsMargins(10, 10, 10, 10)
:设置布局的内边距为 10 像素,即布局内容与面板边缘之间保持 10 像素的空白距离
setSpacing(15)
:设置布局中各个子控件之间的垂直间距为 15 像素
建立一个名字为计算分析的QGroupBox控件,它内部的控制方式QGridLayout,也就是网格布局。之后再建立一个QLabe将他方式到 计算分析的组里面这样可以通过QGridLayout的方式来控制它的布局。
perimeterCheckBox->setChecked(true); // 选中"周长"复选框
areaCheckBox->setChecked(true); // 选中"面积"复选框
默认选中状态
之后就是对控件进行排序看一下效果
还是比较美观的之后进行下一步拟合分析组的设计和上面的设计是一样的可以看一下
后面的过滤设置有一些不一样我么看一下,首先是一个范围值,要有最大和最小来将其圈起来
还是建立一个QGroupBox将将我需要的组件放进去
建立Qlabel来放置周长的最大值最小值筛选,显示最小值
QDoubleSpinBox
是 Qt 提供的一个数值输入控件,允许用户通过上下箭头或直接输入来选择一个带小数的数值(即双精度浮点数)
以此建立我们需要的控制数值的变量
我们看一下效果
最后就是我们的显示和前面也是一样的,直接看效果
最后我们还需要一个轮廓信息帮我们显性的显示每一个找到的轮廓的面积信息
要显示
之后算法方面
二.算法方面
cv::findContours(InputArray image, // 输入图像(通常为二值图)OutputArrayOfArrays contours, // 检测到的轮廓点集OutputArray hierarchy, // 轮廓的层级关系int mode, // 轮廓检索模式int method, // 轮廓点近似方法Point offset = Point() // 可选的偏移量
);
比较基础的APi主要检测图像的外轮廓,后面会更新增加内部轮廓,如孔洞
下面是遍历整个关键点存储容器以此取出点位等价
for (size_t i = 0; i < contours.size(); ++i) {const std::vector<cv::Point>& contour = contours[i];// ...
}
这里有一个知识点就是contour到底是什么,里面包含了什么?
在 OpenCV 中,contour
是一个存储轮廓点集的数据结构,本质是 std::vector<cv::Point>
(或 std::vector<cv::Point2f>
),表示由一系列连续点构成的曲线。
// 示例:contour 的类型定义
using Contour = std::vector<cv::Point>; // 二维点集
struct Point {int x; // x坐标int y; // y坐标
};
- 存储形式:
contour
是有序的点序列,相邻点之间用直线连接,形成闭合或开放的曲线。
例如,一个矩形轮廓可能存储为四个顶点的坐标:[(x1,y1), (x2,y2), (x3,y3), (x4,y4)]
根据这个特性我们就可以求一些关键信息,例如周长面积矩,重心等信息
double perimeter = cv::arcLength(contour, true); // 周长
double area = cv::contourArea(contour); // 面积
cv::Moments moments = cv::moments(contour); // 矩(用于计算重心等)
cv::Point2f centroid(moments.m10/moments.m00, moments.m01/moments.m00); // 重心
1.形状分析
bool isConvex = cv::isContourConvex(contour); // 是否为凸多边形
std::vector<cv::Point> approx;
cv::approxPolyDP(contour, approx, epsilon, true); // 多边形逼近
double circularity = 4 * CV_PI * area / (perimeter * perimeter); // 圆形度
2.边界框计算
cv::Rect boundingRect = cv::boundingRect(contour); // 直立外接矩形
cv::RotatedRect minRect = cv::minAreaRect(contour); // 最小外接矩形(带旋转)
cv::Point2f center; float radius;
cv::minEnclosingCircle(contour, center, radius); // 最小外接圆
3.绘制与可视化
// 绘制轮廓
cv::drawContours(image, std::vector<std::vector<cv::Point>>{contour}, -1, color, thickness);// 绘制关键点
for (const auto& point : contour) {cv::circle(image, point, 2, cv::Scalar(0, 255, 0), -1);
}
我们先通过这个contour来获得我们需要的信息
之后通过筛选来选择我们要的图像轮廓
这里有一个坑要>=不能是== ==
要求参数精确等于某个值,在现实中几乎不可能满足;而 >=
和 <=
允许参数在合理范围内波动,更符合实际情况。
之后实时绘制轮廓,上面的筛选轮廓用于后续需要点位信息的时候调用
轮廓信息有了我们开始记录信息便于轮廓可视化处理,我们需要建立一个结构体用来存储信息
2.1几个关键代码解释:
1. 计算并存储轮廓质心
cv::Moments m = cv::moments(contour);
if (m.m00 != 0) {info.center = cv::Point2f(m.m10 / m.m00, m.m01 / m.m00);
}
cv::moments(contour)
:计算轮廓的几何矩,包括面积矩m00
(即轮廓面积)、一阶矩m10
和m01
。- 质心公式:
- 质心横坐标 =
m10 / m00
- 质心纵坐标 =
m01 / m00
- 质心横坐标 =
- 条件检查:当轮廓面积
m.m00
为 0(如空轮廓或单点)时,跳过赋值以避免除零错误。此时info.center
的值保持未初始化(潜在风险!)。
2. 计算并存储轮廓凸度
info.convexity = cv::isContourConvex(contour) ? 1.0 : 0.0;
cv::isContourConvex(contour)
:判断轮廓是否为凸多边形(所有内角 ≤ 180°)。- 凸度值:
1.0
:轮廓是凸的。0.0
:轮廓是非凸的(有凹陷)。
3. 计算并存储最小外接圆
cv::minEnclosingCircle(contour, info.enclosingCircleCenter, info.enclosingCircleRadius);
cv::minEnclosingCircle()
:计算完全包含轮廓的最小圆。- 输出参数:
info.enclosingCircleCenter
:圆心坐标(cv::Point2f
)。info.enclosingCircleRadius
:圆半径(float
)。
4. 计算并存储最小外接矩形的角度
cv::RotatedRect minRect = cv::minAreaRect(contour);
info.angle = minRect.angle;
cv::minAreaRect(contour)
:计算完全包含轮廓的最小旋转矩形(可能倾斜)。minRect.angle
:返回矩形的旋转角度(单位:度),范围为[-90, 0)
。角度定义为水平轴与矩形短边的夹角。
5. 将结构体添加到列表
contourInfoList.push_back(info);
- 将当前轮廓的所有特征信息存入容器
contourInfoList
轮廓分析的核心代码可以用与下面的两种方式。1:当我点击轮廓信息的时候将信息以弹窗的方式体现出来。2:将这些信息以绘图的方式画出来。
三.弹窗方面
构建ContourInfoDialog这个构造函数
- 这个类借助构造函数接收一个轮廓信息列表和一个可选的父窗口部件。
- 父窗口部件的初始化工作由 Qt 框架处理,通常是通过调用基类的构造函数来实现。
- 析构函数会确保在对象被销毁时,所有资源都能被正确释放。
类的内部实现
1.构建窗口的格式
2.窗口内表格构建
void ContourInfoDialog::initUI(const std::vector<ContourInfo>& contourInfoList) {QVBoxLayout* mainLayout = new QVBoxLayout(this);// 创建表格m_infoTable = new QTableWidget(static_cast<int>(contourInfoList.size()), 10, this);m_infoTable->setHorizontalHeaderLabels({"ID", "位置(X,Y)", "尺寸(W,H)", "周长", "面积","圆度", "凸度", "横纵比", "外接圆(半径)", "角度"});// 设置表头样式m_infoTable->horizontalHeader()->setSectionResizeMode(QHeaderView::Interactive);m_infoTable->setColumnWidth(0, 50); // IDm_infoTable->setColumnWidth(1, 120); // 位置m_infoTable->setColumnWidth(2, 120); // 尺寸m_infoTable->setColumnWidth(3, 80); // 周长m_infoTable->setColumnWidth(4, 80); // 面积m_infoTable->setColumnWidth(5, 60); // 圆度m_infoTable->setColumnWidth(6, 60); // 凸度m_infoTable->setColumnWidth(7, 60); // 横纵比m_infoTable->setColumnWidth(8, 100); // 外接圆m_infoTable->setColumnWidth(9, 60); // 角度// 填充数据for (size_t i = 0; i < contourInfoList.size(); ++i) {const auto& info = contourInfoList[i];m_infoTable->setItem(static_cast<int>(i), 0, new QTableWidgetItem(QString::number(i + 1)));m_infoTable->setItem(static_cast<int>(i), 1, new QTableWidgetItem(QString("(%1, %2)").arg(info.center.x).arg(info.center.y)));m_infoTable->setItem(static_cast<int>(i), 2, new QTableWidgetItem(QString("%1 x %2").arg(info.boundingRect.width).arg(info.boundingRect.height)));m_infoTable->setItem(static_cast<int>(i), 3, new QTableWidgetItem(QString::number(info.perimeter, 'f', 1)));m_infoTable->setItem(static_cast<int>(i), 4, new QTableWidgetItem(QString::number(info.area, 'f', 1)));// 计算圆度 = 4π*面积/周长²double circularity = 4 * CV_PI * info.area / (info.perimeter * info.perimeter);m_infoTable->setItem(static_cast<int>(i), 5, new QTableWidgetItem(QString::number(circularity, 'f', 3)));m_infoTable->setItem(static_cast<int>(i), 6, new QTableWidgetItem(info.convexity > 0.5 ? "是" : "否"));m_infoTable->setItem(static_cast<int>(i), 7, new QTableWidgetItem(QString::number(info.aspectRatio, 'f', 2)));m_infoTable->setItem(static_cast<int>(i), 8, new QTableWidgetItem(QString("R:%1").arg(info.enclosingCircleRadius, 0, 'f', 1)));m_infoTable->setItem(static_cast<int>(i), 9, new QTableWidgetItem(QString::number(info.angle, 'f', 1) + "°"));}mainLayout->addWidget(m_infoTable);// 添加关闭按钮QDialogButtonBox* buttonBox = new QDialogButtonBox(QDialogButtonBox::Close, this);connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject);mainLayout->addWidget(buttonBox);
}
这样我们就能完成想要的寻找轮廓的操作