OpenCV入门
OpenCV入门
本文简要介绍了OpenCV中的图像二值化操作、几种线性滤波和非线性滤波、边缘检测和轮廓检测以及图像的形态学操作。
1.图像二值化
图像二值化(Binarization)是将图像转换为只有两种颜色(通常是黑和白)的过程。它是图像预处理中的常见操作,尤其是在图像分割、边缘检测和特征提取等任务中,二值化可以帮助简化图像,使得后续的处理变得更为高效。
在 OpenCV 中,图像二值化常用的函数有 cv::threshold() 和 cv::adaptiveThreshold()。这些函数可以将灰度图像转换为二值图像,通过设置不同的阈值(threshold),将像素值大于阈值的部分设为白色,小于阈值的部分设为黑色。
1.1 基本二值化函数:cv::threshold()
cv::threshold() 是 OpenCV 中最常用的二值化函数,用于将图像的像素值与给定的阈值进行比较,进而转换为二值图像。
函数原型:
double cv::threshold(
cv::InputArray src, // 输入图像,通常是灰度图像
cv::OutputArray dst, // 输出图像,二值化后的图像
double thresh, // 阈值
double maxValue, // 最大值,通常设为 255
int thresholdType // 阈值类型,决定了二值化的方式
);
参数表:
src:输入图像,必须是单通道(灰度)图像,通常类型为CV_8U(无符号8位整数)。dst:输出图像,二值化后的图像,类型与输入图像相同。thresh:阈值,所有大于该值的像素将设置为maxValue(通常为 255),否则设置为 0。maxValue:大于阈值的像素值将被设置为此值(通常为 255,表示白色)。thresholdType:二值化类型,决定了如何将像素值与阈值进行比较,常用的阈值类型有:cv::THRESH_BINARY:如果像素值大于阈值,则设置为maxValue,否则设置为 0。cv::THRESH_BINARY_INV:如果像素值小于阈值,则设置为maxValue,否则设置为 0。cv::THRESH_TRUNC:如果像素值大于阈值,则将其截断为阈值。cv::THRESH_TOZERO:如果像素值大于阈值,则保持原值,否则设置为 0。cv::THRESH_TOZERO_INV:如果像素值小于阈值,则保持原值,否则设置为 0。
返回值:
threshold返回一个double,表示实际使用的阈值,通常这个值和 thresh 相同。
简单的二值化示例:
#include <opencv2/opencv.hpp>
#include <iostream>
int main() {
// 读取灰度图像
cv::Mat img = cv::imread("image.jpg", cv::IMREAD_GRAYSCALE);
if (img.empty()) {
std::cerr << "Failed to load image!" << std::endl;
return -1;
}
// 创建输出图像
cv::Mat binaryImg;
// 使用 cv::threshold 进行二值化
double thresh = 128; // 选择阈值 128
double maxValue = 255;
cv::threshold(img, binaryImg, thresh, maxValue, cv::THRESH_BINARY);
// 显示结果
cv::imshow("Original Image", img);
cv::imshow("Binary Image", binaryImg);
cv::waitKey(0);
return 0;
}
1.2 自适应阈值:cv::adaptiveThreshold()
在某些情况下,图像的亮度可能存在不均匀的情况(例如,图像不同部分的光照不同)。在这种情况下,固定阈值可能无法有效地分割图像。自适应阈值(Adaptive Thresholding)是应对这一问题的好方法,它会根据图像局部区域的像素值动态调整阈值。
函数原型:
void cv::adaptiveThreshold(
cv::InputArray src, // 输入图像,必须是灰度图像
cv::OutputArray dst, // 输出图像,二值化后的图像
double maxValue, // 最大值
int adaptiveMethod, // 自适应方法,详见下面的参数表
int thresholdType, // 二值化类型,同上
int blockSize, // 邻域大小,定义计算阈值的区域大小,必须是奇数
double C // 常数,调整阈值的值(减去常数来避免阈值过高)
);
参数表:
src:输入图像,必须是单通道的灰度图像。dst:输出图像,二值化后的图像。maxValue:大于局部阈值的像素值将设置为此值(通常为 255)。adaptiveMethod:自适应方法:cv::ADAPTIVE_THRESH_MEAN_C:计算邻域区域的均值,并作为阈值。cv::ADAPTIVE_THRESH_GAUSSIAN_C:计算邻域区域的**加权均值(使用高斯权重)**作为阈值。
thresholdType:二值化类型,通常使用cv::THRESH_BINARY或cv::THRESH_BINARY_INV。blockSize:邻域区域的大小,必须是奇数(例如 3, 5, 7 等)。C:偏移值调整常数,计算出的阈值减去C即为最终结果。这个常数值通常设置为正数,来避免过高的阈值。
示例:
#include <opencv2/opencv.hpp>
#include <iostream>
int main() {
// 读取灰度图像
cv::Mat img = cv::imread("image.jpg", cv::IMREAD_GRAYSCALE);
if (img.empty()) {
std::cerr << "Failed to load image!" << std::endl;
return -1;
}
// 创建输出图像
cv::Mat adaptiveBinaryImg;
// 使用自适应阈值进行二值化
double maxValue = 255;
int adaptiveMethod = cv::ADAPTIVE_THRESH_MEAN_C;
int thresholdType = cv::THRESH_BINARY;
int blockSize = 11; // 邻域大小
double C = 2; // 偏移值调整量
cv::adaptiveThreshold(img,
adaptiveBinaryImg,
maxValue,
adaptiveMethod,
thresholdType,
blockSize,
C);
// 显示结果
cv::imshow("Original Image", img);
cv::imshow("Adaptive Binary Image", adaptiveBinaryImg);
cv::waitKey(0);
return 0;
}
在这个例子中:
cv::adaptiveThreshold()使用了自适应均值法cv::ADAPTIVE_THRESH_MEAN_C,并且设置了一个邻域大小为 11 的区域来计算每个像素的阈值。C用于从计算出来的均值中减去一个常数,调整二值化效果。
1.3 Otsu方法
Otsu方法,也叫最大类间方差法或者大津法(由大津展之(英语:Nobuyuki Otsu)提出),是一种自动计算最优阈值的方法,它可以在全局范围内通过最大化类间方差来选择最佳的分割阈值。
例如:
cv::Mat img = cv::imread("image.jpg", cv::IMREAD_GRAYSCALE);
cv::Mat binaryImg;
cv::threshold(img, binaryImg, 0, 255, cv::THRESH_BINARY + cv::THRESH_OTSU);
2.图像滤波
图像滤波(Image Filtering)是计算机视觉和图像处理中的重要技术之一,通常用于图像的降噪、增强、模糊以及边缘检测等任务。OpenCV 提供了丰富的图像滤波功能,主要包括线性滤波和非线性滤波。
在 OpenCV 中,图像滤波的核心操作是通过一个叫做 卷积核(Kernel)或 滤波器的矩阵对图像进行处理。这个过程是通过卷积运算实现的,即对图像中的每个像素及其邻域像素进行加权计算。
限于笔者水平有限,这里主要介绍均值滤波、中值滤波、高斯滤波和双边滤波。
2.1 线性滤波(Linear Filtering)
线性滤波是最常见的一种滤波方法,通过加权平均来平滑图像。我们这里介绍均值滤波和高斯滤波。
首先我们介绍一下什么是平滑滤波。
平滑滤波(Smoothing Filter)是一种线性滤波的信号处理方法,常用于去除信号中的噪声或减少数据的波动,使得信号变化更加平稳、连续,便于分析和理解。其基本思想是通过对信号进行某种形式的**“平均”或“加权平均”**,从而去除信号中的高频成分(通常是噪声),保留信号的主要趋势和低频成分。
2.1.1 均值滤波
均值滤波(Mean Filtering)是将每个像素替换为其邻域内所有像素的平均值。这是一种常见的平滑滤波方法,能够去除图像中的随机噪声。
cv::blur() 函数和 cv::boxFilter()函数
这两个函数实现了均值滤波,其中 cv::boxFilter() 允许更多的定制。
OpenCV中的积分图算法
值得注意的是,cv::blur()函数 在执行均值滤波时通常使用积分图技术。积分图是图像中每个像素的累积和,即对于图像中任意位置 (x,y),积分图S(x, y)存储了从图像的左上角(0, 0)到当前位置 (x, y)的所有像素值的和。
如果直接计算一个矩形区域的像素和,必须遍历区域中的每个像素,并累加其值。对于一个大小为 W×H的矩形区域,这样的操作需要W×H次加法,计算量是线性的。但是积分图允许我们在常数时间O(1)内计算任意矩形区域的和。因为积分图通过预先计算并存储了从图像左上角到任意点的累积和,之后我们只需要通过简单的四次减法来获得任意矩形区域的和。因此,无论矩形区域的大小如何,求和的时间复杂度都不会增加。
在标准的均值滤波中(不使用积分图等优化方法),每个像素的值是在它所在的窗口内的所有像素的平均值。如果你将窗口从左到右、从上到下进行滑动,每次计算时,新的计算是基于原始图像像素值或者上一窗口的结果。 如果在计算过程中,像素值被实时更新,那么新计算的像素值可能会影响到后续的计算。
但是,使用积分图进行优化后,计算顺序就不会影响结果,因为每次计算都是基于整个窗口内的像素和,而不是已经更新过的像素值。
示例
言归正传,下面我们通过几个例子实现均值滤波。
cv::blur() 简单实现均值滤波:
cv::Mat img = cv::imread("image.jpg");
cv::Mat result;
cv::blur(img, result, cv::Size(5, 5)); // 使用 5x5 的均值滤波器
cv::imshow("Blurred Image", result);
cv::waitKey(0);
cv::boxFilter():boxFilter() 更加灵活,可以指定是否归一化加权,是否使用边界填充等。
cv::Mat img = cv::imread("image.jpg");
cv::Mat result;
cv::boxFilter(img, result, -1, cv::Size(5, 5)); // 5x5 的均值滤波
cv::imshow("Box Filtered Image", result);
cv::waitKey(0);
cv::blur() 是 cv::boxFilter() 的一个简化版本,默认会使用归一化(将窗口内像素的权重和归一化为 1),并且在大多数情况下,cv::blur() 足以满足需要均值滤波的应用。
2.1.2 高斯滤波
高斯滤波(Gaussian Filtering)的卷积核与均值滤波的不同,它使用一个高斯核(Gaussian kernel)对图像进行卷积,以实现平滑效果。高斯核是一个二维矩阵,它的值由二维高斯函数计算得来。该矩阵中的每个值表示图像中相应位置的加权系数,权重由高斯函数(正态分布)决定。高斯滤波会给靠近卷积核中心像素赋予更大的权重,远离中心的像素赋予较小的权重,从而减少图像的模糊。
高斯滤波的计算过程
- 构造高斯核:根据所需的标准差 σ和核的大小(如 3×3、5×5等),计算得到高斯核矩阵。
- 卷积操作:将图像与高斯核进行卷积,滑动窗口(高斯核大小)遍历图像,每个像素值都与对应的高斯核元素相乘并求和,得到新的平滑后的图像。
高斯滤波常用于去噪声,特别是去除高斯噪声。
高斯滤波的关键参数:
1.高斯核的大小
核的大小决定了你在图像中进行加权平均时,会考虑多大的邻域范围。大核通常意味着计算时会考虑更广泛的像素邻域,产生较强的平滑效果。大核会导致图像更加模糊,因为每个像素的加权平均会覆盖更大的区域;反之,小核计算时只会考虑较小的邻域,对图像的平滑效果较弱,细节部分保留较多。
2.标准差σ
标准差σ控制高斯分布的宽度,也就是影响像素的加权程度。
$$
G(x, y) = \frac{1}{2 \pi \sigma^2} \exp\left( -\frac{x^2 + y^2}{2 \sigma^2} \right)
$$
由上述公式可知,当 σ较大时,分母变大,这意味着指数函数中的衰减速度较慢。因此,距离中心较远的像素的权重仍然比较大,整体的权重分布比较“宽广”。当 σ较小时,分母变小,这意味着指数函数中的衰减速度较快,权重会迅速减小,距离中心较远的像素对结果的影响会急剧减少。
高斯核的大小与标准差 σ是相互关联的,通常情况下,标准差和核的大小需要根据特定的应用场景来选择。一般来说,高斯核的大小与标准差 σ有如下关系:
- 常见经验法则:如果高斯核的标准差为 σ,为了覆盖大部分高斯分布的概率质量,常用的核大小通常选择 6σ 左右。即:
- 如果 σ=1,常见的高斯核大小是 3×3或 5×5。
- 如果 σ=2,常见的高斯核大小是 5×5或 7×7。
- 理由:高斯函数的值在离开中心位置一定距离后会迅速衰减。理论上,高斯函数在距离中心 3σ处就衰减到 0.05%了,因此使用一个包含 6σ范围内的像素值的矩阵通常能捕获大部分信息。
cv::GaussianBlur()
cv::Mat img = cv::imread("image.jpg");
cv::Mat result;
cv::GaussianBlur(img, result, cv::Size(5, 5), 1.5); // 5x5 的高斯滤波,标准差为 1.5
cv::imshow("Gaussian Blurred Image", result);
cv::waitKey(0);
其中:
cv::Size(5, 5):表示高斯滤波的内核大小(通常是奇数)。1.5:是高斯分布的标准差,控制滤波的强度。
值得注意的是,OpenCV中的高斯滤波通常会使用一个新的矩阵(或缓冲区)来存储输出结果,所以和均值滤波一样新计算的像素值不会影响到后续的计算。
2.2.非线性滤波
2.2.1中值滤波
中值滤波是通过取邻域像素的中位数来代替原像素值。它是一种非线性滤波,可以有效去除椒盐噪声。
cv::medianBlur()
cv::Mat img = cv::imread("image.jpg");
cv::Mat result;
cv::medianBlur(img, result, 5); // 5x5 的中值滤波
cv::imshow("Median Blurred Image", result);
cv::waitKey(0);
2.2.2双边滤波
双边滤波是一种常用于去噪且保持边缘信息的滤波方法。它不仅考虑空间邻域的像素,还考虑像素值之间的相似性,因此在去除噪声的同时,能够很好地保留图像的边缘信息。因为在进行滤波时,双边滤波不仅考虑邻域内的像素,还会根据像素值的相似性来加权。这样,颜色相似的像素会对滤波结果产生较大影响,而颜色差异较大的像素影响较小。因此,边缘附近的像素会被保持,而平坦区域会被平滑。
函数原型:
void cv::bilateralFilter(
InputArray src, // 输入图像
OutputArray dst, // 输出图像
int d, // 邻域的边长,表示滤波窗口的大小
double sigmaColor, // 强度标准差(颜色差异的标准差)
double sigmaSpace, // 空间标准差(距离的标准差)
int borderType = BORDER_DEFAULT // 边界填充方式
);
src:输入图像。dst:输出图像。d:滤波窗口的直径。大于0时,它定义了一个 d×d的邻域窗口;如果为负值,表示根据 σ自动计算邻域窗口大小。sigmaColor:强度标准差,控制颜色差异对滤波的影响。较小的值表示只有颜色差异较小的像素会被用来加权,较大的值表示较大的颜色差异也能影响结果。sigmaSpace:空间标准差,控制空间距离对滤波的影响。较小的值表示只有距离中心较近的像素会有较大权重,较大的值表示较远的像素也会影响结果。borderType:边界填充方式,决定了如何处理图像的边界部分,默认为BORDER_DEFAULT。
调整参数:
sigmaSpace(空间标准差):
- 较大的 σ可以处理更大的平滑区域,但可能会引入更多模糊。
- 较小的 σ会导致滤波只在邻域内的近邻像素上起作用,能够较好地保留细节。
sigmaColor(强度标准差):
- 较大的 σ 会导致更多相似颜色的像素参与计算,进而产生更大的模糊效果。
- 较小的 σ会使得只有非常相似的像素参与计算,适合去除细节较少的噪声。
示例:
cv::Mat img = cv::imread("image.jpg");
cv::Mat result;
cv::bilateralFilter(img,
result,
9,
75,
75); // 使用双边滤波,9 是边长,75 是颜色空间标准差,75 是坐标空间标准差
cv::imshow("Bilateral Filtered Image", result);
cv::waitKey(0);
3.边缘检测
在计算机视觉和图像处理领域,边缘检测是用来识别图像中物体的轮廓或突变区域的技术。边缘通常是图像中灰度变化最剧烈的地方,反映了物体的形状、纹理、轮廓等重要信息。边缘检测在许多应用中都至关重要,例如对象识别、图像分割、视频分析等。
算子(Operator)通常指的是一组操作或数学函数,用于对图像的像素值进行处理或变换。算子的作用是对图像中的每个像素应用特定的数学操作,通常用来提取、变换或滤波图像中的特征。
OpenCV 提供了多种边缘检测的方法,最常用的包括 Sobel 算子、Canny 边缘检测、Laplacian 算子等。接下来,我们将逐一讲解这些方法及其在 OpenCV 中的实现。
3.1 Sobel算子
Sobel 算子是一种经典的边缘检测方法,通过计算图像的梯度来识别边缘。Sobel 算子应用了两组简单的卷积核,分别用于检测水平方向和垂直方向的边缘。通过计算图像在这两个方向上的梯度,可以得到图像的边缘信息。
Sobel 算子原理
Sobel 算子有两个主要的核:
-
水平方向的 Sobel 核(用于检测水平方向的边缘):
$$
G_x = \begin{bmatrix} -1 & 0 & 1 \ -2 & 0 & 2 \ -1 & 0 & 1 \end{bmatrix}
$$ -
垂直方向的 Sobel 核(用于检测垂直方向的边缘):
$$
G_y = \begin{bmatrix} -1 & -2 & -1 \ 0 & 0 & 0 \ 1 & 2 & 1 \end{bmatrix}
$$
通过对图像进行卷积运算,分别得到水平方向和垂直方向的梯度Gx和 Gy。
-
边缘强度:梯度大小
$$
G = \sqrt{G_x^2 + G_y^2}
$$ -
边缘方向:梯度方向
$$
\theta = \text{atan2}(G_y, G_x)
$$代码实现:
#include <opencv2/opencv.hpp> #include <iostream> int main() { // 读取图像 cv::Mat src = cv::imread("image.jpg", cv::IMREAD_GRAYSCALE); // 存储 Sobel 边缘检测结果 cv::Mat grad_x, grad_y, abs_grad_x, abs_grad_y, grad; // 使用 Sobel 进行边缘检测 cv::Sobel(src, grad_x, CV_16S, 1, 0, 3); // 水平方向 cv::Sobel(src, grad_y, CV_16S, 0, 1, 3); // 垂直方向 // 转换为绝对值 cv::convertScaleAbs(grad_x, abs_grad_x); cv::convertScaleAbs(grad_y, abs_grad_y); // 合并梯度 cv::addWeighted(abs_grad_x, 0.5, abs_grad_y, 0.5, 0, grad); // 显示结果 cv::imshow("Sobel", grad); cv::waitKey(0); return 0; }
3.2 Canny边缘检测
Canny 边缘检测是一种多阶段的边缘检测算法,由 John F. Canny 提出。它的优点是可以精确检测到图像中的边缘,并有效减少噪声的干扰。
Canny 算法有四个主要步骤:
-
高斯滤波:首先对图像进行高斯平滑,以去除噪声。
-
梯度计算:使用 Sobel 或其他算子计算图像的梯度,得到边缘的方向和强度。
-
非最大抑制:对梯度幅值进行细化,将非边缘点的值抑制掉,只保留局部最大值。使得边缘变得更加精确和清晰。
-
双阈值化:通过两个阈值(低阈值和高阈值)来决定哪些像素是边缘点。
-
强边缘:如果一个像素的梯度值大于高阈值,它就被认为是强边缘。
-
弱边缘:如果一个像素的梯度值介于低阈值和高阈值之间,它被认为是弱边缘。
-
非边缘:如果一个像素的梯度值小于低阈值,它就被认为是非边缘,直接被去除。
-
-
边缘连接:边缘连接的目的是去掉那些“孤立”的弱边缘像素,并将它们与强边缘连接起来。
- 如果一个弱边缘像素与一个强边缘像素相邻,它就被认为是一个真实的边缘像素。
- 如果一个弱边缘像素没有与强边缘像素相邻,那么它就会被去掉。
函数原型
void cv::Canny( InputArray image,
OutputArray edges,
double threshold1, //滞后阈值1
double threshold2, //滞后阈值2
int apertureSize = 3, //Sobel算子直径
bool L2gradient = false
)
threshold1和threshold2的差距变大,检测出的边缘就会越来越少。因为他们之间的阈值间距越大,出现在阈值之间的梯度值就越多,出现不连续的边缘也就越多,导致很多都被过滤掉,所以最后检测出的边缘较少。
在OpenCV中的实现
#include <opencv2/opencv.hpp>
#include <iostream>
int main() {
// 读取图像
cv::Mat src = cv::imread("image.jpg", cv::IMREAD_GRAYSCALE);
// Canny 边缘检测
cv::Mat edges;
cv::Canny(src, edges, 100, 200); // 100 和 200 是低阈值和高阈值
// 显示结果
cv::imshow("Canny", edges);
cv::waitKey(0);
return 0;
}
4. 轮廓检测
在 OpenCV 中,轮廓检测是一个重要的操作,通常用于图像分割、物体检测、形状分析等任务。轮廓是图像中具有相同颜色或灰度值的连通区域的边界。OpenCV 提供了丰富的工具来提取图像中的轮廓,并对其进行各种操作,如绘制、筛选、分析等。
4.1 轮廓检测的基本步骤
轮廓检测的基本步骤通常包括以下几个部分:
- 图像预处理:
- 转换为灰度图像
- 应用高斯模糊(去噪)
- 使用边缘检测(如 Canny 算法)或阈值处理来二值化图像
- 轮廓查找:
- 使用
findContours()函数检测图像中的轮廓。
- 使用
- 轮廓绘制与分析:
- 使用
drawContours()函数绘制轮廓。 - 分析轮廓的大小、形状等属性。
- 使用
4.2 主要的函数
在 OpenCV 中,轮廓检测通常使用两个主要的函数:
findContours():用于查找轮廓。drawContours():用于绘制轮廓。
findContours() 函数
findContours() 用于查找图像中的轮廓,并将其保存在一个容器中。它的函数原型如下:
void findContours(
InputOutputArray image,
std::vector<std::vector<Point>>& contours,
OutputArray hierarchy,
int mode,
int method,
Point offset = Point()
);
-
image:输入图像,通常是二值化的图像。 -
contours:存储轮廓的容器。每个轮廓都是一个点集,表示轮廓的边界。 -
hierarchy:层次结构,描述轮廓之间的关系(如父子关系、嵌套关系等)。 -
mode:轮廓检索模式,常用的有:
RETR_EXTERNAL:只检测最外层的轮廓。RETR_LIST:检测所有轮廓,但不建立层级关系。RETR_TREE:检测所有轮廓,并建立层级关系。
-
method:轮廓近似方法,常用的有:
CHAIN_APPROX_SIMPLE:仅保存轮廓的端点(压缩轮廓)。CHAIN_APPROX_NONE:保存轮廓的所有点(不压缩)。
-
offset:可选参数,指定轮廓的偏移量。
drawContours() 函数
drawContours() 用于在图像上绘制轮廓,其函数原型如下:
void drawContours(
InputOutputArray image,
const std::vector<std::vector<Point>>& contours,
int contourIdx,
const Scalar& color,
int thickness = 1,
int lineType = 8,
const Mat& hierarchy = noArray(),
int maxLevel = INT_MAX,
Point offset = Point()
);
image:输出图像,在其上绘制轮廓。contours:存储轮廓的容器。contourIdx:要绘制的轮廓的索引。如果是-1,则绘制所有轮廓。color:轮廓的颜色(使用 BGR 格式)。thickness:轮廓的线宽(默认为 1)。lineType:线型,默认为 8,表示连接点的线段类型。hierarchy:轮廓层次结构(通常不需要指定)。maxLevel:绘制的最大层级(通常不需要指定)。offset:偏移量。
4.3.轮廓检测的实现步骤
下面是一个完整的示例代码,演示了如何使用 OpenCV 进行轮廓检测和绘制。
#include <opencv2/opencv.hpp>
#include <iostream>
using namespace cv;
using namespace std;
int main()
{
// 1. 读取图像
Mat image = imread("example.jpg", IMREAD_COLOR); // 读取彩色图像
if (image.empty()) {
cout << "Could not open or find the image!" << endl;
return -1;
}
// 2. 转换为灰度图像
Mat gray;
cvtColor(image, gray, COLOR_BGR2GRAY);
// 3. 应用高斯模糊,去除噪声
Mat blurred;
GaussianBlur(gray, blurred, Size(5, 5), 0);
// 4. 使用 Canny 边缘检测
Mat edges;
Canny(blurred, edges, 100, 200);
// 5. 查找轮廓
vector<vector<Point>> contours;
vector<Vec4i> hierarchy;
findContours(edges, contours, hierarchy, RETR_TREE, CHAIN_APPROX_SIMPLE);
// 6. 在原图上绘制轮廓
Mat result = image.clone(); // 克隆原图
drawContours(result, contours, -1, Scalar(0, 255, 0), 2); // 绘制所有轮廓,绿色,线宽为2
// 7. 显示结果
imshow("Original Image", image);
imshow("Edge Image", edges);
imshow("Contours Image", result);
waitKey(0); // 等待按键
return 0;
}
5.形态学操作
图像的形态学操作(Morphological Operations)是一种基于图像形状的处理方法,通常用于二值图像的分析和处理。形态学操作通过对图像中各个区域的结构进行改变或分析,来提取或增强图像中的形态特征(如边缘、物体、空洞等)。
这些操作在许多计算机视觉任务中非常常见,例如噪声去除、边缘检测、图像分割、物体识别等。它们主要基于图像的几何形状进行分析,通过设置形态学核(通常是小的二值结构元素)来操作图像。
基本的形态学操作
常见的形态学操作有以下几种:
- 膨胀(Dilation)与腐蚀(Erosion)
- 开运算(Opening)与闭运算(Closing)
- 顶帽运算(Top-hat)与黑帽运算(Black-hat)
5.1 腐蚀和膨胀
5.1.1 基本概念与原理
膨胀:膨胀操作会使图像中的白色区域变大,黑色区域缩小。它将卷积核(通常是一个小的矩形或圆形区域)放置在图像的每个像素上,然后对应位置相乘,并选取所得结果中的最大值。
腐蚀:腐蚀操作与膨胀相反,它会使图像中的白色区域缩小,黑色区域变大。它会将所得结果的最小值作为输出。
例如,现在假设有一个简单的二值图像,其中前景是白色,背景是黑色,卷积核为3*3的全1矩阵。
原图:
1 1 1 1 1
1 0 0 0 1
1 0 0 0 1
1 1 1 1 1
-
腐蚀:
- 进行腐蚀后,白色区域会变小:
腐蚀后: 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -
膨胀:
- 进行膨胀后,白色区域会变大:
膨胀后: 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
5.1.2OpenCV中的实现
①生成结构元素
在进行图像腐蚀等操作前,我们得先生成结构元素。
Mat cv::getStructuringElement(int shape, //生成结构元素的种类,比如MORPH_CROSS
cv::Size ksize, //尺寸,一般有3*3,5*5,7*7
cv::Point anchor = cv::Point(-1,-1) //中心的位置,默认为几何中心
)
其中shape可以为:
cv::MORPH_ELLIPSE:圆形结构元素cv::MORPH_CROSS:十字形结构元素cv::MORPH_RECT:矩形结构元素
结构元素决定了操作时如何对每个像素的邻域进行处理,进而影响图像处理的形态(即结果的边界形状)。
例如:
- 矩形结构元素:
- 使用矩形结构元素,图像的物体边缘会以矩形的方式扩展或收缩,因为矩形结构元素会考虑到水平方向和垂直方向的邻域。腐蚀操作时,图像中的白色区域会以矩形形状收缩。
- 适用:对于一个有明显直线和角落的物体,矩形结构元素会使物体的边缘保持直线或者矩形的形状。
- 圆形结构元素:
- 使用圆形结构元素,图像中的物体边缘会呈现圆形的扩展或收缩,因为圆形结构元素会考虑到周围像素的圆形邻域。腐蚀操作时,图像中的白色区域会以圆形形状收缩。
- 适用:对于一个不规则形状的物体,使用圆形结构元素会使得物体的边缘变得更加平滑,尤其是弯曲的边缘会变得更加圆滑。圆形结构元素能够更自然地连接物体的曲线。
- 十字形结构元素:
- 使用十字形结构元素,膨胀和腐蚀操作的结果会沿着水平和垂直方向扩展或收缩。这意味着边缘在这些方向上会受到影响,但在对角线方向上不会被影响。
- 适用:如果物体的边缘是直线,十字形结构元素的膨胀操作会沿着这些直线方向扩展边缘,但不会影响到对角线上的物体形状。
总结一下,矩形结构元素会让边缘保持直线形状(矩形或方形)。圆形结构元素会让边缘变得圆滑,适合对不规则形状的物体进行处理。十字形结构元素则在水平和垂直方向上扩展边界,适用于对有明确水平或垂直边缘的图像进行处理。
②cv::dilate 与cv::erode
1.cv::dilate()函数
cv::dilate()函数用于执行膨胀操作(dilation),其函数原型为:
void cv::dilate(
InputArray src, // 输入图像(通常是二值图像)
OutputArray dst, // 输出图像
InputArray kernel, // 结构元素(卷积核)
Point anchor = Point(-1, -1), // 锚点,默认为结构元素的中心
int iterations = 1, // 膨胀操作的迭代次数
int borderType = BORDER_CONSTANT, // 边界类型,默认为常数边界
const Scalar& borderValue = Scalar() // 边界值
);
2.cv::erode()函数
cv::erode()函数用于执行腐蚀操作(erosion),其函数原型为:
void cv::erode(
InputArray src, // 输入图像(通常是二值图像)
OutputArray dst, // 输出图像
InputArray kernel, // 结构元素(卷积核)
Point anchor = Point(-1, -1), // 锚点,默认为结构元素的中心
int iterations = 1, // 腐蚀操作的迭代次数,默认为1
int borderType = BORDER_CONSTANT, // 边界类型,默认为常数边界
const Scalar& borderValue = Scalar() // 边界值
);
iterations 参数控制了膨胀和腐蚀操作的次数。迭代次数越多,效果越显著。
-
多次腐蚀:会使物体边界收缩得更厉害,甚至完全去除细小物体。
-
多次膨胀:会使物体边界扩展得更大,填补更多的空洞,连接更多的区域。
③完整代码
膨胀示例代码
#include <opencv2/opencv.hpp>
#include <iostream>
int main() {
// 读取输入图像(假设是二值图像)
cv::Mat inputImage = cv::imread("binary_image.png", cv::IMREAD_GRAYSCALE);
// 检查图像是否成功读取
if (inputImage.empty()) {
std::cerr << "Image not loaded!" << std::endl;
return -1;
}
// 创建一个3x3的矩形结构元素
cv::Mat kernel = cv::getStructuringElement(cv::MORPH_RECT, cv::Size(3, 3));
// 执行膨胀操作
cv::Mat dilatedImage;
cv::dilate(inputImage, dilatedImage, kernel, cv::Point(-1, -1), 1); // 迭代次数为1
// 显示原图和膨胀后的图像
cv::imshow("Original Image", inputImage);
cv::imshow("Dilated Image", dilatedImage);
cv::waitKey(0);
return 0;
}
腐蚀示例代码:
#include <opencv2/opencv.hpp>
#include <iostream>
int main() {
// 读取输入图像(假设是二值图像)
cv::Mat inputImage = cv::imread("binary_image.png", cv::IMREAD_GRAYSCALE);
// 检查图像是否成功读取
if (inputImage.empty()) {
std::cerr << "Image not loaded!" << std::endl;
return -1;
}
// 创建一个3x3的矩形结构元素
cv::Mat kernel = cv::getStructuringElement(cv::MORPH_RECT, cv::Size(3, 3));
// 执行腐蚀操作
cv::Mat erodedImage;
cv::erode(inputImage, erodedImage, kernel, cv::Point(-1, -1), 1); // 迭代次数为1
// 显示原图和腐蚀后的图像
cv::imshow("Original Image", inputImage);
cv::imshow("Eroded Image", erodedImage);
cv::waitKey(0);
return 0;
}
5.1.3 膨胀和腐蚀的实际应用
- 去噪:
- 腐蚀:用于去除二值图像中的小白点或噪声。
- 膨胀:用于填补物体边缘的小黑点或空洞。
- 物体边界处理:
- 膨胀:用来增强物体的边界或将小物体连接起来。
- 腐蚀:用来减少物体边界,移除图像中的小物体。
- 形状提取:
- 通过连续的腐蚀和膨胀操作,可以提取图像中的物体形状、去除不必要的部分。
- 连接和分离物体:
- 通过膨胀,可以将分离的物体连接在一起;通过腐蚀,可以将连接在一起的物体分开。
5.2 开运算和闭运算
开运算(Opening)和闭运算(Closing)是形态学图像处理中的两种重要操作,它们都是基于膨胀和腐蚀操作的组合。这两种运算能够有效地去除噪声、平滑物体的边界、填补小的空洞等。开运算和闭运算在许多图像处理任务中被广泛使用,尤其是在二值图像的处理和分析中。
morphologyEx()函数
在介绍开运算和闭运算之前,首先我们要介绍一下morphologyEx()函数,因为在OpenCV中的形态学操作可以通过此函数实现。而且,上面提到的腐蚀和膨胀操作也可以通过这个函数完成。
函数原型:
void morphologyEx(
InputArray src,
OutputArray dst,
int op,
InputArray kernel,
Point anchor = Point(-1, -1),
int iterations = 1,
BorderTypes borderType = BORDER_CONSTANT,
const Scalar& borderValue = morphologyDefaultBorderValue()
);
参数说明:
src: 输入图像,必须是单通道的图像,通常为灰度图。它是需要应用形态学操作的源图像。dst: 输出图像,即形态学操作后的结果。op: 操作类型。这个参数指定了执行哪种形态学操作,它是一个整数值。OpenCV 提供了几种不同的操作类型,通过以下常量来指定:MORPH_ERODE:腐蚀操作(erosion)MORPH_DILATE:膨胀操作(dilation)MORPH_OPEN:开运算(opening),先腐蚀再膨胀MORPH_CLOSE:闭运算(closing),先膨胀再腐蚀MORPH_TOPHAT:顶帽运算(top hat),原图减去开运算结果MORPH_BLACKHAT:黑帽运算(black hat),闭运算结果减去原图
kernel: 结构元素(kernel),和上文介绍的结构元素一样:MORPH_RECT:矩形结构元素MORPH_ELLIPSE:椭圆形结构元素MORPH_CROSS:十字形结构元素
anchor: 锚点位置,指定结构元素的中心点,默认值为Point(-1, -1),表示结构元素的中心点自动设置为结构元素的几何中心。iterations: 迭代次数,指定形态学操作执行的次数。通常,我们只需要执行一次操作,但有时需要多次迭代来得到期望的效果。borderType: 边界类型,定义图像边界外的区域如何处理。常用的边界类型有:BORDER_CONSTANT:常量边界(默认值)BORDER_REFLECT:边界反射BORDER_REPLICATE:边界复制BORDER_WRAP:边界环绕BORDER_REFLECT_101:反射类型 101BORDER_TRANSPARENT:透明边界
borderValue: 边界值,指定边界类型时,设置常量边界的值。默认使用morphologyDefaultBorderValue()
1. 开运算(Opening)
1.1 定义
开运算是腐蚀和膨胀操作的组合。具体来说,开运算先对图像进行腐蚀操作,再进行膨胀操作。开运算可以用来去除小的噪点,并平滑物体的边界。
1.2 过程
- 腐蚀:首先使用腐蚀操作减少图像中前景(白色)部分的大小,移除小的白色噪点。
- 膨胀:然后对腐蚀后的图像进行膨胀,恢复物体的边界,但不会恢复之前腐蚀过程中去除的小噪点。
这种操作可以去除图像中的小的白色噪点或小的连接区域,同时保留物体的主要结构。
1.3 应用
- 去噪:去除二值图像中的小的白色噪点。
- 平滑物体边界:去除物体边缘的小缺陷,使边界更加平滑。
- 物体分割:去除连接在一起的小物体,使它们独立出来。
1.4 代码示例(开运算)
#include <opencv2/opencv.hpp>
#include <iostream>
int main() {
// 读取输入图像(假设是二值图像)
cv::Mat inputImage = cv::imread("binary_image.png", cv::IMREAD_GRAYSCALE);
// 检查图像是否成功读取
if (inputImage.empty()) {
std::cerr << "Image not loaded!" << std::endl;
return -1;
}
// 创建一个3x3的矩形结构元素
cv::Mat kernel = cv::getStructuringElement(cv::MORPH_RECT, cv::Size(3, 3));
// 执行开运算(腐蚀 + 膨胀)
cv::Mat openedImage;
cv::morphologyEx(inputImage, openedImage, cv::MORPH_OPEN, kernel);
// 显示原图和开运算后的图像
cv::imshow("Original Image", inputImage);
cv::imshow("Opened Image", openedImage);
cv::waitKey(0);
return 0;
}
2. 闭运算(Closing)
2.1 定义
闭运算是膨胀和腐蚀操作的组合。具体来说,闭运算先对图像进行膨胀操作,再进行腐蚀操作。闭运算可以用来填补物体中的小孔洞、连接邻近的物体,并平滑物体的边界。
2.2 过程
- 膨胀:首先使用膨胀操作扩展图像中前景(白色)部分的大小,填补物体中的小黑洞或缝隙。
- 腐蚀:然后对膨胀后的图像进行腐蚀,恢复物体的边界,但不会影响已经填补的孔洞或连接的区域。
闭运算通过膨胀填补空洞,通过腐蚀恢复边界的细节,常用于连接分离的物体或填补小的缺失区域。
2.3 应用
- 填补小孔洞:闭运算可以填补图像中的小黑洞或小裂缝。
- 物体连接:当图像中的物体因为噪声或其他原因被分割成小块时,闭运算可以将它们连接起来。
- 平滑边界:闭运算也可以平滑物体边界,使物体边界更加清晰。
2.4 代码示例(闭运算)
#include <opencv2/opencv.hpp>
#include <iostream>
int main() {
// 读取输入图像(假设是二值图像)
cv::Mat inputImage = cv::imread("binary_image.png", cv::IMREAD_GRAYSCALE);
// 检查图像是否成功读取
if (inputImage.empty()) {
std::cerr << "Image not loaded!" << std::endl;
return -1;
}
// 创建一个3x3的矩形结构元素
cv::Mat kernel = cv::getStructuringElement(cv::MORPH_RECT, cv::Size(3, 3));
// 执行闭运算(膨胀 + 腐蚀)
cv::Mat closedImage;
cv::morphologyEx(inputImage, closedImage, cv::MORPH_CLOSE, kernel);
// 显示原图和闭运算后的图像
cv::imshow("Original Image", inputImage);
cv::imshow("Closed Image", closedImage);
cv::waitKey(0);
return 0;
}
3. 开运算与闭运算的区别
| 特性 | 开运算 | 闭运算 |
|---|---|---|
| 操作顺序 | 先腐蚀,再膨胀 | 先膨胀,再腐蚀 |
| 作用 | 去除小的白色噪点,平滑物体的边界 | 填补小的黑色空洞,连接物体 |
| 主要应用 | 去噪、物体分割、平滑边界 | 填补小孔洞、物体连接、平滑边界 |
| 影响效果 | 物体边界收缩,细节被消除 | 物体边界扩展,细节被连接 |
5.3 顶帽运算与黑帽运算
顶帽运算(Top Hat)和黑帽运算(Black Hat)是形态学操作中的两种常见运算,它们都是通过开运算和闭运算的差值来处理图像的。它们能够帮助提取图像中某些特定区域的细节,常用于图像的特征提取、背景建模和物体检测等任务。
5.3.1 顶帽运算(Top Hat)
顶帽运算是原图像与其开运算结果的差值。开运算(Opening)是先腐蚀后膨胀,它通常用于去除小的噪声或细小的物体。顶帽运算则帮助我们提取出原图像中比背景亮的区域,也就是去除掉了背景后剩下的细节部分。
数学公式:
$$
\text{TopHat}(I) = I - \text{Opening}(I)
$$
其中:
I是原图像。- Opening 是开运算:先腐蚀再膨胀。
作用:
- 提取图像中比背景亮的部分。
- 强调细节部分,如物体表面的细小特征、噪声等。
应用场景:
- 细节增强:在图像中提取明亮区域,可以用于增强物体的边缘或小区域。
- 噪声去除:去除背景的平坦区域,只保留小的亮区域。
5.3.2 黑帽运算(Black Hat)
黑帽运算是闭运算结果与原图像的差值。闭运算(Closing)是先膨胀后腐蚀,它通常用于填补图像中的小黑洞或噪声。黑帽运算则帮助我们提取出原图像中比背景暗的区域,也就是去除掉物体后的背景部分。
数学公式:
$$
\text{BlackHat}(I) = \text{Closing}(I) - I
$$
其中:
I是原图像。- Closing 是闭运算:先膨胀再腐蚀。
作用:
- 提取图像中比背景暗的部分。
- 强调背景中的细节或小物体。
应用场景:
- 阴影提取:提取图像中的暗区域,可以用于检测背景中的阴影或其他暗物体。
- 背景增强:通过去除物体,提取出背景中较暗的区域。
5.3.3 在OpenCV中的实现
#include <opencv2/opencv.hpp>
#include <iostream>
using namespace cv;
using namespace std;
int main()
{
// 1. 读取图像
Mat image = imread("example.jpg", IMREAD_GRAYSCALE); // 以灰度模式读取图像
if (image.empty()) {
cout << "Could not open or find the image!" << endl;
return -1;
}
// 2. 显示原始图像
imshow("Original Image", image);
waitKey(0); // 等待按键事件
// 3. 创建结构元素(Kernel)
Mat kernel = getStructuringElement(MORPH_RECT, Size(3, 3)); // 使用3x3矩形结构元素
// 4. 顶帽运算(原图像 - 开运算结果)
Mat tophat;
morphologyEx(image, tophat, MORPH_TOPHAT, kernel);
// 5. 黑帽运算(闭运算结果 - 原图像)
Mat blackhat;
morphologyEx(image, blackhat, MORPH_BLACKHAT, kernel);
// 6. 显示结果
imshow("Top Hat Image", tophat); // 显示顶帽运算结果
imshow("Black Hat Image", blackhat); // 显示黑帽运算结果
waitKey(0); // 等待按键事件
// 释放窗口
destroyAllWindows();
return 0;
}
相信读者也能看出来,morphologyEx() 是一个非常灵活的函数。它可以执行各种形态学操作,帮助我们处理图像中的形状、边界、噪声等。通过调整结构元素的大小和形状,morphologyEx() 可以应用于许多不同的图像处理任务,如去噪、物体分割、边缘检测、细节提取等。