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_BINARYcv::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)对图像进行卷积,以实现平滑效果。高斯核是一个二维矩阵,它的值由二维高斯函数计算得来。该矩阵中的每个值表示图像中相应位置的加权系数,权重由高斯函数(正态分布)决定。高斯滤波会给靠近卷积核中心像素赋予更大的权重,远离中心的像素赋予较小的权重,从而减少图像的模糊。

高斯滤波的计算过程

  1. 构造高斯核:根据所需的标准差 σ和核的大小(如 3×3、5×5等),计算得到高斯核矩阵。
  2. 卷积操作:将图像与高斯核进行卷积,滑动窗口(高斯核大小)遍历图像,每个像素值都与对应的高斯核元素相乘并求和,得到新的平滑后的图像。

高斯滤波常用于去噪声,特别是去除高斯噪声。

高斯滤波的关键参数:

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}
    $$

通过对图像进行卷积运算,分别得到水平方向和垂直方向的梯度GxGy

  • 边缘强度:梯度大小
    $$
    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 轮廓检测的基本步骤

轮廓检测的基本步骤通常包括以下几个部分:

  1. 图像预处理
    • 转换为灰度图像
    • 应用高斯模糊(去噪)
    • 使用边缘检测(如 Canny 算法)或阈值处理来二值化图像
  2. 轮廓查找
    • 使用 findContours() 函数检测图像中的轮廓。
  3. 轮廓绘制与分析
    • 使用 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:矩形结构元素

结构元素决定了操作时如何对每个像素的邻域进行处理,进而影响图像处理的形态(即结果的边界形状)。

例如:

  1. 矩形结构元素
    • 使用矩形结构元素,图像的物体边缘会以矩形的方式扩展或收缩,因为矩形结构元素会考虑到水平方向和垂直方向的邻域。腐蚀操作时,图像中的白色区域会以矩形形状收缩
    • 适用:对于一个有明显直线和角落的物体,矩形结构元素会使物体的边缘保持直线或者矩形的形状。
  2. 圆形结构元素
    • 使用圆形结构元素,图像中的物体边缘会呈现圆形的扩展或收缩,因为圆形结构元素会考虑到周围像素的圆形邻域。腐蚀操作时,图像中的白色区域会以圆形形状收缩
    • 适用:对于一个不规则形状的物体,使用圆形结构元素会使得物体的边缘变得更加平滑,尤其是弯曲的边缘会变得更加圆滑。圆形结构元素能够更自然地连接物体的曲线。
  3. 十字形结构元素
    • 使用十字形结构元素,膨胀和腐蚀操作的结果会沿着水平和垂直方向扩展或收缩。这意味着边缘在这些方向上会受到影响,但在对角线方向上不会被影响
    • 适用:如果物体的边缘是直线,十字形结构元素的膨胀操作会沿着这些直线方向扩展边缘,但不会影响到对角线上的物体形状。

总结一下,矩形结构元素会让边缘保持直线形状(矩形或方形)。圆形结构元素会让边缘变得圆滑,适合对不规则形状的物体进行处理。十字形结构元素则在水平和垂直方向上扩展边界,适用于对有明确水平或垂直边缘的图像进行处理。

cv::dilatecv::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 膨胀和腐蚀的实际应用

  1. 去噪
    • 腐蚀:用于去除二值图像中的小白点或噪声
    • 膨胀:用于填补物体边缘的小黑点或空洞。
  2. 物体边界处理
    • 膨胀:用来增强物体的边界或将小物体连接起来。
    • 腐蚀:用来减少物体边界,移除图像中的小物体。
  3. 形状提取
    • 通过连续的腐蚀和膨胀操作,可以提取图像中的物体形状、去除不必要的部分。
  4. 连接和分离物体
    • 通过膨胀,可以将分离的物体连接在一起;通过腐蚀,可以将连接在一起的物体分开。

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()
);
参数说明:
  1. src: 输入图像,必须是单通道的图像,通常为灰度图。它是需要应用形态学操作的源图像。
  2. dst: 输出图像,即形态学操作后的结果。
  3. op: 操作类型。这个参数指定了执行哪种形态学操作,它是一个整数值。OpenCV 提供了几种不同的操作类型,通过以下常量来指定:
    • MORPH_ERODE:腐蚀操作(erosion)
    • MORPH_DILATE:膨胀操作(dilation)
    • MORPH_OPEN:开运算(opening),先腐蚀再膨胀
    • MORPH_CLOSE:闭运算(closing),先膨胀再腐蚀
    • MORPH_TOPHAT:顶帽运算(top hat),原图减去开运算结果
    • MORPH_BLACKHAT:黑帽运算(black hat),闭运算结果减去原图
  4. kernel: 结构元素(kernel),和上文介绍的结构元素一样:
    • MORPH_RECT:矩形结构元素
    • MORPH_ELLIPSE:椭圆形结构元素
    • MORPH_CROSS:十字形结构元素
  5. anchor: 锚点位置,指定结构元素的中心点,默认值为 Point(-1, -1),表示结构元素的中心点自动设置为结构元素的几何中心。
  6. iterations: 迭代次数,指定形态学操作执行的次数。通常,我们只需要执行一次操作,但有时需要多次迭代来得到期望的效果。
  7. borderType: 边界类型,定义图像边界外的区域如何处理。常用的边界类型有:
    • BORDER_CONSTANT:常量边界(默认值)
    • BORDER_REFLECT:边界反射
    • BORDER_REPLICATE:边界复制
    • BORDER_WRAP:边界环绕
    • BORDER_REFLECT_101:反射类型 101
    • BORDER_TRANSPARENT:透明边界
  8. 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() 可以应用于许多不同的图像处理任务,如去噪、物体分割、边缘检测、细节提取等。


OpenCV入门
https://avisun.moe/archives/opencvf_basic
作者
Sunster
发布于
2025年10月25日
许可协议