OpenCV4 快速入门 学习笔记分享【第六章】
本人正在学习 OpenCV 计算机视觉相关知识,此篇为个人学习笔记,参考书籍为冯振等人的《OpenCV4 快速入门》,主要记录 OpenCV 在 Linux 环境下的编译、运行实操流程与相关知识点总结。
笔记内容均为亲自实操验证,过程中踩过不少编译配置、运行参数的坑,整理出来一方面用于自己复盘巩固,另一方面也希望能帮助到刚入门 OpenCV、在编译运行示例程序时遇到问题的小伙伴。文中内容如有疏漏或错误之处,欢迎各位大佬评论区指正交流,共同进步~
第六章 图像形态学操作
6.1 像素距离与连通域
6.1.1 图像像素距离变换 distanceTransform()函数
- 欧氏距离:指两个像素点之间的直线距离
|
2.8 |
2.2 |
2 |
2.2 |
2.8 |
|
2.2 |
1.4 |
1 |
1.4 |
2.2 |
|
2 |
1 |
0 |
1 |
2 |
|
2.2 |
1.4 |
1 |
1.4 |
2.2 |
|
2.8 |
2.2 |
2 |
2.2 |
2.8 |
- 街区距离:一个像素点到另一个像素点只能沿着轴走
|
4 |
3 |
2 |
3 |
4 |
|
3 |
2 |
1 |
2 |
3 |
|
2 |
1 |
0 |
1 |
2 |
|
3 |
2 |
1 |
2 |
3 |
|
4 |
3 |
2 |
3 |
4 |
- 棋盘距离:两个像素点移动到同一行或同一列时需要的最大距离
|
2 |
2 |
2 |
2 |
2 |
|
2 |
1 |
1 |
1 |
2 |
|
2 |
1 |
0 |
1 |
2 |
|
2 |
1 |
1 |
1 |
2 |
|
2 |
2 |
2 |
2 |
2 |
void cv::distanceTransform(输入图像,输出图像,二维的标签数组,
选择两个像素之间距离方法的标志,距离变换掩码矩阵尺寸,
要构建的标签数组类型)

#include <opencv2/opencv.hpp>
#include <iostream>
using namespace cv;
using namespace std;
int main()
{
//构建建议矩阵,用于求取像素之间的距离
Mat a = (Mat_<uchar>(5, 5) << 1, 1, 1, 1, 1,
1, 1, 1, 1, 1,
1, 1, 0, 1, 1,
1, 1, 1, 1, 1,
1, 1, 1, 1, 1);
Mat dist_L1, dist_L2, dist_C, dist_L12;
//计算街区距离
distanceTransform(a, dist_L1, 1, 3, CV_8U);
cout << "街区距离:" << endl << dist_L1 << endl;
//计算欧式距离
distanceTransform(a, dist_L2, 2, 5, CV_8U);
cout << "欧式距离:" << endl << dist_L2 << endl;
//计算棋盘距离
distanceTransform(a, dist_C, 3, 5, CV_8U);
cout << "棋盘距离:" << endl << dist_C << endl;
//对图像进行距离变换
Mat rice = imread("rice.png", IMREAD_GRAYSCALE);
if (rice.empty())
{
cout << "请确认图像文件名称是否正确" << endl;
return -1;
}
Mat riceBW, riceBW_INV;
//将图像转成二值图像,同时把黑白区域颜色呼唤
threshold(rice, riceBW, 50, 255, THRESH_BINARY);
threshold(rice, riceBW_INV, 50, 255, THRESH_BINARY_INV);
//距离变换
Mat dist, dist_INV;
distanceTransform(riceBW, dist, 1, 3, CV_32F); //为了显示清晰,将数据类型变成CV_32F
distanceTransform(riceBW_INV, dist_INV, 1, 3, CV_8U);
//显示变换结果
imshow("riceBW", riceBW);
imshow("dist", dist);
imshow("riceBW_INV", riceBW_INV);
imshow("dist_INV", dist_INV);
waitKey(0);
return 0;
}
6.1.2 图像连通域分析 connectedComponents()函数
定义:图像具有相同像素值并位置相邻的区域,常分析的是二值化后的图像
4-邻域 8-邻域
- 两遍扫描法
第一次给每个非零像素赋予标签,当上方和左侧都有标签时,去两者小的值作为当前像素的标签
- 种子填充法
将所有非零像素放到一个集合,在集合随机选取一个座位种子像素,根据邻域关系扩充种子像素的连通域
- connectedComponents()函数
int cv::connectedComponents(
InputArray image, // 输入图像:单通道、8位二值图(0=背景,非0=前景)
OutputArray labels, // 输出标签矩阵:和输入图像同尺寸,CV_32S类型(整型)
int connectivity = 8, // 连通性:4(4邻域)或8(8邻域),默认8
int ltype = CV_32S // 标签矩阵的数据类型:只能是CV_32S(默认)
);
#include <opencv2\opencv.hpp>
#include <iostream>
#include <vector>
using namespace cv;
using namespace std;
int main()
{
//对图像进行距离变换
Mat img = imread("rice.png");
if (img.empty())
{
cout << "请确认图像文件名称是否正确" << endl;
return -1;
}
Mat rice, riceBW;
//将图像转成二值图像,用于统计连通域
cvtColor(img, rice, COLOR_BGR2GRAY);
threshold(rice, riceBW, 50, 255, THRESH_BINARY);
//生成随机颜色,用于区分不同连通域
RNG rng(10086);
Mat out;
int number = connectedComponents(riceBW, out, 8, CV_16U); //统计图像中连通域的个数
vector<Vec3b> colors;
for (int i = 0; i < number; i++)
{
//使用均匀分布的随机数确定颜色
Vec3b vec3 = Vec3b(rng.uniform(0, 256), rng.uniform(0, 256), rng.uniform(0, 256));
colors.push_back(vec3);
}
//以不同颜色标记出不同的连通域
Mat result = Mat::zeros(rice.size(), img.type());
int w = result.cols;
int h = result.rows;
for (int row = 0; row < h; row++)
{
for (int col = 0; col < w; col++)
{
int label = out.at<uint16_t>(row, col);
if (label == 0) //背景的黑色不改变
{
continue;
}
result.at<Vec3b>(row, col) = colors[label];
}
}
//显示结果
imshow("原图", img);
imshow("标记后的图像", result);
waitKey(0);
return 0;
}
- connectedComponentsWithStats()函数 【获得更多统计信息】
int cv::connectedComponentsWithStats(
InputArray image, // 输入:单通道8位二值图(0=背景,非0=前景)
OutputArray labels, // 输出:标签矩阵(CV_32S类型,和输入同尺寸)
OutputArray stats, // 输出:统计信息矩阵(CV_32S类型)
OutputArray centroids, // 输出:中心坐标矩阵(CV_64F类型)
int connectivity = 8, // 连通性:4(4邻域)/8(8邻域),默认8
int ltype = CV_32S // 标签矩阵类型:只能是CV_32S,默认即可
);
#include <opencv2\opencv.hpp>
#include <iostream>
#include <vector>
using namespace cv;
using namespace std;
int main()
{
system("color F0"); //更改输出界面颜色
//对图像进行距离变换
Mat img = imread("rice.png");
if (img.empty())
{
cout << "请确认图像文件名称是否正确" << endl;
return -1;
}
imshow("原图", img);
Mat rice, riceBW;
//将图像转成二值图像,用于统计连通域
cvtColor(img, rice, COLOR_BGR2GRAY);
threshold(rice, riceBW, 50, 255, THRESH_BINARY);
//生成随机颜色,用于区分不同连通域
RNG rng(10086);
Mat out, stats, centroids;
//统计图像中连通域的个数
int number = connectedComponentsWithStats(riceBW, out, stats, centroids, 8, CV_16U);
vector<Vec3b> colors;// 存储颜色的容器(Vec3b=3个uchar,对应BGR三色)
for (int i = 0; i < number; i++)
{
//使用均匀分布的随机数确定颜色
Vec3b vec3 = Vec3b(rng.uniform(0, 256), rng.uniform(0, 256), rng.uniform(0, 256));
colors.push_back(vec3);// 颜色存入容器
}
//以不同颜色标记出不同的连通域
Mat result = Mat::zeros(rice.size(), img.type());
int w = result.cols;
int h = result.rows;
for (int i = 1; i < number; i++)
{
// 中心位置
int center_x = centroids.at<double>(i, 0);
int center_y = centroids.at<double>(i, 1);
//矩形边框
int x = stats.at<int>(i, CC_STAT_LEFT);
int y = stats.at<int>(i, CC_STAT_TOP);
int w = stats.at<int>(i, CC_STAT_WIDTH);
int h = stats.at<int>(i, CC_STAT_HEIGHT);
int area = stats.at<int>(i, CC_STAT_AREA);
// 中心位置绘制,// 3. 绘图:标中心(绿色实心圆,半径2,线宽2)
circle(img, Point(center_x, center_y), 2, Scalar(0, 255, 0), 2, 8, 0);
// 外接矩形, // 4. 绘图:标外接矩形(随机颜色,线宽1)
Rect rect(x, y, w, h);
rectangle(img, rect, colors[i], 1, 8, 0);
// 5. 绘图:标连通域编号(红色文字,字体大小0.5,线宽1)
putText(img, format("%d", i), Point(center_x, center_y),
FONT_HERSHEY_SIMPLEX, 0.5, Scalar(0, 0, 255), 1);
cout << "number: " << i << ",area: " << area << endl;
}
//显示结果
imshow("标记后的图像", img);
waitKey(0);
return 0;
}
6.2 腐蚀 erode()和膨胀 dilate()
作用:去除图像的噪声、分割出独立的区域、将两个连通域连接在一起
6.2.1 图像腐蚀
结构元素===卷积模板
用结构元素的中心(锚点,默认是矩阵中心)对准图像中的某个前景像素(255),如果结构元素覆盖的所有邻域像素都是前景(255),则保留该中心像素为前景;否则将其置为背景(0)。
- 生成常用矩形结构元素、十字结构元素和椭圆结构元素 getStructuringElement()函数
Mat getStructuringElement(
int shape, // 结构元素的形状(0是矩形,1是十字结构,2是椭圆结构)
Size ksize, // “勺子”的尺寸(比如Size(3,3)是3×3,Size(5,5)是5×5)
Point anchor = Point(-1,-1) // 锚点(勺子的中心,默认-1,-1就是正中心)
);
- erode()函数: 可以去除噪声引起较小的连通域,得到正确的米粒数
void erode(
InputArray src, // 输入图像:必须是单通道二值图(如大米二值图)
OutputArray dst, // 输出图像:腐蚀后的结果(和输入同尺寸、同类型)
InputArray kernel, // 结构元素:就是“勺子”(getStructuringElement生成)
Point anchor = Point(-1,-1), // 锚点:勺子的中心(默认-1,-1=正中心,不用改)
int iterations = 1, // 腐蚀次数:默认1次,次数越多腐蚀越狠
int borderType = BORDER_CONSTANT, // 像素外推法标志,边界填充方式(默认即可)
const Scalar& borderValue = morphologyDefaultBorderValue() // 边界填充值(默认即可)
);
#include <opencv2\opencv.hpp>
#include <iostream>
#include <vector>
using namespace cv;
using namespace std;
//绘制包含区域函数
void drawState(Mat &img, int number, Mat centroids, Mat stats, String str) {
RNG rng(10086);
vector<Vec3b> colors;
for (int i = 0; i < number; i++)
{
//使用均匀分布的随机数确定颜色
Vec3b vec3 = Vec3b(rng.uniform(0, 256), rng.uniform(0, 256), rng.uniform(0, 256));
colors.push_back(vec3);
}
for (int i = 1; i < number; i++)
{
// 中心位置
int center_x = centroids.at<double>(i, 0);
int center_y = centroids.at<double>(i, 1);
//矩形边框
int x = stats.at<int>(i, CC_STAT_LEFT);
int y = stats.at<int>(i, CC_STAT_TOP);
int w = stats.at<int>(i, CC_STAT_WIDTH);
int h = stats.at<int>(i, CC_STAT_HEIGHT);
// 中心位置绘制
circle(img, Point(center_x, center_y), 2, Scalar(0, 255, 0), 2, 8, 0);
// 外接矩形
Rect rect(x, y, w, h);
rectangle(img, rect, colors[i], 1, 8, 0);
putText(img, format("%d", i), Point(center_x, center_y),
FONT_HERSHEY_SIMPLEX, 0.5, Scalar(0, 0, 255), 1);
}
imshow(str, img);
}
int main()
{
//生成用于腐蚀的原图像
Mat src = (Mat_<uchar>(6, 6) << 0, 0, 0, 0, 255, 0,
0, 255, 255, 255, 255, 255,
0, 255, 255, 255, 255, 0,
0, 255, 255, 255, 255, 0,
0, 255, 255, 255, 255, 0,
0, 0, 0, 0, 0, 0);
Mat struct1, struct2;
struct1 = getStructuringElement(0, Size(3, 3)); //矩形结构元素
struct2 = getStructuringElement(1, Size(3, 3)); //十字结构元素
Mat erodeSrc; //存放腐蚀后的图像
erode(src, erodeSrc, struct2);
namedWindow("src", WINDOW_GUI_NORMAL);
namedWindow("erodeSrc", WINDOW_GUI_NORMAL);
imshow("src", src);
imshow("erodeSrc", erodeSrc);
Mat LearnCV_black = imread("LearnCV_black.png", IMREAD_ANYCOLOR);
Mat LearnCV_write = imread("LearnCV_write.png", IMREAD_ANYCOLOR);
Mat erode_black1, erode_black2, erode_write1, erode_write2;
//黑背景图像腐蚀
erode(LearnCV_black, erode_black1, struct1);
erode(LearnCV_black, erode_black2, struct2);
imshow("LearnCV_black", LearnCV_black);
imshow("erode_black1", erode_black1);
imshow("erode_black2", erode_black2);
//白背景腐蚀
erode(LearnCV_write, erode_write1, struct1);
erode(LearnCV_write, erode_write2, struct2);
imshow("LearnCV_write", LearnCV_write);
imshow("erode_write1", erode_write1);
imshow("erode_write2", erode_write2);
//验证腐蚀对小连通域的去除
Mat img = imread("rice.png");
if (img.empty())
{
cout << "请确认图像文件名称是否正确" << endl;
return -1;
}
Mat img2;
copyTo(img, img2, img); //克隆一个单独的图像,用于后期图像绘制
Mat rice, riceBW;
//将图像转成二值图像,用于统计连通域
cvtColor(img, rice, COLOR_BGR2GRAY);
threshold(rice, riceBW, 50, 255, THRESH_BINARY);
Mat out, stats, centroids;
//统计图像中连通域的个数
int number = connectedComponentsWithStats(riceBW, out, stats, centroids, 8, CV_16U);
drawState(img, number, centroids, stats, "未腐蚀时统计连通域"); //绘制图像
erode(riceBW, riceBW, struct1); //对图像进行腐蚀
number = connectedComponentsWithStats(riceBW, out, stats, centroids, 8, CV_16U);
drawState(img2, number, centroids, stats, "腐蚀后统计连通域"); //绘制图像
waitKey(0);
return 0;
}
6.2.2 图像膨胀 dilate()函数
与图像腐蚀相反,代码原型与腐蚀相同
#include <opencv2\opencv.hpp>
#include <iostream>
#include <vector>
using namespace cv;
using namespace std;
int main()
{
//生成用于腐蚀的原图像
Mat src = (Mat_<uchar>(6, 6) << 0, 0, 0, 0, 255, 0,
0, 255, 255, 255, 255, 255,
0, 255, 255, 255, 255, 0,
0, 255, 255, 255, 255, 0,
0, 255, 255, 255, 255, 0,
0, 0, 0, 0, 0, 0);
Mat struct1, struct2;
struct1 = getStructuringElement(0, Size(3, 3)); //矩形结构元素
struct2 = getStructuringElement(1, Size(3, 3)); //十字结构元素
Mat erodeSrc; //存放膨胀后的图像
dilate(src, erodeSrc, struct2);
namedWindow("src", WINDOW_GUI_NORMAL);
namedWindow("dilateSrc", WINDOW_GUI_NORMAL);
imshow("src", src);
imshow("dilateSrc", erodeSrc);
Mat LearnCV_black = imread("LearnCV_black.png", IMREAD_ANYCOLOR);
Mat LearnCV_write = imread("LearnCV_write.png", IMREAD_ANYCOLOR);
if (LearnCV_black.empty() || LearnCV_write.empty())
{
cout << "请确认图像文件名称是否正确" << endl;
return -1;
}
Mat dilate_black1, dilate_black2, dilate_write1, dilate_write2;
//黑背景图像膨胀
dilate(LearnCV_black, dilate_black1, struct1);
dilate(LearnCV_black, dilate_black2, struct2);
imshow("LearnCV_black", LearnCV_black);
imshow("dilate_black1", dilate_black1);
imshow("dilate_black2", dilate_black2);
//白背景图像膨胀
dilate(LearnCV_write, dilate_write1, struct1);
dilate(LearnCV_write, dilate_write2, struct2);
imshow("LearnCV_write", LearnCV_write);
imshow("dilate_write1", dilate_write1);
imshow("dilate_write2", dilate_write2);
//比较膨胀和腐蚀的结果
Mat erode_black1, resultXor, resultAnd;
erode(LearnCV_black, erode_black1, struct1);
bitwise_xor(erode_black1, dilate_write1, resultXor);
bitwise_and(erode_black1, dilate_write1, resultAnd);
imshow("resultXor", resultXor);
imshow("resultAnd", resultAnd);
waitKey(0);
return 0;
}
6.3 形态学应用
作用:避免腐蚀和膨胀主要区域的形状变化
6.3.1 开运算
定义:先对图像进行腐蚀,消除图像中的噪声和较小的连通域,
再通过膨胀弥补较大连通域因腐蚀减小的问题
morphologyEx()函数:实现图像开运算、闭运算、形态学梯度、顶帽运算、黑帽运算、击中不击中变换
void cv::morphologyEx(
InputArray src, // 输入图像
OutputArray dst, // 输出图像
int op, // 形态学操作类型
InputArray kernel, // 结构元素(卷积核)
Point anchor = Point(-1,-1), // 核锚点(默认中心)
int iterations = 1, // 操作执行次数
int borderType = BORDER_CONSTANT, // 边界填充类型
const cv::Scalar& borderValue =morphologyDefaultBorderValue() // 边界填充值
)

6.3.2 闭运算
先膨胀后腐蚀
6.3.3 形态学梯度
分类:基本梯度(膨胀后和腐蚀后的差值图像)、
内部梯度(原图像和腐蚀后的差值图像)、
外部梯度(膨胀后和原图像的差值图像)
6.3.4 顶帽运算
原图像和开运算的差值,用来分离比邻近点亮一些的斑块
6.3.5 黑帽运算
原图像和顶帽运算的差值,用来分离比邻近点暗一些的斑块
先闭运算,再减去原图像
6.3.6 击中不击中变换
要求原图像中需要存在于结构元素一模一样的结构
在使用矩形结构元素时,此方法效果和图像腐蚀结果相同
6.3.7 图像细化
定义:将图像的线条从多像素宽度减少到单位像素宽度的过程
常用在文字识别中,可以有效细化文字,增加文字的辨识度,降低图像存储难度
- 迭代细化算法和非迭代细化算法
非迭代算法不以像素为基础,经过一次遍历,产生线条的某一中值或中心线,但容易产生噪声点的缺陷
迭代细化算法(串行+并行):重复删除图像边缘满足一定条件的像素,,,
串行算法:第n次迭代取决于前次的迭代效果
并行算法:zhang细化算法
- 将二值图像细化的thinning()函数
注意:在使用时要通过ximgproc::ximgproc()调用该函数
void cv::ximgproc::thinning(输入图像,输出图像,
细化算法选择标志(THINNING_ZHANGSUEN(简记0,默认)和THINNING_GUOHALL(简记1)))
示例:
#include <opencv2\opencv.hpp>
#include <opencv2\ximgproc.hpp> //细化函数thining所在的头文件
#include <iostream>
using namespace cv;
using namespace std;
int main()
{
//中文字进行细化
Mat img = imread("LearnCV_black.png", IMREAD_ANYCOLOR);
if (img.empty())
{
cout << "请确认图像文件名称是否正确" << endl;
return -1;
}
//英文字+实心圆和圆环细化
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("thin1", thin1);
imshow("img", img);
namedWindow("thin2", WINDOW_NORMAL);
imshow("thin2", thin2);
namedWindow("words", WINDOW_NORMAL);
imshow("words", words);
waitKey(0);
return 0;
}
6.4 总结

至此,第六章就结束了,望各位读者“一键三连”,再次感谢读到这里的各位读者。有任何问题欢迎评论区留言或者私信。
更多推荐
所有评论(0)