[opencv] 기본적인 GUI관련 윈도우, 키보드, 마우스 이벤트처리
영상처리라 하면 2차원의 Template행렬에 대한 연산과 처리를 의미한다.
영상의 적절한 핸들링과 결과를 위해서 기본클래스들을 익혀왔고,
이제는 GUI관련 처리, 영상 핸들링에 친숙해지는 단계를 거칠 차례이다.
먼저 윈도우 관련 함수.
void namedWindow(const string& winname, int flags = 1) void imshow(const string& winname, InputArray mat) void destroyWindow(const string& winname) void destoryAllWindows() void moveWindow(const string& winname, int x, int y) void resizeWindow(const string& winname, int width, int height)
간단한 것 같은데 적어놓았다. 오인하고 있던 개념이 있기 때문.
- imshow를 바로 사용할 경우 namedWindow에서 flags를 WINDOW_AUTOSIZE로 준 것과 동일한 효과.
- namedWindow의 flag에는 WINDOW_NORMAL, WINDOW_AUTOSIZE, WINDOW_OPENGL이 있으며 OPENGL은 지원하는 라이브러리가 포함되어있어야 정상적으로 작동한다.
- moveWindow를 위해서는 WINDOW_NORMAL로 flag를 주어야한다. 즉, AUTOSIZE로 설정시 윈도우 크기 자체는 변경되지만 매핑된 영상은 resize가 되지 않으므로 유의.
- const string& winname에 해당하는 문자열은 WIN API에서의 winname을 생각해야한다. 등록한다는 개념에 가깝다고 생각된다.
- (이어서) 따라서, namedWindow로 지정한 문자열의 창(클래스정보)은 imshow를 통해 불러오는 것이다. 물론 등록되어있지 않은 이름이라면 새로운 창정보를 생성한다.(WINDOW_AUTOSIZE로)
그러고 다음으로는 키보드 이벤트 처리를 확인한다.
나의 경우, waitKey()를 사용하면 Navigation Key등 VK가 인식이 안되어 CV API함수인 cvWaitKey를 이용하였다.
#include <opencv2\opencv.hpp> #include <iostream> using namespace cv; using namespace std; int main(void) { Mat srcImage = Mat(512, 512, CV_8UC3, Scalar(255, 255, 0)); if (srcImage.empty()) { return -1; } int testWindow_X= 500, testWindow_Y = 500; namedWindow("testWindow"); imshow("testWindow", srcImage); //testWindow로 이름을 바꿔서 띄우는게아님. // testWindow라고 명명된(SET OPTIONAL VALUES) 윈도우를 띄우는데, 거기의 소스가 srcImage라는 Mat인 것. // WIN API 프로그램에서 Window Class별 유일의 특성을 생각하면 될 것 같다. moveWindow("testWindow", testWindow_X, testWindow_Y); waitKey(); int readKey; while (true) { readKey = cvWaitKey(0); printf("read : %x \n", readKey); if (readKey == 0x1B) { // read ESC, break; } else if (readKey == 0xffffffff) { break; } switch (readKey) { case 0x250000: // left testWindow_X -= 10; break; case 0x260000: // up testWindow_Y -= 10; break; case 0x270000: // right testWindow_X += 10; break; case 0x280000: // down testWindow_Y += 10; break; } moveWindow("testWindow", testWindow_X, testWindow_Y); } return 0; }
중간에 0xffffffff(INT_MAX)에 대한 처리는 imshow로 띄운 window class가 X버튼이나 Alt+F4등으로 종료 되었을 때 리턴되는 값이라
잘못하면 무한루프에 빠질 수 있어 이에대한 예외처리를 한 부분이다.
마우스 이벤트를 처리하기 위해서는 cvSetMouseCallback함수에 이벤트 핸들링 함수를 지정해줘야한다.
void setMouseCallback(const string& winname, MouseCallback onMouse, void* userdata = 0) static void onMouse(int event, int x, int y, int flags, void* userdata)
WIN API 프로그램과 거~~의 같다. 굳이 콜백함수이름을 onMouse로 지을 필요는 없다.
하지만 내부 파라미터들은 반드시 규격을 맞추어줘야한다. WIN API와는 다르게 LRESULT CALLBACK 리턴형 대신 void형을 택한다.
콜백함수의 event와 flags의 상수목록은 아래와 같다.
[supsystic-tables id=1]
#include <opencv2\opencv.hpp> #include <iostream> using namespace cv; using namespace std; void callbackMouseHandling(int _event, int _x, int _y, int _flags, void *userParam); #define WIN_NAME "winSrcImage" int main(void) { Mat srcImage = Mat(512, 512, CV_8UC3, Scalar(255, 255, 0)); imshow(WIN_NAME, srcImage); setMouseCallback(WIN_NAME, callbackMouseHandling, (void*)&srcImage); waitKey(0); return 0; } void callbackMouseHandling(int _event, int _x, int _y, int _flags, void *userParam) { // userParam에는 이미지가 날라온다. Mat *tmpScene = (Mat *)userParam; Mat newAllocateImage = Mat(*tmpScene); switch (_event) { case EVENT_LBUTTONDOWN: if (_flags & EVENT_FLAG_SHIFTKEY) { // 비트연산을 통해 flags중 SHIFTKEY가 포함되어있는지 확인 rectangle(newAllocateImage, Rect(_x - 5, _y - 5, 10, 10), Scalar(120,60,0)); } else { // 일반 클릭이면 원을 그림 circle(newAllocateImage, Point(_x, _y), 10, Scalar(60, 120, 0)); } break; case EVENT_RBUTTONDOWN: circle(newAllocateImage, Point(_x, _y), 15, Scalar(60, 120, 0), 5); break; case EVENT_LBUTTONDBLCLK: // DBLCLK의 판정기준은 WIN 정책에 따르는 듯?.. destroyWindow(WIN_NAME); // 더블클릭시 윈도우를 파괴함. return; } imshow(WIN_NAME, newAllocateImage); }
imshow를 하고 callback을 지정하는 이유는 위에 윈도우 함수 설명에 따라,
등록되지 않은 윈도우명에 특정 function을 적용할 수 없다. 따라서 namedWindow로 먼저 등록하거나 imshow로 show와 동시에 등록한 뒤 callback을 지정해야한다.
userdata로 보았던 인자 userParam은 별 거 없다. callback 호출 시 어떤 사용자 정의 parameter를 넘겨줄 것이냐 이다.
그래서 인자형도 void. 받아와서는 알아서 타입캐스팅하여 사용하면된다. 물론 지정했던 타입에 맞게 적절히 올바른 타입으로.
다음으로 WIN GUI 트랙바를 사용하여 thresholding 하기..
처음에 책하고 다르게 진행했다가 무엇이 잘못됐는지 화상변화가 정상적이지 않았다.
#include <opencv2\opencv.hpp> #include <iostream> using namespace cv; using namespace std; #define WIN_NAME "winSrcImage" void onChange(int pos, void *userdata); int main(void) { Mat srcImage = imread("lena.jpg", IMREAD_GRAYSCALE); if (srcImage.empty()) { return -1; } namedWindow(WIN_NAME); int posVal = 0; createTrackbar("threshold", WIN_NAME, &posVal, UCHAR_MAX, onChange, (void *)&srcImage); imshow(WIN_NAME, srcImage); waitKey(0); return 0; } void onChange(int pos, void *userdata) { Mat *tmp = (Mat*)userdata; Mat destImage = Mat(*tmp); for (int i = 0; i < destImage.rows; i++) { for (int j = 0; j < destImage.cols; j++) { if (destImage.at<uchar>(i, j) > pos) { destImage.at<uchar>(i, j) = 255; } else { destImage.at<uchar>(i, j) = 0; } } } imshow(WIN_NAME, destImage); }
최초로 onChange가 호출될때는 화상이 변하지만, 그 다음 값은 0 or 255일때만 작동하였다.
왜 그럴까하다가 원본화상을 보관해야한다는 것을 깨달았다. 이미 threshold된 이미지를 가지고 trackbar를 사용하면
0 or 255인 값만 남아있으므로 0 or 255에서만 작동하는 것이었다.
따라서, Mat 배열을 이용하여 시도하였다.
(실질적으로 완성된 코드)
#include <opencv2\opencv.hpp> #include <iostream> using namespace cv; using namespace std; #define WIN_NAME "winSrcImage" void onChange(int pos, void *userdata); int main(void) { Mat srcImage[2]; srcImage[0] = imread("lena.jpg", IMREAD_GRAYSCALE); srcImage[1] = Mat(srcImage[0].rows, srcImage[0].cols, CV_8UC1); // srcImage[1] = Mat(srcImage[0].size(), CV_8UC1); if (srcImage[0].empty()) { return -1; } namedWindow(WIN_NAME); int posVal = 0; onChange(0, (void *)srcImage); // srcImage는 배열이므로 포인터임. createTrackbar("threshold", WIN_NAME, &posVal, UCHAR_MAX, onChange, (void *)srcImage); imshow(WIN_NAME, srcImage[0]); waitKey(0); return 0; } void onChange(int pos, void *userdata) { Mat *tmp = (Mat*)userdata; Mat srcImage = Mat(tmp[0]); Mat destImage = Mat(tmp[1]); for (int i = 0; i < srcImage.rows; i++) { for (int j = 0; j < srcImage.cols; j++) { if (srcImage.at<uchar>(i, j) > pos) { destImage.at<uchar>(i, j) = 255; } else { destImage.at<uchar>(i, j) = 0; } } } imshow(WIN_NAME, destImage); }
정상적으로 작동했다. 이유없이 책이랑 다르게 할 필요는 없지만, 이러한 세세한 것들을 놓치고 갈 수 있으므로
책과 같이 한 번, 책과는 다르게 응용을 한 번
실습하는 나의 습관이 뿌듯했다… ㅋㅋ
다음으로는 영상소스에 대한 핸들링에 관해.
79개의 댓글