left mouse button - set rectangle
SHIFT+left mouse button - set GC_FGD pixels
CTRL+left mouse button - set GC_BGD pixels
这段代码是一个使用OpenCV库的GrabCut算法进行图像分割的C++程序。它允许用户通过交互式方式选择图像中的一个区域,并利用GrabCut算法尝试将其分割出来。代码中包含用户操作指南、颜色定义、事件处理以及GrabCut算法的迭代过程,实现了用户通过鼠标交互和按键控制来迭代分割效果。程序逻辑结构清晰,采用面向对象的方式封装了GrabCut分割相关的操作。
// 引入OpenCV库中处理图像编解码的相关头文件
#include "opencv2/imgcodecs.hpp"
// 引入OpenCV库中高级GUI的相关头文件
#include "opencv2/highgui.hpp"
// 引入OpenCV库中图像处理的相关头文件
#include "opencv2/imgproc.hpp"
// 引入输入输出流的库文件
#include <iostream>
// 使用标准命名空间std,免去std::前缀
using namespace std;
// 使用OpenCV命名空间cv,免去cv::前缀
using namespace cv;
// 定义静态辅助函数,用于显示程序帮助信息
static void help(char** argv)
{
cout << "\nThis program demonstrates GrabCut segmentation -- select an object in a region\n"
"and then grabcut will attempt to segment it out.\n"
"Call:\n"
<< argv[0] << " <image_name>\n" // 显示如何调用程序
"\nSelect a rectangular area around the object you want to segment\n"
// 以下是热键操作说明
"\nHot keys: \n"
"\tESC - quit the program\n" // 按ESC键退出程序
"\tr - restore the original image\n" // 按'r'键还原到原始图像
"\tn - next iteration\n" // 按'n'键执行下一迭代步骤
"\n"
// 以下是鼠标操作说明
"\tleft mouse button - set rectangle\n" // 左键点击设置矩形区域
"\n"
"\tCTRL+left mouse button - set GC_BGD pixels\n" // 按住CTRL键同时左键点击设置背景像素
"\tSHIFT+left mouse button - set GC_FGD pixels\n" // 按住SHIFT键同时左键点击设置前景像素
"\n"
"\tCTRL+right mouse button - set GC_PR_BGD pixels\n" // 按住CTRL键同时右键点击设置可能的背景像素
"\tSHIFT+right mouse button - set GC_PR_FGD pixels\n" << endl; // 按住SHIFT键同时右键点击设置可能的前景像素
}
// 定义一些标量常量表示不同的颜色
const Scalar RED = Scalar(0,0,255);
const Scalar PINK = Scalar(230,130,255);
const Scalar BLUE = Scalar(255,0,0);
const Scalar LIGHTBLUE = Scalar(255,255,160);
const Scalar GREEN = Scalar(0,255,0);
// 定义背景键和前景键的标志,用于鼠标操作事件中
const int BGD_KEY = EVENT_FLAG_CTRLKEY;
const int FGD_KEY = EVENT_FLAG_SHIFTKEY;
// 声明静态函数,用于从复合掩模图中提取二值掩模图
static void getBinMask( const Mat& comMask, Mat& binMask )
{
// 确保comMask非空且类型为CV_8UC1,否则抛出错误
if( comMask.empty() || comMask.type()!=CV_8UC1 )
CV_Error( Error::StsBadArg, "comMask is empty or has incorrect type (not CV_8UC1)" );
// 如果binMask为空或大小与comMask不一致,则重新创建与comMask大小一致的binMask
if( binMask.empty() || binMask.rows!=comMask.rows || binMask.cols!=comMask.cols )
binMask.create( comMask.size(), CV_8UC1 );
// 将comMask中的最低位复制到binMask中
binMask = comMask & 1;
}
// 声明GrabCut应用类,用于实现GrabCut算法的交互操作
// 定义用于GrabCut算法的应用类
class GCApplication
{
public:
// 定义选择状态的枚举类型
enum{ NOT_SET = 0, IN_PROCESS = 1, SET = 2 };
// 定义圆的半径
static const int radius = 2;
// 定义圆的线条厚度,-1表示填充圆
static const int thickness = -1;
// 成员函数声明
void reset(); // 重置函数
void setImageAndWinName( const Mat& _image, const string& _winName ); // 设置图像和窗口名
void showImage() const; // 展示图像
void mouseClick( int event, int x, int y, int flags, void* param ); // 鼠标点击事件处理函数
int nextIter(); // 迭代处理函数
int getIterCount() const { return iterCount; } // 获取迭代次数的函数
private:
// 私有成员函数和变量声明
void setRectInMask(); // 设置矩形到掩模中
void setLblsInMask( int flags, Point p, bool isPr ); // 设置标签到掩模中的函数
const string* winName; // 窗口名称
const Mat* image; // 图像引用
Mat mask; // 掩模矩阵
Mat bgdModel, fgdModel; // 背景和前景模型
uchar rectState, lblsState, prLblsState; // 矩形和标签状态变量
bool isInitialized; // 是否已经初始化的标志
Rect rect; // 矩形区域
vector<Point> fgdPxls, bgdPxls, prFgdPxls, prBgdPxls; // 记录前景和背景像素位置的向量
int iterCount; // 迭代计数
};
// 成员函数定义
// 重置GCApplication
void GCApplication::reset()
{
if( !mask.empty() )
mask.setTo(Scalar::all(GC_BGD)); // 如果掩模非空,设置掩模的所有值为GC_BGD
// 清除前景、背景像素位置的记录
bgdPxls.clear(); fgdPxls.clear();
prBgdPxls.clear(); prFgdPxls.clear();
// 重置相关的状态标志和迭代计数
isInitialized = false;
rectState = NOT_SET;
lblsState = NOT_SET;
prLblsState = NOT_SET;
iterCount = 0;
}
// 设置图像和窗口名
void GCApplication::setImageAndWinName( const Mat& _image, const string& _winName )
{
if( _image.empty() || _winName.empty() )
return; // 如果图像或窗口名为空,直接返回
image = &_image; // 设置图像引用
winName = &_winName; // 设置窗口名
mask.create( image->size(), CV_8UC1); // 创建与图像相同大小的掩模矩阵
reset(); // 重置GCApplication
}
// 展示图像
void GCApplication::showImage() const
{
if( image->empty() || winName->empty() )
return; // 如果图像为空或窗口名为空,直接返回
Mat res;
Mat binMask;
image->copyTo( res ); // 复制图像到res
if( isInitialized ){
getBinMask( mask, binMask); // 如果已经初始化,获取二值掩模
Mat black (binMask.rows, binMask.cols, CV_8UC3, cv::Scalar(0,0,0));
black.setTo(Scalar::all(255), binMask); // 设置黑色图像的掩模区域为白色
addWeighted(black, 0.5, res, 0.5, 0.0, res); // 将黑色图像与原图混合
}
vector<Point>::const_iterator it;
// 绘制背景和前景像素位置
for( it = bgdPxls.begin(); it != bgdPxls.end(); ++it )
circle( res, *it, radius, BLUE, thickness );
for( it = fgdPxls.begin(); it != fgdPxls.end(); ++it )
circle( res, *it, radius, RED, thickness );
for( it = prBgdPxls.begin(); it != prBgdPxls.end(); ++it )
circle( res, *it, radius, LIGHTBLUE, thickness );
for( it = prFgdPxls.begin(); it != prFgdPxls.end(); ++it )
circle( res, *it, radius, PINK, thickness );
// 绘制矩形框
if( rectState == IN_PROCESS || rectState == SET )
rectangle( res, Point( rect.x, rect.y ), Point(rect.x + rect.width, rect.y + rect.height ), GREEN, 2);
imshow( *winName, res ); // 显示图像
}
// GCApplication类的成员函数:设置矩形框到掩模中
void GCApplication::setRectInMask()
{
CV_Assert( !mask.empty() ); // 确认掩模不为空
mask.setTo( GC_BGD ); // 将掩模的全部像素值设置为背景
// 修正矩形坐标,确保矩形框不会超出图像范围
rect.x = max(0, rect.x);
rect.y = max(0, rect.y);
rect.width = min(rect.width, image->cols-rect.x);
rect.height = min(rect.height, image->rows-rect.y);
// 将矩形框内的像素值设置为可能的前景
(mask(rect)).setTo( Scalar(GC_PR_FGD) );
}
// GCApplication类的成员函数:根据用户输入,在掩模上设置前景或背景标签
void GCApplication::setLblsInMask( int flags, Point p, bool isPr )
{
vector<Point> *bpxls, *fpxls; // 指向背景和前景像素点的向量
uchar bvalue, fvalue; // 背景和前景的像素值
// 根据是否是概率图(isPr)设置不同的指针和像素值
if( !isPr )
{
bpxls = &bgdPxls;
fpxls = &fgdPxls;
bvalue = GC_BGD;
fvalue = GC_FGD;
}
else
{
bpxls = &prBgdPxls;
fpxls = &prFgdPxls;
bvalue = GC_PR_BGD;
fvalue = GC_PR_FGD;
}
// flags中对应的按键标志位被设置,更新背景像素点向量,并在掩模上画圆
if( flags & BGD_KEY )
{
bpxls->push_back(p);
circle( mask, p, radius, bvalue, thickness );
}
// flags中对应的按键标志位被设置,更新前景像素点向量,并在掩模上画圆
if( flags & FGD_KEY )
{
fpxls->push_back(p);
circle( mask, p, radius, fvalue, thickness );
}
}
// GCApplication类的成员函数:处理鼠标点击事件
void GCApplication::mouseClick( int event, int x, int y, int flags, void* )
{
switch( event )
{
// 左键按下事件:设置矩形或者GC_BGD/GC_FGD像素标签
case EVENT_LBUTTONDOWN:
{
bool isb = (flags & BGD_KEY) != 0,
isf = (flags & FGD_KEY) != 0;
if( rectState == NOT_SET && !isb && !isf )
{
rectState = IN_PROCESS; // 开始绘制矩形框
rect = Rect( x, y, 1, 1 ); // 初始化矩形框大小
}
if ( (isb || isf) && rectState == SET )
lblsState = IN_PROCESS; // 开始设置标签状态
}
break;
// 右键按下事件:设置GC_PR_BGD/GC_PR_FGD像素标签
case EVENT_RBUTTONDOWN:
{
bool isb = (flags & BGD_KEY) != 0,
isf = (flags & FGD_KEY) != 0;
if ( (isb || isf) && rectState == SET )
prLblsState = IN_PROCESS; // 开始设置概率标签状态
}
break;
// 左键释放事件:完成矩形绘制或者设置像素标签
case EVENT_LBUTTONUP:
if( rectState == IN_PROCESS )
{
// 如果起点和终点一样,则不设置矩形框
if(rect.x == x || rect.y == y){
rectState = NOT_SET;
}
else{
rect = Rect( Point(rect.x, rect.y), Point(x,y) );
rectState = SET; // 设置矩形框状态为已设置
setRectInMask(); // 在掩模上绘制矩形框
CV_Assert( bgdPxls.empty() && fgdPxls.empty() && prBgdPxls.empty() && prFgdPxls.empty() );
}
showImage(); // 显示图像
}
if( lblsState == IN_PROCESS )
{
setLblsInMask(flags, Point(x,y), false); // 设置像素标签
lblsState = SET; // 设置标签状态为已设置
nextIter(); // 进行一次迭代
showImage(); // 显示图像
}
else{
// 如果矩形框状态已设置,进行一次迭代后显示图像
if(rectState == SET){
nextIter();
showImage();
}
}
break;
// 右键释放事件:完成概率标签的设置
case EVENT_RBUTTONUP:
if( prLblsState == IN_PROCESS )
{
setLblsInMask(flags, Point(x,y), true); // 设置概率标签
prLblsState = SET; // 设置概率标签状态为已设置
}
// 如果矩形框状态已设置,进行一次迭代后显示图像
if(rectState == SET){
nextIter();
showImage();
}
break;
// 鼠标移动事件:更新矩形框大小或者继续设置像素标签
case EVENT_MOUSEMOVE:
if( rectState == IN_PROCESS )
{
rect = Rect( Point(rect.x, rect.y), Point(x,y) );
CV_Assert( bgdPxls.empty() && fgdPxls.empty() && prBgdPxls.empty() && prFgdPxls.empty() );
showImage(); // 显示图像
}
else if( lblsState == IN_PROCESS )
{
setLblsInMask(flags, Point(x,y), false); // 设置像素标签
showImage(); // 显示图像
}
else if( prLblsState == IN_PROCESS )
{
setLblsInMask(flags, Point(x,y), true); // 设置概率标签
showImage(); // 显示图像
}
break;
}
}
// 主函数入口
int main( int argc, char** argv )
{
// 解析命令行参数
cv::CommandLineParser parser(argc, argv, "{@input| messi5.jpg |}");
// 调用help函数显示帮助信息
help(argv);
// 获取图片文件名
string filename = parser.get<string>("@input");
// 如果文件名为空,输出错误信息并返回1
if( filename.empty() )
{
cout << "\nDurn, empty filename" << endl;
return 1;
}
// 读取图片
Mat image = imread(samples::findFile(filename), IMREAD_COLOR);
// 如果读取失败,输出错误信息并返回1
if( image.empty() )
{
cout << "\n Durn, couldn't read image filename " << filename << endl;
return 1;
}
// 创建窗口
const string winName = "image";
namedWindow( winName, WINDOW_AUTOSIZE );
// 设置鼠标回调函数
setMouseCallback( winName, on_mouse, 0 );
// 设置gcapp对象的图片和窗口名称
gcapp.setImageAndWinName( image, winName );
// 显示图片
gcapp.showImage();
// 无限循环,等待用户按键
for (;;)
{
// 等待按键事件
char c = (char)waitKey(0);
switch (c)
{
case '\x1b': // 按Esc键退出
cout << "Exiting ..." << endl;
goto exit_main;
case 'r': // 按'r'键重置
cout << endl;
gcapp.reset(); // 调用重置函数
gcapp.showImage(); // 显示图像
break;
case 'n': // 按'n'键进行下一次迭代
int iterCount = gcapp.getIterCount(); // 获取当前迭代次数
cout << "<" << iterCount << "... ";
int newIterCount = gcapp.nextIter(); // 调用下一个迭代
if (newIterCount > iterCount)
{
gcapp.showImage(); // 显示新的图像
cout << iterCount << ">" << endl;
}
else
cout << "rect must be determined>" << endl; // 提示需要先确定矩形框
break;
}
}
exit_main:
// 销毁窗口并退出
destroyWindow( winName );
return 0;
}
vector<Point>::const_iterator it;
// 绘制背景和前景像素位置
for (it = bgdPxls.begin(); it != bgdPxls.end(); ++it)
circle(res, *it, radius, BLUE, thickness);
for (it = fgdPxls.begin(); it != fgdPxls.end(); ++it)
circle(res, *it, radius, RED, thickness);
for (it = prBgdPxls.begin(); it != prBgdPxls.end(); ++it)
circle(res, *it, radius, LIGHTBLUE, thickness);
for (it = prFgdPxls.begin(); it != prFgdPxls.end(); ++it)
circle(res, *it, radius, PINK, thickness);
grabCut(*image, mask, rect, bgdModel, fgdModel, 1);
// 如果标签已经设置或概率标签已经设置,用mask初始化grabCut
if (lblsState == SET || prLblsState == SET)
grabCut(*image, mask, rect, bgdModel, fgdModel, 1, GC_INIT_WITH_MASK);
else
// 否则,用rect初始化grabCut
grabCut(*image, mask, rect, bgdModel, fgdModel, 1, GC_INIT_WITH_RECT);