Home [데브코스] 6주차 - OpenCV Gaussian blurring
Post
Cancel
Preview Image

[데브코스] 6주차 - OpenCV Gaussian blurring


블러링

평균 값 필터 (mean filter)

영상의 특정 좌표 값을 주변 픽셀 값들의 산술 평균으로 설정하는 것이다. 픽셀 들 간의 그레이스케일 값 변화가 줄어들어 날카로운 엣지가 무뎌지고, 영상에 있는 잡음의 영향이 사라지는 효과가 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include <iostream>
#include "opencv2/opencv.hpp"

using namespace std;
using namespace cv;

int main()
{
	Mat src = imread("lenna.bmp", IMREAD_GRAYSCALE);

	if (src.empty()) {
		cerr << "Image laod failed!" << endl;
		return -1;
	}
	float data[] = {1 / 9.f, 1 / 9.f, 1 / 9.f,
					1 / 9.f,1 / 9.f,1 / 9.f,
					1 / 9.f,1 / 9.f,1 / 9.f
	};

	Mat kernel(3, 3, CV_32FC1, data);

	/* Mat kernel = (Mat_<float>(3,3,CV_32FC1, ...) */

	Mat dst;
	filter2D(src, dst, -1, kernel);


	imshow("src", src);
	imshow("dst", dst);
	waitKey();
}

3x3 필터는 각각에 1/9로 다 채우는 것이다. 5x5필터도 있지만 3x3을 가장 많이 사용한다. 이 때, 모든 합이 1이 되어야 평균 밝기가 유지된다.

   
1/91/91/9
1/91/91/9
1/91/91/9


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#include <iostream>
#include "opencv2/opencv.hpp"

using namespace std;
using namespace cv;

int main()
{
	Mat src = imread("lenna.bmp", IMREAD_GRAYSCALE);

	if (src.empty()) {
		cerr << "Image laod failed!" << endl;
		return -1;
	}
#if 0
	float data[] = {1 / 9.f, 1 / 9.f, 1 / 9.f,
					1 / 9.f,1 / 9.f,1 / 9.f,
					1 / 9.f,1 / 9.f,1 / 9.f
	};
	Mat kernel(3, 3, CV_32FC1, data);
#else
	Mat kernel = Mat::ones(3, 3, CV_32FC1); // 주변의 값들을 다 더해서 산술하기 때문에 255가 다 넘어서 흰색 화면이 나오게 된다.
	

	/* Mat kernel = (Mat_<float>(3,3,CV_32FC1, ...) */

	Mat dst;
	filter2D(src, dst, -1, kernel);


	imshow("src", src);
	imshow("dst", dst);
	waitKey();
}

그래서

1
Mat kernel = Mat::ones(3, 3, CV_32FC1) / 9.f;

또는

1
blur(src, dst, Size(3,3));

blur 함수를 사용해서 블러링을 한다. 여기서 3,3 대신 5,5로 하면 더 심하게 블러 처리가 된다. 즉 사이즈가 커질 수록 블러링이 더 잘된다. 그러나 연산량이 픽셀당 곱셈이 필터의 크기만큼(9,25,49..)로 늘어나기 때문에 주의해야 한다.

1
2
3
4
blur(src, dst2, Size(5, 5)); // 픽셀 당 25

blur(src, dst3, Size(3, 3));
blur(dst3, dst3, Size(3, 3)); //  픽셀 당 9+9 = 18

3x3을 두 번하면 픽셀당 총 18번의 연산을 하지만, 5x5를 한 번하면 픽셀당 총 25번의 연산을 한다. 따라서 작은 필터를 여러 번 거는 것이 연산이 더 작아질 수 있다.


1
void blur()

블러링 자체를 많이 주려면 필터 크기를 늘리면 되지만, 연산량이 늘어난다.


평균값 필터에 의한 블러링의 단점

필터링 대상 위치에서 가까이 있는 픽셀과 멀리 있는 픽셀이 모두 같은 가중치를 사용하여 평균을 계산한다. 멀리 있는 픽셀의 영향을 많이 받을 수 있다.

다시 말해, 1칸 옆에 있는 것과 3칸 옆에 있는 것의 비중이 같은데, 원래는 둘의 비중이 달라야 한다. 그래서 가우시안을 많이 사용하게 된다.


정규 분포와 가우시안

정규 분포(Normal distribution)이란?

  • 평균을 중심으로 좌우대칭인 종모양을 갖는 확률 분포
  • 가우시안 분포
  • 자연계에서 일어나는 수많은 일이 설명될 수 있다.
  • 키 몸무게 시험점수 잡음 측정오차 등등
  • Central limit theorem


μ : 평균 σ : 표준 편차

σ가 클수록 그래프가 펴져 있고, σ가 작을수록 뾰족하다. σ=1인 분포를 표준 정규 분포라 한다. σ=1일 때, 대략적으로 높이가 0.4가 된다.

  • mean(평균) = median(중앙값) = mode(튀어나온 곳)
  • 가우시안 분포의 영역의 넓이는 1이다.


가우시안의 예

  • 가우시안 필터 마스크의 크기는 (8σ+1) 또는 (6σ+1)

대체로 σ=1로 가정한다. 그래서 가장 중간(0,0)이 가장 큰 값을 가지고, 바깥쪽으로 갈수록 0에 수렴한다.


2차원함수를 1차원 2개로 분리가 가능하다. 이를 separable하다고 말한다.

또는, mask 9x9 행렬이 있다고 할 때, 이를 전치행렬을 통해 분리하여 1x9 * 9x1 로 계산을 하면 동일하게 9x9로 나오지만, 연산량이 줄어드는 결과를 가져올 수 있다.

1
void GaussianBlur(InputArray src,OutputArray dst,Size ksize,double sigmaX,double sigmaY,int borderType);
  • src : 입력 영상, 각 채녈 별로 처리된다.(CV_8U, CV_16U, CV_16S)
  • dst : 출력 영상, src와 같은 크기와 같은 타입
  • ksize : 가우시안 커널 크기, Size()를 지정하면 sigma값에 의해 자동 결정된다. truecolor : (8sigma + 1) or gray : (6sigma + 1)
    • Size(9,9) 라고 하면 전체 -9~9 크기 중에서 -5~5 정도의 가우시안 분포만 사용하겠다는 것이다. 그러나 이는 효과가 좋지 않다. 가급적으로 Size()으로 지정하는 것이 좋다.
  • sigmaX : X방향 표준편차
  • sigmaY : Y방향 표준편차, 0이면 sigmaX와 같게 설정
  • borderType : 가장자리 픽셀 처리 방식


1차원 즉 mean필터를 사용하는 것이 같은 크기의 가우시안 분포를 사용하는 것보다 훨씬 더 많이 블러링이 된다. 그러나 자연스럽게 블러링을 하고 싶다면 가우시안을 사용하는 것이 좋다. 따라서 가우시안의 σ의 크기를 키우면서 블러링 효과를 높이는 것이 좋다.



샤프닝

언샤프 마스크(unsharp mask) 필터링

부드러운 영상을 이용하여 날카로운 영상을 생성한다.

변화하는 지점에서 더 큰 변화를 줌으로써 날카롭게 하는 것이다.

①번이 원래의 이미지, ②번은 블러링된 이미지다. 즉, 부드럽게 만들어준다. ③번의 경우 ①-② 를 한 것이다. 마지막은 ①*2 - ②를 한 그래프다.

1
2
3
4
5
6
Mat = src = imread("camera.bmp", IMREAD_GRAYSCALE);

Mat blr;
blur(src,blr,Size(3,3));

Mat dst = 2 * src - blr;

이러한 방법으로 구현할 수 있다.


위의 식을 마스크의 형태로 판단해봤을 때, f(x,y), 원본에 대한 마스크는 중앙에만 1이 되어야 한다. 그리고 블러링한 g”(x,y)에 대한 마스크는 1/9가 각각 들어가있다. 이를 2*f(x,y)-f”(x,y) 를 마스크의 형태로 결과를 볼 수 있다.

따라서 sharpen이라는 마스크의 값을 구현할 수 있다.

1
2
3
4
5
6
7
8
9
float sharpen[] = {
		-1 / 9.f, -1 / 9.f, -1 / 9.f,
		-1 / 9.f, 17 / 9.f, -1 / 9.f,
		-1 / 9.f, -1 / 9.f, -1 / 9.f
	};
	Mat filter(3, 3, CV_32F, sharpen);

	Mat dst;
	filter2D(src, dst, -1, filter);

이렇게 구현해볼 수도 있다.


가우시안 언샤프 필터

1
2
3
4
5
6
Mat = src = imread("camera.bmp", IMREAD_GRAYSCALE);

Mat blr;
GaussianBlur(src, blr, Size(), 1.0);

Mat dst = 2 * src - blr;


입력 영상에서 블러링 영상을 빼면, 날카로운 성분만 남는다고 볼 수 있다. 이를 입력에 더해주면 날카로움을 강조한다는 개념이 될 것이다. 이 때, 그냥 더하는 것이 아니라 더할 때 가중치(weight)를 추가해줄 수 있다. 위 그래프의 3번 그래프를 g(x,y)라 하면

unsharp mask f’(x,y) = f(x,y) + alpha * g(x,y) = f(x,y) + alpha(f(x,y) - f”(x,y)) = (1+alpha) * f(x,y) - alpha * f”(x,y)

1
2
float alpha = 1.0f;
Mat dst = (1.f + alpha) * src - alpha * blr;


어떤 데이터를 0과1로 나눈다는 것, 영상의 경우 0과255로 나누는 것을 이진화라 하는데, 이 g(x,y)를 단순하게 날카로운 성분만이라고는 볼 수 없지만, 단순하게 날카로움이라고 본다는 것이 이진화의 원리다.

샤프닝은 사실 잘 사용하지 않는다. 영상 처리를 할 때 샤프닝을 이용하는 일은 잘 없지만, 이 내용과 개념에 대해 설명하기 위해 배웠다.



잡음 제거 필터

영상의 잡음(Noise)

영상의 픽셀 값에 추가되는 원치 않는 형태의 신호를 말한다. 카메라에서 광학 신호를 전기적 신호로 변환하는 과정에서 잡음을 추가할 수 있다. 센서의 발열과 같은 것들에 의해 발생할 수 있다.

획득한 영상 = 원본 신호 + 잡음

== f(x,y) = s(x,y) + n(x,y)


잡음의 종류

  1. 가우시안 잡음(Gaussian noise)

가우시안의 경우 σ가 클수록 잡음이 많다는 것이고, σ가 작을수록 잡음이 작음을 의미한다.


  1. 소금&후추 잡음(salt&pepper)

옛날에 많이 나오는 것으로 흰/검정색의 값이 점처럼 찍히는 것을 말한다.



프로파일

영상에서 특정 경로 상에 있는 픽셀의 밝기 값을 그래프로 나타낸 것

  • line profile
  • intensity profile

흰색 선이 지나가는 곳에서의 profile을 보는 것이다. 중간 부분을 보면 변동이 심하게 나온다.


이를 없애기 위해서는 가우시안 노이즈를 잡으면 된다. 저런 지그재그 형태는 가우시안 잡음이기 때문이다.

1
GaussianBlur(src, src, Size(), 2);

sigma를 2로 하면 윤곽이 너무 무뎌지기 때문에 1이 적당하다. 1을 하더라도 약간 흐려지지만, 노이즈를 제거하는 것을 통해 영상에서 잘못 판단할 효과를 줄일 수 있다. 차선 인식이나 객체 검출의 경우에도 가우시안 블러를 사용하기도 한다. 블러했다고 해서 찾던 것을 못찾는 일은 거의 없기 때문이다.



  • 영상에 가우시안 잡음 추가하기
1
2
3
4
5
6
7
8
9
10
int main(void){
    Mat src = imread("lenna.bmp",IMREAD_GRAYSCALE);

    Mat noise(src,size(), CV_32S);
    randn(noise,0,10);

    Mat dst;
    add(src, noise, dst, noArray(), CV_8U);
    ...
}

만약 sigma가 10이라면 그에 대해 randn(noise,0,10)의 범위로 노이즈를 추가하는 것이다. sigma가 20이라면 randn(noise,0,20)



미디언 필터

주변 픽셀들의 값들을 정렬하여 그 중앙값으로 픽셀 값을 대체하는 것을 말한다.

전체 이미지를 스캔하여 정렬시킨다음(std:sort), 중앙값을 중간에 넣어주면 된다. 이 필터는 salt pepper 이미지에 효과적이다.


  • opencv에서 제공하는 미디언 필터
1
void medianBlur( InputArray src, OutputArray dst, int ksize )
  • src/dst : 입력/출력 영상
  • ksize : 구멍 크기, 1보다 큰 홀수로 지정해야 함.


1
2
3
4
5
Mat src = imread("lenna.bmp", IMREAD_GRAYSCALE);

Mat noise = addNoise(src, 10); // 10% noise

medianBlur(noise, dst1, 3);
  • 직접 만든 미디언 필터
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
void myMedian(const Mat& src, Mat& dst)
{
	CV_Assert(!src.empty());
	CV_Assert(src.type() == CV_8UC1); // 런타임에서 조건을 점검한다. 

	// src 영상을 dst로 깊은 복사를 수행
	src.copyTo(dst);

	uchar p[9];
	for (int y = 1; y < src.rows - 1; y++) {
		for (int x = 1; x < src.cols - 1; x++) {
			// src 영상의 픽셀 값이 0 또는 255인 경우에만 적용
			if ( src.at<uchar>(y,x) == 0 || src.at<uchar>(y, x) == 255 )  {
				p[0] = src.at<uchar>(y - 1, x - 1);
				p[1] = src.at<uchar>(y - 1, x);
				p[2] = src.at<uchar>(y - 1, x + 1);
				p[3] = src.at<uchar>(y, x - 1);
				p[4] = src.at<uchar>(y, x);
				p[5] = src.at<uchar>(y, x + 1);
				p[6] = src.at<uchar>(y + 1, x - 1);
				p[7] = src.at<uchar>(y + 1, x);
				p[8] = src.at<uchar>(y + 1, x + 1);

				sort(p, p + 9);

				// 정렬된 src 픽셀 값 중에서 중앙값을 dst 픽셀 값으로 설정
				dst.at<uchar>(y, x) = p[4];
			}
		}
	}
}

이 때 중요한 것은 전체를 다 하는 것이 아니라, 0 또는 255인 경우에만 적용할 것이다. 원래 있는 medianBlur를 하면 모든 픽셀에 대해 수행하는데 이는 불필요하므로 0 or 255에 대해서만 적용하고자 하는 것이다.



양방향 필터(Bilateral filter)

에지 보전 잡은 제거 필터의 하나로 평균 값 필터 또는 가우시안 필터는 에지 부근에서도 픽셀 값을 평탄하게 만드는 단점이 있다. 기준 픽셀과 이웃 픽셀과의 거리, 그리고 픽셀 값의 차이를 함께 고려하여 스무딩 정도를 조절한다. 즉 값이 조금씩 변화하는 것은 평탄화하고, 크게 변화하는 것은 그대로 둔다. 이 필터는 조금 느리다는 담점이 있다.

  • p,q : p점과 q점의 픽셀 좌표(벡터)
  • Ip, Iq : p점과 q점에서 픽셀 값
  • Wp : 필터 커널 합이 1이 되도록 만드는 정규화 함수

초록색과 노란색 둘다 가우시안(Gσ)의 형태이다. 두 개의 가우시안을 합쳐서 만드는 형태지만, 초록색 부분의 p,q는 픽셀 좌표의 거리, 노란색 부분의 Ip,Iq는 픽셀 값의 차이이다.

표기법 - 굵은 소문자는 벡터 - 볼드체는 행렬 - || || 는 norm이라 하여 거리 계산한다는 뜻 - | |는 절대값


가우시안 필터링의 경우 영상 전체에 똑같은 값으로 블러링한다. 그러나 양방향의 경우 어두운 곳은 커널을 0으로 , 밝은 곳은 그대로 블러링한다. 값이 평탄한 곳은 블러링하고, 엣지부분은 그대로 둔다.


위의 식을 다시 보면, 앞의 초록색 부분은 space weight, 노란색 부분은 픽셀 값의 차이를 통해 range weight를 준다. 값의 차이가 큰부분에 대해서는 블러링을 안하고, 값의 차이가 작은 부분에서만 블러링을 한다.


이를 3차원으로 표시하면 다음과 같다.


1
void bilateralFilter(src, dst, int d, double sigmaColor, double sigmaSpace, int borderType = 4);
  • d : 음수라면 필터 사이즈를 자동으로 결정해준다.
  • sigmaColor : 픽셀 값의 차이가 이 값보다 작은 것에 대해 블러링한다는 기준점, 3σ를 기준으로 σ=10이라면 최대 60(-30~30)의 차이 중에서 10 정도의 차를 기준으로 한다.
  • sigmaSpace : 블러링 정도
  • borderType : 블러링 타입 방법
1
bilateralFilter(src, dst2, -1, 10, 5);


단순하게 노이즈를 제거할 때는 가우시안 블러를 많이 사용한다.

This post is licensed under CC BY 4.0 by the author.