박스 블러와 가우시안 블러가 무엇인가?
약간 이렇게 부드러워지는 필터 효과임.
둘은 무슨 차이인가?
박스블러에서 약간만 확장하면 가우시안 블러가 됨 ,또 자세히 보면 가우시안 블러가
이미지 본래의 형태를 잘 유지해주고 있음.
정확한 차이 점은 이론 설명할 때, 확실히 알 수 있음.
이론
커널(Kernel)
출처 Wikidia : https://en.wikipedia.org/wiki/Kernel_(image_processing) )
가운데에 있는 Kernel Metrix 효과를 적용하면 맨 오른쪽의 사진과 같은
효과를 적용해낼 수 있는 표 임.
특징1. Blur쪽 메트릭스를 자세히보면, 모든 더 한 값이 분모의 값과 같다는 특징이 있는데.
Box Blur의 경우 1을 9번 더한 값과 분모의 값이 같고, 가우시안 블러 또한 마찬가지 이다.
특징2. 중앙의 수를 제외하고 메트릭스가 대칭 구조를 갖고 있다.
커널 메트릭스가 어떻게 이미지에 적용되는가?
간단하게 설명해서 원본이미지의 픽셀 하나하나에 컨벌루션을 곱한 뒤 더한다는 개념이다.
이미지에 보이는 바와 같이 모든 것을 곱한 뒤 더하면 된다. 이미지의 수식을 참고하면 이해하기
편할 것이다.
근데 맨 왼쪽, 즉 x,y 값 기준으로 0인 아이는 어떻게 적용해야하나?
이 경우 이미지를 참고하면, 범위를 벗어난 수는 옆의 숫자를 참조한다는 것을 알 수 있다.
즉 만약에 중앙의 픽셀 값이 7이 아니라 4라면, 아래와 같은 메트릭스 형태를 가질 것이다.
4,4,6
4,4,6
6,6,4
코드로 만들어보자
일단 코드로 만들기 전, 한 가지 주목해야 할 점은 위 사진과 같이. 하나의 픽셀에 대해서 가로부터
계산 한뒤, 세로를 계산 한다.
이제 코드로 직접 보자.
BOX Blur
코드
void Image::BoxBlur5() { std::vector<Vec4> pixelsBuffer(this->pixels.size()); // 사본 복사 /* * Separable convolution * 한 번에 2차원 Kernel을 적용하는 대신에 1차원 Kernel을 두 번 적용 * 이해하기 쉽고 효율적이다. */ // 가로 방향 (x 방향) #pragma omp parallel for for (int j = 0; j < this->height; j++) { for (int i = 0; i < this->width; i++) { // 주변 픽셀들의 색을 평균내어서 (i, j)에 있는 픽셀의 색을 변경 // this->pixels로부터 읽어온 값들을 평균내어서 pixelsBuffer의 값들을 바꾸기 Vec4 neighborColorSum{ 0.0f,0.0f,0.0f,0.0f}; for (int si = 0; si < 5; si++) { Vec4 neighborColor = this->GetPixel(i + si-2, j ); neighborColorSum.v[0] += neighborColor.v[0]; neighborColorSum.v[1] += neighborColor.v[1]; neighborColorSum.v[2] += neighborColor.v[2]; } int idx = i + width * j; pixelsBuffer[idx].v[0] = neighborColorSum.v[0] * 0.2f; pixelsBuffer[idx].v[1] = neighborColorSum.v[1] * 0.2f; pixelsBuffer[idx].v[2] = neighborColorSum.v[2] * 0.2f; } } // Swap std::swap(this->pixels, pixelsBuffer); //return; // 여기까지 구현하고 테스트 // 세로 방향 (y 방향) #pragma omp parallel for for (int j = 0; j < this->height; j++) { for (int i = 0; i < this->width; i++) { // 주변 픽셀들의 색을 평균내어서 (i, j)에 있는 픽셀의 색을 변경 // this->pixels로부터 읽어온 값들을 평균내어서 pixelsBuffer의 값들을 바꾸기 Vec4 neighborColorSum{ 0.0f,0.0f,0.0f,0.0f }; for (int si = 0; si < 5; si++) { Vec4 neighborColor = this->GetPixel(i , j + si - 2); neighborColorSum.v[0] += neighborColor.v[0]; neighborColorSum.v[1] += neighborColor.v[1]; neighborColorSum.v[2] += neighborColor.v[2]; } int idx = i + width * j; pixelsBuffer[idx].v[0] = neighborColorSum.v[0] * 0.2f; pixelsBuffer[idx].v[1] = neighborColorSum.v[1] * 0.2f; pixelsBuffer[idx].v[2] = neighborColorSum.v[2] * 0.2f; } } // Swap std::swap(this->pixels, pixelsBuffer); }
결과물
설명
일단 pixelsBuffer를 통해 pixel의 사본을 복사한다.
차후, swap을 통해 PixelsBuffer와 Pixel의 값을 바꿔줄 것이다.
이제 이미지의 전체 사이즈 만큼 2중 루프를 돈다. 돌면서 컨벌루션을 곱한 값을 저장할
neighborColorSum 변수를 만들어둔다.
이후 한 번 더, si 루프를 돌면서 중앙에서, 2 칸 왼쪽으로 떨어진 Pixel로부터, 2 칸 오른쪽으로 떨어진 Pixel까지 값을 더하기 위함이다.
GetPixel
Vec4& Image::GetPixel(int i, int j) { i = std::clamp(i, 0, this->width - 1); j = std::clamp(j, 0, this->height - 1); return this->pixels[i + this->width * j]; }
해당 함수는, 범위에 벗어난 인덱스의 경우 Clamp를 통해, 0으로 맞춰주겠다는 의미이다.
아까 위에서 이론을 보면, 어차피 벗어난 값들은 가장 가까운 값을 따르기 때문이다.
이후, neightborColorSum에 GetPixel을 통해 얻은 Pixel값들을 모두 더해준다.
마지막으로 더한 값들을, 최종적으로 pixelsBuffer에 넣어줌으로써 끝나는데.
이때 0.2f를 곱한 이유에 대해서는 아직 정확한 원인은 잘 모르겠지만.
아예 값을 곱해주지 않거나 0.2 외에 다른 값을 곱하게 되면 블랙 스크린 혹은 화이트 스크린
만 나온다.
GaussianBlur
코드
void Image::GaussianBlur5() { std::vector<Vec4> pixelsBuffer(this->pixels.size()); /* * 참고자료 * https://en.wikipedia.org/wiki/Gaussian_filter * https://followtutorials.com/2013/03/gaussian-blurring-using-separable-kernel-in-c.html */ const float weights[5] = { 0.0545f, 0.2442f, 0.4026f, 0.2442f, 0.0545f }; // 가로 방향 (x 방향) #pragma omp parallel for for (int j = 0; j < this->height; j++) { for (int i = 0; i < this->width; i++) { // 주변 픽셀들의 색을 평균내어서 (i, j)에 있는 픽셀의 색을 변경 // this->pixels로부터 읽어온 값들을 평균내어서 pixelsBuffer의 값들을 바꾸기 Vec4 neighborColorSum = { 0.0f,0.0f,0.0f,0.0f }; for (int si = 0; si < 5; si++) { Vec4 neighborColor = this->GetPixel(i + si - 2, j); neighborColorSum.v[0] += neighborColor.v[0] * weights[si]; neighborColorSum.v[1] += neighborColor.v[1] * weights[si]; neighborColorSum.v[2] += neighborColor.v[2] * weights[si]; } int idx = i + width * j; pixelsBuffer[idx].v[0] = neighborColorSum.v[0]; pixelsBuffer[idx].v[1] = neighborColorSum.v[1]; pixelsBuffer[idx].v[2] = neighborColorSum.v[2]; } } // Swap std::swap(this->pixels, pixelsBuffer); // 세로 방향 (y 방향) #pragma omp parallel for for (int j = 0; j < this->height; j++) { for (int i = 0; i < this->width; i++) { // 주변 픽셀들의 색을 평균내어서 (i, j)에 있는 픽셀의 색을 변경 // this->pixels로부터 읽어온 값들을 평균내어서 pixelsBuffer의 값들을 바꾸기 Vec4 neighborColorSum = { 0.0f,0.0f ,0.0f ,0.0f }; for (int si = 0; si < 5; si++) { Vec4 neigborColor = this->GetPixel(i, j + si - 2); neighborColorSum.v[0] += neigborColor.v[0] * weights[si]; neighborColorSum.v[1] += neigborColor.v[1] * weights[si]; neighborColorSum.v[2] += neigborColor.v[2] * weights[si]; } int idx = i + width * j; pixelsBuffer[idx].v[0] = neighborColorSum.v[0]; pixelsBuffer[idx].v[1] = neighborColorSum.v[1]; pixelsBuffer[idx].v[2] = neighborColorSum.v[2]; } } // Swap std::swap(this->pixels, pixelsBuffer); }
결과물
설명
코드상 딱히 Box Blur와 차이점이 없다는 걸 알 수 있다.
다만 한 가지 차이점이라면 weight (가중치)라는 것이 추가 되었다는 점이다.
또 루프안에 넣어줘야한다.
아까 Kernel의 메트릭스를 확인 했을 때, BoxBlur의 경우 모든 값이 1이었다면, 가우시안 블러는
각각의 값이 대칭적으로 달랐음을 알 수 있었다.
그러한 가중치를 의미하는 것이며 weight의 수치는 컴퓨터 비전을 하시는 분들이 일반적으로 사용
하는 수치이다.
아까 BoxBlur의 경우 가중치가 모두 같았지만, GaussianBlur는 가중치가 이웃마다 다르기 때문에.
si 루프 안에 넣어줘야 한다.
마치며
이렇게 각 블러효과가 어떻게 적용이 되었는지 학습하였다. 이제 해당 내용을 바탕으로 블룸효과를
학습해볼 예정이다.
해당 내용은 아무래도 유료 강의이다 보니, 자세한 내용은 아래 출처를 통해 알아보도록 하자.
출처 : 홍정모 그래픽스 새싹강의1 편.