반응형

마우스로 마스크 포인트의 삽입, 삭제, 이동을 구현해 보자.

 

#include <iostream>
#include <opencv2/opencv.hpp>

cv::Point nearPointOnSegment(cv::Point2f p, cv::Point2f a, cv::Point2f b) {
	float l2 = std::powf(a.x - b.x, 2) + std::powf(a.y - b.y, 2); // 선분의 길이 제곱
	if (l2 == 0.0f) // a와 b가 같은 경우
		return cv::Point();

	// 선분 위로 점 p의 사영(Projection) 위치 t 계산(투영된 지점의 비율)
	// t = [(p-a) dot (b-a)] / |b-a|^2
	float t = ((p.x - a.x) * (b.x - a.x) + (p.y - a.y) * (b.y - a.y)) / l2;

	// t가 0~1 사이면 선분 위(0이면 a, 1이면 b), 그렇지 않으면 선분 밖
	// 0보다 작으면 a쪽, 1보다 크면 b쪽
	// 선분 밖에 사영된 점은 무시
	if (t < 0.0f || t > 1.0f)
		return cv::Point();

	// 선분 위에 사영된 점 위치 계산
	cv::Point2f projection = a + t * (b - a);
	//std::cout << "Projection Point: (" << projection.x << ", " << projection.y << ")" << std::endl;

	// 사영된 점과 원래 점 p 사이의 거리가 10 픽셀 이상이면 무시
	if (std::sqrt(std::powf(p.x - projection.x, 2) + std::powf(p.y - projection.y, 2)) > 10)
		return cv::Point();

	// 선분 위의 사영된 점 반환
	return projection;
}

void onMouse(int event, int x, int y, int flags, void* userdata)
{
	static std::vector<cv::Point> points; // Static vector to store points across function calls
	cv::Scalar pointColor(0, 255, 0); // Green color for drawing points
	cv::Scalar lineColor(0, 0, 255); // Red color for drawing lines
	static cv::Mat* img = (cv::Mat*)userdata; // Cast the user data to a pointer to cv::Mat

	// 이미지 복사본 생성
	static cv::Mat originalimg = cv::imread("palvin1.png");

	// 마스크 생성
	static cv::Mat mask1 = cv::Mat::zeros((*img).size(), CV_8UC1);
	static cv::Mat mask2 = cv::Mat::zeros((*img).size(), CV_8UC1);
	static cv::Mat mask = cv::Mat::zeros((*img).size(), CV_8UC1);

	cv::Point newPoint(x, y);

	static bool clickedOnExistingPoint = false; // Flag to track if the click was on an existing point
	static int clickedExistingPointIndex = -1; // Index of the existing point that was clicked
	static bool clickHold = false;

	bool inserted = false;

	switch (event)
	{
	case cv::EVENT_LBUTTONDOWN:
		// 기존 점과 동일한 위치에 클릭된 경우 (범위는 8x8 픽셀)
		if (!points.empty())
			for (int i = 0; i < points.size(); i++)
				if (newPoint.x - 4 < points[i].x && newPoint.x + 4 > points[i].x &&
					newPoint.y - 4 < points[i].y && newPoint.y + 4 > points[i].y)
				{
					// 이미 존재하는 점을 클릭하면 우클릭으로 제거할 수 있도록 인덱스 저장
					clickedExistingPointIndex = i;
					clickedOnExistingPoint = true; // Set the flag to indicate that an existing point was clicked

					// 이동하려는 목적일 수 있으므로, 클릭이 유지되는 동안 점을 이동할 수 있도록 clickHold 플래그 설정
					clickHold = true;

					std::cout << "Clicked on existing point: " << i << " (" << points[i].x << ", "
						<< points[i].y << ")" << std::endl;

					return;
				}


		// 마우스 왼쪽 버튼이 클릭되었을 때, 새로운 점을 추가하거나 선분 사이에 점을 삽입

		// 선분 사이에 점 추가
		// 최소 2개의 점이 있어야 선분이 형성되므로, 2개 이상의 점이 있을 때만 선분 사이에 점을 추가
		if (points.size() >= 2)
		{
			for (int i = 0; i < points.size() - 1; i++)
			{
				cv::Point2f a = points[i];
				cv::Point2f b = points[i + 1];
				cv::Point projection = nearPointOnSegment(newPoint, a, b);
				if (projection == cv::Point()) // 사영된 점이 선분 밖이거나 너무 멀리 떨어져 있으면
					continue;
				else
				{
					points.insert(points.begin() + 1 + i, projection); // 새로운 점을 선분 사이에 삽입
					std::cout << "Point inserted: (" << projection.x << ", " << projection.y << ")" << std::endl;

					originalimg.copyTo(*img); // 이미지 갱신
					inserted = true;

					break;
				}
			}
		}

		// 선분 사이에 점이 추가되지 않았다면, 새로운 점을 추가
		if (!inserted && !points.empty() && newPoint != points.back())
		{
			points.push_back(newPoint);
			std::cout << "Point added: (" << x << ", " << y << ")" << std::endl;

			// 이전에 클릭된 점에 대한 정보를 리셋, 삭제시 전에 클릭된 점이 삭제되지 않도록 한다
			// 이 부분이 없으면, 이미 존재하던 점을 클릭한 후 다른 곳을 클릭해 새로운 점을 생성하고 우클릭으로 삭제하면,
			// 새로운 점이 삭제되지 않고 이전에 클릭된 점이 삭제되는 문제가 발생한다
			clickedExistingPointIndex = -1;
			clickedOnExistingPoint = false;
		}
		// 첫 번째 점 추가
		else if (points.empty())
		{
			points.push_back(newPoint);
			std::cout << "First point added: (" << x << ", " << y << ")" << std::endl;
		}

		break;

	case cv::EVENT_LBUTTONUP:
		// 클릭이 끝났으므로, 클릭이 유지되는 동안 점을 이동할 수 있도록 설정한 clickHold 플래그를 false로 리셋
		clickHold = false;

		break;

	case cv::EVENT_MOUSEMOVE:
		// 클릭이 유지되는 동안 점을 이동할 수 있도록 설정한 clickHold 플래그가 true인 경우
		if (clickHold && clickedOnExistingPoint && clickedExistingPointIndex != -1)
		{
			points[clickedExistingPointIndex] = newPoint; // Move the existing point to the new location
			//std::cout << "Moving point: " << clickedExistingPointIndex << " (" << x << ", " << y << ")" << std::endl;

			originalimg.copyTo(*img, mask); // 마스크를 적용하여 원본 이미지에서 점과 선이 있는 부분만 갱신
			// *img = cv::imread("palvin1.png");
			// 이런 식으로 매번 이미지를 다시 로드하는게 오래 걸려서 점 이동시 버벅이는거 같아 마스크를 적용해
			// 원본 이미지에서 복사해 봤지만 별 효과는 없다.
			// cv::imshow()가 이런 작업에 최적화되어 있지 않은 것 같기도 하다. 점 이동시 버벅이는 문제는 해결되지 않았다.
		}
		break;

	case cv::EVENT_RBUTTONDOWN:
		if (!points.empty())
		{
			// 점이 삽입된 상태에서 우클릭이 발생하면 삽입된 점을 제거, 또는 기존 점을 클릭한 상태에서 우클릭이 발생하면
			// 클릭된 점을 제거
			if (clickedOnExistingPoint)
			{
				std::cout << "Clicked point removed: " << clickedExistingPointIndex << " (" <<
					points[clickedExistingPointIndex].x << ", " << points[clickedExistingPointIndex].y << ")" <<std::endl;
				points.erase(points.begin() + clickedExistingPointIndex); // Remove the last added point
				clickedExistingPointIndex = -1; // Reset the last added point index
				clickedOnExistingPoint = false; // Reset the added flag
			}
			// 아니면 마지막으로 추가된 점을 제거
			else
			{
				points.pop_back();
				std::cout << "Last point removed" << std::endl;
			}

			originalimg.copyTo(*img); // 이미지 갱신
		}
		break;
	}

	// Draw points
	for (const cv::Point& point : points)
		cv::circle(*(cv::Mat*)userdata, point, 4, pointColor, -1); // userdata로도 접근 가능

	// Draw lines between points
	if (points.size() >= 2)
		for (int i = 0; i < points.size() - 1; i++)
			cv::line(*img, points[i], points[i + 1], lineColor, 2);

	// 빨간색 점과 초록색 선분이 있는 부분을 흰색으로, 나머지 부분을 검은색으로 하는 마스크 생성
	cv::inRange(*img, cv::Scalar(0, 0, 255), cv::Scalar(0, 0, 255), mask1);
	cv::inRange(*img, cv::Scalar(0, 255, 0), cv::Scalar(0, 255, 0), mask2);
	mask = mask1 + mask2;

	//cv::imshow("mask", mask);
	//cv::imshow("originalimg", originalimg);
	cv::imshow("image", *(cv::Mat*)userdata);
}

int main()
{
	cv::Mat image = cv::imread("palvin1.png");
	cv::namedWindow("image");
	cv::setMouseCallback("image", onMouse, (void*)&image);

	cv::imshow("image", image);

	cv::waitKey(0);
	cv::destroyAllWindows();

	return 0;
}

 

 

 

 

 

 

 

반응형
Posted by J-sean
: