OpenCV实战——基于GrabCut算法的图像分割

时间:2023-01-16 10:52:53

OpenCV实战——基于GrabCut算法的图像分割

1. GrabCut 算法

OpenCV 策略设计模式一节中,我们已经了解了颜色信息如何用于将图像分割成与场景特定元素相对应的区域。每类对象通常具有独特的颜色,通常可以通过识别相似颜色的区域来提取。OpenCV 提供了一种流行的图像分割算法—— GrabCut 算法的实现。GrabCut 是一种复杂且计算量大的算法,但它通常会得到非常准确的结果。该算法特别适合提取图像中的前景对象,例如,将目标对象从一张图片剪切并粘贴到另一张图片中。

2. 图像分割实战

cv::grabCut 函数的使用方法非常简单,只需要输入一个图像并将其中的一些像素标记为属于背景或前景。基于这些标记,算法可以分割图像的前景/背景。

(1) 为输入图像指定部分前景/背景标签的一种方法是定义一个矩形,其中包含前景对象:

// 定义边界框
cv::Rect rectangle(290, 180, 170, 215);

以上代码定义了图像中的以下区域:

OpenCV实战——基于GrabCut算法的图像分割

在输入图像中,该矩形之外的所有像素都将被标记为背景。

(2) 调用 cv::grabCut 函数需要定义两个矩阵,其中包含算法构建的模型:

// 分割结果
cv::Mat result;
// GrabCut 分割
cv::grabCut(
    image,      // 输入图像
    result,     // 分割结果
    rectangle,  // 包含前景的矩形
    bgModel, fgModel, // 模型
    5,          // 迭代次数
    cv::GC_INIT_WITH_RECT   // 使用矩形
);

我们使用 cv::GC_INIT_WITH_RECT 标志作为函数的最后一个参数,用于指定边界矩形模式。

(3) 输入/输出分割图像中的像素值可能属于以下四个值之一:

  • cv::GC_BGD:肯定属于背景的像素的值(例如,上图中矩形外的像素)
  • cv::GC_FGD:肯定属于前景的像素的值
  • cv::GC_PR_BGD:可能属于背景的像素值
  • cv::GC_PR_FGD:可能属于前景的像素的值(例如,上图中矩形内像素的初始值)

(4) 我们通过提取像素值等于 cv::GC_PR_FGD 的像素来获得分割的二值图像:

// 获取标记为可能前景的像素
cv::compare(result, cv::GC_PR_FGD, result, cv::CMP_EQ);
// 创建白色图像
cv::Mat foreground(image.size(), CV_8UC3, cv::Scalar(255, 255, 255));
image.copyTo(foreground, result);

(5) 要提取所有前景像素,即值等于 cv::GC_PR_FGDcv::GC_FGD,可以使用位运算获取:
result = result & 1;。常量 cv::GC_PR_FGDcv::GC_FGD 被定义为值 13,而 cv::GC_BGDcv::GC_PR_BGD 被定义为 02

执行以上程序,可以得到下图:

OpenCV实战——基于GrabCut算法的图像分割
在以上代码中,GrabCut 算法能够通过指定包含感兴趣对象的矩形来提取前景对象。或者,也可以将值 cv::GC_BGDcv::GC_FGD 分配给输入图像的某些特定像素,这些像素通过使用蒙版图像作为 cv::grabCut 函数的第二个参数提供,然后指定 GC_INIT_WITH_MASK 作为输入模式标志。例如,可以通过要求用户以交互方式标记图像中的一些元素来获得这些输入标签。也可以组合这两种输入模式。
使用以上输入信息,GrabCut 算法按以下步骤创建背景/前景分割。首先,将前景标签 (cv::GC_PR_FGD) 暂时分配给所有未标记的像素。算法根据当前的分类,将像素分为相似颜色的簇(即背景为 K 个簇,前景为 K 个簇);接下来,通过在前景和背景像素之间引入边界来确定背景/前景分割,这是通过优化过程完成的,该过程尝试将像素与相似标签进行连接,并在强度相对均匀的区域中放置边界施加惩罚,这一优化问题可以使用 Graph Cuts 算法解决,此算法方法通过将问题表示为应用切割的连通图来找到问题的最佳解,以获取最佳分割,获得的分割为像素分配新的标签。
然后重复聚类过程,并再次找到新的最佳分割,因此,GrabCut 算法是一个逐步改进分割结果的迭代过程。根据场景的复杂程度,可以在更多或更少的迭代次数中得到一个较好的解。
因此,用户可以指定要应用的迭代次数作为函数的参数。由算法维护的两个内部模型作为函数的参数传递(并返回),因此,如果希望通过执行额外的迭代来改进分割结果,可以再次使用上次运行的模型调用该函数。

3. 完整代码

完整代码 (extractObject.cpp) 如下所示:

#include <iostream>
#include <opencv2/core/core.hpp>
#include <opencv2/imgproc/imgproc.hpp>
#include <opencv2/highgui/highgui.hpp>

int main() {
    // 读取输入图像
    cv::Mat image = cv::imread("1.png");
    if (!image.data) return 0;
    cv::namedWindow("Original Image");
    cv::imshow("Original Image", image);
    // 定义边界框
    cv::Rect rectangle(290, 180, 170, 215);
    cv::Mat bgModel, fgModel;
    // 分割结果
    cv::Mat result;
    // GrabCut 分割
    cv::grabCut(
        image,      // 输入图像
        result,     // 分割结果
        rectangle,  // 包含前景的矩形
        bgModel, fgModel, // 模型
        5,          // 迭代次数
        cv::GC_INIT_WITH_RECT   // 使用矩形
    );
    // 获取标记为可能前景的像素
    cv::compare(result, cv::GC_PR_FGD, result, cv::CMP_EQ);
    // 或者
    // result = result & 1;
    // 创建白色图像
    cv::Mat foreground(image.size(), CV_8UC3, cv::Scalar(255, 255, 255));
    image.copyTo(foreground, result);
    // 在原图中绘制矩形框
    cv::rectangle(image, rectangle, cv::Scalar(255, 255, 255), 1);
    cv::namedWindow("Image with rectangle");
    cv::imshow("Image with rectangle", image);
    // 显示结果
    cv::namedWindow("Foreground object");
    cv::imshow("Foreground object", foreground);
    cv::waitKey();
    return 0;
}

相关链接

OpenCV实战(1)——OpenCV与图像处理基础
OpenCV实战(2)——OpenCV核心数据结构
OpenCV实战(3)——图像感兴趣区域
OpenCV实战(4)——像素操作
OpenCV实战(5)——图像运算详解
OpenCV实战(6)——OpenCV策略设计模式