在之前的创作中心-CSDN滚动条调整图片亮度-CSDN博客创作中心-CSDN中,我们已经了解了滚动条实现亮度以及对比度调节,为了实现对图像中感兴趣区域(ROI, Region of Interest)的交互式选取,本文利用 OpenCV 提供的鼠标事件回调机制,设计并实现了一套基于鼠标操作的图像区域标注功能。用户通过点击鼠标左键确定矩形区域的起点,并在拖动过程中实时显示选框,释放鼠标左键后自动绘制最终的矩形框并截取所选区域。
首先,我们先了解所有的鼠标事件:
事件常量 | 描述(鼠标事件) |
---|---|
EVENT_MOUSEMOVE | 鼠标移动 |
EVENT_LBUTTONDOWN | 鼠标左键按下 |
EVENT_LBUTTONUP | 鼠标左键释放 |
EVENT_LBUTTONDBLCLK | 鼠标左键双击 |
EVENT_RBUTTONDOWN | 鼠标右键按下 |
EVENT_RBUTTONUP | 鼠标右键释放 |
EVENT_RBUTTONDBLCLK | 鼠标右键双击 |
EVENT_MBUTTONDOWN | 鼠标中键按下 |
EVENT_MBUTTONUP | 鼠标中键释放 |
EVENT_MBUTTONDBLCLK | 鼠标中键双击 |
EVENT_MOUSEWHEEL | 鼠标滚轮垂直滚动(上滚为正,下滚为负) |
EVENT_MOUSEHWHEEL | 鼠标滚轮水平滚动(右滚为正,左滚为负) |
简单了解之后,需要深度理解鼠标响应函数:cv::setMouseCallback()
void cv::setMouseCallback(const String& winname, MouseCallback onMouse, void* userdata = 0);
参数 | 类型 | 说明 |
---|---|---|
winname | const String& | 要监听鼠标事件的窗口名称,必须是通过 cv::namedWindow() 创建的窗口 |
onMouse | MouseCallback | 用户定义的鼠标事件回调函数指针 |
userdata | void* (可选) | 可选的用户数据指针,可传递额外参数,如图像指针或上下文结构体 |
有了之前的基础后,对于回调函数的定义就较为容易了:
void onMouse(int event, int x, int y, int flags, void* userdata);
参数 | 说明 |
---|---|
event | 当前触发的鼠标事件(如 EVENT_LBUTTONDOWN ) |
x, y | 鼠标当前在图像坐标系中的位置 |
flags | 当前的鼠标键/修饰键状态(如 EVENT_FLAG_CTRLKEY ) |
userdata | 通过 setMouseCallback() 传递的用户指针 |
在了解相关的函数后,就可以开始实现开头的功能了,首先,我们依旧是在hpp文件定义这个方法函数:
class Demo{
public:void colorspace_Demo(Mat &image);void Mat_creat(Mat &image);void pixel_RW_Demo(Mat &image);void operator_Demo(Mat &image);void Tracking_Demo(Mat &image);void Color_Demo(Mat &image);void bitwise_Demo(Mat &image);void channel_Demo(Mat &image);void inrange_Demo(Mat &image);void pixel_statistics_Demo(Mat &image);void Shapes_Demo(Mat &image);void polygon_drawing_Demo();void random_Demo();void mouse_Demo(Mat &image);
};
紧接着回到Demo.cpp定义该函数:
void Demo::mouse_Demo(Mat &image)
{namedWindow("mouse_draw",WINDOW_AUTOSIZE);setMouseCallback("mouse_draw",draw,(void*)(&image));imshow("mouse_draw",image);
}
首先生成一个窗口,用于鼠标操作显示,draw是回调换函数;接下来定义draw回调函数:接下来,我们一步一步来实现鼠标框选区域的功能:
1.鼠标左键按下;
2.左键按下的同时鼠标移动;
3.左键松去,画出所框区域;
那么针对上述三步,一共有三个鼠标事件:左键按下,鼠标移动,左键松掉,所框区域可以是绘制矩形;思路清晰了;
绘制矩形需要,起始点坐标,结束坐标,长宽,这些就需要程序来获得,我们上述的回调函数中,会获取当前鼠标事件发生时的坐标xy,那起始点以及结束点坐标就有了,对于长宽,也只需要结束点x-起始点x;
首先我们定义两个点,起始点以及结束点:
Point sp(-1,-1);
Point ep(-1,-1);
回调函数定义:
static void draw(int event,int x,int y,int flags, void *userdata)
{Mat image = *((Mat*)userdata);
}
Mat image = *((Mat*)userdata);这一步就不过多解释;
1.鼠标左键按下;我们要做的就是记录下起始点的坐标:
if (event == EVENT_LBUTTONDOWN){sp.x = x;sp.y = y;cout<<sp<<endl;}
我们先编写鼠标松掉,看能否绘制矩形:
else if (event == EVENT_LBUTTONUP){ep.x = x;ep.y = y;int dx = ep.x - sp.x;int dy = ep.y - sp.y;Rect box(sp.x,sp.y,dx,dy);rectangle(image,box,Scalar(255,0,0),4,LINE_8);imshow("ROI",image(box));imshow("mouse_draw",image);sp.x=-1;sp.y=-1;ep.x=-1;ep.y=-1;}
同样先做的是记录下结束的坐标,这样我们就得到了两个先决条件 :那么长宽就计算出来了:
int dx = ep.x - sp.x;
int dy = ep.y - sp.y;
问题来了,这里的dx,dy是存在负数的情况的,看下面的图片;
左上角的点为(0,0);如果从左上角拉到右下角就是正数,而从右下角拉到左上角就是负数;
我们只需要分情况就行了:
第一种正数:
if (dx>0&&dy>0){Rect box(sp.x,sp.y,dx,dy);rectangle(image,box,Scalar(255,0,0),4,LINE_8);imshow("ROI",image(box));}
第二种负数:
{dx=-dx;dy=-dy;Rect box(ep.x,ep.y,dx,dy);rectangle(image,box,Scalar(255,0,0),4,LINE_8);imshow("ROI",image(box));}
负数我们只需要将dxdy取反得正,那么应该将结束点作为起始点绘制矩形就可以了;这里的imshow("ROI",image(box));用于显示所框选的区域;到这一步我们的程序应该是这样:
Point sp(-1,-1);
Point ep(-1,-1); static void draw(int event,int x,int y,int flags, void *userdata)
{Mat image = *((Mat*)userdata);if (event == EVENT_LBUTTONDOWN){sp.x = x;sp.y = y;cout<<sp<<endl;}else if (event == EVENT_LBUTTONUP){ep.x = x;ep.y = y;int dx = ep.x - sp.x;int dy = ep.y - sp.y;if (dx>0&&dy>0){Rect box(sp.x,sp.y,dx,dy);rectangle(image,box,Scalar(255,0,0),4,LINE_8);imshow("ROI",image(box));}if (dx<0&&dy<0){dx=-dx;dy=-dy;Rect box(ep.x,ep.y,dx,dy);rectangle(image,box,Scalar(255,0,0),4,LINE_8);imshow("ROI",image(box));}imshow("mouse_draw",image);sp.x=-1;sp.y=-1;ep.x=-1;ep.y=-1;}
}
运行发现,整体大致没什么问题,但是我们在拉动的时候,希望这个矩形能跟着鼠标移动:并且画面也没有更新,绘制的框在绘制下一个依旧显示在上面;
现在,我们还缺少鼠标移动事件的情况,编写之后,再接着纠错,实际上很简单,鼠标移动时,矩形跟着放大或者变小,实际上也是鼠标移动,绘制矩形;只需要复制粘贴,
else if (event == EVENT_MOUSEMOVE){if(sp.x > 0 && sp.y > 0){ep.x = x;ep.y = y;int dx = ep.x - sp.x;int dy = ep.y - sp.y;if (dx>0&&dy>0){Rect box(sp.x,sp.y,dx,dy);rectangle(image,box,Scalar(255,0,0),4,LINE_8);}if (dx<0&&dy<0){dx=-dx;dy=-dy;Rect box(ep.x,ep.y,dx,dy);rectangle(image,box,Scalar(255,0,0),4,LINE_8);}imshow("mouse_draw", image); }}
运行发现好像有1.问题:
所幸我打开GPT,得知问题所在:鼠标移动绘制,但是我们应该给一个原图刷新不让其绘制很多矩形,解决方案就是在定义一个全局变量temp;在方法函数中拷贝原图;在绘制完矩形后,立刻将这个temp拷贝到绘制矩形的图片上,就可以只绘制第一个矩形,其他都被原图覆盖了;
else if (event == EVENT_MOUSEMOVE){if(sp.x > 0 && sp.y > 0){ep.x = x;ep.y = y;int dx = ep.x - sp.x;int dy = ep.y - sp.y;if (dx>0&&dy>0){Rect box(sp.x,sp.y,dx,dy);temp.copyTo(image);rectangle(image,box,Scalar(255,0,0),4,LINE_8);}if (dx<0&&dy<0){dx=-dx;dy=-dy;Rect box(ep.x,ep.y,dx,dy);temp.copyTo(image);rectangle(image,box,Scalar(255,0,0),4,LINE_8);}imshow("mouse_draw", image); // 🟢 实时刷新窗口}}
运行发现成功;
那整体的回调函数定义就是:
static void draw(int event,int x,int y,int flags, void *userdata)
{Mat image = *((Mat*)userdata);if (event == EVENT_LBUTTONDOWN){sp.x = x;sp.y = y;cout<<sp<<endl;}else if (event == EVENT_MOUSEMOVE){if(sp.x > 0 && sp.y > 0){ep.x = x;ep.y = y;int dx = ep.x - sp.x;int dy = ep.y - sp.y;if (dx>0&&dy>0){Rect box(sp.x,sp.y,dx,dy);temp.copyTo(image);rectangle(image,box,Scalar(255,0,0),4,LINE_8);}if (dx<0&&dy<0){dx=-dx;dy=-dy;Rect box(ep.x,ep.y,dx,dy);temp.copyTo(image);rectangle(image,box,Scalar(255,0,0),4,LINE_8);}imshow("mouse_draw", image); // 🟢 实时刷新窗口}}else if (event == EVENT_LBUTTONUP){ep.x = x;ep.y = y;int dx = ep.x - sp.x;int dy = ep.y - sp.y;if (dx>0&&dy>0){Rect box(sp.x,sp.y,dx,dy);rectangle(image,box,Scalar(255,0,0),4,LINE_8);imshow("ROI",image(box));}if (dx<0&&dy<0){dx=-dx;dy=-dy;Rect box(ep.x,ep.y,dx,dy);rectangle(image,box,Scalar(255,0,0),4,LINE_8);imshow("ROI",image(box));}imshow("mouse_draw",image);sp.x=-1;sp.y=-1;ep.x=-1;ep.y=-1;}
}