Ubuntu系统VScode实现opencv(c++)鼠标操作与响应

在之前的创作中心-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;
    }
}