基本概念
grabCut 算法是 OpenCV 中一个非常强大的图像分割方法,主要用于前景与背景的分割。它基于图割(Graph Cut)算法,并通过迭代的方式优化能量函数,以达到更好的分割效果。在 OpenCV 中,grabCut 函数通常用于处理包含复杂背景的图片,以分离出感兴趣的前景对象。
grabCut() 是 OpenCV 提供的一个高级图像分割算法,它可以用来自动地分割图像中的对象。GrabCut 算法最初由 Carsten Rother 等人在 2004 年提出,它结合了图割技术与机器学习方法,可以在用户提供少量的指导信息后,自动地完成复杂的分割任务。
grabCut 函数详解
grabCut 函数的原型通常如下(基于 OpenCV 4.x):
void cv::grabCut(InputArray img,
InputOutputArray mask,
Rect rect,
InputOutputArray bgdModel,
InputOutputArray fgdModel,
int iterCount,
int mode=GC_INIT_WITH_RECT);
参数详解
img:输入图像,通常应为 8 位 3 通道图像(例如,CV_8UC3 类型的图像)。
mask:操作掩码,用于指定每个像素的状态(可能是背景、前景、可能是背景、可能是前景)。掩码在调用函数前需要初始化,通常是将感兴趣的区域(ROI)内的像素标记为 GC_PR_FGD(可能是前景),ROI 外的像素标记为 GC_BGD(背景),ROI 边界上的像素标记为 GC_PR_BGD(可能是背景)或 GC_PR_FGD(可能是前景)。
rect:包含感兴趣区域的矩形。这个矩形应该尽可能小但完全包含目标对象。
bgdModel 和 fgdModel:分别用于存储背景和前景的模型。这些通常是浮点数数组,但在 OpenCV 中通常作为 Mat 对象处理。它们的大小依赖于迭代次数和图像大小,但通常不需要手动设置。
iterCount:算法执行的最大迭代次数。迭代次数越多,分割效果通常越好,但计算时间也越长。
mode:算法的工作模式。GC_INIT_WITH_RECT 表示使用矩形初始化;GC_INIT_WITH_MASK 表示使用掩码初始化。
使用示例1
以下是一个简单的使用 grabCut 的示例:
#include <opencv2/opencv.hpp>
#include <iostream>
int main() {
cv::Mat img = cv::imread("path_to_image.jpg");
if (img.empty()) {
std::cerr << "Could not read the image." << std::endl;
return 1;
}
cv::Rect rect(50, 50, 450, 290); // 假设的感兴趣区域
cv::Mat mask = cv::Mat::zeros(img.size(), CV_8UC1);
cv::Rect rect_bgd(0, 0, img.cols, img.rows);
mask(rect_bgd).setTo(cv::GC_BGD);
mask(rect).setTo(cv::Scalar::all(cv::GC_PR_FGD));
cv::Mat bgdModel, fgdModel;
int iterCount = 5; // 迭代次数
cv::grabCut(img, mask, rect, bgdModel, fgdModel, iterCount, cv::GC_INIT_WITH_RECT);
// 创建一个结果图像
cv::Mat result;
img.copyTo(result, mask == cv::GC_FGD);
cv::imshow("Result", result);
cv::waitKey(0);
return 0;
}
运行结果1
下面是如何使用 OpenCV 的 grabCut() 函数进行图像分割的详细步骤:
1.导入必要的头文件:
#include <opencv2/opencv.hpp>
#include <iostream>
using namespace cv;
using namespace std;
2.加载图像:
Mat src = imread("path/to/your/image.jpg");
if (src.empty()) {
cout << "Could not open or find the image" << endl;
return -1;
}
3.创建掩码:
grabCut() 需要一个掩码来指示哪些区域是确定前景、确定背景或不确定的。我们可以初始化一个与图像大小相同的矩阵,然后手动填充一些区域作为提示。
Mat mask(src.size(), CV_8UC1, Scalar(0)); // 初始化为全背景
Rect rect(x, y, w, h); // 指定一个矩形框选区域作为提示
int cvtRect[] = {rect.x, rect.y, rect.width, rect.height};
grabCut(src, mask, Rect(cvtRect[0], cvtRect[1], cvtRect[2], cvtRect[3]), Mat(), Mat(), 5, GC_INIT_WITH_RECT);
4.细化掩码:
grabCut() 执行后,掩码会被更新。你可以根据需要进一步细化掩码。
Mat bgModel, fgModel; // 这两个模型用于存储背景和前景的概率模型
grabCut(src, mask, Rect(), bgModel, fgModel, 5, GC_INIT_WITH_MASK);
5.应用掩码:
应用掩码到原图上,得到分割后的图像。
Mat dst;
src.copyTo(dst, mask == 1); // 只复制确定为前景的部分
6.显示结果:
显示原始图像和分割
namedWindow("Original Image", WINDOW_AUTOSIZE);
imshow("Original Image", src);
namedWindow("Segmented Image", WINDOW_AUTOSIZE);
imshow("Segmented Image", dst);
7.等待按键退出:
waitKey(0); // Wait for a keystroke in the window
8.清理:
不要忘记释放所有使用的内存资源。
destroyAllWindows();
注意事项
grabCut() 函数需要用户提供的提示来区分前景和背景。这些提示可以通过矩形框或者手动绘制的掩码来提供。
grabCut() 可以通过迭代来逐步改进分割结果。参数 iterations 控制迭代次数。
GC_INIT_WITH_RECT 表示使用矩形框作为初始提示;GC_INIT_WITH_MASK 则表示使用掩码作为初始提示。
分割结果储存在掩码中,其中:
GC_BGD (0) 表示确定的背景像素。
GC_FGD (1) 表示确定的前景像素。
GC_PR_BGD (2) 表示可能是背景的像素。
GC_PR_FGD (3) 表示可能是前景的像素。
这个例子演示了如何使用 grabCut() 函数来分割图像中的对象。实际应用中,你可能需要根据具体需求调整矩形框的位置和大小,或者使用交互式的方式让用户手动标注部分区域来提高分割精度
完整代码2
#include "pch.h"
//1.导入必要的头文件:
#include <opencv2/opencv.hpp>
#include <iostream>
using namespace cv;
using namespace std;
int main()
{
//2.加载图像:
Mat src = imread("88.jpeg");
if (src.empty())
{
cout << "Could not open or find the image" << endl;
return -1;
}
//3.创建掩码:
// grabCut() 需要一个掩码来指示哪些区域是确定前景、确定背景或不确定的。我们可以初始化一个与图像大小相同的矩阵,然后手动填充一些区域作为提示。
Mat mask(src.size(), CV_8UC1, Scalar(0)); // 初始化为全背景
Rect rect(120, 120, 500, 1120); // 指定一个矩形框选区域作为提示
int cvtRect[] = { rect.x, rect.y, rect.width, rect.height };
grabCut(src, mask, Rect(cvtRect[0], cvtRect[1], cvtRect[2], cvtRect[3]), Mat(), Mat(), 5, GC_INIT_WITH_RECT);
//4.细化掩码:
// grabCut() 执行后,掩码会被更新。你可以根据需要进一步细化掩码。
Mat bgModel, fgModel; // 这两个模型用于存储背景和前景的概率模型
grabCut(src, mask, Rect(), bgModel, fgModel, 5, GC_INIT_WITH_MASK);
//5.应用掩码:
//应用掩码到原图上,得到分割后的图像。
Mat dst;
src.copyTo(dst, mask == 1); // 只复制确定为前景的部分
//6.显示结果:
//显示原始图像和分割
namedWindow("Original Image", WINDOW_NORMAL);
imshow("Original Image", src);
namedWindow("Segmented Image", WINDOW_NORMAL);
imshow("Segmented Image", dst);
//7.等待按键退出:
waitKey(0); // Wait for a keystroke in the window
//8.清理:
//不要忘记释放所有使用的内存资源。
destroyAllWindows();
return 0;
}
//注意事项
//grabCut() 函数需要用户提供的提示来区分前景和背景。这些提示可以通过矩形框或者手动绘制的掩码来提供。
//grabCut() 可以通过迭代来逐步改进分割结果。参数 iterations 控制迭代次数。
//GC_INIT_WITH_RECT 表示使用矩形框作为初始提示;GC_INIT_WITH_MASK 则表示使用掩码作为初始提示。
//分割结果储存在掩码中,其中:
//GC_BGD(0) 表示确定的背景像素。
//GC_FGD(1) 表示确定的前景像素。
//GC_PR_BGD(2) 表示可能是背景的像素。
//GC_PR_FGD(3) 表示可能是前景的像素。
//这个例子演示了如何使用 grabCut() 函数来分割图像中的对象。实际应用中,你可能需要根据具体需求调整矩形框的位置和大小,或者使用交互式的方式让用户手动标注部分区域来提高分割精度
运行结果2
注意
1.grabCut 需要良好的初始估计,特别是 ROI 的选择对最终结果有很大影响。
2.迭代次数是一个重要的参数,需要根据实际情况调整以达到最佳效果。
3.grabCut 在处理复杂背景或前景对象与背景颜色相近时可能效果不佳。
4.结果图像 result 是通过将原图 img 复制到新图 result 中,但只复制那些被标记为前景的像素来得到的。
实验代码3
#include "pch.h"
//#pragma comment(lib, "opencv_world450d.lib")
#include "opencv2/imgproc.hpp"
#include "opencv2/highgui.hpp"
#include <opencv2/imgproc/types_c.h>
#include <iostream>
using namespace std;
using namespace cv;
void main()
{
Mat src = imread("02.png");
Rect rect(84, 84, 406, 318);//左上坐标(X,Y)和长宽
Mat result, bg, fg;
grabCut(src, result, rect, bg, fg, 1, GC_INIT_WITH_RECT);
imshow("grab", result);
/*threshold(result, result, 2, 255, CV_THRESH_BINARY);
imshow("threshold", result);*/
compare(result, GC_PR_FGD, result, CMP_EQ);//result和GC_PR_FGD对应像素相等时,目标图像该像素值置为255
imshow("result", result);
Mat foreground(src.size(), CV_8UC3, Scalar(255, 255, 255));
src.copyTo(foreground, result);//copyTo有两种形式,此形式表示result为mask
namedWindow("foreground", WINDOW_NORMAL);
imshow("foreground", foreground);
waitKey(0);
}