本人正在学习 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 总结

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

Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐