메뉴 닫기

OpenCV 모폴로지 연산 (1) – 침식, 팽창

이전 포스팅에서 이진화에 대해 설명을 하였습니다. 그런데 이진화 결과가 생각보다 깔끔하지 않을 때가 많습니다. 노이즈가 많아서 정리해야 하거나, 또는 특정 객체만 추출하였는데 구멍이 숭숭 나서 후처리가 필요할 때도 있습니다. 이런 상황에서 모폴로지 연산을 통해 어느 정도 간단하게 해결할 수 있습니다.

모폴로지 연산 개요

모폴로지(morphology)는 사전적 의미로 ‘형태’를 뜻합니다. 영상처리에서는 Morphologial transformation, 즉 형태학적 변환을 수행하여 객체의 형태를 변형할 수 있습니다. 모폴로지 연산은 영상을 이진화 하였을 때 남아있는 노이즈 제거 or 이진화 된 특정 객체만 깔끔하게 남기고 싶을 때 쉽게 활용할 수 있습니다. (※ 주로 이진 영상에서 많이 쓴다는 것이지, 반드시 이진 영상에서만 쓸 수 있다는 것은 아닙니다.)

  • 침식(erosion) – 객체의 주변을 깎아냅니다.
  • 팽창(dilation) – 객체의 주변을 팽창시킵니다.

객체의 주변을 깎아내거나, 또는 팽창시키는 과정은 필터 연산과 비슷한 점이 있습니다.  특정 픽셀을 중심으로 $n times n$ 영역에 $n times n$ 커널(=마스크)을 맵핑했을 때 완전히 맵핑되지 않을 경우에 픽셀값을 0으로 바꾸면 침식될 것이고, 반대로 한 개의 픽셀이라도 맵핑될 때 최대값(1채널 grayscale 영상의 경우 255)를 적용하면 팽창할 것입니다.
특별히 모폴로지 연산에서는 커널 Structuring element라고 합니다.


Structuring element 함수

OpenCV에서는 모폴로지 연산을 위한 structuring element를 생성하여 반환하는 함수를 다음과 같이 제공합니다.

Mat getStructuringElement(int shape, Size ksize, Point anchor = Point(-1, -1))

  • int shape: element의 모양을 의미하며, MORPH_RECT, MORPH_CROSS, MORPH_ELLIPSE 중 선택.
  • Size ksize: structuring element의 사이즈. 짝수를 지정해도 에러는 나지 않으나 보통은 홀수를 사용
  • Point anchor: element 내 앵커 위치로 기본값(-1, -1)은 앵커의 중심을 의미함. shape을 MORPH_CROSS로 셋팅했을 때, 앵커의 위치에 따라 element 모양이 달라질 수 있음.

MORPH_RECT

앵커의 위치

getStructuringElement(cv::MORPH_RECT,
Size(5, 5));
getStructuringElement(cv::MORPH_RECT,
Size(5, 5), Point(2, 4));




앵커의 위치를 Point(2, 4)로 옮겨도 Structuring element의 모양은 동일합니다.

MORPH_CROSS

getStructuringElement(cv::MORPH_CROSS,
Size(5, 5));
getStructuringElement(cv::MORPH_CROSS,
Size(5, 5), Point(2, 4));




앵커의 위치를 옮겼더니 Structuring element 모양이 ┼ 에서 ┴ 으로 바뀌었습니다.

MORPH_ELLIPSE

getStructuringElement(cv::MORPH_ELLIPSE,
Size(7, 5));
getStructuringElement(cv::MORPH_ELLIPSE,
Size(7, 5), Point(2, 4));




앵커의 위치를 Point(2, 4)로 옮겨도 Structuring element의 모양은 동일합니다.

※ 위의 StructuringElement를 표현하는 전체 코드는 ‘더보기’를 눌러 확인할 수 있습니다.

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

using namespace std;
using namespace cv;

int main()
{
	Mat rect1 = getStructuringElement(cv::MORPH_RECT, Size(5, 5));
	Mat rect2 = getStructuringElement(cv::MORPH_RECT, Size(5, 5), Point(2, 4));
	
	Mat cross1 = getStructuringElement(cv::MORPH_CROSS, Size(5, 5));
	Mat cross2 = getStructuringElement(cv::MORPH_CROSS, Size(5, 5), Point(2, 4));
	
	Mat ellipse1 = getStructuringElement(cv::MORPH_ELLIPSE, Size(7, 5));
	Mat ellipse2 = getStructuringElement(cv::MORPH_ELLIPSE, Size(7, 5), Point(2, 4));

	cout << rect1 << endl;
	cout << rect2 << endl;
	cout << "---------------------------" << endl;
	
	cout << cross1 << endl;
	cout << cross2 << endl;
	cout << "---------------------------" << endl;
	
	cout << ellipse1 << endl;
	cout << ellipse2 << endl;
	
	return 0;
}

침식 연산 – erode 함수

void erode(InputArray src, OutputArray dst, InputArray kernel, Point anchor = Point(-1, -1),
int iterations = 1, int borderType = BORDER_CONSTANT,
const Scalar & borderValue = morphologyDefaultBorderValue())

  • InputArray src: 입력 영상; 채널 수는 임의로 지정할 수 있으나 depth는 CV_8U, CV_16U, CV_16S, CV_32F, CV_64F 중 하나여야 함.
  • OutputArray dst: 출력영상. 입력영상과 크기 및 타입 같음.
  • InputArray kernel: structuring element; Mat()을 지정하면 $3 times 3$ 사이즈의 사각 element가 사용됨. 위에서 언급한 getStructuringElement 를 사용할 수도 있음.
  • Point anchor: element 내 앵커 위치로 기본값(-1, -1)은 앵커의 중심을 의미함.
  • int iteration: 침식 연산 반복 횟수.
  • int borderType: 가장자리 픽셀의 extrapolation 메소드 타입 선택. (가장자리에 대한 연산 타입은 BORDER_CONSTANT, BORDER_REPLICATE 등이 있으며 BORDER_WRAP는 지원 안함)
  • const Scalar & borderValue: 가장자리 픽셀 처리 방식이 BORDER_CONSTANT일 경우, 가장자리를 채울 값

침식 연산을 수행하는 함수는 아래와 같습니다.

$$ texttt{dst} (x,y) = min _{(x’,y’): , texttt{element} (x’,y’) ne0 } texttt{src} (x+x’,y+y’) $$


팽창 연산 – dilate 함수

void dilate(InputArray src, OutputArray dst, InputArray kernel, Point anchor = Point(-1, -1),
int iterations = 1, int borderType = BORDER_CONSTANT,
const Scalar & borderValue = morphologyDefaultBorderValue())

dilate 함수의 파라미터는 erode와 동일하므로 설명을 생략합니다.

팽창 연산을 수행하는 함수는 아래와 같습니다.

$$ texttt{dst} (x,y) = max _{(x’,y’): , texttt{element} (x’,y’) ne0 } texttt{src} (x+x’,y+y’) $$

모폴로지 연산 적용 예제

침식과 팽창 연산의 결과를 보다 확실하게 확인하기 위해 직접 그림을 그려보았습니다.

예제 코드

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

using namespace std;
using namespace cv;

int main()
{
	Mat src = imread("paint.bmp", IMREAD_GRAYSCALE);
	if(src.empty())
	{
		cerr << "file error" << endl;
		return 1;
	}
		
	Mat dst_erode;
	erode(src, dst_erode, Mat());

	Mat dst_dilate;
	dilate(src, dst_dilate, Mat());
	
	imshow("src", src);	
	imshow("erode", dst_erode);
	imshow("dilate", dst_dilate);
	
	waitKey();
	destroyAllWindows();
	
	return 0;
}

실행 결과



제일 왼쪽이 원본 영상입니다. 침식 연산을 수행하면 주변 노이즈는 사라지지만, 객체 내부의 구멍이 더욱 숭숭 뚫리게 되어 문제가 됩니다. 반면 팽창 연산을 수행하게 되면 객체 내부 구멍은 메워지지만, 노이즈 역시 진해진다는 단점이 있습니다. 다음 포스팅에서는 침식, 팽창 연산을 활용한 Open / Close 연산을 통해 위의 문제를 개선해보도록 하겠습니다.

※ 참조: OpenCV 사이트

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다