OpenCV C++ 边缘检测与图像分割
一、边缘检测
在数字图像处理领域,边缘检测是一项至关重要的基础技术。它如同为图像赋予 “骨架”,帮助计算机快速识别图像中的物体轮廓、形状与结构,广泛应用于目标识别、图像分割、图像配准等多个领域。
1.1 概念
边缘检测的核心目标是找出图像中像素灰度发生剧烈变化的区域边界。这些边界往往对应着图像中物体的轮廓、不同物体的交界处或纹理变化明显的地方。通过提取这些边缘信息,可以有效减少图像数据量,同时保留图像中最关键的结构特征,为后续的高级图像处理任务奠定基础。
1.2 边缘定义和类型
边缘定义
在图像中,边缘可以看作是局部区域内像素灰度值的不连续性。从数学角度理解,灰度值的变化率在边缘处会出现显著变化。例如,在一幅黑白图像中,从黑色区域过渡到白色区域的边界处,像素灰度值会从较低值突然跃升至较高值,这个过渡区域就是边缘所在之处。
边缘类型
-
阶跃型边缘:像素灰度值在边缘两侧呈现明显的阶梯状变化,一侧灰度值较低,另一侧较高,中间过渡区域极窄,类似于一个理想的阶跃函数。
-
屋顶型边缘:灰度值从较低水平逐渐上升至峰值,然后又迅速下降到较低水平,边缘像素位于灰度值变化的峰值位置,形状类似屋顶。
1.3 梯度概念
为了准确检测出图像中的边缘,需要引入梯度的概念。在二维图像 f ( x , y ) f(x, y) f(x,y) 中,梯度是一个向量,它的方向指向图像灰度增长最快的方向,其幅值反映了灰度变化的剧烈程度。
图像在点 ( x , y ) (x, y) (x,y) 处的梯度向量 ∇ f ( x , y ) \nabla f(x, y) ∇f(x,y) 可表示为:
∇ f ( x , y ) = [ ∂ f ∂ x ∂ f ∂ y ] \nabla f(x, y) = \begin{bmatrix} \frac{\partial f}{\partial x} \\ \frac{\partial f}{\partial y} \end{bmatrix} ∇f(x,y)=[∂x∂f∂y∂f]
其中, ∂ f ∂ x \frac{\partial f}{\partial x} ∂x∂f 和 ∂ f ∂ y \frac{\partial f}{\partial y} ∂y∂f 分别是图像在 x x x 方向和 y y y 方向上的偏导数。在实际计算中,通常使用有限差分来近似偏导数,常用的算子有Roberts 算子、Prewitt 算子和Sobel 算子。
Roberts 算子
原理
Roberts 算子是一种基于一阶差分的简单边缘检测算子,它利用局部差分来寻找边缘,对具有陡峭的低噪声图像效果较好。该算子采用 2×2 的卷积核,通过计算图像在对角线方向上的灰度差分来近似梯度。
假设图像在点 ( x , y ) (x,y) (x,y)处的灰度值为 f ( x , y ) f(x,y) f(x,y),Roberts 算子通过以下两个卷积核分别计算两个对角线方向的梯度:
G x = [ 1 0 0 − 1 ] ∗ f ( x , y ) G_x = \begin{bmatrix} 1 & 0 \\ 0 & -1 \end{bmatrix} * f(x, y) Gx=[100−1]∗f(x,y)
G y = [ 0 1 − 1 0 ] ∗ f ( x , y ) G_y = \begin{bmatrix} 0 & 1 \\ -1 & 0 \end{bmatrix} * f(x, y) Gy=[0−110]∗f(x,y)
这里的 “*” 表示卷积运算。以计算 G x G_x Gx为例,它会将卷积核覆盖在图像对应位置,将卷积核元素与图像像素值对应相乘后求和,得到该位置在 G x G_x Gx方向上的梯度近似值。然后通过公式 G = G x 2 + G y 2 G = \sqrt{G_x^2 + G_y^2} G=Gx2+Gy2计算梯度幅值,通过 θ = arctan ( G y G x ) \theta = \arctan(\frac{G_y}{G_x}) θ=arctan(GxGy)计算梯度方向 。
假设现在有图像为:
[ p 1 p 2 p 3 p 4 p 5 p 6 p 7 p 8 p 9 ] \begin{bmatrix} p1 & p2 & p3 \\ p4 & p5 & p6 \\ p7 & p8 & p9 \end{bmatrix} p1p4p7p2p5p8p3p6p9
像素点p5处,x,y方法向上的梯度:
g x = p 9 − p 5 g_x = p9-p5 gx=p9−p5
g y = p 8 − p 6 g_y = p8-p6 gy=p8−p6
当图像边缘接近±45°时,该算法效果最好。
Prewitt 算子
原理
Prewitt 算子同样基于一阶差分,它使用 3×3 的卷积核来计算图像在水平和垂直方向上的梯度。Prewitt 算子的卷积核考虑了像素点周围邻域的信息,相比 Roberts 算子能更好地抑制噪声。其两个卷积核如下:
G x = [ − 1 0 1 − 1 0 1 − 1 0 1 ] ∗ f ( x , y ) G_x = \begin{bmatrix} -1 & 0 & 1 \\ -1 & 0 & 1 \\ -1 & 0 & 1 \end{bmatrix} * f(x, y) Gx= −1−1−1000111 ∗f(x,y)
G y = [ − 1 − 1 − 1 0 0 0 1 1 1 ] ∗ f ( x , y ) G_y = \begin{bmatrix} -1 & -1 & -1 \\ 0 & 0 & 0 \\ 1 & 1 & 1 \end{bmatrix} * f(x, y) Gy= −101−101−101 ∗f(x,y)
G x G_x Gx卷积核用于检测水平方向的边缘, G y G_y Gy卷积核用于检测垂直方向的边缘。计算过程与 Roberts 算子类似,通过卷积运算得到水平和垂直方向的梯度,再计算梯度幅值和方向。
Sobel 算子
原理
Sobel 算子是一种更为常用的边缘检测算子,它在 Prewitt 算子的基础上进行了改进,通过对邻域像素赋予不同的权重,能更准确地计算图像梯度。Sobel 算子也使用 3×3 的卷积核,分别计算水平和垂直方向的梯度:
G x = [ − 1 0 1 − 2 0 2 − 1 0 1 ] ∗ f ( x , y ) G_x = \begin{bmatrix} -1 & 0 & 1 \\ -2 & 0 & 2 \\ -1 & 0 & 1 \end{bmatrix} * f(x, y) Gx= −1−2−1000121 ∗f(x,y)
G y = [ − 1 − 2 − 1 0 0 0 1 2 1 ] ∗ f ( x , y ) G_y = \begin{bmatrix} -1 & -2 & -1 \\ 0 & 0 & 0 \\ 1 & 2 & 1 \end{bmatrix} * f(x, y) Gy= −101−202−101 ∗f(x,y)
从卷积核可以看出,在水平方向的卷积核 G x G_x Gx中,中间一行的系数绝对值更大,意味着在计算水平方向梯度时,中间像素的权重更高;同理,在垂直方向的卷积核 G y G_y Gy中,中间一列的系数绝对值更大。这种加权方式使得 Sobel 算子对边缘的响应更加准确,同时对噪声也有一定的抑制能力。
使用方法
在 OpenCV 中,提供了Sobel
函数方便地实现 Sobel 算子:
#include <opencv2/opencv.hpp>
#include <iostream>using namespace cv;
using namespace std;int main() {Mat image = imread("lena.jpg", IMREAD_GRAYSCALE);if (image.empty()) {cout << "Could not open or find the image" << endl;return -1;}Mat gradient_x, gradient_y;Sobel(image, gradient_x, CV_16S, 1, 0, 3);Sobel(image, gradient_y, CV_16S, 0, 1, 3);Mat sobel_edges;convertScaleAbs(gradient_x, gradient_x);convertScaleAbs(gradient_y, gradient_y);addWeighted(gradient_x, 0.5, gradient_y, 0.5, 0, sobel_edges);imshow("Original Image", image);imshow("Sobel Edge Detection", sobel_edges);waitKey(0);return 0;
}
Sobel
函数参数说明:
-
image
:输入图像。 -
gradient_x/gradient_y
:输出的梯度图像,CV_16S
表示输出图像的数据类型为 16 位有符号整数,这是为了防止在计算梯度过程中出现溢出。 -
1, 0
或0, 1
:分别表示在 x x x方向或 y y y方向上计算梯度,若为1
则表示该方向上进行求导,0
表示不求导。 -
3
:表示 Sobel 算子的孔径大小,即卷积核大小为 3×3。
这三种算子各有特点:Roberts 算子计算简单、速度快,但对噪声敏感;Prewitt 算子通过扩大邻域考虑更多像素,有一定抗噪能力;Sobel 算子通过加权计算,在检测精度和抗噪性能上表现更优,是实际应用中较为常用的选择。在具体的边缘检测任务中,可根据图像特点和需求灵活选择合适的算子 。
1.4 Canny 原理
Canny 边缘检测算法是目前应用最广泛的边缘检测算法之一,由 John F. Canny 在 1986 年提出。它通过一系列步骤,在有效检测边缘的同时,最大程度地减少噪声干扰和虚假边缘,其核心步骤如下:
1. 高斯滤波
为了减少图像中的噪声对边缘检测的影响,首先使用高斯滤波器对图像进行平滑处理。高斯滤波的原理是利用高斯函数对图像进行加权平均,通过对邻域内像素值赋予不同权重,使得距离中心越近的像素影响越大,从而在平滑图像的同时保留一定的边缘信息。其公式为:
G ( x , y ) = 1 2 π σ 2 e − x 2 + y 2 2 σ 2 G(x,y) = \frac{1}{2\pi\sigma^2} e^{-\frac{x^2+y^2}{2\sigma^2}} G(x,y)=2πσ21e−2σ2x2+y2
其中, σ \sigma σ 是高斯分布的标准差,决定了高斯滤波器的平滑程度。
2. 计算梯度幅值和方向
使用 Sobel 算子等方法计算图像在 x x x 方向和 y y y 方向的梯度分量 G x G_x Gx 和 G y G_y Gy,进而得到梯度幅值 G G G 和梯度方向 θ \theta θ。梯度幅值反映了边缘的强度,梯度方向则指示了边缘的走向。
3. 非极大值抑制
经过梯度计算后,图像中的每个像素都有了对应的梯度幅值和方向。非极大值抑制的目的是将梯度幅值图像中潜在的边缘细化为单像素宽的边缘。具体做法是:对于每个像素,沿着其梯度方向,比较该像素与其两侧相邻像素的梯度幅值。如果该像素的梯度幅值不是局部最大值,则将其赋值为 0,即抑制该像素,认为它不是真正的边缘点;只有梯度幅值为局部最大值的像素才被保留,作为可能的边缘点。
4. 双阈值检测和边缘连接
设置两个阈值:高阈值 T h T_h Th 和低阈值 T l T_l Tl(通常 T h T_h Th 是 T l T_l Tl 的 2 - 3 倍)。将梯度幅值大于高阈值 T h T_h Th 的像素点确定为强边缘点;将梯度幅值小于低阈值 T l T_l Tl 的像素点直接排除,认为它们不是边缘点;对于梯度幅值介于 T l T_l Tl 和 T h T_h Th 之间的像素点,若其与强边缘点相连,则保留为弱边缘点,否则也将其排除。最后,通过边缘连接操作,将弱边缘点与其相连的强边缘点合并,形成完整的边缘。
1.5 Canny 实现
函数原型(OpenCV)
void Canny(InputArray image, OutputArray edges, double threshold1, double threshold2, int apertureSize = 3, bool L2gradient = false);
参数说明:
-
image
:输入图像,必须是单通道 8 位图像。 -
edges
:输出的边缘图像,与输入图像大小相同,也是单通道 8 位图像。 -
threshold1
:低阈值。 -
threshold2
:高阈值。 -
apertureSize
:用于计算梯度的 Sobel 算子的孔径大小,默认为 3。 -
L2gradient
:表示是否使用更精确的 L2 范数计算梯度幅值,若为false
,则使用 L1 范数,默认为false
。
示例代码
#include <opencv2/opencv.hpp>
#include <iostream>using namespace cv;
using namespace std;int main() {Mat image = imread("lena.jpg", IMREAD_GRAYSCALE);if (image.empty()) {cout << "Could not open or find the image" << endl;return -1;}Mat edges;// 使用Canny边缘检测,低阈值为50,高阈值为150Canny(image, edges, 50, 150);imshow("Original Image", image);imshow("Canny Edge Detection", edges);waitKey(0);return 0;
}
二、图像分割
在数字图像处理领域,图像分割是一项关键技术,其目标是将图像划分为若干个具有独特特性的区域,每个区域内的像素在某种特征(如灰度、颜色、纹理等)上较为相似,而不同区域之间的差异明显。通过图像分割,能够将复杂的图像简化为更易于分析和处理的结构,提取出感兴趣的目标,为后续的图像分析、目标识别、场景理解等任务奠定基础。
2.1 灰度图的阈值分割
灰度图像的阈值分割是最基础且常用的图像分割方法之一,它的核心作用是根据图像中目标与背景在灰度值上的差异,通过设定一个或多个阈值,将图像中的像素划分为不同的类别(通常为目标和背景两类),从而实现目标区域的提取和背景的分离。这种方法计算简单、效率高,适用于目标和背景灰度差异明显的图像。
阈值分割
原理
阈值分割的基本原理是:对于一幅灰度图像 f ( x , y ) f(x,y) f(x,y),选取一个合适的阈值 T T T,将图像中的每个像素点根据其灰度值 I ( x , y ) I(x,y) I(x,y)与阈值 T T T的大小关系进行分类:
-
若 I ( x , y ) ≥ T I(x,y) \geq T I(x,y)≥T,则将该像素点归为一类(通常视为目标区域);
-
若 I ( x , y ) < T I(x,y) < T I(x,y)<T,则将该像素点归为另一类(通常视为背景区域)。
通过这种方式,将灰度图像转换为二值图像,突出目标与背景的差异。通过这种方式,将灰度图像转换为二值图像,突出目标与背景的差异。
函数原型
double threshold(InputArray src, OutputArray dst, double thresh, double maxval, int type);
参数说明:
-
src
:输入的单通道图像(灰度图像)。 -
dst
:输出的二值化图像,与输入图像大小和类型相同。 -
thresh
:设定的阈值。 -
maxval
:当像素值满足条件时,赋予的最大值(通常在二值化中为 255)。 -
type
:阈值类型,常见的有:-
THRESH_BINARY
:二值化阈值处理,大于等于阈值的像素设为maxval
,小于阈值的设为 0。 -
THRESH_BINARY_INV
:反二值化阈值处理,大于等于阈值的像素设为 0,小于阈值的设为maxval
。 -
THRESH_TRUNC
:截断阈值处理,大于阈值的像素设为阈值,小于等于阈值的保持不变。 -
THRESH_TOZERO
:大于阈值的像素保持不变,小于阈值的设为 0。 -
THRESH_TOZERO_INV
:大于阈值的像素设为 0,小于等于阈值的保持不变。
-
示例代码
#include <opencv2/opencv.hpp>
#include <iostream>using namespace cv;
using namespace std;int main() {Mat image = imread("lena.jpg", IMREAD_GRAYSCALE);if (image.empty()) {cout << "Could not open or find the image" << endl;return -1;}Mat binary_image;// 使用THRESH_BINARY类型进行阈值分割,阈值设为127threshold(image, binary_image, 127, 255, THRESH_BINARY);imshow("Original Image", image);imshow("Binary Image (Thresholding)", binary_image);waitKey(0);return 0;
}
自适应阈值分割
原理
在实际应用中,图像的光照条件、背景复杂度可能不均匀,固定阈值的分割方法往往无法取得理想效果。自适应阈值分割能够根据图像局部区域的特征,动态地计算每个像素点的阈值,从而在复杂场景下实现更准确的分割。常见的自适应阈值计算方法是基于局部邻域的平均灰度值或高斯加权平均灰度值。
以基于局部邻域平均灰度值的方法为例,对于图像中的每个像素点 ( x , y ) (x,y) (x,y),计算其周围一个大小为 k × k k \times k k×k邻域内像素的平均灰度值 m ( x , y ) m(x,y) m(x,y),然后以 m ( x , y ) m(x,y) m(x,y)为基础,通过减去一个常数 C C C得到该像素点的阈值 T ( x , y ) T(x,y) T(x,y):
T ( x , y ) = m ( x , y ) − C T(x,y) = m(x,y) - C T(x,y)=m(x,y)−C
再根据上述阈值分割原理,将像素点进行分类。
函数原型
void adaptiveThreshold(InputArray src, OutputArray dst, double maxValue, int adaptiveMethod, int thresholdType, int blockSize, double C);
参数说明:
-
src
:输入的单通道图像(灰度图像)。 -
dst
:输出的二值化图像,与输入图像大小和类型相同。 -
maxValue
:当像素值满足条件时,赋予的最大值(通常在二值化中为 255)。 -
adaptiveMethod
:自适应阈值计算方法,常见的有:-
ADAPTIVE_THRESH_MEAN_C
:基于局部邻域的平均灰度值计算阈值。 -
ADAPTIVE_THRESH_GAUSSIAN_C
:基于局部邻域的高斯加权平均灰度值计算阈值。
-
-
thresholdType
:阈值类型,通常使用THRESH_BINARY
或THRESH_BINARY_INV
。 -
blockSize
:计算局部阈值时的邻域大小,必须为奇数。 -
C
:从平均灰度值或高斯加权平均灰度值中减去的常数。
示例代码
#include <opencv2/opencv.hpp>
#include <iostream>using namespace cv;
using namespace std;int main() {Mat image = imread("text_image.jpg", IMREAD_GRAYSCALE);if (image.empty()) {cout << "Could not open or find the image" << endl;return -1;}Mat adaptive_binary_image;// 使用ADAPTIVE_THRESH_GAUSSIAN_C方法,THRESH_BINARY类型,邻域大小为11,常数C为2adaptiveThreshold(image, adaptive_binary_image, 255, ADAPTIVE_THRESH_GAUSSIAN_C, THRESH_BINARY, 11, 2);imshow("Original Image", image);imshow("Adaptive Binary Image", adaptive_binary_image);waitKey(0);return 0;
}
2.2 彩色图像分割
彩色图像包含丰富的颜色信息,彩色图像分割旨在利用这些颜色信息,将图像中的不同物体或区域分割开来。相比于灰度图像分割,彩色图像分割能够更准确地提取目标,在物体识别、图像编辑、医学图像分析等领域有着广泛的应用。
HSV 分割
原理
HSV(Hue, Saturation, Value)色彩空间相较于常见的 RGB 色彩空间,更符合人类对颜色的感知方式。其中,H
(色调)表示颜色的种类,如红色、绿色、蓝色等;S
(饱和度)表示颜色的鲜艳程度;V
(明度)表示颜色的明亮程度。
HSV 分割的基本思路是:根据目标颜色在 HSV 空间中的取值范围,将图像从 RGB 色彩空间转换到 HSV 色彩空间,然后通过设定H
、S
、V
三个通道的阈值范围,筛选出符合条件的像素点,从而实现对特定颜色目标的分割。
例如,对于红色物体的分割,红色在 HSV 空间中的色调值通常分布在两个区间(因为红色跨越了 0 度和 360 度边界): [ 0 , 10 ] [0, 10] [0,10]和 [ 160 , 180 ] [160, 180] [160,180],同时可以设定饱和度和明度的合理范围,如 S ∈ [ 50 , 255 ] S \in [50, 255] S∈[50,255], V ∈ [ 50 , 255 ] V \in [50, 255] V∈[50,255],将满足这些条件的像素点提取出来。
void cvtColor(InputArray src, OutputArray dst, int code);
用于将图像从一种色彩空间转换到另一种色彩空间,在 HSV 分割中,使用COLOR_BGR2HSV
参数将 RGB 图像转换为 HSV 图像。
inRange(InputArray src, InputArray lowerb, InputArray upperb, OutputArray dst);
参数说明:
-
src
:输入图像(在 HSV 分割中为 HSV 图像)。 -
lowerb
:设定的下限阈值(包含三个通道的下限值)。 -
upperb
:设定的上限阈值(包含三个通道的上限值)。 -
dst
:输出的二值图像,符合阈值范围的像素设为 255,否则设为 0。
示例代码
#include <opencv2/opencv.hpp>
#include <iostream>using namespace cv;
using namespace std;int main() {Mat image = imread("fruits.jpg");if (image.empty()) {cout << "Could not open or find the image" << endl;return -1;}Mat hsv_image;// 将图像从BGR色彩空间转换到HSV色彩空间cvtColor(image, hsv_image, COLOR_BGR2HSV);Mat mask;// 设定红色的HSV阈值范围Scalar lower_red = Scalar(0, 50, 50);Scalar upper_red = Scalar(10, 255, 255);Scalar lower_red2 = Scalar(160, 50, 50);Scalar upper_red2 = Scalar(180, 255, 255);Mat mask1, mask2;inRange(hsv_image, lower_red, upper_red, mask1);inRange(hsv_image, lower_red2, upper_red2, mask2);bitwise_or(mask1, mask2, mask);Mat segmented_image;// 根据掩码提取红色物体bitwise_and(image, image, segmented_image, mask);imshow("Original Image", image);imshow("Segmented Image (HSV)", segmented_image);waitKey(0);return 0;
}
GrabCut
原理
GrabCut 算法全称是 “Grab and Cut”,即 “抓取并分割”,它是一种交互式的图像分割算法,旨在通过用户少量的交互操作,实现对图像中前景目标和背景的精准分割.
-
初始化:用户需要在图像上标记一个包含目标的矩形框,这个矩形框划定了算法处理的大致范围。此时,矩形框内的区域被初始化为 “可能的前景” 和 “可能的背景”,而矩形框外的区域则直接被认定为 “确定的背景”。这就好比在一幅画中,先圈出想要分割的大致区域,框外的部分直接排除在目标之外 。
-
模型训练 - 高斯混合模型(GMM):高斯混合模型是一种概率模型,它假设前景和背景中的像素分布都可以由多个高斯分布组合而成。算法会利用 GMM 分别对前景和背景进行参数估计,通过计算每个像素点与不同高斯分布的匹配程度,来判断该像素属于前景或背景的概率。简单来说,就是为前景和背景分别建立 “模板”,看每个像素更符合哪个 “模板” 。
-
能量函数最小化:GrabCut 定义了一个能量函数,这个函数综合考虑了数据项和光滑项。数据项衡量的是像素与 GMM 模型的匹配程度,即像素属于前景或背景的可能性大小;光滑项则关注相邻像素之间的一致性,希望相邻像素尽可能地属于同一类别(前景或背景),避免出现杂乱的分割结果。算法通过图割算法(Graph Cut)来迭代地最小化这个能量函数,不断调整每个像素的标记(前景或背景),使得整体的能量达到最小,也就意味着分割结果达到最优 。
-
重复迭代:不断重复模型训练和能量函数最小化的过程,每一次迭代都会让前景和背景的标记更加准确,直到前景和背景的标记不再发生显著变化,此时就得到了最终的分割结果。这个过程就像反复打磨一件艺术品,让分割边界越来越精准 。
函数原型
void grabCut(InputArray src, InputOutputArray mask, Rect rect, InputOutputArray bgdModel, InputOutputArray fgdModel, int iterCount, int mode=GC_EVAL);
参数说明:
-
src
:输入的 8 位 3 通道彩色图像,是待分割的原始图像。 -
mask
:输入输出的单通道 8 位掩码图像,用于存储每个像素的分类标记。初始时需设置部分区域,算法运行中不断更新。其取值有GC_BGD
(确定背景)、GC_FGD
(确定前景)、GC_PR_BGD
(可能背景)、GC_PR_FGD
(可能前景) 。 -
rect
:包含目标的矩形框,用于划定算法处理范围,格式为Rect(x, y, width, height)
,x
、y
是矩形左上角坐标,width
、height
是矩形宽和高。 -
bgdModel
:背景的高斯混合模型参数,算法内部使用和更新,是一个Mat
类型变量。 -
fgdModel
:前景的高斯混合模型参数,同样由算法内部使用和更新,也是Mat
类型变量。 -
iterCount
:算法执行的迭代次数,数值越大,分割结果越精细,但计算耗时也越长。 -
mode
:运行模式,常用GC_INIT_WITH_RECT
(使用矩形框初始化)和GC_INIT_WITH_MASK
(使用掩码初始化) ,默认值GC_EVAL
用于在已有初始化基础上进行评估和更新。
#include <opencv2/opencv.hpp>
#include <iostream>using namespace cv;
using namespace std;int main() {Mat image = imread("person.jpg");if (image.empty()) {cout << "Could not open or find the image" << endl;return -1;}// 初始化掩码图像,大小与输入图像相同,初始值全为GC_BGD(确定背景)Mat mask = Mat::zeros(image.size(), CV_8UC1); // 手动设置包含目标的矩形框Rect rect(100, 100, 300, 400); Mat bgdModel, fgdModel;// 使用矩形框初始化,迭代5次进行GrabCut分割grabCut(image, mask, rect, bgdModel, fgdModel, 5, GC_INIT_WITH_RECT); // 根据掩码生成最终分割结果,将前景像素保留,背景像素设为黑色Mat result;compare(mask, GC_PR_FGD, result, CMP_EQ); //GC_PR_FGD:这是一个常量值 (3),代表 "可能的前景" 标记;CMP_EQ:比较操作类型,表示 "相等" 比较Mat foreground(image.size(), CV_8UC3, Scalar(0, 0, 0));image.copyTo(foreground, result);imshow("Original Image", image);imshow("GrabCut Segmented Image", foreground);waitKey(0);return 0;
}
在上述代码中,首先读取图像,创建并初始化掩码图像,设置包含目标的矩形框。接着调用grabCut
函数进行分割,迭代 5 次优化分割结果。最后通过比较掩码图像,将属于 “可能前景” 的像素从原图复制到结果图像中,实现前景目标的提取与显示。
FloodFill
原理
FloodFill(漫水填充)算法,从名字就能直观地理解其原理 —— 像洪水淹没一样,从一个起始点开始,逐渐填充满足条件的区域。它不仅可以用于填充封闭区域,还能根据颜色相似性实现图像分割,在图像编辑、简单目标提取等场景中应用广泛。
-
选择种子点:首先,需要在图像中指定一个 “种子点” ( x , y ) (x,y) (x,y),这个点就像是洪水的源头,后续的填充操作都从这里开始扩散。种子点的选择可以由用户手动指定,也可以通过程序根据一定规则自动确定,比如图像的中心位置,或者某个颜色特征明显的点 。
-
邻域检查与填充:算法会检查种子点的像素值,并与周围邻域(通常是 4 连通邻域或 8 连通邻域)的像素值进行比较。这里的 “4 连通邻域” 指的是种子点上下左右四个方向的相邻像素,“8 连通邻域” 则还包括四个对角方向的像素 。若邻域像素值与种子点像素值的差异在设定的容差范围内,就认为这些邻域像素与种子点属于同一区域,将它们标记为填充区域,并把这些像素点加入到待处理队列中。这就好比洪水会向周围地势相近的区域蔓延 。
-
迭代填充:从待处理队列中取出一个像素点,将其作为新的 “种子点”,重复步骤 2 的操作,继续检查它的邻域像素并进行填充,直到待处理队列为空。此时,整个与初始种子点颜色相近且满足容差条件的连通区域都被填充完毕,实现了该区域的分割。整个过程就像洪水不断扩散,直到没有可以淹没的新区域为止 。
函数原型
int floodFill(InputOutputArray image, InputOutputArray mask, Point seedPoint, Scalar newVal, Rect* rect=0, Scalar loDiff=Scalar(), Scalar upDiff=Scalar(), int flags=4);
参数说明:
-
image
:输入输出的 8 位单通道或 3 通道图像,填充操作会直接修改该图像。 -
mask
:输入输出的单通道 8 位掩码图像,用于记录填充区域,其大小需比image
大一圈(宽和高各增加 2),且边缘需初始化为 0。 -
seedPoint
:填充操作的起始种子点,类型为Point
,指定x
、y
坐标。 -
newVal
:填充的新颜色值,对于单通道图像是一个标量,对于 3 通道图像是包含三个通道值的Scalar
类型 。 -
rect
:可选参数,用于记录填充区域的外接矩形,类型为Rect*
。 -
loDiff
:当前像素值与种子点像素值之间的下限容差,即像素值可以比种子点像素值小的最大差值。 -
upDiff
:当前像素值与种子点像素值之间的上限容差,即像素值可以比种子点像素值大的最大差值。 -
flags
:填充方式和操作标志,低 8 位表示邻域连通性(4 连通4
或 8 连通8
);高 8 位常用FLOODFILL_FIXED_RANGE
(基于种子点颜色固定容差范围)或FLOODFILL_MASK_ONLY
(只填充掩码图像,不修改原图) 。
#include <opencv2/opencv.hpp>
#include <iostream>using namespace cv;
using namespace std;int main() {Mat image = imread("colorful_image.jpg");if (image.empty()) {cout << "Could not open or find the image" << endl;return -1;}// 创建掩码图像,比原图大一圈,初始值为0Mat mask(image.rows + 2, image.cols + 2, CV_8UC1, Scalar(0)); Point seed(100, 100); // 设定种子点Scalar new_color(0, 255, 0); // 绿色// 进行漫水填充,8连通,容差设为50,基于种子点颜色固定容差范围floodFill(image, mask, seed, new_color, 0, Scalar(50), Scalar(50), 8 | FLOODFILL_FIXED_RANGE); imshow("Original Image", image);imshow("FloodFill Segmented Image", image);waitKey(0);return 0;
}
代码中先读取图像,创建合适的掩码图像并设定种子点、填充颜色。调用floodFill
函数时,设置为 8 连通填充,上下容差均为 50,采用基于种子点颜色固定容差范围的方式。执行后,图像中与种子点颜色相近的连通区域被填充为绿色,实现了基于颜色相似性的区域分割与显示。