honglab
All Courses, Graphics Introduction to Computer Graphics with DirectX 11 - Part 2. Realtime Pipeline
honglab.co.kr
강의 링크
흐림 효과(Blur)
블러 효과란 위의 이미지처럼 원래 이미지를 흐릿해 보이게 만드는 효과이다.
이미지 처리에서 이를 구현하기 위해서는 커널이라는 것을 이용한다.
커널(Kernel)
이미지 처리에서 커널이란 흐림(Blur), 선명화(Sharpen), 엠보싱(Emboss), 가장자리 감지(Edge Detection) 등에 사용 되는 작은 행렬을 말한다.
이미지 처리를 위해
커널을 가지고 이미지의 모든 픽셀의 값에 대해 계산하는 과정을 컨볼루션(Convolution)이라고 한다.
한 픽셀과 그 주변 픽셀들에 대해
각 커널에 대응되는 값과 곱하여 그 합을 새로운 픽셀값으로 한다.
Box Blur 효과 구현
박스 블러는 주변 픽셀의 값과의 평균을 구하는 형태로 계산된다.
원래는 배열 형태의 커널로 연산하게 되어 있지만 강의에서는 이해를 쉽게하기 위해
Seperable convolution 을 적용했다.
Seperable convolution
2차원 배열 형태의 kernel을 적용하는 대신 1차원 Kernel을 두번 적용하는 방법이다.
아래는 separable convolution이 가능한 필터에 대한 설명이다.
What is a separable filter?
A two-dimensional filter kernel is separable if it can be expressed as the outer product of two vectors.
- 두 벡터의 곱으로 표현될 수 있으면 separable filter kernel이고 separable convolution을 적용할 수 있다고 한다.
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];
}
강의에서는 1차원 배열에 있는 픽셀을 2차원 배열처럼 불러올 수 있는 GetPixel(x, y) 함수를 제공한다.
여기서 Clamp()가 사용된 이유
※예를 들어 3x3컨볼루션을 한다고 했을 때,
GetPixel(0, 0)을 하게되면 주변 픽셀을 가져올 때 정의되지 않은 (-1, -1)과 같은 픽셀의 값에 접근하게 되어 의도하지 않은 오류가 발생할 수 있기 때문에 clamp 함수로 매개변수 i, j 의 범위를 제한해준 것.
내가 구현한 방식
#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 sum = {0.0f, 0.0f, 0.0f, 1.0f};
for (int k = 0; k < 5; k++)
{
//i-2+k -> -2, -1, -0, 1, 2
sum.v[0] += GetPixel(i - 2 + k, j).v[0];
sum.v[1] += GetPixel(i - 2 + k, j).v[1];
sum.v[2] += GetPixel(i - 2 + k, j).v[2];
}
sum.v[0] *= 0.2f;
sum.v[1] *= 0.2f;
sum.v[2] *= 0.2f;
pixelsBuffer[i + width * j] = sum;
}
}
강의에서는 5x5만큼 box blur를 적용한다고 했으므로
for문을 5번 돌려서 -2~2까지의 픽셀을 가져오고
sum이라는 Vec4 변수에 주변 픽셀들의 합을 저장해주고 0.2f(1/5)로 평균을 구해서 pixelsBuffer에 저장해주었다.
x축 양쪽 방향으로 blur가 적용된 결과
GetPixel(i - 2 + k, j).v[2] -> GetPixel(i, j - 2 + k).v[2]
GetPixel부분을 j에 대한 값으로 바꿔주면 y축으로도 적용할 수 있다.
둘 다 적용하면 아래와 같은 블러 효과를 얻을 수 있다.
Gaussian Blur 효과 구현
가우시안 블러도 박스 블러와 같은 흐릿한 효과를 주지만
대상 픽셀과 멀리 떨어져 있는 주변부에는 가중치를 조금만 주고 가까운 곳에는 가중치를 크게 주어 평균을 계산하는 기법이다.
왼쪽이 box 블러로 구한 평균값 분포라면 오른쪽 Gaussian 블러는 완만한 평균값을 보여준다.
가우시안 블러도 Separable Convolution을 적용하여 계산할 수 있다.
$ \begin{bmatrix}1\\4\\6\\4\\1\end{bmatrix}\times \begin{bmatrix}1 & 4 & 6 & 4 & 1 \end{bmatrix} $
이 식을 계산해보면 위의 Gaussian blur 5x5의 행렬 값을 두 개의 벡터의 곱으로 표현할 수 있으므로 Seperable Convolution을 적용할 수 있다.
const float weights[5] = { 0.0545f, 0.2442f, 0.4026f, 0.2442f, 0.0545f };
weights[5]는
다음과 같은 식을 통해 나온 Gaussian 필터 생성 결과이다.
Vec4 sum = { 0.0f, 0.0f, 0.0f, 1.0f };
for (int k = 0; k < 5; k++)
{
sum.v[0] += GetPixel(i - 2 + k, j).v[0] * weights[k];
sum.v[1] += GetPixel(i - 2 + k, j).v[1] * weights[k];
sum.v[2] += GetPixel(i - 2 + k, j).v[2] * weights[k];
}
pixelsBuffer[i + this->width * j] = sum;
이 값을 대상 픽셀로부터의 거리에 맞게 weights[k]를 차례로 곱해주면 가우시안 블러 효과를 얻을 수 있다.
Separable Convolution 이므로 Box Blur때와 마찬가지로 x축 y축에 대해서 각 한 번씩 연산해야 한다.
Bloom 효과 구현
그동안 배운 이미지 처리 내용만 가지고 화려한 Bloom 효과를 구현할 수 있다!
Bloom 효과에 대한 정의
카메라 렌즈에 빛이 들어올 때 렌즈를 통해서는 100% 빛에 대한 정보를 받아들이지 못하는 문제가 있다. 이로 인해 강한 빛을 찍으면 실제 빛보다 더 퍼져 보이는 상태로 보이는데 이런 효과가 나도록 처리하는 것.
먼저 일정 밝기 이하인 부분을 까맣게 만드는 부분부터 진행했다.
밝기의 판단 기준은 Relative Luminance라는 수치를 이용하는데
Relative Luminance Y = 0.2126*R + 0.7152*G + 0.0722*B
Relative luminance is the relative brightness of any point in a colorspace, normalized to 0 for darkest black and 1 for lightest white.
우선 원본 이미지에 대해 복사해놓는다.
const std::vector<Vec4> pixelsBackup = this->pixels;
색상 공간의 모든 점에 대해 상대적인 밝음 범위인 0(검은색)~1(흰색)로 정규화 하는 식이라고 한다.
for (int j = 0; j < height; j ++)
for (int i = 0; i < width; i++)
{
auto& pixel = this->GetPixel(i, j);
const float rl = pixel.v[0] * 0.2126f + pixel.v[1] * 0.7152f + pixel.v[2]*0.0722f;
//th = 기준
if (rl < th)
{
pixel.v[0] = 0.0f;
pixel.v[1] = 0.0f;
pixel.v[2] = 0.0f;
}
}
이 식을 그대로 적용해서 내가 설정한 밝기 기준인 th보다 어두운 부분은 검정색(0.0, 0.0, 0.0)으로 처리한다.
밝은 부분만 남았으므로 아까 사용한 가우시안 blur를 적용해 준다.
for (int i = 0; i < numRepeat; i++)
{
this->GaussianBlur5();
}
for (int i = 0; i < pixelsBackup.size(); i++)
{
this->pixels[i].v[0] = std::clamp(pixels[i].v[0] * weight + pixelsBackup[i].v[0], 0.0f, 1.0f);
this->pixels[i].v[1] = std::clamp(pixels[i].v[1] * weight + pixelsBackup[i].v[1], 0.0f, 1.0f);
this->pixels[i].v[2] = std::clamp(pixels[i].v[2] * weight + pixelsBackup[i].v[2], 0.0f, 1.0f);
}
처음에 복사해 두었던 원본 이미지에 blur값을 더해주면 bloom 효과를 만들 수 있다.
- 이 연산으로 색의 범위가 1.0을 넘어갈 수도 있으므로 clamp를 통해 범위를 0.0f~1.0f로 제한
- blur 결과에 대해 가중치를 곱하는 것으로 효과의 강도를 조절
강의에서 배운 것
- 이미지 처리에서의 커널과 컨볼루션 기법
- Seperable Convolution
- Box Blur, Gaussian Blur 효과
- Bloom 효과
'Graphics > Directx11' 카테고리의 다른 글
Fbx SDK Import 시의 Polygon을 삼각형으로 (0) | 2023.08.17 |
---|---|
그래픽스 새싹코스 파트 1 - 이미지 처리 기초 (2) | 2023.03.24 |