본문 바로가기

프로그래밍 강좌/C++ - OpenCV

[OpenCV][C++] 평균 필터 스무딩(smoothing) 연산 블러링(blurring) 총정리 - boxFilter filter2D

이번에는 필터링에 대해 간단히 알아 본 후에 영상 노이즈 제거하는 방법 중 가장 기본인 평균 필터에 대해 알아보겠습니다.

그리고 opencv에서 제공하는 평균 필터들의 속도를 측정하여 평균 필터를 적용할 때 어떠한 함수를 사용하는 것이 가장 좋은지를 확인해보겠습니다.

 

필터링(Filtering) 이란?

필터링은 계산하고자하는 대상 픽셀과 그 주변 픽셀들을 활용하여 새로운 픽셀 값을 얻는 방법입니다. 이 때 주변 픽셀을 어느 범위까지 어떻게 해야 할지를 결정해야 합니다.

이런 역할을 하는 것이 바로 커널(kernel) 또는 윈도우(window), 마스크(mask)라고 불리는 행렬입니다.

일반적으로 우리가 사용하는 커널은 아래와 같은 모양들이 있습니다.

사실 정사각형의 커널을 가장 많이 사용하고 있으며, 원소를 어떻게 배치하는지 어떤 값을 넣는지에 따라 다양하게 구성할 수도 있습니다. 또한 사이즈도 다양하게 설계를 할 수 있습니다.

값들을 어떻게 정의하는가에 따라 영상을 부드럽게 만들수도 있고, 날카롭게 만들수도 있습니다. 또는 잡음을 제거하거나 에지 성분을 강조할 수 있습니다.

아래와 같은 영상이 있고 커널 값이 모두 1인 window가 있다고 한다면,

오른쪽에 있는 커널을 영상 전체에 순회하면서 커널 값들과 영상 값들을 계산하여 가운데에 해당하는 픽셀에 값을 넣어 줍니다.

커널 시작점이 아래와 같다면 오른쪽과 같은 위치에 18이란 값이 들어가게 됩니다.

기존 값이 2였는데 18로 밝아 졌네요.

이런 현상을 방지하기 위해 정규화(normalize)란 것을 하게 됩니다.

 커널의 값 1이 아닌 1/총합으로 넣으면 되는 것이죠.

오늘은 스무딩 또는 블러링이란 기법을 소개할텐데, 이는 영상을 흐릿하게 만드는 방법으로 가장 간단한 방법으로 평균 필터링(average filtering) 방법이 있습니다. 이는 주변 픽셀들의 평균 값을 적용하면 픽셀 간 차이가 적어져서 선명도가 떨어지게 됩니다.

 

평균 필터링을 하는 방법은 3가지가 있습니다.

 

1. filter2D() 사용하는 방법

filter2D 함수의 원형을 보겠습니다.

src
입력 영상
dst
출력 영상, src와 같은 크기, 같은 채널수를 가짐
ddepth
결과 영상의 depth, -1일 경우 src와 같음, CV_8U, CV_16U 등 입력 가능,
depth는 src의 depth 이상임
kernel
필터링 할 커널
anchor
고정점 좌표, 기본값 (-1, -1) 입력시 커널 중심이 고정값이 됨
delta
필터링 이후 추가적으로 더해줄 값
borderType
가장자리 확장 방식

 

이 커널을 소스로 만들어 보면 아래와 같습니다.

커널 사이즈는 홀수로 만들어야 합니다. 그리고 정규화를 하면 소수점 자리 숫자가 필요하기 때문에 CV_32F Matrix를 생성하였습니다.

그리고 이 커널을 사용하여 filter2D() 함수로 영상에 convolution을 취해주면 되는 것 입니다.

cv::Mat avg_kernel = cv::Mat::ones(ksize, ksize, CV_32F) / (ksize * ksize);	
cv::filter2D(src, dst1, -1, avg_kernel);
 

2. blur() 사용하는 방법

함수 원형은 아래와 같습니다.

src
입력 영상
dst
출력 (결과) 영상
ksize
cv::Size()로 커널의 (width, height) 크기
anchor
고정점 좌표, 기본값 (-1, -1)인 경우 고정점 좌표가 중앙이 됨
borderType
가장자리 픽셀 확장 방식

 

blur를 사용하여 average filtering 하는 소스는 아래와 같습니다.

cv::blur(src, dst2, cv::Size(ksize, ksize));
 

3. boxFilter() 사용하는 방법

boxFilter의 원형은 아래와 같습니다.

src
입력 영상
dst
출력 영상
ddepth
결과 영상의 depth, -1일 경우 src와 같음, CV_8U, CV_16U 등 입력 가능,
depth는 src의 depth 이상임
ksize
cv::Size()로 커널의 (width, height) 크기
anchor
고정점 좌표, 기본값 (-1, -1)인 경우 고정점 좌표가 중앙이 됨
normalize
normalize를 할지 안할지 결정, 기본값은 true임
borderType
가장자리 픽셀 확장 방식

 

boxFilter를 사용하는 소스는 아래와 같습니다.

cv::boxFilter(src, dst3, -1, cv::Size(ksize, ksize));
 

그럼 먼저 세가지 방법의 결과가 같은지 확인해보도록 하겠습니다.

지난번에 알아본 같은 영상을 확인하는 방법으로 확인해본 결과,

https://m.blog.naver.com/dorergiverny/223094318667

 

[OpenCV][C++] 동일 영상 판별(체크)하는 쉽고 빠른 방법 - 같은 영상인지 확인 countNonZero convertTo

우리가 영상처리를 하다보면 어떠한 처리 결과 영상이 같은지를 확인하고 싶을 때가 있습니다. 이번에는 쉽...

blog.naver.com

 

filter2D(), blur(), boxFilter 의 결과 영상이 kernel Size가 같을 경우 완전히 동일한 것을 확인할 수 있습니다.

if (compareEqual(dst1, dst2))	std::cout << "Equal_1" << std::endl;
if (compareEqual(dst2, dst3))	std::cout << "Equal_2" << std::endl;
 

그럼 결과가 같다면 kernel size 에 따라 각 함수별 속도가 어떤 특징을 가지고 있는지 확인해봐야하겠습니다. 결과의 신빙성을 위해 30회 반복 측정한 속도의 평균을 비교하였습니다.

속도 측정은 OpenCV에서 제공하는 간단한 속도 측정 클래스인 TickMeter 를 사용하였습니다. (아래 블로그 참조)

https://blog.naver.com/dorergiverny/223079726064

 

[OpenCV][C++] 함수 실행 시간 측정 쉬운 방법 - TickMeter 사용법 총정리 elapsed time chrono getTimeMilli reset b

지난 번에 함수 실행 시간 측정하는 방법으로 chrono 라이브러리를 사용하는 방법에 대해 알아 보았습니다....

blog.naver.com

일단 영상 사이즈 2048x2048이고, kernel Size 3x3, 13x13, 23x23, 33x33, 43x43, 53x53으로 바꾸면서 시간을 측정해 본 결과 filter2D()는 원리상 convolution을 하는 방식이기 때문에 size에 비례하여 속도가 느려지는 것을 볼 수 있고, blur() 함수와 boxFilter() 함수는 모두 integral 영상을 사용하여 평균을 구하기 때문에 kernel Size에 상관없이 속도가 거의 비슷한 것을 알 수 있습니다. 속도 단위는 ms 입니다.

그럼 영상 사이즈에 대해서는 어떠한 속도 특성을 가지고 있는지 확인해봅시다.

kernel Size는 7x7로 고정을 하였고, 영상 사이즈 512x512, 1024x1024, 2048x2048, 8192x8192, 16384x16384 로 생성하여 측정을 하였습니다.

확실히 모든 방식이 영상이 크기에 따라 속도가 오래걸리는 것을 확인할 수 있습니다.

filter2D()의 경우 exponential하게 증가하지만 blur() boxFilter()의 경우 integral 영상을 생성해야 하는 시간 + 평균 계산하는 시간이 픽셀 개수에 정비례하여 증가하여 연산 시간도 정비례하게 증가하는 것을 볼 수 있습니다.

전체 소스는 아래와 같습니다.

#include <iostream>
#include "opencv2/opencv.hpp"
#include <Windows.h>

cv::Mat getTiledImage(const cv::Mat& src)
{
	cv::Mat temp, temp2;
	cv::hconcat(src, src, temp);
	cv::vconcat(temp, temp, temp2);
	return temp2;
}

template<typename T>
bool checkImageDiff(cv::Mat& src, cv::Mat& dst, double diffVal)
{
	for (int y = 0; y < src.rows; ++y) {
		T* pSrc = src.ptr<T>(y);
		T* pSrc_end = pSrc + src.cols * src.channels();
		T* pDst = dst.ptr<T>(y);
		T* pDst_end = pDst + dst.cols * dst.channels();

		for (; pSrc < pSrc_end;) {
			for (int i = 0; i < src.channels(); ++i) {
				if (fabs(*pSrc++ - *pDst++) > diffVal) return false;			
			}			
		}

		return true;
	}
}

bool compareEqual(cv::Mat& src, cv::Mat& dst, double diffVal = 0.)
{
	if (src.size() != dst.size()) return false;
	if (src.type() != dst.type()) return false;

	switch (src.type()) {
	case CV_8UC1:
		return checkImageDiff<uchar>(src, dst, diffVal);
	case CV_8UC3:
		return checkImageDiff<uchar>(src, dst, diffVal);
	case CV_16UC1:
		return checkImageDiff<ushort>(src, dst, diffVal);
	case CV_16UC3:
		return checkImageDiff<ushort>(src, dst, diffVal);		
	case CV_32SC1:
		return checkImageDiff<int>(src, dst, diffVal);
	case CV_32FC1:
		return checkImageDiff<float>(src, dst, diffVal);
	case CV_64FC1:
		return checkImageDiff<double>(src, dst, diffVal);
	default:
		break;
	}	
}

int main()
{
    cv::Mat src = cv::imread("lena_color.bmp", cv::IMREAD_GRAYSCALE);

	cv::Mat lena_gray_1024 = getTiledImage(src);
	cv::Mat lena_gray_2048 = getTiledImage(lena_gray_1024);
	cv::Mat lena_gray_4096 = getTiledImage(lena_gray_2048);
	cv::Mat lena_gray_8192 = getTiledImage(lena_gray_4096);
	cv::Mat lena_gray_16384 = getTiledImage(lena_gray_8192);

	std::vector<cv::Mat> vecImages;
	vecImages.push_back(src);
	vecImages.push_back(lena_gray_1024);
	vecImages.push_back(lena_gray_2048);
	vecImages.push_back(lena_gray_4096);
	vecImages.push_back(lena_gray_8192);
	vecImages.push_back(lena_gray_16384);

	cv::TickMeter tm1, tm2, tm3;

	int ksize = 7;
	cv::Mat dst1, dst2, dst3;
	for (int i = 0; i < 6; ++i) {
		tm1.reset(); tm2.reset(); tm3.reset();
		
		for (int j = 0; j < 1; ++j) {			
			
			tm1.start();
			cv::Mat avg_kernel = cv::Mat::ones(ksize, ksize, CV_32F) / (ksize * ksize);			
			cv::filter2D(vecImages[i], dst1, -1, avg_kernel);
			tm1.stop();
			
			tm2.start();
			cv::blur(vecImages[i], dst2, cv::Size(ksize, ksize));
			tm2.stop();
			
			tm3.start();
			cv::boxFilter(vecImages[i], dst3, -1, cv::Size(ksize, ksize));
			tm3.stop();			
		}

		if (compareEqual(dst1, dst2))	std::cout << "Equal_1" << std::endl;
		if (compareEqual(dst2, dst3))	std::cout << "Equal_2" << std::endl;
        
        std::cout << i << ": (" << tm1.getAvgTimeMilli() << ", " << tm2.getAvgTimeMilli() << ", " 
			<< tm3.getAvgTimeMilli() << std::endl;
	}

    return 0;
}
 

자, 이제 blur() 함수와 boxFilter() 함수가 속도도 비슷하고 결과도 같은데,

왜 두가지의 함수가 존재할까요? 두 함수의 차이는 뭘까요?

바로~

출력 영상의 depth를 바꿀 수 있느냐 없느냐 입니다.

blur() 함수는 src와 dst의 depth가 같습니다.

하지만 boxFilter() 함수는 ddepth 변수를 -1로 할 경우 src와 dst의 depth 가 같지만

ddepth에 CV_16UC1 등 다른 depth를 가진 변수를 넣으면 그 값으로 변환되어 출력이 될 수 있습니다.

 

이번에 살펴본 결과,

평균 필터(Average Filter)를 사용할 때에는
cv::blur() 또는 cv::boxFilter() 를 사용하시면 됩니다.