[C++ openCV] 소벨 마스크를 사용한 엣지 검출

3 minute read

소벨 마스크를 알아보기 전, ‘컨볼루션이(Convolution)’라는 개념을 알아야 한다. 컨볼루션은 한국 말로는 합성곱을 나타내며, 하나의 함수와 또 다른 함수를 반전 이동한 값을 곱한 다음, 구간에 대해 적분하여 새로운 함수를 구하는 수학 연산자이다(출처: 위키백과).

소벨 마스크

‘소벨 마스크’는 영상에서 경계선을 검출하는데에 있어서 사용되는 마스크 중 하나이다. 영상 이미지를 3X3에 해당하는 소벨 마스크를 사용하여 한 번 쭉 훑으면서 컨볼루션을 하고, 값이 급격히 변하는 곳을 찾아 그 부분만을 밝게 나타내는 것이다.

컨볼루션은 다음과 같이 실행한다. png
이미지에서 마스크 크기만큼 짤라서 각 픽셀값과 마스크의 각 값을 곱한 후 더한 값으로 다시 이미지를 만든다. 다시 만든 이미지는 원본 영상보다 작아지므로 제로패딩(Zeropadding)을 해주어야 한다.

우선, x방향과 y방향의 3x3 소벨 마스크 필터를 통해 컨볼루션을 해주어야 한다. 각 방향의 소벨 마스크는 다음과 같다.

png

Code

main.cpp는 다음과 같다.

#include "iostream"
#include "opencv2/opencv.hpp"
#include "opencv2/highgui/highgui.hpp"
#include "header.h"

#define IMAGE_PATH "opencv_test_image.jpg"

int main() {
	//image 받아오기(gray로..)
	cv::Mat input = cv::imread(IMAGE_PATH, cv::IMREAD_GRAYSCALE);
	//sobelMask 사용하여 얻은 x, y의 image를 합치기 위한 matrix
	cv::Mat sobel_sum;

	//zeropadding 함수 사용 - 영상 줄어듦 방지
	cv::Mat zeropd = zeropadding(input);

	//자연스럽게 하기위해 zeropadding한 부분에 original image의 테두리 부분 값으로 채워줌
	cv::Mat samevlpd = samevlpadding(input);

	//구현한 sobelMask 함수 사용
	cv::Mat sobel_x = sobelMask_x(samevlpd);
	cv::Mat sobel_y = sobelMask_y(samevlpd);

	//x, y 합
	add(sobel_x, sobel_y, sobel_sum);

	//image 보기 및 닫기
	cv::imshow("origin", input);
	cv::imshow("zero", zeropd);
	cv::imshow("same", samevlpd);
	cv::imshow("sobel_x", sobel_x);
	cv::imshow("sobel_y", sobel_y);
	cv::imshow("sobel_sum", sobel_sum);
	cv::waitKey(0);
	cv::destroyAllWindows;
}

header.cpp는 다음과 같다.

#include "iostream"
#include "opencv2/opencv.hpp"


//x방향 sobelMask 
cv::Mat sobelMask_x(cv::Mat input) {
	cv::Mat sobel_image = cv::Mat::zeros(input.rows, input.cols, input.type());

	float sobeldata_x[] = { 1, 2, 1, 0,  0, 0, -1, -2, -1 };

	//convolution
	for (int row = 1; row < input.rows - 1; row++) {
		for (int col = 1; col < input.cols - 1; col++) {
			int sum_x = 0;
			int idx = 0;
			for (int i = -1; i <= 1; i++) {
				for (int j = -1; j <= 1; j++) {
					sum_x += input.at<uchar>(row + i, col + j) * sobeldata_x[idx];
					idx++;
				}
			}
			
			//0보다 작은 값은 0으로, 255보다 큰 값은 255로 채워줌
			if (sum_x < 0) { sum_x = 0; }
			else if (sum_x > 255) { sum_x = 255; }
			sobel_image.at<uchar>(row - 1, col - 1) = (uchar)sum_x;
		}
	}
	return sobel_image;
}

//y방향 sobelMask
cv::Mat sobelMask_y(cv::Mat input) {
	cv::Mat sobel_image = cv::Mat::zeros(input.rows, input.cols, input.type());

	float sobeldata_y[] = { -1, 0, 1, -2, 0 ,2, -1, 0, 1 };

	//convolution
	for (int row = 1; row < input.rows - 1; row++) {
		for (int col = 1; col < input.cols - 1; col++) {
			int sum_y = 0;
			int idx = 0;
			for (int i = -1; i <= 1; i++) {
				for (int j = -1; j <= 1; j++) {
					sum_y += input.at<uchar>(row + i, col + j) * sobeldata_y[idx];
					idx++;
				}
			}
			
			//0보다 작은 값은 0으로, 255보다 큰 값은 255로 채워줌
			if (sum_y <= 0) { sum_y = 0; }
			else if (sum_y >= 255) { sum_y = 255; }
			sobel_image.at<uchar>(row - 1, col - 1) = (uchar)sum_y;
		}
	}
	return sobel_image;
}

//Zeropadding
cv::Mat zeropadding(cv::Mat input) {

	cv::Mat zeropd = cv::Mat::zeros(input.rows + 2, input.cols + 2, input.type());

	for (int row = 0; row < input.rows; row++) {
		for (int col = 0; col < input.cols; col++) {
			zeropd.at<uchar>(row + 1, col + 1) = input.at<uchar>(row, col);
		}
	}
	return zeropd;
}

//자연스럽게 패딩하기
cv::Mat samevlpadding(cv::Mat input) {

	//zeropadding
	cv::Mat samevlpd = cv::Mat::zeros(input.rows + 2, input.cols + 2, input.type());

	for (int row = 0; row < input.rows; row++) {
		for (int col = 0; col < input.cols; col++) {
			samevlpd.at<uchar>(row + 1, col + 1) = input.at<uchar>(row, col);
		}
	}

	//zeropadding 부분을 original image의 테두리 값으로 넣어줌
	//python에서는 -1이 배열의 마지막 index를 나타내는데 c++에서는 사용할 수 없음
	samevlpd.at<uchar>(0, 0) = input.at<uchar>(0, 0);
	samevlpd.at<uchar>(input.rows + 1, 0) = input.at<uchar>(input.rows - 1, 0);
	samevlpd.at<uchar>(0, input.cols + 1) = input.at<uchar>(0, input.cols - 1);
	samevlpd.at<uchar>(input.rows + 1, input.cols + 1) = input.at<uchar>(input.rows - 1, input.cols - 1);

	for (int row = 0; row < input.rows; row++) {
		samevlpd.at<uchar>(row + 1, 0) = input.at<uchar>(row, 0);
		samevlpd.at<uchar>(row + 1, input.cols + 1) = input.at<uchar>(row, input.cols - 1);
	}

	for (int col = 0; col < input.cols; col++) {
		samevlpd.at<uchar>(0, col + 1) = input.at<uchar>(0, col);
		samevlpd.at<uchar>(input.rows + 1, col + 1) = input.at<uchar>(input.rows - 1, col);
	}

	return samevlpd;
}

header.h는 다음과 같다.

#ifndef HEADER
#define HEADER

cv::Mat sobelMask_x(cv::Mat input);
cv::Mat sobelMask_y(cv::Mat input);
cv::Mat zeropadding(cv::Mat input);
cv::Mat samevlpadding(cv::Mat input);

#endif // !HEADER

Result

png
original - gray색으로 받아온 원본 이미지
zero - 원본 이미지를 Zeropadding한 이미지
same - 원본 이미지를 Zeropadding 해주고 원본 이미지의 테두리 값을 넣어준 이미지
sobel_x - x방향 소벨 마스크 적용한 이미지
sobel_y - y방향 소벨 마스크 적용한 이미지
sobel_sum - x방향 소벨 마스크 적용한 이미지 와 y방향 소벨 마스크 적용한 이미지를 합친 이미지