一、图像细化
1、图像细化原理
**作用:**图像细化是将图像的线条从多像素宽度减少到单位像素宽度的过程,又称为"骨架化"。
细化过程:

细化判断依据:
- 内部点不能删除:防止目标整体消失
- 孤立点不能删除:防止噪声断裂
- 端点不能删除:防止线条越来越短
- 删除后保持连通:防止目标断开
ZS细化流程:
1、输入二值图像:首先对图像进行二值化处理,然后建立目标点 P1 的8邻域(如上图所示)
2、第一次迭代:遍历图像中的所有前景像素点,对每个像素点 P1 进行如下判断:
(1)判断是否为边界点
计算P2+P3+P4+P5+P6+P7+P8+P9。若满足:2 ≤ 和 ≤ 6,则说明:不是孤立点、不是内部点、属于边界点,才允许继续判断。
(2)判断连通性
计算S(P1),即统计8邻域顺时针方向:0 → 1变化的次数。若S(P1)=1,说明删除 P1 后图像仍保持连通。
(3)判断结构保持条件
满足:P2 × P4 × P6 = 0 和 P4 × P6 × P8 = 0,说明不会破坏骨架结构。
(4)删除像素点
将P1置为0,即删除该边界像素。
3、第二次迭代:再次遍历图像中的所有前景像素。
满足:条件(1)、条件(2)并且满足:P2 × P4 × P8 = 0 和 P2 × P6 × P8 = 0,则删除该像素点。
4、重复迭代:不断重复,第一次迭代→ 第二次迭代→ 继续循环,直到图像中没有新的像素被删除。此时说明:骨架已经稳定,图像细化完成。
5、输出结果
2、相关函数
cpp
/* 用途:用于对二值图像进行骨架化(细化)处理,
将较宽的目标区域逐步收缩为单像素宽度的中心骨架线,
同时尽可能保持原有目标的拓扑结构和连通性不变 */
void cv::ximgproc::thinning(InputArray src, OutputArray dst,
int thinningType = THINNING_ZHANGSUEN);
/*
src:输入待细化图像,必须是CV_8U单通道图像(前景为255,背景为0)
dst:细化后的输出图像,与src具有相同尺寸和数据类型
thinningType:细化算法类型,
THINNING_ZHANGSUEN 表示 Zhang-Suen 细化算法
THINNING_GUOHALL 表示 Guo-Hall 细化算法
*/
3、示例代码
cpp
QString imgPath = QApplication::applicationDirPath() + "/Images";
cv::String s_imgPath = imgPath.toLocal8Bit().data();
Mat img = imread(s_imgPath + "/LearnCV_black.png", IMREAD_GRAYSCALE);
if (img.empty())
{
qDebug() << "图片加载失败, 请确认图像文件名称是否正确";
return;
}
/*英文字+实心圆和圆环细化*/
Mat words = Mat::zeros(100, 200, CV_8UC1);/*创建一个圆形的背景图片*/
putText(words, "Learn", Point(30, 30), 2, 1, Scalar(255), 2);/*添加英文*/
putText(words, "OpenCV 4", Point(30, 60), 2, 1, Scalar(255), 2);
circle(words, Point(80, 75), 10, Scalar(255), -1);/*添加实心圆*/
circle(words, Point(130, 75), 10, Scalar(255), 3);/*添加圆环*/
/*进行细化*/
Mat thin1, thin2;
ximgproc::thinning(img,thin1,0);
ximgproc::thinning(words,thin2,0);
imshow("img", img);
imshow("thin1", thin1);
namedWindow("words", WINDOW_NORMAL);
imshow("words", words);
namedWindow("thin2", WINDOW_NORMAL);
imshow("thin2", thin2);
waitKey(0);
destroyAllWindows();
二、轮廓检测
1、轮廓概念介绍
轮廓是图像中连续的、具有相同颜色或灰度值的像素点所组成的曲线,用于描述目标物体的边界形状。通常情况下,轮廓检测是在二值图像上进行的,即图像中的像素值只有:0:背景,255:前景目标。
常用4个参数来描述不同层级之间的结构关系,分别是:[同层下一个轮廓索引, 同层上一个轮廓索引, 下一层第一个子轮廓索引, 上层父轮廓索引]。
| 对比项 | 边缘(Edge) | 轮廓(Contour) |
| 定义 | 图像中灰度值发生突变的位置 | 由连续点组成的目标边界曲线 |
| 本质 | 像素级变化 | 区域级边界 |
| 表现形式 | 离散的像素点 | 连续、有序的点集 |
| 是否连续 | 不一定连续 | 通常是连续闭合的 |
| 产生原因 | 灰度、颜色突变 | 目标区域与背景分离 |
| 输入图像 | 灰度图即可 | 通常基于二值图 |
| 输出结果 | 边缘点集合 | 边界曲线 |
| 常用算法 | Sobel、Canny、Laplacian | findContours() |
| 是否具有方向顺序 | 一般没有 | 有顺序,可形成曲线 |
| 应用场景 | 边缘检测、特征提取 | 形状分析、目标检测、面积计算 |
|---|---|---|
| [轮廓与边缘的区别] |
2、轮廓检测与绘制
2.1.1 相关函数
- 轮廓检测
cpp
/* 用途:用于从二值图像中检测目标轮廓,
提取图像中物体边界的连续点集信息。
该函数不仅能够获取目标外轮廓,还可以分析轮廓之间的层级关系 */
void cv::findContours( InputArray image, OutputArrayOfArrays contours,
OutputArray hierarchy, int mode,
int method, Point offset = Point());
/*
image:输入图像,数据类型为CV_8U的单通道灰度图像或者二值化图像
contours:检测到的轮廓,每个轮廓中存放着像素的坐标
hierarchy:轮廓层级关系信息
mode:轮廓检测模式标志
method:轮廓逼近方法标志
offset:每个轮廓点移动的可选偏移量,这个函数主要用在从ROI图像中找出的轮廓
并基于整个图像分析轮廓的场景中
*/
- 轮廓绘制
cpp
/* 用途:用于将检测得到的轮廓绘制到图像中,
可以绘制单个轮廓、多个轮廓以及层级轮廓结构 */
void cv::drawContours( InputOutputArray image, InputArrayOfArrays contours,
int contourIdx, const Scalar& color,
int thickness = 1, int lineType = LINE_8,
InputArray hierarchy = noArray(),
int maxLevel = INT_MAX, Point offset = Point() );
/*
image:绘制轮廓的目标图像
contours:轮廓集合,通常由findContours()获取
contourIdx:需要绘制的轮廓索引,
大于等于0表示绘制指定轮廓,
小于0表示绘制所有轮廓
color:轮廓颜色(如 Scalar(B,G,R))
thickness:轮廓线宽,负值表示填充轮廓内部
lineType:轮廓边界线类型
hierarchy:轮廓层级关系信息,通常由findContours()返回
maxLevel:绘制轮廓层级的最大深度
offset:轮廓整体坐标偏移量
*/
2.1.1 示例代码
cpp
QString imgPath = QApplication::applicationDirPath() + "/Images";
cv::String s_imgPath = imgPath.toLocal8Bit().data();
Mat img = imread(s_imgPath + "/keys.png");
if (img.empty())
{
qDebug() << "图片加载失败, 请确认图像文件名称是否正确";
return;
}
imshow("Original keys", img);
Mat gray, binary;
cvtColor(img, gray, COLOR_BGR2GRAY);/*转化成灰度图*/
GaussianBlur(gray, gray, Size(13, 13), 4, 4);/*平滑滤波*/
threshold(gray, binary, 170, 255, THRESH_BINARY | THRESH_OTSU);/*自适应二值化*/
vector<vector<Point>> contours;/*轮廓*/
vector<Vec4i> hierarchy;/*存放轮廓结构变量*/
findContours(binary, contours, hierarchy, RETR_TREE, CHAIN_APPROX_SIMPLE, Point());
/*绘制轮廓*/
for (int i = 0; i < hierarchy.size(); i++)
{
cout << hierarchy[i] << endl;
}
for (int t = 0; t < contours.size(); t++)
{
drawContours(img, contours, t, Scalar(0, 0, 255), 2, 8);
imshow("Contour detection result",img);
waitKey(0);
}
destroyAllWindows();
三、轮廓信息统计
1、轮廓面积
cpp
/* 用途:用于计算轮廓所包围区域的面积大小 */
double cv::contourArea( InputArray contour, bool oriented = false );
/*
contour:轮廓的像素点
oriented:区域面积是否具有方向的标志,true表示面积具有方向性,false表示不具有方向性,
默认值为不具有方向性的false
*/
2、轮廓长度
cpp
/* 用途:用于计算轮廓或曲线的周长(弧长),
即所有相邻像素点之间距离的总和 */
double cv::arcLength( InputArray curve, bool closed );
/*
curve:轮廓或者曲线的2D像素点
closed:轮廓或者曲线是否闭合标志,true表示闭合
*/
3、示例代码
cpp
vector<Point> contour;
contour.push_back(Point2f(0, 0));
contour.push_back(Point2f(10, 0));
contour.push_back(Point2f(10, 10));
contour.push_back(Point2f(5, 5));
double area = contourArea(contour);
cout << "area = " << area << endl;
double length0 = arcLength(contour, true);
double length1 = arcLength(contour, false);
cout << "length0 = " << length0 << endl;
cout << "length1 = " << length1 << endl;
QString imgPath = QApplication::applicationDirPath() + "/Images";
cv::String s_imgPath = imgPath.toLocal8Bit().data();
Mat img = imread(s_imgPath + "/keys.png");
if (img.empty())
{
qDebug() << "图片加载失败, 请确认图像文件名称是否正确";
return;
}
Mat gray, binary;
cvtColor(img, gray, COLOR_BGR2GRAY);/*转化成灰度图*/
GaussianBlur(gray, gray, Size(13, 13), 4, 4);/*平滑滤波*/
threshold(gray, binary, 170, 255, THRESH_BINARY | THRESH_OTSU);/*自适应二值化*/
vector<vector<Point>> contours;/*轮廓*/
vector<Vec4i> hierarchy;/*存放轮廓结构变量*/
findContours(binary, contours, hierarchy, RETR_TREE, CHAIN_APPROX_SIMPLE, Point());
/*输出轮廓面积*/
for (int t = 0; t < contours.size(); t++)
{
double area1 = contourArea(contours[t]);
cout << t << " area: " << area1 << endl;
}
/*输出轮廓长度*/
for (int t = 0; t < contours.size(); t++)
{
double length2 = arcLength(contours[t],true);
cout << t << " length: " << length2 << endl;
}
四、轮廓外接多边形
1、轮廓外接最大矩形
cpp
/* 用途:用于计算能够完整包围目标区域的最小正矩形(水平矩形),
返回一个与图像坐标轴平行的矩形区域 */
Rect cv::boundingRect( InputArray array );
/*
array:输入的二维点集,可以是轮廓点、非零像素点集合等,数据类型为vector<Point>或者Mat
*/
2、轮廓外接最小矩形
cpp
/* 用途:用于计算能够包围目标区域的最小面积旋转矩形,
与boundingRect()不同,该矩形允许旋转角度,
因此更加贴合目标真实方向 */
RotatedRect cv::minAreaRect( InputArray points );
/*
points:输入二维点集,通常为轮廓点集合
*/
3、轮廓外接多边形
cpp
/* 用途:用于对轮廓或曲线进行多边形逼近,
在尽量保持原始形状的前提下,
减少轮廓中的点数量 */
void cv::approxPolyDP( InputArray curve,
OutputArray approxCurve,
double epsilon, bool closed );
/*
curve:输入曲线或轮廓点集
approxCurve:多边形逼近后的输出点集
epsilon:逼近精度,
值越小越接近原轮廓,
值越大点数越少
closed:曲线是否闭合
*/
4、示例代码
cpp
void drawapp(Mat result, Mat img2)
{
for (int i = 0; i < result.rows; i++)
{
/*最后一个坐标点与第一个坐标点连接*/
if (i == result.rows - 1)
{
Vec2i point1 = result.at<Vec2i>(i);
Vec2i point2 = result.at<Vec2i>(0);
line(img2, point1, point2, Scalar(0, 255, 0), 2, 8, 0);
break;
}
Vec2i point1 = result.at<Vec2i>(i);
Vec2i point2 = result.at<Vec2i>(i + 1);
line(img2, point1, point2, Scalar(0, 255, 0), 2, 8, 0);
}
}
/*************************************************************************/
QString imgPath = QApplication::applicationDirPath() + "/Images";
cv::String s_imgPath = imgPath.toLocal8Bit().data();
Mat img = imread(s_imgPath + "/stuff.jpg");
if (img.empty())
{
qDebug() << "图片加载失败, 请确认图像文件名称是否正确";
return;
}
Mat img1, img2;
img.copyTo(img1);/*深拷贝用来绘制最大外接矩形*/
img.copyTo(img2);/*深拷贝用来绘制最小外接矩形*/
imshow("img", img);
/*去噪声与二值化*/
Mat canny;
Canny(img, canny, 80, 160, 3, false);
imshow("canny", canny);
/*膨胀运算,将细小缝隙填补上*/
Mat kernel = getStructuringElement(0, Size(3, 3));
dilate(canny, canny, kernel);
/*轮廓发现与绘制*/
vector<vector<Point>> contours;
vector<Vec4i> hierarchy;
findContours(canny, contours, hierarchy, RETR_EXTERNAL, CHAIN_APPROX_SIMPLE, Point());
/*寻找轮廓的外接矩形*/
for (int n = 0; n < contours.size(); n++)
{
/*最大外接矩形*/
Rect rect = boundingRect(contours[n]);
rectangle(img1, rect, Scalar(0, 0, 255), 2, 8);
/*最小外接矩形*/
RotatedRect rrect = minAreaRect(contours[n]);
Point2f points[4];
rrect.points(points);/*读取最小外接矩形的四个顶点*/
Point2f cpt = rrect.center;/*最小外接矩形的中心*/
/*绘制旋转矩形与中心位置*/
for (int i = 0; i < 4; i++)
{
if (i == 3)
{
line(img2, points[i], points[0], Scalar(0, 255, 0), 2, 8, 0);
break;
}
line(img2, points[i], points[i + 1], Scalar(0, 255, 0), 2, 8, 0);
}
/*绘制矩形的中心*/
circle(img2, cpt, 4, Scalar(255, 0, 0), -1, 8, 0);
}
imshow("Max", img1);
imshow("Min", img2);
Mat approx = imread(s_imgPath + "/approx.png");
if (approx.empty())
{
qDebug() << "图片加载失败, 请确认图像文件名称是否正确";
return;
}
/*边缘检测*/
Mat canny2;
Canny(approx, canny2, 80, 160, 3, false);
/*膨胀运算*/
Mat kernel2 = getStructuringElement(0, Size(3, 3));
dilate(canny2, canny2, kernel2);
/*轮廓发现与绘制*/
vector<vector<Point>> contours2;
vector<Vec4i> hierarchy2;
findContours(canny2, contours2, hierarchy2, RETR_EXTERNAL, CHAIN_APPROX_SIMPLE, Point());
/*绘制多边形*/
for (int t = 0; t < contours2.size(); t++)
{
/*用最小外接矩形求取轮廓中心*/
RotatedRect rrect = minAreaRect(contours2[t]);
Point2f center = rrect.center;
/*绘制矩形的中心*/
circle(approx, center, 4, Scalar(255, 0, 0), -1, 8, 0);
Mat result;
approxPolyDP(contours2[t], result, 4, true);/*多边形拟合*/
drawapp(result, approx);
cout << "corners: " << result.rows << endl;
/*判断形状和绘制轮廓*/
if (result.rows == 3)
{
putText(approx, "triangle", center, 0, 1, Scalar(0, 255, 0), 1, 8);
}
if (result.rows == 4)
{
putText(approx, "rectangle", center, 0, 1, Scalar(0, 255, 0), 1, 8);
}
if (result.rows == 6)
{
putText(approx, "poly-6", center, 0, 1, Scalar(0, 255, 0), 1, 8);
}
if (result.rows > 12)
{
putText(approx, "circle", center, 0, 1, Scalar(0, 255, 0), 1, 8);
}
}
imshow("result", approx);
waitKey(0);
destroyAllWindows();