본문 바로가기
  • 우당탕탕속의 잔잔함
Programming/Computer Vision

[OpenCV] 특정 영역 지정 후 해당 영역에서 이미지 비교

by zpstls 2023. 2. 2.
반응형

 

 

이번 포스트에서는 이미지 비교를 수행해보고자 합니다.

특정 영역을 추출한 후, 해당 영역과 어떠한 기준이 되는 이미지를 비교해 볼 예정입니다.

 

 

예전 포스트에 두개의 이미지를 비교하는 부분을 다룬 적이 있습니다. 해당 포스트는 다음과 같습니다.

 

[Image Comparison] 두 이미지의 일치율 비교

두 개의 이미지를 비교하는 방법에 대해 다루고자 합니다. Deep Learning 방식은 아니고 전통적인 방식을 통해 수행해볼 것입니다. 두 가지 방법을 통해 이미지를 비교해볼 것입니다. 첫 번째 방법

mj-thump-thump-story.tistory.com

아무튼 위 포스트에서는 무작정 Input 된 2개의 이미지를 비교하는 방식이었습니다. 그러나 이번 포스트에서는 비교를 위한 특정 영역을 선택하고 해당 영역에서만 이미지를 비교해 볼 것입니다. 이렇게 되면 비교하고 싶지 않은 부분은 배제되니 일치율 비교 측면에서 더 유리할 것입니다.

 

우선, 이 프로젝트는 Windows 10 환경에서 수행할 예정이며, Visual Studio 2017, OpenCV, C/C++을 사용할 것 입니다.

그럼 이제 본격적으로 시작해 보겠습니다.

 

작업의 순서에 대해 설명해 보면 다음과 같습니다.

Program Flow

먼저 Camera에서 이미지를 받아오고 해당 이미지에서 추출하고자 하는 영역을 마우스 이벤트를 통해 지정합니다. 그리고 추출할 영역에 대한 Position 값을 토대로 Warping을 수행하고 결과를 이미지 파일 형태로 저장합니다. 이때 추출 영역은 사각형의 형태를 띤다고 가정합니다.

비교 이미지를 만들기 위해 위와 같은 과정을 두 번 수행합니다. 결과적으로 추출된 두 장의 Warp 된 이미지를 서로 비교하여 일치율을 계산합니다. 그리고 최종적으로 두 이미지가 일치하는지 그렇지 않은지 결과를 출력합니다.

 

위 프로그램에서 필요한 각각의 함수들에 대한 내용은 다음과 같습니다.

 

우선, 다음은 마우스 이벤트를 처리하는 함수입니다.

Left Top X/Y, Left Bottom X/Y, Right Top X/Y, Right Bottom X/Y 값을 추출하여 ROI Data로 사용할 것인데, 마우스 왼쪽 버튼을 눌러 현재 Mode에 해당되는 좌표 데이터를 갱신합니다. 이 함수는 Webcam 화면이 출력되는 Window에 Callback함수로 들어가게 될 것입니다.

void onMouse(int event, int x, int y, int flags, void* param)
{
	switch (event)
	{
	case EVENT_LBUTTONDOWN:

		int Xpos = x;
		int Ypos = y;

		// store position value into array
		if (Edge_mode == 1) {
			LT_PosX = Xpos;
			LT_PosY = Ypos;
			cout << "Left Top :: ";
		}
		else if (Edge_mode == 2) {
			LB_PosX = Xpos;
			LB_PosY = Ypos;
			cout << "Left Bottom :: ";
		}
		else if (Edge_mode == 3) {
			RT_PosX = Xpos;
			RT_PosY = Ypos;
			cout << "Right Top :: ";
		}
		else if (Edge_mode == 4) {
			RB_PosX = Xpos;
			RB_PosY = Ypos;
			cout << "Right Bottom :: ";
		}
        
		break;
	}
}

 

이제 Input 이미지를 처리하는 함수들을 다뤄보겠습니다. 다음은 각 꼭짓점 좌표에 따라 Warpping 작업을 수행하는 함수입니다. Left Top X/Y는 (0,0) 좌표로, Left Botton X/Y는 (0, Height)로, Right Top X/Y는 (Width, 0)으로 Right Bottom X/Y는 (Width, Height)로 이동되면서 ROI 영역이 원본 이미지 크기에 맞게 꽉 차게 됩니다.

Mat Warp_projectorRange(Mat& _origin) 
{
	Mat warpImg(_origin.size(), _origin.type());

	// Before Warp
	vector<Point2f> corners(4);
	corners[0] = Point2f(LT_PosX, LT_PosY);
	corners[1] = Point2f(RT_PosX, RT_PosY);
	corners[2] = Point2f(LB_PosX, LB_PosY);
	corners[3] = Point2f(RB_PosX, RB_PosY);

	//After Warping
	vector<Point2f> warpCorners(4);
	warpCorners[0] = Point2f(0, 0);
	warpCorners[1] = Point2f(warpImg.cols, 0);
	warpCorners[2] = Point2f(0, warpImg.rows);
	warpCorners[3] = Point2f(warpImg.cols, warpImg.rows);

	//Transformation Matrix
	Mat trans = getPerspectiveTransform(corners, warpCorners);

	//Warping
	warpPerspective(_origin, warpImg, trans, _origin.size());

	return warpImg;
}

 

다음은 두 이미지를 비교하는 함수입니다. 단순히 Subtraction 방식으로 비교할 것입니다.

int count_whitePixels(Mat _img) 
{
	int i, j, count = 0;
	for (i = 0; i < _img.cols; i++) {
		for (j = 0; j < _img.rows; j++) {
			int color = _img.at<uchar>(j, i);
			if (color < 100) count++;
		}
	}

	return count;
}
double compare_with_subtract(Mat targetImage, Mat standardImage, int width, int height) 
{
	Mat TargetImage, StandardImage;

	// target image
	cvtColor(targetImage, TargetImage, COLOR_RGB2GRAY);
	threshold(TargetImage, TargetImage, 140, 255, THRESH_BINARY);

	// standard image
	cvtColor(standardImage, StandardImage, COLOR_RGB2GRAY);
	threshold(StandardImage, StandardImage, 140, 255, THRESH_BINARY);

	// subtract two images
	Mat subtraction;
	absdiff(TargetImage, StandardImage, subtraction);

	double diff_count = count_whitePixels(subtraction);
	double matching_score = (double)((double)diff_count / (double)(width * height));

	return matching_score;
}

두 이미지를 이진화시키고 absdiff를 이용해 Subtration을 수행합니다. 그리고 결과 이미지에서 White Pixel의 비율을 계산하여 유사도를 %로 표현합니다.

 

위와 같은 함수들을 이용하여 Main 부분을 작성하면 다음과 같습니다.

#include <opencv2/highgui/highgui.hpp>
#include <opencv2/core/core.hpp>
#include <opencv2/imgproc/imgproc.hpp>
#include <opencv2\opencv.hpp>

#include <iostream>
#include <fstream>
#include <string>

using namespace cv;
using namespace std;

// Mode >> 1 : Left Top, 2 : Left Bottom, 3 : Right Top, 4 : Right Bottom
int Edge_mode = -1;

// Track Edge Position
float LT_PosX = 0, LB_PosX = 0, RT_PosX = 0, RB_PosX = 0;
float LT_PosY = 0, LB_PosY = 0, RT_PosY = 0, RB_PosY = 0;

int main(int argc, char *argv[])
{
	Mat frame;
	VideoCapture cap;

	int imageNum = 0;
	int deviceID = 0;
	int apiID = cv::CAP_ANY;

	cap.open(deviceID, apiID);
	cap.set(CAP_PROP_FRAME_WIDTH, 320);
	cap.set(CAP_PROP_FRAME_HEIGHT, 240);
	if (!cap.isOpened()) {
		cerr << "ERROR! Unable to open camera\n";
		return -1;
	}

	for (;;)
	{
		cap.read(frame);
		if (frame.empty()) {
			cerr << "ERROR! blank frame grabbed\n";
			break;
		}

		namedWindow("Live");
		cv::setMouseCallback("Live", onMouse);

		// drawing
		line(frame, Point(LT_PosX, LT_PosY), Point(LB_PosX, LB_PosY), Scalar(255, 255, 0), 2, 8);
		line(frame, Point(LT_PosX, LT_PosY), Point(RT_PosX, RT_PosY), Scalar(255, 255, 0), 2, 8);
		line(frame, Point(RT_PosX, RT_PosY), Point(RB_PosX, RB_PosY), Scalar(255, 255, 0), 2, 8);
		line(frame, Point(LB_PosX, LB_PosY), Point(RB_PosX, RB_PosY), Scalar(255, 255, 0), 2, 8);
		circle(frame, Point(LT_PosX, LT_PosY), 5.0, Scalar(0, 0, 255), -1);
		circle(frame, Point(LB_PosX, LB_PosY), 5.0, Scalar(255, 0, 0), -1);
		circle(frame, Point(RT_PosX, RT_PosY), 5.0, Scalar(0, 255, 0), -1);
		circle(frame, Point(RB_PosX, RB_PosY), 5.0, Scalar(255, 0, 255), -1);

		imshow("Live", frame);

		int keycode = waitKey(5);
		if (keycode == 'a') Edge_mode = 1;
		else if (keycode == 'b') Edge_mode = 2;
		else if (keycode == 'c') Edge_mode = 3;
		else if (keycode == 'd') Edge_mode = 4;
		else if (keycode == 'q') {
			Mat warpResult = Warp_projectorRange(frame);
			imwrite("WARPING_VIEW_" + to_string(imageNum) +".jpg", warpResult);
			imageNum++;

			if(imageNum == 2) break;
		}
	}

	// calculate similarity
	Mat targetImage = imread("WARPING_VIEW_0.jpg", IMREAD_ANYCOLOR);
	Mat standardImage = imread("WARPING_VIEW_1.jpg", IMREAD_ANYCOLOR);

	double final_score = compare_with_subtract(targetImage, standardImage, targetImage.rows, targetImage.cols);
	cout << "Similarity : " << final_score << endl;

	return 0;
}

코드는 매우 간단합니다.

Webcam Image를 320x240으로 Load 한 후, 해당 이미지에서 사용자가 지정하는 ROI를 추출하여 320x240으로 Warping 합니다. 이때 Warp 하기 위한 과정은 다음과 같습니다.

키보드 a를 누른 후 Left Top Position을, b를 누른 후 Left Bottom Position을, c를 누른 후 Right Top Position을, d를 누른 후 Right Bottom Position을 각각 클릭하고 q 버튼을 통해 Warp 한 결과 이미지를 저장합니다.

저장된 2장의 이미지를 Load 하고 Subtration을 수행한 후, 최종적으로 유사도 값을 반환합니다.

 

 

 

 

 

그러면 이제 테스트를 수행해 보도록 하겠습니다.

Target Image와 Standard Image에서 각각 ROI를 추출하고 Warp 시키면 다음과 같습니다.

Extract ROIs from Target and Standard Images

Warp 시킨 두 이미지에 이진화를 적용하고 Subtraction을 수행합니다. 결과는 다음과 같습니다.

Result Of Comparison - 1

White Pixel이 서로 다른 부분을 의미합니다. 결과적으로는 유사도 값이 79%로 나왔습니다.

ROI를 추출하긴 했지만 Pixel Position으로 비교하면 어긋나는 부분이 있을 것입니다. 그렇기에 윤곽선을 추출한 것과 같은 결과를 얻게 됩니다. 조금 낮은 수치이긴 하지만 79%가 비슷하다고 하니 유사한 이미지라고 판단해도 무방할 것입니다.

 

이번에는 조금 다른 2개의 이미지를 비교해 볼 것입니다. 같은 아두이노 나노 보드이지만, 보드 내의 리셋 버튼과 일부 문자들이 조금 상이합니다.

Result Of Comparison - 2

앞서 수행한 방식대로 유사도를 측정해 보면 59%가 비슷하다고 나옵니다.

어느 정도는 합리적인 결과인 것 같다고 판단됩니다. "비슷하게 생겼지만 조금은 다른 보드이다"라고 판단할 수치 정도는 될 것 같습니다.

 

이제껏 다룬 방식은 어떠한 두 이미지를 비교해서 합리적인 유사도를 판단하는 부분에 있어서는 조금 부적절할 수 있는 프로그램입니다.

특정한 제약 사항이 적용되는 상황, 예를 들면 N개의 서로 다른 이미지 중에서 가장 비슷한 이미지를 선택하거나 특정 위치에 특정 이미지가 관찰되면 이벤트를 수행하는 등의 경우를 생각해 볼 수 있을 것입니다.

 

 

딥러닝 방식으로 이미지의 유사도를 측정하는 것에는 한참 못 미치지만, 제한된 상황이라면 이렇게 아주 간단하게 구현하는 것도 나쁘진 않을 것 같습니다.

이번 포스트는 여기서 마무리하도록 하겠습니다.

 

 

반응형

댓글