본문 바로가기
코딩/OpenCV

[C++ opencv] 평균필터 적용하여 노이즈 제거하기 average filter, filter2d()

by DIYver 2020. 6. 26.

본문 목표

영상처리에 있어서 제일 중요한 것들 중 하나는 당연 '노이즈 제거' 이다.

 

노이즈 제거 방법에는 정말 여러가지가 있지만, 제일 쉬운 방법은 blur 처리를 하는 것이다.

 

blur 처리란 영상을 흐리게하는 방법이다.

 

OpenCV에서는 filter2D( ) 라는 함수가 있다.

 

이번에는 filter2D( ) 함수를 이용해서 영상을 blur 처리하는 방법을 알아본다.

 

 

 

키워드 : average filter, filter2d(), 평균필터, zero padding, mirror padding

 

 

 

평균필터 - 원리 이해

 

필터는 말 그대로 걸러주는 또는 처리를 해주는 것이다.

 

그러니 평균필터란 말 그대로 평균값을 이용한 필터링 방식이다.

 

여기에는 커널 또는 마스크 크기가 필요하다.

 

(커널은 마스크 크기에 계산 가중치까지 적용한 것이다)

 

쉽게 생각하면 필터 연산에 포함될 범위를 뜻한다.

 

위의 이미지를 보면

 

왼쪽에 mask 크기는 (3,5) 이다.

 

가운데 있는 좌표 (x,y) 에 해당하는 픽셀을 중심으로 (3,5) 마스크 범위의 모든 픽셀들을 값의 평균을 낸다.

 

그 평균을 낸 값을 다시 중심 픽셀 (x,y) 값에 저장시키는 것이 평균 필터이다.

 

이러한 과정을 원본 이미지 픽셀 하나하나에 적용을 하는 것이다.

 

그러면 이미지에서 가끔 값이 확 튀던 픽셀들은 주변값의 영향으로 값이 정상적으로 돌아오게 된다.

 

 

 

더 쉽게 이해해 보자.

 

위처럼 mask 크기가 (3,3) 일 때, 

 

마스크 범위 내의 평균을 구해보면 우측과 같다고 할 수 있다.

 

그 값이 마스크의 중앙 픽셀인 e 에 저장이 되는 것이 평균 필터이다.

 

 

 

 

 

 

 

이제는 좀 심화된 과정을 살펴보자.

 

이미지의 중앙쪽에는 마스크 범위에 다 들어오게 된다.

 

근데 이미지의 테두리에서는 마스크 적용을 어떻게 하지? 

 

라는 생각을 해봐야한다.

 

만약 맨위 맨 좌측의 픽셀 좌표를 (1,1) 이라고 한다면

 

(0,0), (0,1), (0,2), (1,0), (2,0) 의 값은 뭐라고 계산이 되는거지?

 

생각을 해봐야한다.

 

 

 

과연 ? 에는 무슨 값들이 있어서

 

(1,1) 에 필터가 적용이 되는 것일까...

 

다행히도 우리가 이런걸 생각하기도 전에 앞선 개발자들이 이미 고민을 끝내고 해결책을 다 마련해 주었다.

 

 

위의 사진을 해석해보면

 

Ignore the edges : 테두리쪽 데이터는 무시해버리고 filter 적용

 

Pad with zeros : 테두리 넘어의 데이터는 그냥 0으로 두고 filter 적용

 

Mirroring : 테두리 넘어의 데이터를 테두리를 경계로해서 복제를 하고 filter 적용

 

 

이렇게 3가지 방법이 있겠다.

 

왜 3가지가 있는지도 생각해 봐야겠지만,

 

쉽게 말하자면, 아래와 같은 이유가 있겠다.

 

1. 연산처리 속도를 빠르게 하기 위해서 ( 테두리쪽 데이터는 별로 필요 없는 경우)

2. 테두리쪽 데이터도 스무스하게 이어져야 하는 경우

 

이 글을 보는 필자들도 그냥 코드 긁어다가 필터를 적용하기 보다는

 

내가 어느상황에서 이 필터가 필요한지 충분한 고민을 해보고 적용해 보기를 추천한다.

 

 

 

 

 

 

 

 

 

알아볼 함수 원형

-  평균필터  ( filter2d )

...
	Mat avg_kernel = Mat::ones(5, 5, CV_32F) / 25;	// mask 가 (5,5) 인 평균필터 커널

	filter2D(img, img_average, -1, avg_kernel, Point(-1, -1), (0, 0), 4);
...

 

filter2D( srcdst, ddepth, kernel, Point anchor, delta, borderType

  

  ○ src : 입력할 이미지 변수

 

  ○ dst : 필터가 적용되어 저장될 이미지 변수

 

  ○ ddepth : 저장될 이미지의 깊이 기본 값 : -1  /  건드릴 필요 없음

 

  ○ kernel : 이미지에 적용할 필터의 마스크와 연산 정보를 담고있는 행렬

    - 자신이 직접 평균필터 적용할 마스크와 연산식을 입력해야 함

 

  ○ Point anchor : 건드릴 필요 없음 기본값 Point(-1, -1) 사용

 

  ○ delta : 건드릴 필요 없음 기본값 (0,0) 사용

 

  ○ border Type : 이미지 테두리 밖의 필셀 처리방법

    - zero padding 또는 reflect(mirror) padding 을 적용할 수 있음

      ■ BORDER_CONSTANT  또는  0

      ■ BORDER_REPLICATE  또는 1

      ■ BORDER_REFLECT  또는  2 
      ■ BORDER_DEFAULT  또는  4 

      ■ BORDER_REFLECT101  또는  4 

      ■ BORDER_TRANSPARENT  또는  5 
      ■ BORDER_ISOLATED  또는  16

border type을 봤을 때, 

OpenCV에서는 기본값으로 mirror padding 을 사용하고 있음을 확인할 수 있었다.


 

 

 

 

kernel 에 대해서 잘 알아야한다.

 

앞으로 모든 필터 적용은 이 kernel 에 따라서 결과가 달라지기 때문이다.

 

high pass/ low pass 필터 모두 이 kernel로 조절 가능하다.

 

앞서 말했었지만, 커널은 마스크 크기와 연산 결과를 포함하는 행렬이어야 한다.

 

 

만약 mask 크기가 (3,3) 인 평균필터 커널은 위의 행렬이어야 한다.

 

이를 코드로 간략하게 한다면

 

Mat kernel = Mat::ones(3,3,CV_32F) / 9 ;    가 된다.

 

 

아직 이해가 안 되었으면 밑의 예시를 더 봐보도록 하자.

 

만약 mask 크기가 (5,5) 인 평균필터 커널의 식은 어떻게 될까?

 

- Mat kernel = Mat::ones(5,5,CV_32F) / 25 ;    가 된다.

 

만약 mask 크기가 (5, 9) 인 평균필터 커널의 식은 어떻게 될까?

 

- Mat kernel = Mat::ones(5,9,CV_32F) / 45 ;    가 된다.

 

 

원리는 1 로만 이루어진 행렬을 Mat::ones(행, 열, CV_32F ) 를 통해서 만들어 준다.

 

그리고 행렬의 크기만큼 나눠주면 된다.

 

이것이 평균필터의 원리이다.

 

 

 

 

 

 

 

 

 

 

 

코드 테스트 결과

- CODE

#include <opencv2/opencv.hpp>

using namespace cv;
using namespace std;

int main(int ac, char** av) {

	Mat img = imread("Lenna.png", 0);	//이미지를 grayscale로 불러옴
	Mat img_average_constant;
	Mat img_average_REPLICATE;
	Mat img_average_REFLECT;
	Mat img_average_REFLECT101;
	Mat img_average_ISOLATED;


	Mat avg_kernel = Mat::ones(5, 5, CV_32F) / 25;	// mask 가 (5,5) 인 평균필터 커널

	filter2D(img, img_average_constant, -1, avg_kernel, Point(-1, -1), (0, 0), 0);
	filter2D(img, img_average_REPLICATE, -1, avg_kernel, Point(-1, -1), (0, 0), 1);
	filter2D(img, img_average_REFLECT, -1, avg_kernel, Point(-1, -1), (0, 0), 2);
	filter2D(img, img_average_REFLECT101, -1, avg_kernel, Point(-1, -1), (0, 0), 4);
	filter2D(img, img_average_ISOLATED, -1, avg_kernel, Point(-1, -1), (0, 0), 16);
	
	//	BORDER_CONSTANT		//0
	//	BORDER_DEFAULT		//4
	//	BORDER_ISOLATED		//16
	//	BORDER_REFLECT		//2
	//	BORDER_REFLECT101	//4
	//	BORDER_TRANSPARENT	//5
	//	BORDER_REPLICATE	//1

	imshow("img_average_constant", img_average_constant);
	imshow("img_average_REPLICATE", img_average_REPLICATE);
	imshow("img_average_REFLECT", img_average_REFLECT);
	imshow("img_average_REFLECT101", img_average_REFLECT101);
	imshow("img_average_ISOLATED", img_average_ISOLATED);
	waitKey(0);


	return 0;
}

 

- RESULT

설명

위는 BORDER_CONSTANT 결과 사진이다.

 

Zero padding 이 적용된 것을 확인할 수 있다.

 

 

 

 

 

위는 BORDER_REFLECT 결과 사진들이다.

 

차이가 없는것으로 확인된다.

 

테두리가 검은색을 띄지 않고 자연스러운것을 확인할 수 있다.

 

 

 

 

위는 BORDER_ISOLATED 결과 사진이다.

 

BORDER_CONSTANT 와 결과가 같은것을 볼 수 있다.

 

즉, 이 방법도 Zero padding 이라는 뜻이다.

 

 

 

 

 

 

이상하게도 BORDER_TRANSPARENT 방법은 코드에 오류가 나서

사용을 하지 못했다.

중요한건 Zero padding 이냐 Mirror padding 이냐의 차이이므로

무시해도 될 것 같다.

 

이외에도 궁금한 것이 있다면

아래 블로그를 확인해보는 것도 좋을 것 같다.

 

https://blog.naver.com/PostView.nhn?blogId=pckbj123&logNo=100203464210&proxyReferer=https:%2F%2Fwww.google.com%2F

 

[OpenCV] 테두리, 가장자리 늘리는 함수, copyMakeBorder()

커널을 사용해서 필터를 입힐 경우, 대부분 가장자리에 대해서는 필터를 입히는 것이 불가능할 때가 있다. ...

blog.naver.com

 

 

 

 

이번에는 좀 더 극단적으로 zero padding 과 mirror padding 을 비교해보자.

 

마스크 크기를 (25, 25)로 했을 때의 비교이다.

 

 

 

확실히 zero padding 이 테두리가 검은색을 띄는것을 볼 수 있다.

 

필자의 생각인데

 

연산 처리속도가 빠르려면 zero padding이 우세할 것이라 생각한다.

 

물론 이런 조그마한 이미지 또는 한장 이미지에서는 큰 차이 없겠지만

 

카메라를 이용한 영상처리에서나 이미지 크기가 커지면 차이가 커질 것으로 생각한다.

 

물론 시험을 해봐야 확답할 수 있겠다.

 

 

 

(5,5) mask 적용 _ average filter

그리고 당연히 컬러 이미지에서도 적용이된다.

 

코드도 올려놓은 코드 그대로 이용하면 된다.

 

 

 

 

 

 

 

 

 

 

도움이 되었거나, 문제가 있는 경우 댓글로 알려주세요~!

감사의 댓글은 작성자에게 큰 힘이 됩니다 ^^

댓글