QT 와 OpenCV 연동하여 그림그리기

SangHoon Lee·2020년 4월 12일
2

안녕하세요 C++을 공부하고 있는 대학생입니다.

이번에는 QT를 이용하여 UI를 만들고, OpenCV와 연동하여 페인트하는 작업을 구현 해 보았습니다.

개발환경은 Visual Studio 에서 작업하였습니다.

간단하게 코드 먼저 보여드리고 정리를 할 예정입니다.

프로젝트 이름은 QTProject 라고 하였습니다.

QTProject.h

class QTProject : public QMainWindow {
	Q_OBJECT
public:
	QTProject(QWidget* parent = Q_NULLPTR);
	QMessageBox messagebox;
	QPixmap* pixamp;
	QPixmap buffer;
	QImage image;
	QPixmap newimg;
	QFileDialog imgLoad;
	QFileDialog imgSave;

signals:
	void clicked();

private:
	Ui::QTProjectClass ui;
	int oneclick;
	bool mouse_state;
	void wheelEvent(QWheelEvent* event);

public slots:
	void imageOpen();
	void closeClicked();
	void imageSaveAs();
	void Newfile();
	void version();
	void undo();
	void redo();
	void Screenshot();
	void DrawLine();
	void brushcountfunc();
	void fontsizeup();
	void fontsizedown();
	void colorRedselect();
	void colorBlueselect();
	void colorBlackselect();
	void DrawRect();
	void DrawCir();
	void Erase();
	void AreaButton();
	void Ellipse();
	void extract();
};

QT를 공부하면서 기본적으로 가장 중요 한 것은, Slot , connect를 이해하는 것 이었습니다.

Slot은 제가 UI를 만들면서 버튼 같은 동작을 하기 위해 필요하며, connect는 UI로 만든 기능을 동작시키기 위해 연결하기 위해 사용합니다.
저는 OpenCV의 imshow 기능을 이용하였고, 파일을 저장하고 불러오는 기능은, QT에서 QString 으로 받아서, QFileDialog 를 이용하여 가져왔습니다.

그 다음으로 가장 중요 한 것은, Mouse callback 함수를 사용하는 것 입니다.
코드로 보여드리겠습니다.

cpp 파일 중 일부.

void QTProject::imageOpen() {
	QString filePath = QFileDialog::getOpenFileName(this, "Open Image File", QDir::currentPath());
	QString fileName = filePath.section("/", -1);


	string stdstring;

	stdstring = filePath.toStdString();

	firstImage = imread(stdstring, IMREAD_UNCHANGED);
	if (firstImage.empty()) {
		messagebox.setText("none image");
		return;
	}
	cv::resize(firstImage, firstImage, Size(512, 512), 0, 0, INTER_LINEAR);
	imageCloneOrigin = firstImage.clone();
	copyHeightOrigin = firstImage.rows;
	copyWidthOrigin = firstImage.cols;
	imshow("OriginImage", firstImage);
	setMouseCallback("OriginImage", onMouseEventOrigin, (void*)& firstImage);
	int w = image.width();
	ui.WidthLabel->setText(QString::number(w));

	int h = image.height();
	ui.HeightLabel->setText(QString::number(h));
	ui.FileLabel->setText(fileName);
}

첫줄에 제가 QFileDialog로 받아서, 파일을 열고 작업하는 과정입니다. currentpath 의 경우, exe 파일 기준으로 하여, 실행되고있는 폴더로 path를 잡았습니다.

copyHeightOrigin 과 , copyWidthOrigin은 열었던 파일에 대한 가로 세로길이를 통해 예외처리하기 위해 int형으로 선언해서 잡았습니다.
resize 경우, 파일마다 크기가 다르기때문에 512 by 512로 변환하여 출력 할 수 있게 잡았습니다.

setMouseCallback은 QT에 기본적으로 제공되어있는 기능입니다.
쉽게 원형으로 보자면,

cv::setMouseCallback(const cv::String &winname, cv::MouseCallback onMouse, void *userdata = (void *)0)

이렇게 되어있습니다.

onMouse 부분이 기능을 구현하는 함수를 직접 구현해야합니다.
저의 경우는,

void onMouseEventOrigin(int event, int x, int y, int flags, void* param) {
	Mat mouseImageOrigin = *(Mat*)param;
	Scalar scolor;
	if (brushcount == 1) { //Line draw
		switch (event) {
		case EVENT_MOUSEMOVE:
			if (flags & EVENT_LBUTTONDOWN) {
				circle(mouseImageOrigin, Point(x, y), Pensize, Scalar(blueset, greenset, redset), -1);
			}
			break;
		}
		imshow("OriginImage", firstImage);
	}

	else if (brushcount == 2) { //Rectangle draw
		switch (event) {
		case EVENT_LBUTTONDOWN:
			drawRect = true;
			cpX = x;
			cpY = y;
			break;
		case EVENT_LBUTTONUP:
			if (drawRect == true) {
				if (x < cpX) {
					swapNumber(&x, &cpX);
				}
				if (y < cpY) {
					swapNumber(&y, &cpY);
				}
				rectangle(mouseImageOrigin, Point(cpX, cpY), Point(x, y), Scalar(blueset, greenset, redset), Pensize);
			}
			break;
		}
		imshow("OriginImage", firstImage);
	}

	else if (brushcount == 3) { //Circle draw
		switch (event) {
		case EVENT_LBUTTONDOWN:
			drawCir = true;
			cpX = x;
			cpY = y;
			break;
		case EVENT_LBUTTONUP:
			if (drawCir == true) {
				if (x < cpX) {
					swapNumber(&x, &cpX);
				}
				if (y < cpY) {
					swapNumber(&y, &cpY);
				}
				if (x > (x + cpX) / 2) {
					if (x < cpX) {
						swapNumber(&x, &cpX);
					}
					if (y < cpY) {
						swapNumber(&y, &cpY);
					}
					circle(mouseImageOrigin, Point((x + cpX) / 2, (y + cpY) / 2), x - ((x + cpX) / 2), Scalar(blueset, greenset, redset), Pensize);
				}
				else {
					circle(mouseImageOrigin, Point((x + cpX) / 2, (y + cpY) / 2), cpX - ((x + cpX) / 2), Scalar(blueset, greenset, redset), Pensize);
				}
			}
			break;
		}
		imshow("OriginImage", firstImage);
	}

	else if (brushcount == -1) { //cut Image
		if (!mouseImageOrigin.data) {
			QMessageBox messagebox;
			messagebox.setText("none image");
		}
		switch (event) {
		case EVENT_LBUTTONDOWN:
			areaSelect = true;
			areaX = x;
			areaY = y;
			break;
		case EVENT_LBUTTONUP:
			if (areaSelect == true) {
				if (x < areaX) {
					swapNumber(&x, &areaX);
				}
				if (y < areaY) {
					swapNumber(&y, &areaY);
				}
				Rect rect(areaX, areaY, x, y);

				Mat cutImage = mouseImageOrigin(rect);
				imshow("Cut Image (Origin)", cutImage);
			}
			areaSelect = false;
			break;
		}
	}
	else if (brushcount == -2) { //erase drawing
		switch (event) {
		case EVENT_LBUTTONDOWN:
			eraseSelect = true;
			break;
		case EVENT_MOUSEMOVE:
			if (flags & EVENT_LBUTTONDOWN) {
				if (eraseSelect == true) {
					eraseX = x;
					eraseY = y;
					for (int i = eraseX - Pensize; i < eraseX + Pensize; i++) {
						if (eraseX - Pensize > 0 && eraseX + Pensize < copyWidthOrigin) {
							for (int j = eraseY - Pensize; j < eraseY + Pensize; j++) {
								if (j > 0 && j < copyHeightOrigin) {
									firstImage.at<Vec3b>(j, i)[0] = imageCloneOrigin.at<Vec3b>(j, i)[0];
									firstImage.at<Vec3b>(j, i)[1] = imageCloneOrigin.at<Vec3b>(j, i)[1];
									firstImage.at<Vec3b>(j, i)[2] = imageCloneOrigin.at<Vec3b>(j, i)[2];
								}
							}
						}
					}
				}
			}
			break;
		case EVENT_LBUTTONUP:
			eraseSelect = false;
			imshow("OriginImage", firstImage);
			break;
		}
		imshow("OriginImage", firstImage);
	}

	else if (brushcount == -3) { // floodFill image
		if (event == EVENT_LBUTTONDOWN) {
			extractX = x;
			extractY = y;
			//	floodFill(mouseImage, Point(extractX, extractY), Scalar(blueset, greenset, redset));
			floodFill(firstImageRst, Point(extractX, extractY), Scalar(blueset, greenset, redset));
			floodFill(secondImageRst, Point(extractX, extractY), Scalar(blueset, greenset, redset));
		}
		imshow("OriginImage", firstImage);
	}
}

이렇게 구현하였습니다. 라인,사각형, 원, 지우개, 채우기 기능을 넣었습니다.

다만 제가 작업하면서 걱정 된 부분이, 지우기 기능인데, 예외처리하기도 좌표를 잘 이해해야하기 때문에 어려움이 있었고, at 의 기능이 지금은 대단한 연산을 하는 과정이 아니기때문에, 속도면에서는 상관이 없지만, 연산의 크기가 커지면 커질수록 느릴 수 있기때문에 걱정이지만, 일단은 문제가 없으므로, 사용하였습니다.
페인트 작업 자체가 대단한 연산을 요구하는게 아니기때문에, 문제는 없지만 본래대로라면 메모리 관리에도 신경써야하므로 시간복잡도 면에 있어서 불이익을 볼 수 있기때문에 조금 신중하게 생각해서 구현하였습니다.

어려운 작업은 아니었지만, 처음 시도한 만큼 고생도 많았고, visual studio 에서 QT로 작업하기엔 자료가 생각보다 많이 없어서 stackoverflow 에서 setMousecallbak 함수가 있다는것을 참고하였고, QT에 관한 reference를 참고하여서 구현하였습니다.

저번에 테트리스 하면서 컴퓨터 좌표에 대한 공부를 하였기때문에 이번 과정에 있어서 생각보다 수월하게 할 수 있었고, switch case 문을 사용해서 마우스 이벤트 처리를 통해, 마우스를 눌렀을때, 떼었을때 에 대한 기능을 구현하였고, 여기에는 적어두지않았지만. 자료구조 stack을 사용해서 undo redo 기능까지 해 보았습니다. 처음에는 직접 stack을 구현하였는데, class의 다중상속을 해야 할 거라고 판단하여서 되도록이면 이렇게 간단한 작업은 다중상속을 피하고싶어서 STL의 stack을 이용해서 간단하게 구현했었습니다.

Mat 타입은 동적으로 할당해서 생성해서 하려고했는데, 큰 프로젝트가 아니라, 공부하기위해서 하는것이라 필요없다고 판단해서 사용하지 않았습니다. 제 코드보다 더 좋은 방법은 많지만, 조금씩 전진하려고합니다.

색상도 일단은 RED BLUE BLACK 3가지 색상으로만 하였는데, QPaint에 더 많은 기능이 있기때문에 필요에따라 여러 색상을 구현 할 수 있습니다. rgb 배열이 3차원으로 255,255,255 가 있지만, 투명도를 더하고싶다면, rgba로 255, 255, 255, 255로 할 수 있습니다. alpha값을 개발자에 따라 조절 할 수 있습니다.

색상을 커스터마이징 하여서 한다면 링크 드 리스트를 사용하는것도 나쁘지않을 것 같습니다.

링크 드 리스트 랑 배열을 보자면,

링크 드 리스트는 제 미약한 지식으로는 insert , delete 하기에 좋기때문에 사용하므로 동적으로 노드를 할당하여 0~255 값을 설정 해 두고, 마지막에 break를 걸어서 값을 return으로 빼오면 되기때문에 좋다고 생각합니다.

반면, 배열은 단순히 search 하는데에 좋아서 그라데이션이나 여러 색상을 표현하기에는 링크 드 리스트에 비해 장점이 부족하다고 생각합니다.

저는 개인적으로는 배열보다는 STL의 vector를 선호하지만, 상황에따라 배열도 사용하기 때문에 상황마다 잘 고려해야한다는 것을 이렇게 공부하면서 다시 배워갑니다.

이렇게 공부하다보니, 자료구조가 상당히 필요해서 부족한부분은 계속 공부하고 그랬던 것 같습니다. 지금도 이 QT 공부하면서 더 다양한 기능을 구현 해 보았지만, 더 나아가서 메모리를 효율적으로 관리하는 방법 (공부하다가 실행파일이 그냥 종료되는 모습을 많이봐서...) 을 더 공부 해 보려고 합니다.

물론, 메모리 문제가 아니라 스택오버플로우 , 예외처리 같은 문제가 있었을 수도 있지만...

profile
C++ 공부하고있는 대학생입니다.

0개의 댓글