《数字图像处理》第十章 图像分割 学习笔记附部分例子代码(C++ & opencv)

图像分割

    • [0. 前言](#0. 前言)
    • [1. 基础知识](#1. 基础知识)
    • [2. 点、线和边缘检测](#2. 点、线和边缘检测)
      • [2.1 背景知识](#2.1 背景知识)
      • [2.1 孤立点的检测](#2.1 孤立点的检测)
      • [2.2 线检测](#2.2 线检测)
      • [2.3 边缘模型](#2.3 边缘模型)
      • [2.4 基本边缘检测](#2.4 基本边缘检测)
      • [2.5 更先进的边缘检测技术](#2.5 更先进的边缘检测技术)
    • [3. 阈值处理](#3. 阈值处理)
      • [3.1 全局阈值处理](#3.1 全局阈值处理)
      • [3.2 用Otsu方法的最佳全局阈值处理](#3.2 用Otsu方法的最佳全局阈值处理)
    • [4. 基于区域的分割](#4. 基于区域的分割)
      • [4.1 区域生长](#4.1 区域生长)
      • [4.2 区域分裂与聚合](#4.2 区域分裂与聚合)
    • [4. 基于区域的分割](#4. 基于区域的分割)
      • [4.1 区域生长](#4.1 区域生长)
      • [4.2 区域分裂与聚合](#4.2 区域分裂与聚合)
    • [5. 补充:VS添加Image Watch](#5. 补充:VS添加Image Watch)

0. 前言

分割将图像细分为构成它的子区域或物体,当感兴趣的物体或区域被检测出来时,就停止其分割。

第三版教材中图片下载地址: DIP 3/e Book Images

vs2019配置opencv可以查看:Opencv的安装与配置(VS 2019 & opencv4.5.4)

代码中出现未知函数,其实现可以查看往期学习笔记

前情回顾:
《数字图像处理》第三章 灰度变换和空间滤波 学习笔记附部分例子代码
《数字图像处理》第四章 频率域滤波 学习笔记附部分例子代码

数字图像处理第五章 图像复原和重建(内容较简单,就没有详细记录笔记)
《数字图像处理》第六章 彩色图像处理 学习笔记附部分例子代码
《数字图像处理》第七章 小波域多分辨率处理 学习笔记附部分例子代码

数字图像处理第八章 图像压缩 非重点
《数字图像处理》第九章 形态学图像处理 学习笔记附部分例子代码

后续剧情:
《数字图像处理》第11章 表示和描述 学习笔记附部分例子代码

1. 基础知识

针对单色图像的分割算法处理灰度值的基于两类特性-不连续性和相似性

  • 不连续性,基于边缘的分割是基于灰度的局部不连续性来进行边界检测的

  • 相似性,根据事先定义的一组准则把一幅图像分割成相似的几个区域

2. 点、线和边缘检测

本节将集中以灰度局部剧烈变化检测为基础的分割方法上。感兴趣的三种图像特征是孤立点、线和边缘。

2.1 背景知识

局部变化可以通过微分来检测,数字函数的导数使用差分来定义,二维函数中,数字差分使用偏导,所以一阶导表达式:

∂ f ∂ x = f ′ ( x ) = f ( x + 1 ) − f ( x ) {\frac{\partial f}{\partial x}}=f^{\prime}(x)=f(x+1)-f(x) ∂x∂f=f′(x)=f(x+1)−f(x)

二阶导表达式:

∂ 2 f ∂ x 2 = f ′ ′ ( x ) = f ( x + 1 ) + f ( x − 1 ) − 2 f ( x ) \frac{\partial^{2}f}{\partial x^{2}}=f^{\prime\prime}(x)=f(x+1)+f(x-1)-2f(x) ∂x2∂2f=f′′(x)=f(x+1)+f(x−1)−2f(x)

两种类型的边缘:斜坡边缘(灰度变化较小较稳定)和台阶边缘(灰度值迅速变化,形成"台阶")

已知的一些结论:

  1. 一阶导数通常在图像中产生较粗的边缘

  2. 二阶导数对精细细节,如细线和孤立点有较强的响应

  3. 二阶导数在灰度斜坡和灰度台阶过渡处会产生双边缘响应

  4. 二阶导数的符号可用于确定边缘的过渡是从亮到暗还是从暗到亮

2.1 孤立点的检测

点的检测以二阶导数为基础,故拉普拉斯:

∇ 2 f ( x ,   y ) = ∂ 2 f ∂ x 2 + ∂ 2 f ∂ y 2 = f ( x + 1 ,   y ) + f ( x − 1 ,   y ) + f ( x ,   y + 1 ) + f ( x ,   y − 1 ) − 4 f ( x ,   y ) \nabla^{2}f(x,\,y)={\frac{\partial^{2}f}{\partial x^{2}}}+{\frac{\partial^{2}f}{\partial y^{2}}} \\ =f(x+1,\,y)+f(x-1,\,y)+f(x,\,y+1)+f(x,\,y-1)-4f(x,\,y) ∇2f(x,y)=∂x2∂2f+∂y2∂2f=f(x+1,y)+f(x−1,y)+f(x,y+1)+f(x,y−1)−4f(x,y)

如果某点处的二阶响应的绝对值超过某个阈值,那么说明该点被检测到了,置为1,其余点置为0,产生一幅二值图像,数学表达式如下:

g ( x , y ) = { 1 , ∣ R ( x , y ) ∣ ⩾ T 0 , 其他 g(x,y)=\left\{\begin{matrix} 1, & |R(x,y)|\geqslant T \\ 0, & 其他 \end{matrix}\right. g(x,y)={1,0,∣R(x,y)∣⩾T其他

c 复制代码
void ch10_test01(string path) {
    Mat image = imread(path, IMREAD_GRAYSCALE);
    if (image.empty()) {
        cout << "Unable to load the image\n";
        return;
    }
    //自己定义卷积核
    Mat kernel = (Mat_<float>(3, 3) << 1, 1, 1, 1, -8, 1, 1, 1, 1);

    //卷积
    Mat lap_result;
    filter2D(image, lap_result, CV_32F, kernel);
    lap_result = abs(lap_result);

    double maxValue;
    minMaxLoc(lap_result, NULL, &maxValue);

    Mat thresholded;
    threshold(lap_result, thresholded, 0.9 * maxValue, 255, THRESH_BINARY);

    displayImg(image, "原图");
    displayImg(lap_result, "二阶导数");
    displayImg(thresholded, "二值图");
    waitKey(0);
}

2.2 线检测

检测特定方向的线(垂直方向,水平方向,两个对角方向),每个模版的系数和要为0

2.3 边缘模型

边缘模型根据他们的灰度剖面来分类。

一阶导数的幅度可用于检测图像中的某个点处是否存在一个边缘;二阶导数的符号可以确定一个边缘像素位于该边缘暗的一侧还是亮的一侧。

执行边缘检测的三个基本步骤:

  1. 降噪对图像进行平滑处理,因为二阶导数对于噪声非常敏感

  2. 边缘点的检测。

  3. 边缘定位。

2.4 基本边缘检测

梯度算子:

g x = ∂ f ( x , y ) ∂ x = f ( x + 1 , y ) − f ( x , y ) g_x = \frac{\partial f(x,y)}{\partial x}=f(x+1,y)-f(x,y) gx=∂x∂f(x,y)=f(x+1,y)−f(x,y)

g y = ∂ f ( x , y ) ∂ y = f ( x , y + 1 ) − f ( x , y ) g_y = \frac{\partial f(x,y)}{\partial y}=f(x,y+1)-f(x,y) gy=∂y∂f(x,y)=f(x,y+1)−f(x,y)

Prewitt算子:

Sobel算子:

由于平方再开方的计算开销大,所以实际中使用绝对值来近似梯度的幅值,如下所示:

M ( x , y ) ≈ ∣ g x ∣ + ∣ g y ∣ M\left(x,\ y\right)\approx \left|g_{x}\right|+\left|g_{y}\right| M(x, y)≈∣gx∣+∣gy∣

c 复制代码
void ch10_test02(string path) {
    Mat img = imread(path, IMREAD_GRAYSCALE);
    if (img.empty()) {
        cout << "Unable to load the image\n";
        return;
    }
    img.convertTo(img, CV_32F);
    normalize(img, img, 0, 1, NORM_MINMAX);

    Mat gradientX;
    Sobel(img, gradientX, CV_32F, 1, 0);
    gradientX = abs(gradientX);
    Mat gradientY;
    Sobel(img, gradientY, CV_32F, 0, 1);
    gradientY = abs(gradientY);

    displayImg(img, "原图02");
    displayImg(gradientX, "x方向梯度|gx|");
    displayImg(gradientY, "y方向梯度|gy|");
    displayImg(gradientX + gradientY, "|gx|+|gy|");
    waitKey(0);
}

计算梯度前对图像进行平滑处理,或者对梯度图像进行阈值处理,可以达到相同的目的------减少精细细节(因为这种细节往往包含有噪声)

2.5 更先进的边缘检测技术

Marr-Hildreth边缘检测算法:

  1. 用对式 G ( x , y ) = e x 2 + y 2 2 σ 2 G(x,y)=e^{\frac{x^2+y^2}{2\sigma ^2}} G(x,y)=e2σ2x2+y2取样得到n×n的高斯低通滤波器对输入图像滤波。

  2. 计算由第一步得到的图像的拉普拉斯。

  3. 找到步骤2所得到图像的零交叉。

坎尼边缘检测算法:

  1. 用一个高斯滤波器平滑输入图像

  2. 计算梯度幅值图像和角度图像

  3. 对梯度幅值图像应用非最大抑制(最大抑制方案见教材)

  4. 用双阀值处理和连接分析来检测并连接边缘

3. 阈值处理

3.1 全局阈值处理

  1. 为全局阈值T选择一个初始估计值。
  2. 在式(10.3-1)中用7分割该图像。这将产生两组像素:G1由灰度值大于T的所有像素组成, G2由所有小于等于T的像素组成。
  3. 对G1和G2的像素分别计算平均灰度值(均值)m1和m2
  4. 计算一个新的阈值: T = 1 2 ( m 1 + m 2 ) T=\frac{1}{2}(m_1 + m_2) T=21(m1+m2)
  5. 重复步骤2到步骤4,直到连续迭代中的T值间的差小于一个预定义的参数为止。

3.2 用Otsu方法的最佳全局阈值处理

Otsu算法步骤:

  1. 计算输入图像的归一化直方图,使用 p i p_i pi表示。

  2. 根据式 $P_1(k) =\sum_{i=0}^{k}p_i $ 计算累计和。

  3. 根据式 m ( k ) = ∑ i = 0 k i p i m(k)=\sum_{i=0}^{k}ip_i m(k)=∑i=0kipi 计算累积均值

  4. 计算全局灰度均值 m G = m ( L − 1 ) m_G=m(L-1) mG=m(L−1)

  5. 根据式 σ B 2 ( k ) = ( m G P 1 − m k ) 2 P 1 ( 1 − P 1 ) \sigma^2_B(k)=\frac{(m_G P_1 -m_k)^2}{P_1(1-P_1)} σB2(k)=P1(1−P1)(mGP1−mk)2 计算类间方差

  6. 遍历一整遍,得到类间方差最大时对应的灰度。

c 复制代码
//测试函数,Otsu函数的实现在下面
void ch10_test03(string path) {
	Mat image = imread(path, IMREAD_GRAYSCALE);
	if (image.empty()) {
		cout << "unable to load the image\n";
		return;
	}

	Mat binaryImage = Otsu(image);

	displayImg(image, "原图03");
	displayImg(binaryImage, "阈值处理的结果");
	waitKey(0);
}
c 复制代码
Mat Otsu(Mat input) {
	const int scale = 256;
	int grayNum[scale] = { 0 };
	int r = input.rows;
	int c = input.cols;
	// 直方图统计
	for (int i = 0; i < r; i++) {
		for (int j = 0; j < c; j++) {
			uchar val = input.at<uchar>(i, j);
			grayNum[val]++;
		}
	}
	double totalPixels = r * c;
	double P[scale];
	double Pk[scale];
	double Mk[scale];
	double pkSum = 0, mkSum = 0;
	for (int i = 0; i < scale; i++) {
		P[i] = grayNum[i] / totalPixels;	//每个灰度级出现的概率
		Pk[i] = pkSum + P[i];		//计算累积和Pk
		pkSum = Pk[i];
		Mk[i] = mkSum + i * P[i];	//计算累积均值Mk
		mkSum = Mk[i];
	}
	//计算全局灰度均值Mg,即Mk[scale - 1]
	int bestThre = 0, maxVar = 0;
	for (int i = 0; i < scale; i++) {
		double var = pow(Mk[scale - 1] * Pk[i] - Mk[i], 2);
		var = var / (Pk[i] * (1 - Pk[i]));
		if (var > maxVar) {
			maxVar = var;
			bestThre = i;
		}
	}
	cout << "最佳阈值为" << bestThre << endl;
	Mat binaryImage;
	threshold(input, binaryImage, bestThre, 255, THRESH_BINARY);
	return binaryImage;
}

如果直方图的波峰是高、窄、对称的,且被深的波谷分开,则选取一个"较好的"阈值是有很大机会的。

4. 基于区域的分割

4.1 区域生长

首先需确定相似性准则,当不再有像素满足加入某个区域的准则时,区域就会停止生长。

4.2 区域分裂与聚合

如果直方图的波峰是高、窄、对称的,且被深的波谷分开,则选取一个"较好的"阈值是有很大机会的。

4. 基于区域的分割

4.1 区域生长

首先需确定相似性准则,当不再有像素满足加入某个区域的准则时,区域就会停止生长。

4.2 区域分裂与聚合

5. 补充:VS添加Image Watch

下载Image Watch插件,Visual Studio 2019 的安装地址 ,下载完成后,双击该.vsix(Visual Studio 扩展)文件,这个时候关闭vs所有进程,就会自动安装完成。

其他版本的vs及使用教程,可以查看OpenCV: Image Watch: viewing in-memory images in the Visual Studio debugger

点击"放大镜",可以将该Mat加入到"watch"中

分为"local"和"watch",即局部变量和添加的监视变量

相关推荐
周哈里窗的编程2 小时前
CSP-CCF★201912-2回收站选址★
c++·算法·图论
ImomoTo3 小时前
HarmonyOS学习(十三)——数据管理(二) 关系型数据库
数据库·学习·harmonyos·arkts·鸿蒙
未来可期LJ4 小时前
【C++ 设计模式】单例模式的两种懒汉式和饿汉式
c++·单例模式·设计模式
Trouvaille ~5 小时前
【C++篇】C++类与对象深度解析(六):全面剖析拷贝省略、RVO、NRVO优化策略
c++·c++20·编译原理·编译器·类和对象·rvo·nrvo
little redcap5 小时前
第十九次CCF计算机软件能力认证-乔乔和牛牛逛超市
数据结构·c++·算法
开MINI的工科男5 小时前
【笔记】自动驾驶预测与决策规划_Part3_路径与轨迹规划
人工智能·笔记·自动驾驶·预测与决策
wang_book5 小时前
Gitlab学习(007 gitlab项目操作)
java·运维·git·学习·spring·gitlab
机器视觉知识推荐、就业指导5 小时前
Qt/C++事件过滤器与控件响应重写的使用、场景的不同
开发语言·数据库·c++·qt
weixin_455446175 小时前
Python学习的主要知识框架
开发语言·python·学习
孤寂大仙v6 小时前
【C++】STL----list常见用法
开发语言·c++·list