2021 - 1 영상신호처리 수업 과제 수행
- FFT 를 이용한 노이즈 분석 및 소거과정
- Filtering 에 의한 영상 복원 (mean filter)
- Filtering에 의한 영상 복원 (median filter)
어떤 이미지의 노이즈 발견을 위해, 그 이미지의 스펙트럼을 관찰하는 것은 필수적인 과정이다. 그래서 나는 먼저 2D-FFT 함수를 이미지에 적용해 각 픽셀에서의 스펙트럼을 구해냈다.
스펙트럼 이미지의 시각화를 위해, 스펙트럼의 Real part와 Imag part 값을 제곱해 square root를 적용시켜주고, log scale로 scaling 시키는 Compute_Spectrum 함수까지 작성했다.
하지만 아무리 log scale로 Specturm 값을 scale 했더라도, 실제 이미지 출력에 쓰이는 0~255까지의 값보다는 훨씬 큰 값이기 때문에, 이를 scale 하기 위한 Dnormalize 함수를 이용하였다. 스펙트럼 이미지 내에서 Max 값과 Min 값을 찾은다음 그를 기준으로 모든 픽셀의 값을 0~255 사이로 스케일 해주는 함수이다.
또한 MoveCenter라는 중심이동을 시켜주는 별도의 함수를 만들어 사용하였다. 1사분면과 4사분면의 이미지를 바꾸고 2 사분면과 3사분면의 이미지를 바꿔주는 함수인데, 이렇게 하면 중심에 가장 낮은 스펙트럼 값이 위치하게 되므로, 스펙트럼 이미지의 규칙성을 쉽게 발견할 수 있다는 장점이 있었다.
void MoveCenter(BYTE **img1, BYTE **img2, int width, int height) {
int x, y;
int cx = (int)( width * 0.5 );
int cy = (int)( height * 0.5);
for (y=0; y<cy; y++) {
for (x=0; x<cx; x++) {
img2[y][x] = img1[cy + y][cx + x];
img2[cy + y][cx + x] = img1[y][x];
img2[y][cx+x] = img1[cy+y][x];
img2[cy+y][x] = img1[y][cx+x];
}
}
}
스펙트럼 Real2, Imag2 에 바로 대응하는 이미지 = img1 변수
img1을 보기 좋은 형태로 가공한 이미지 = img2 변수
이다.
다음과 같이 노이즈가 스펙트럼 상에서 원형으로 존재하는 것을 알 수 있으며, 영상 정보를 통해 알아본 대각선 방향 노이즈 까지의 거리는 대략 원점으로부터 102.55 정도 였다.
또한, 중심으로부터 가로, 세로 방향에 놓인 노이즈 까지의 거리는 97 이었기에, 102.55와 97의 중간값은 대략 99.415이다.
그래서 Noiselen = 99.415 로 설정하고, Widthlen = 5 정도로 설정한다면 충분히 노이즈를 걸러낼 수 있을 것이다.
void Filter1(double **Real2, double **Imag2, int width, int height, double Noiselen,double Widthlen)
{
int cx = (int)(width * 0.5); // 원점에서부터 중간 x 지점까지를 나타내는 변수
int cy = (int)(height * 0.5); // 원점에서부터 중간 y 지점까지를 나타내는 변수
int x, y; // 이미지의 픽셀을 조회하기 위한 변수
for(y = 0; y< cy; y++) {
for(x = 0; x< cx; x++) {
// for 문에서 x=0부터 x<cx 까지로, y= 0부터 y < cy 까지로 지정해, 1 사분면에 대해서만 노이즈와의 거리를 계산하기로 결정했다.
double d = sqrt((double)(x*x + y*y));
// d라는 변수는 원점 (0,0) 에서부터, 현재 픽셀 (x, y) 까지의 거리로 설정했다.
if(abs(d - Noiselen)<=Widthlen) {
// d - Noiselen 의 절댓값이 제거하고 싶은 bandwidth의 폭 범위 내에 존재할 경우에는, 현재 픽셀이 노이즈 범위 내에 있는 픽셀이라고 판단할 것이다. 이 때 현재 픽셀의 Real 값, imag 성분을 전부 0으로 만들도록 코드를 작성했다.
// 그런데 현재 for 문이 1사분면의 픽셀에 대해서만 돌고 있으므로, 픽셀의 대칭 특성을 이용해 2,3,4 분면의 지점에서도 값을 제거하는 과정을 진행해 줄 것이다.
//if문 조건 만족시 제 1사분면에 대해 스펙트럼 성분을 제거해주는 코드
Real2[y][x] = 0;
Imag2[y][x] = 0;
//if문 조건 만족시 제 2사분면에 대해 스펙트럼 성분을 제거해주는 코드
Real2[y][width - x - 1] = 0;
Imag2[y][width - x - 1] = 0;
//if문 조건 만족시 제 3사분면에 대해 스펙트럼 성분을 제거해주는 코드
Real2[height - y - 1][x] = 0;
Imag2[height - y - 1][x] = 0;
//if문 조건 만족시 제 4사분면에 대해 스펙트럼 성분을 제거해주는 코드
Real2[height - y - 1][width - x - 1] = 0;
Imag2[height - y - 1][width - x - 1] = 0;
}
}
}
}
다음과 같이, ring 의 형태로 band reject filtering이 된 것을 확인할 수 있다.
원인을 탐색하다보니 2D – IFFT 함수 구현을 잘못했기 때문이라는 것을 알게 되었다. ifft 함수를 적용해야할 Imag 파트 부분에, 계속 Real part 값만 집어 넣고 있었다.
[처음 잘못 작성한 IFFT_2D의 일부코드]
for (y=0; y<height; y++)
{
for(x=0; x<width; x++)
{
tmp[x].real = Real[y][x];
tmp[x].imag = Imag[y][x];
}
ifft(tmp,LN);
for(x=0; x<width; x++)
{
Real2[y][x] = tmp[x].real;
Imag2[y][x] = tmp[x].real;
}
}
for(x=0; x<width; x++)
{
for(y=0; y<height; y++)
{
tmp[y].real = Real2[y][x];
tmp[y].imag = Imag2[y][x];
}
ifft(tmp, LN);
for (y=0; y<height; y++){
Real2[y][x] = tmp[y].real;
Imag2[y][x] = tmp[y].real;
}
}
Imag part에는 이미지의 위상정보가 들어와야 하는데, 그러한 정보가 들어오지 못하니 이미지가 반전된 형태로 출력하는 것이라고 추측할 수 있었다.
이미지의 스펙트럼 중, imaginary 성분이라는 것이 무엇인지 정확히 와닿지 않았는데, 이번 실수를 계기로 이미지의 위상성분은 이미지의 회전에 관여한다는 사실을 확인할 수 있어 유익했다. 다음에 코드를 작성할 때도, 이런 위상정보를 누락하는 실수를 저지르지는 않았는지 면밀히 확인해야겠다는 결심도 하게 되었다.
double d = sqrt((double)(x*x + y*y));
for (y=0; y<cy; y++)
{
for (x=0; x<cx; x++)
{
if(abs(d - Noiselen)<=Widthlen)
{
Real2[y][x] = 0;
Imag2[y][x] = 0;
}
}
}
d = sqrt((double)((width-x-1)*(width-x-1) + y*y));
for (y=0; y<cy; y++)
{
for (x=cx; x<width; x++)
{
if(abs(d - Noiselen)<=Widthlen)
{
Real2[y][x] = 0;
Imag2[y][x] = 0;
}
}
d = sqrt((double)(x*x + (height - y - 1)*(height - y - 1)));
for (y=cy; y<height; y++)
{
for (x=0; x<cx; x++)
{
if(abs(d - Noiselen)<=Widthlen)
}
Real2[y][x] = 0;
Imag2[y][x] = 0;
}
}
d = sqrt((double)((width-x-1)*(width-x-1) + (height - y - 1)*(height - y - 1)));
for (y=cy; y<height; y++)
{
for (x=cx; x<width; x++)
{
if(abs(d - Noiselen)<=Widthlen)
{
Real2[y][x] = 0;
Imag2[y][x] = 0;
}
}
// 이런 방법으로, 총 4번의 for문을 돌렸다
// 개선한 코드의 모습, for 문을 1번만 돌려서 처리할 수 있음을 확인할 수 있다.
for(y = 0; y< cy; y++) {
for(x = 0; x< cx; x++) {
Real2[y][x] = 0;
Imag2[y][x] = 0;
Real2[y][width - x - 1] = 0;
Imag2[y][width - x - 1] = 0;
Real2[height - y - 1][x] = 0;
Imag2[height - y - 1][x] = 0;
Real2[height - y - 1][width - x - 1] = 0;
Imag2[height - y - 1][width - x - 1] = 0;
}
}
노이즈가 중심으로부터 같은 거리에 떨어져 있다는 특성을 이용하면, 모든 픽셀을 조회할 필요가 없었다. (0,0) 에서 영상의 중심까지의 픽셀 (즉, 1사분면) 을 조회한 다음 노이즈를 걸러주고, 점대칭의 특성을 이용해 나머지 2,3,4 사분면에 대해서 노이즈를 제거해주면, for문의 사용을 대폭 줄일 수 있고, 연산속도도 절감할 수 있다.
FFT에 비해 DFT 과정에서 시간이 오래 소모된 이유를 생각해보면, 중첩된 for loop문을 오랜 시간 돌기 때문이었다. 고로 이미지 처리 속도를 높이기 위해서는 for loop 문의 과정을 최대한 간소화 해야한다. 위의 코드를 짜는 과정에서도 마찬가지다. 전체 픽셀을 조회하는 대신 점대칭의 특성을 이용해 일부 픽셀만 조회하는 방법으로 for문을 대폭 간소화할 수 있었고, 이를 통해 계산속도도 절감할 수 있다는 결과를 얻었다. 다음 과제에서도 최대한 for문의 사용을 최소화 하는 방향으로 코드를 짜야겠다는 결심을 하게 되었다.
void Filter2(double **Real2, double **Imag2, int width, int height, int quadrant, int Twidth, int Theight)
몇가지 새로운 argument 를 소개 하자면,
∎ int quadrant
: 노이즈가 위치한 사분면을 지시하는 변수이다. quadrant = 1 이면 1, 4 사분면에 위치한 노이즈를 제거하고, quadrant = 2 이면 2, 3 사분면에 위치한 노이즈를 제거한다.
∎ int Twidth
: Target width의 줄임말로, 중심으로부터 노이즈까지 떨어진 x 좌표 픽셀값을 의미한다.
∎ int Theight
: Target Height의 줄임말로, 중심으로부터 노이즈까지 떨어진 y 좌표 픽셀값을 의미한다.
위와 같은 방법을 이용한다면, 1)어떤 노이즈가 어떤 사분면에 있는지 알고 2) 중심으로부터 얼마 만큼의 좌표에 떨어져 있는지만 안다면 복소 대칭의 형태로 등장하는 모든 노이즈를 제거할 수 있다는 이점이 있다.
void Filter2(double **Real2, double **Imag2, int width, int height, int quadrant, int Twidth, int Theight)
{
int cx = (int)(width * 0.5);
int cy = (int)(height * 0.5);
int x, y;
//아래의 코드는 1,4 사분면에 대한 노이즈를 처리하는 과정이다.
if(quadrant == 1)
{
for(y = Theight-5; y< Theight + 6; y++)
{
for(x = Twidth - 5; x< Twidth + 6; x++)
{
// Noise Target 좌표를 기점으로, ±5 픽셀만큼 떨어진 부분들의 스펙트럼 성분들을 0으로 바꿔주었다.
Real2[y][x] = 0;
Imag2[y][x] = 0;
// 그 후 1사분면에 대칭인 4사분면에 대해서도 노이즈를 제거해주었다.
Real2[height - y - 1][width - x - 1] = 0;
Imag2[height - y - 1][width - x - 1] = 0;
}
}
}
//아래의 코드는 2, 3 사분면에 대한 노이즈를 처리하는 과정이다.
else if(quadrant == 2)
{
for(y = Theight-5; y < Theight + 6; y++)
{
for(x = width - (Twidth + 5); x< width - (Twidth - 6); x++)
{
Real2[y][x] = 0;
Imag2[y][x] = 0;
Real2[height - y - 1][width - x - 1] = 0;
Imag2[height - y - 1][width - x - 1] = 0;
}
}
}
}
영상 정보를 통해, HouseN.256 이미지의 1,4사분면 노이즈는 중심으로부터 56x24 픽셀만큼 떨어져 있음을 알 수 있었고, 2,3 사분면 노이즈는 중심으로부터 38x12 픽셀만큼 떨어져 있음을 알 수 있었다. 이 값들을 Filter2의 Twidth, Theight 인자로 사용할 것이다.
-결과 구현 모습
다음과 같이 점대칭 형태의 노이즈가 잘 제거된 것을 확인할 수 있다.
Mean-filtering 과정
Mean filter는 NxN window를 이미지에 적용시켜, window 내에 들어온 픽셀값들을 모두 평균하여 바꿔주는 필터링 기법이다. (본 과제에서는 3x3 window를 이용했다.) 이를 구현하기 위해서 원본 이미지를 img1에 담고, img1의 평균 픽셀 값을 img2 라는 새로운 변수에 담는 코드를 작성하기로 결정했다.
문제 해결을 위해 작성한 subroutine program
void MeanFilter(BYTE **img1, BYTE **img2, int width, int height)
{
int x,y,u,v;
//x, y 는 img1의 전체 픽셀을 조회하기 위한 변수들이고, u,v는 3x3 window를 조회하기 위한 변수들이다.
int sum;
//sum 이라는 변수는 3x3 픽셀의 값을 전부 더한 후 평균하기 위한 목적의 변수이다.
sum = 0;
for(y = 1; y < height - 1; y++){
for(x = 1; x<width -1; x++)
{ // 위와 같이, 3x3 window를 움직이기 위한 x 픽셀의 범위는 1부터 width-2 까지이고, y 픽셀의 범위는 1 부터 height-2 까지가 되어야 한다.
for(v = y-1; v<y+2; v++)
{
for(u = x-1; u < x+2; u++)
{
//window는 x,y 좌표를 중심으로, (x-1 ~ x+1), (y-1 ~y+1) 까지 총 9개의 픽셀의 값을 조회한다.
sum += img1[v][u];
//그후 그 값들을 sum에 더한다.
}
}
sum = (int)(sum / 9);
//sum에 더해진 9개의 값을 평균 내준 다음, 정수형 변환을 해준다.
for(v = y-1; v<y+2; v++)
{
for(u = x-1; u < x+2; u++) {
img2[v][u] = sum;
//그 후 img2의 3x3 만큼의 픽셀들에 img1의 3x3 픽셀들의 평균값으로 채워서 필터링해준다.
}
}
sum = 0;
//sum은 반복문 내에서 다시 사용되어야하기 때문에, 한차례의 반복문이 끝난 후에는 꼭 그 값을 0으로 초기화한다.
}
}
}
결과 구현 모습
discussion
mean filter의 구현은 픽셀들을 평균값 내는 것이기 때문에 구현 자체는 어렵지 않았다. 하지만 한 가지 궁금한 부분이 있었다. 바로 window의 이동 간격을 다르게 움직이면 어떻게 이미지가 달라지는가 였다. 위의 이미지는 window를 이동하는 간격을 1로 한 것인데, 3으로 이동 간격을 움직이면 어떻게 될지 궁금하여 한번 실행해보았다.
window를 움직이는 간격을 3으로 키워보니, 간격이 1일 때보다는 resolution이 좋지 않다는 것을 알 수 있다. 그 대신 이미지 처리에 드는 시간은 훨씬 적어졌음을 확인할 수 있다. window 이동크기가 커지면서, 연산과정도 당연히 더 적어지기 때문이다. 이를 통해 높은 노이즈 제거성능보다 빠른 노이즈 제거 속도를 요하는 작업에선, window 이동폭의 크기를 크게 하여 필터링을 적용해도 좋을 것 같다는 생각이 들었다.
Median-filtering 과정
Median filter는 NxN window를 이미지에 적용시켜, window 내에 들어온 픽셀값들을 정렬해 그 중간값을 3x3 의 픽셀값으로 채택하는 필터이다. 이를 구현하기 위해서 1)원본 이미지를 img1에 담고, 2) img1의 3x3 픽셀들의 값을 정렬해 중간값을 찾고, 3) 그 값을 img2 라는 새로운 변수에 담는 코드를 작성하기로 결정했다.
문제 해결을 위해 작성한 subroutine program
void MedianFilter(BYTE **img1, BYTE **img2, int width, int height)
{
BYTE *tmp = (BYTE *)malloc(sizeof(BYTE) * 9);
//window에 들어온 픽셀을 정렬하기 위해, tmp라는 1x9의 배열을 동적할당을 이용해 만들어 주었다.
int x,y,u,v;
// x,y는 img1의 픽셀을 조회하기 위한 변수, u,v는 3x3 window의 픽셀을 조회하기 위한 변수이다.
int i, k; // i와 k는 tmp 배열에 버블 정렬 기법을 이용하기 위한 변수들이다.
int temp, d; // d는 tmp 배열의 index를 지정하기 위한 변수이고( tmp[d] 처럼 ), temp는 tmp[d]에 저장된 값을 담기 위한 임시 변수이다.
d=0; //tmp 배열의 시작 index는 0 이므로, d = 0으로 초기화한다.
for(y = 1; y < height - 1; y++)
{
for(x = 1; x<width -1; x++)
{
for(v = y-1; v<y+2; v++)
{
for(u = x-1; u < x+2; u++)
{
tmp[d] = img1[v][u];
// tmp 배열에 3x3 window의 픽셀값을 대입해준다.
d++;
// 한 개의 값을 대입한 후에는 다음 픽셀을 위해 index d의 값을 올려준다.
}
}
d=0; // tmp 배열에 데이터가 모두 담긴 상태이므로, 다음 반복문을 위해 0으로 초기화해준다.
//tmp 배열의 버블 정렬을 위한 코드
for (k=0; k<9; k++)
{
for (i=0; i<8; i++)
{
if(tmp[i] > tmp[i+1])
{
temp = tmp[i];
tmp[i] = tmp[i+1];
tmp[i+1] = temp;
//배열의 첫번째 요소부터 마지막 요소까지에 대해 적용한다. 만약 tmp[i] > tmp[i+1] 이라면 두 위치의 값을 swap 하는 코드이다.
}
}
}
//위의 버블정렬이 끝나고 나면 tmp는 작은값부터 차례로 정렬된 형태일 것이다. 이 행렬의 중간값인 tmp[4]를 취한다.
for(v = y-1; v<y+2; v++)
{
for(u = x-1; u < x+2; u++)
{
img2[v][u] = tmp[4];
//img2의 3x3 영역에 대해, tmp의 중간값인 tmp[4] 로 전부 채워준다.
}
}
}
}
free(tmp);
//tmp는 동적할당된 공간이므로, 꼭 해제를 시켜준다.
}