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