[영상신호처리] Image Labeling

2021 - 1 영상 신호처리
1. Image Labeling에 대한 간단한 고찰
2. Label 이미지의 생성을 위한 Image_Labeling 함수의 구상 과정 및 완성
3. 특정 픽셀 수 이상의 Label 이미지만 출력하고, 나머지는 노이즈로 처리하기 위한 Make _Noise_Removed_Image 함수의 구상 과정 및 완성
4. 특정 픽셀 수 이상의 레이블 이미지만 화면상에 출력하기 위한 Display_Labeled_Images 함수의 구상 과정 및 완성
5. Disscussion

  • 임계치 픽셀 수가 300일 때 PCX1 이미지의 레이블 분류 결과
  • 임계치 픽셀 수가 30일 때 PCX1 이미지의 레이블 분류 결과
  • 임계치 픽셀 수가 300일 때 PCX2 이미지의 레이블 분류 결과
  • 임계치 픽셀 수가 30일 때 PCX2 이미지의 레이블 분류 결과
  • 코드를 작성하며 발생했던 문제점 및 해결 과정

1. Image Labeling에 대한 간단한 고찰

Image Labeling 이란 쉽게 말해 인접하게 모여 있는 같은 밝기의 픽셀들을 같은 레이블로 묶어 주는 작업을 의미한다. 이를 수행하기 위해서는 초기 레이블 그룹 (n table, 새로운 픽셀을 만날 때 마다 갱신되는 값) 과 그 레이블들에 대한 최종적인 레이블 값인 r table (인접한 요소들끼리 레이블이 다르면, 이를 통일 시켜주기 위한 값) 이 모두 필요하다. 물체로 인식되는 새로운 픽셀 값을 만날 때마다 Label을 새롭게 생성시키기도 해야 하고, 그 픽셀이 인접한 픽셀에 속할 경우 같은 레이블로 취급하는 작업도 필요하기 때문이다. 이러한 원리를 잘 이해하고자 다음과 같이 모눈종이에 물체를 그려, 이미지를 Labeling 하는 작업도 시행해 보았다.


위와 같은 과정을 통해 Label을 새롭게 갱신시켜주는 r테이블의 중요성을 깨닫게 되었고, 이를 실제 코드로 구현하기 위해서는 n 테이블을 갖는 변수와 r 테이블을 갖는 변수를 모두 지정해야 겠다는 대략적인 구상 또한 할 수 있었다.

또한 특정 레이블에 속한 픽셀 개수가 임계치를 넘는지, 넘지 않는지의 여부에 따라서 물체와 배경을 구분하는 기능이 추가적으로 있어야겠다는 생각이 들었다. 물체와 배경 및 노이즈로 구분하는 임계치가 너무 작으면 노이즈까지 유의미한 물체로 인식될 수 있고, 임계치가 너무 크면 물체가 배경으로 취급될 수 있기 때문이다. 고로 같은 레이블에 속한 픽셀 수가 몇 개인지 확인하는 작업은 매우 중요하다. 그래서 실제 Labeling 코드를 작성할 때 같은 레이블에 속하는 픽셀 개수를 담아내는 변수가 필요하겠다는 구상도 하게 되었다.

이번 과제는 물체를 인식하고, 배경으로부터 분리해내는 알고리즘을 배우는 중요한 시간이었기 때문에 특히나 심혈을 기울여 임하게 되었다.

2. Label 이미지의 생성을 위한 Image_Labeling 함수의 구상 과정 및 완성

먼저 이미지 속에 여러 물체들과 배경이 존재할 때, 유의미한 물체만을 같은 레이블끼리 묶어내기 위해서는 위와 같이 n table, r table 변수가 필요하겠다는 생각이 들었다. n table은 이미지와 같은 크기의 2차원 int 배열로 만들어 그 내부에 각 픽셀좌표 (x, y) 가 속한 ‘초기’ 의 레이블 값을 넣어야겠다고 생각했다. 또한 r table은 n table의 정보를 기반으로, 인접한 픽셀들의 n table의 값이 다르면 이를 같은 값으로 맞춰주는 1차원 배열 변수로 지정해야겠다는 생각이 들었다. 마지막으로 r table의 값이 정리 되면, 이를 기반으로 n table의 값을 보정해 레이블 이미지를 생성해야겠다는 생각이 들었다.

또한, 위에서 그 필요성을 느꼈듯 같은 레이블에 속한 픽셀의 개수를 세는 배열 변수도 필요하겠다는 생각이 들어, 이를 코드에 반영해 보기로 하였다. 같은 레이블에 속한 픽셀 수가 몇 개인지 확인하는 작업은 물체와 노이즈를 가려내는데 매우 중요하기 때문이다. 그래서 이와 같은 구상을 바탕으로, 다음과 같은 함수의 프로토타입을 생각해 보았다.

void Image_Labeling(BYTE **img1, int w, int h, int **Label, int *Area, int *Num)
  • BYTE **img1은 실제 이미지 값이 담기는 변수를 의미한다. 그래서 자료형은 BYTE 타입으로 지정했다.
  • int **Label은 이미지 픽셀 각 좌표의 Label이 담기는 2차원 배열 변수로, 초기에는 n table의 값이 담겨있고, 코드의 마지막 부분에서 r테이블의 값으로 보정된다.
  • 또한 함수 내에서 r table에 해당하는 변수는 동적으로 할당해 사용할 것이다. r table 변수의 최종적인 목적은, Label 변수를 보정하는데 있기 때문이다.
  • int *Area는 같은 레이블에 속하는 픽셀 개수를 세는 1차원 배열 변수이다. Area의 index는 r table의 값이고, 그 내부에 담기는 값은 픽셀 개수를 의미한다.
  • int *Num은 초기의 n값이 몇 개인지 세기 위한 변수이다. 인접한 레이블을 같은 레이블로 취급하다보면, r테이블의 다른 인덱스 값이 같은 레이블 값을 가지기도 할 것이다. 그래서 r 테이블의 인덱스에 접근해, 그 인덱스에 속한 레이블 값을 담아내기 위해서는 초기의 n 값을 담는 변수 Num이 필요하다.

위와 같은 내용을 바탕으로 실제로 작성한 서브루틴 코드는 다음과 같다.

void Image_Labeling(BYTE **img1, int w, int h, int **Label, int *Area, int *Num)
{
// img1은 원본 이미지를 담는 변수
// Label은 가장 먼저 할당되는 레이블. 배경은 0, 물체는 1로 먼저 초기화
// Num은 새로운 레이블을 세는 변수 (갱신되기 이전의 값)
// Area는 같은 레이블에 속한 픽셀 수가 몇개인지 세는 변수 (1차원 array)

int k, x, y, *r, num, left, top;

r = (int *)malloc(w*h*sizeof(int));
// r은 r 테이블을 의미한다. (1차원 Array) 최종적으로 Label을 갱신시키기 위해 사용한다.

// 초기 설정 내용이다. 물체 레이블은 1, 배경 레이블은 0으로 한다.
for (y= 0 ; y<h; y++) {
  for (x=0; x<w; x++) {
  if(img1[y][x] > 128) Label[y][x] = 1; // 물체 레이블을 1로 설정하는 모습
  else Label[y][x] = 0; // 배경 레이블을 0으로 설정하는 모습
  }
}
// 예외처리, 이미지의 가장 바깥부분의 레이블은 0으로 설정해준다.
for (y= 0 ; y<h; y++) {
  Label[y][0] = 0;
  Label[y][w-1] = 0;
}

for (x = 0 ; x<w; x++) {
  Label[0][x] = 0;
  Label[h-1][x] = 0;
}

num = 0;
for (y= 1 ; y<h-1; y++) {
  for (x = 1 ; x<w-1; x++) {

  if(Label[y][x] > 0) {
  // 배경레이블이 아닌 경우를 의미한다. 현재 픽셀 좌표를 기준으로 왼쪽 픽셀을 left, 위쪽 픽셀을 top으로 정한다.

  left = Label[y][x-1];
  top = Label[y-1][x];

  if(left == 0 && top == 0) {
  num++;
  r[num] = num;
  Label[y][x] = num;
  Area[num] = 1;
  }
  
  else if (left ==0 && top != 0) {
  Label[y][x] = r[top]; 
  // top의 배열 안에는, top이 속하는 레이블 값이 저장되어 있다. 
  // 이를 토대로 r테이블을 가리켜, r 테이블 내에 저장된 진짜 레이블 값을 
  // Label[y][x]에 넣어준다.
  
  Area[top]++;
  }
  
  else if (left !=0 && top == 0) {
    Label[y][x] = r[left];
    Area[left]++;
    }
    
  else if (left !=0 && top != 0) {
  if (r[left] > r[top]) {
  // Label은 그 안에 N 값을 담고 있는 w*h 2차원 배열이고, 
  // r은 r테이블로 실제 인접한 Label 전부 변환 후에 값이 어떻게 되는지 확인하는 1차원 배열이다.
    Label[y][x] = r[top];
    r[left] = r[top];
    Area[r[top]]++;
  }
  
  else {
    Label[y][x] = r[left];
    r[top] = r[left];
    Area[r[left]]++;
  }
}
}
}
}

*Num = num; // n table 에 속한 모든 n 값 (갱신되지 않은 값)
for (k = 1; k<= num; k++) {
  if (k != r[k]) {
  r[k] = r[r[k]];
  Area[r[k]] += Area[k];
  Area[k] = 0;
  // Area 배열 또한 r 테이블의 인덱스에 속하는 값으로 갱신시켜주고, 
  //삭제된 n 테이블의 픽셀 개수는 r 테이블의 값에 더해준다.
	}
}

for (y= 0 ; y<h; y++) {
  for (x=0; x<w; x++) {
    if (Label[y][x] > 0) {
    	Label[y][x] = r[Label[y][x]];
    	//마지막으로, 레이블 값도 초기화 해준다.
    }
  }
}
free(r);
}

3. 특정 픽셀 수 이상의 Label 이미지만 출력하고, 나머지는 노이즈로 처리하기 위한 Make _Noise_Removed_Image 함수의 구상 과정 및 완성

위의 Image_Labeling 함수를 통해 같은 레이블끼리 묶여있는 int 타입의 레이블 영상, Label을 구성할 수 있게 되었다. 또한 같은 레이블에 속한 픽셀 수가 얼마나 되는지 저장하는 1차원 배열 Area도 구성할 수 있게 되었다. 이를 기반으로, 특정한 임계 픽셀 수가 입력으로 들어왔을 때, 입력 픽셀값 이상의 레이블 이미지들은 유의미한 값으로 처리하고, 나머지는 노이즈로 간주해 배경으로 처리하는 함수를 구상해보았다. 프로토 타입은 다음과 같다.

void Make_Noise_Removed_Image(BYTE **img1, int w, int h, int **Label, int *Area, int &Num, int m_ABC)
  • 새로운 변수 int m_ABC는, 사용자로부터 입력 받은 임계 픽셀 수를 의미한다. 이 픽셀 수를 기준으로 Area 배열에서 m_ABC 보다 큰 픽셀 수를 가진 레이블이 무엇이 있는지 알아낸다.
  • m_ABC보다 큰 픽셀수를 가진 레이블은 255로, 작은 픽셀수를 가진 레이블이나 배경이미지는 0으로 처리하여, BYTE **img1 에 넣어준다. 이를 토대로 img1을 출력하면 원하는 레이블의 물체만 걸러진 영상이 출력될 것이다.

위와 같은 구상을 바탕으로 실제로 작성한 코드는 다음과 같다.

void Make_Noise_Removed_Image(BYTE **img1, int w, int h, int **Label, int *Area, int &Num, int m_ABC) {
  int x, y, k;

  for(k = 0; k<Num; k++) {
  
    if(Area[k] < m_ABC) {
    //Area의 값이 픽셀 개수 임계치보다 낮을 때, 배경이나 노이즈로 인식하고 픽셀 밝기를 0로 지정하는 코드이다.
    for (y= 0 ; y<h; y++) {
      for (x=0; x<w; x++) {
      if(Label[y][x] == k) img1[y][x] = 0;
    }
   }


  }
  
  else if(Area[k] >= m_ABC) {
  // Area의 값이 픽셀 개수 임계치와 같거나 임계치보다 높을 때, 
  // 물체로 인식하고 픽셀 밝기를 255로 지정하는 코드이다.
  for(y = 0; y<h; y++) {
  	for (x=0; x<w; x++) {
  		if(Label[y][x] == k) img1[y][x] = 255;
  		}
  	}
  }
  }
}

4. 특정 픽셀 이상의 레이블만 화면상에 출력하기 위한 Display_Labeled_Images 함수의 구상 과정 및 완성

마지막으로, 임계치 픽셀 수 이상의 레이블 이미지만 잘라내어 모두 출력시키기 위한 함수를 구상하였다. 임계치 픽셀 수 이상의 값을 가진 레이블을 찾을 때마다, 레이블에 속한 이미지 픽셀 좌표들의 최대, 최소 (x, y) 좌표인 Xmin, Xmax, Ymin, Ymax를 찾는 과정이 필요하겠다는 생각이 들었다. 이를 바탕으로 높이는 (Ymax – Ymin), 너비는 (Xmax – Xmin) 의 값을 갖는 이미지를 만들어, 레이블에 속한 이미지만을 담아내 출력하는 함수를 만들면 될 것이다. 그 프로토타입은 다음과 같다.

void Display_Labeled_Images(int **Label, BYTE **img1, int w, int h, int *Area, int &Num, int m_ABC, int &x0, int &y0)
  • 사용자로부터 입력 받은 변수 m_ABC (픽셀 수의 임계치를 의미함)를 기반으로, m_ABC보다 큰 픽셀 수를 가진 레이블을 Area 배열을 통해 알아낸다.
  • 레이블을 알아냈으면, 그를 바탕으로 배열 Label을 조회하여 해당 레이블에 속한 픽셀의 x, y 좌표의 최대 최소값을 알아낸다.
  • 함수 내에서 높이는 (Ymax – Ymin), 너비는 (Xmax – Xmin) 인 임시 BYTE 이미지, tempImg 변수를 생성하고, 이 안에 해당하는 레이블의 이미지만을 img1에서 가져와 담아주고, 출력한다. 출력 이후에는 tempImg 변수를 해제시켜준다.
  • 또한 여러 레이블의 이미지가 출력 될 수도 있으므로, 가장 최근의 이미지 좌표인 x0, y0를 받아와 이미지를 출력시키고, 한 레이블의 이미지를 출력시킨 이후에는 y0의 값을 10씩 키워 이미지 끼리 겹치지 않고 나란하게 출력되도록 할 것이다.

이와 같은 프로토타입을 바탕으로 작성한 함수는 다음과 같다.

void Display_Labeled_Images(int **Label, BYTE **img1, int w, int h, int *Area, int &Num, int m_ABC, int &x0, int &y0) {

int x, y, k;


for(k= 0 ; k< Num; k++) {
// 아래는 특정 레이블에 속한 이미지의 최대 최소 좌표를 구하기 위해, 초기화 해주는 부분의 코드이다.
int Xmax = -9999;
int Ymax = -9999;
int Xmin = 9999;
int Ymin = 9999;


if(Area[k] >= m_ABC) {
for (y= 0 ; y<h; y++) {
	for (x=0; x<w; x++) {
	if(Label[y][x] == k) {

	if(x > Xmax) Xmax = x;
	if(x < Xmin) Xmin = x;
	if(y > Ymax) Ymax = y;
	if(y < Ymin) Ymin = y;

// 위와 같이 k라는 레이블에 속한 이미지의 x, y 최대 최소 좌표를 찾아준다. 
	}
}
}

BYTE **tempImg;
tempImg = cmatrix(Ymax - Ymin, Xmax – Xmin);

// 새로운 이미지, tempImg를 생성해주고 x, y의 최대최소 값을 뺴준 만큼의 크기로 이미지를 생성해준다.

for (y= Ymin ; y < Ymax; y++) {
  for (x = Xmin; x < Xmax; x++) {

  if(Label[y][x] == k) {
  tempImg[y - Ymin][x - Xmin] = img1[y][x];
  }
  // 픽셀의 (x,y) 최대 최소 좌표내에서 Label 배열을 조회한다. 
  //Label 배열이 k 레이블인 경우에만 tempImg안에, img1의 영상을 받아와 넣어준다.
  else {
  tempImg[y - Ymin][x - Xmin] = 0;
  // 단순히 x,y 배열의 최대최소 좌표 값을 뽑아오기만 한 경우이므로, 
  // 좌표 내에 k 레이블이 아닌 이미지도 포함되어 있을 것이다. 
  // 레이블이 k가 아닌 픽셀의 밝기는 0으로 처리해 tempimg에 넣어준다.
  }
  }
}
DisplayCimage2D(tempImg, Xmax - Xmin, Ymax - Ymin, x0+w+10, y0, FALSE);
y0 += Ymax - Ymin + 10; 
// 서로 다른 레이블 영상끼리 나란히 출력되도록 한차례 출력을 마친후에는 y0를 10씩 키워줄 것이다.
free_cmatrix(tempImg, Ymax - Ymin,  Xmax - Xmin);

}

}
}

5. Disscussion

위와 같이 총 3개의 서브루틴을 작성하였고, 이를 시행시킨 결과는 다음과 같다.

  • 임계치 픽셀 수가 300일 때 PCX1 이미지의 레이블 분류 결과와 임계치 픽셀 수가 30일 때 PCX1 이미지의 레이블 분류 결과

왼쪽이 임계치 픽셀수가 300일 때, 오른쪽이 임계치 픽셀 수가 30일 때의 경우이다. 또한 맨 첫 번째 이미지가 원본 이미지, 두 번째 이미지가 Label 이미지, 세 번째 이미지가 Label 이미지로부터 추출해낸 유의미한 레이블 이미지이다. 두 번째 이미지인 label 이미지는 아래로 내려갈수록 그 값이 커지기 때문에, 점점 밝아지는 것을 확인할 수 있었고 같은 레이블로 분류된 것들은 같은 픽셀 밝기를 가지고 있다는 것도 확인할 수 있었다.

코드 실행 후에, 임계치 픽셀 수를 크게 할수록 상대적으로 작은 픽셀을 가진 레이블들은 노이즈로 처리되는 것을 확인할 수 있다. 위의 이미지를 보면 임계치가 300인 경우 가장 큰 물체 2개만 출력되는데 비해, 임계치가 30인 경우는 비교적 작은 픽셀수를 가진 레이블들까지 출력되는 것을 확인할 수 있었다. 이를 통해, 이미지 추출을 통해 얻어내려고 하는 값이 무엇이냐에 따라 임계값을 조정해 원하는 이미지를 얻어낼 수 있음을 확인했다. 큰 물체들만 포함하는 결과를 얻고 싶다면 임계치를 크게 주고, 비교적 세세한 물체들까지 포함하는 결과를 얻고 싶다면 작은 임계치를 주면 될 것이다.

  • 임계치 픽셀 수가 300일 때 PCX2 이미지의 레이블 분류 결과 및 임계치 픽셀 수가 30일
    때 PCX2 이미지의 레이블 분류 결과

또한, PCX 2 이미지에 대해서도 위 코드가 잘 작동되는지 확인해 보았다.

이 또한 왼쪽은 300이 임계치인 경우이고, 오른쪽은 30이 임계치인 경우이다. 300인 경우에 대해서는 가장 큰 물체 4개만 출력되는 것을 확인할 수 있고, 30인 경우에 대해서는 작은 픽셀 값들까지 출력되는 것을 확인할 수 있다. 다시 한 번 임계치를 얼마로 주느냐에 따라 원하는 레이블의 영상을 추출해낼 수 있음을 확인할 수 있었다.

이미지가 곡선이든, 사각형이든 어떤 형태이던 간에 이미지와 노이즈를 효과적으로 분류할 수 있는 방법이기 때문에, 위와 같은 Image Labeling 알고리즘이 매우 강력한 알고리즘임을 느끼게 되었다. 원리는 간단하면서 최고의 출력결과를 보여주기 때문이다. 아마도 픽셀을 위, 왼쪽으로 각각 하나씩만 비교하기 때문에 정밀도가 높아지고, r 테이블이라는 유용한 메소드를 도입했기 때문이라는 생각이 들었다.

일례로, 기존에 진행했던 과제들을 살펴보자. 여러 개의 픽셀 값을 평균내서 쓰는 필터링의 경우나, Stereo 이미지 분류에서 여러 개의 픽셀을 탐색하는 경우들이 있었다. 이 과제들에서는 이미지 픽셀을 1개가 아닌 여러 개끼리 비교하다보니 이미지 분류가 너무 흐릿하게 되거나, r테이블처럼 기준점이 되는 메소드가 없어 이미지 출력시 노이즈가 많이 끼어있는 상태로 출력되곤 했었다. 이와 달리 Image Labling 알고리즘은 2개의 픽셀만 비교하며, r 테이블이라는 효과적인 기법을 도입했다. 그래서 이렇게 성능이 높아진 것이라 결론지을 수 있다.
나아가, 이미지 레이블링 알고리즘이 효과적으로 동작하기 위해서는 이미지 thresholding이 잘 되어있어야 한다는 사실을 다시 한 번 체감할 수 있었다.

코드를 작성하며 발생했던 문제점 및 해결 과정

Display_Labeled_Images 함수 실행 시, 임계치 이상의 레이블에 해당되지 않는 이미지까지 출력되는 문제점

PCX1 이미지는 서로 다른 레이블의 이미지들끼리 비교적 멀리 떨어져 있어, 레이블 된 이미지만 잘라서 출력할 때 문제가 없었다. 하지만 PCX2 이미지는 서로 다른 레이블의 이미지들끼리 인접하게 붙어있어서, 레이블 된 이미지만 잘라서 출력할 때 다음과 같은 오류화면이 출력 되었다.

영상의 잘라진 레이블 이미지들을 보면, 해당 레이블이 아닌데도 출력된 불필요한 이미지들이 보인다. 원인은 쉽게 찾을 수 있었다. 내가 작성한 Display_Labeled_Images의 함수는 단순히 레이블 이미지의 x,y 최대 최소 좌표를 기준으로 자른 게 전부이므로, 잘린 이미지 내에 해당 레이블이 아닌 이미지가 포함될 수도 있는 것이었다. 위와 같은 고찰을 바탕으로 코드를 다음과 같이 수정하였다.


// 수정 전

BYTE **tempImg;

tempImg = cmatrix(Ymax - Ymin, Xmax - Xmin);
for (y= Ymin ; y<Ymax; y++) {
  for (x=Xmin; x<Xmax; x++) {
  	tempImg[y - Ymin][x - Xmin] = img1[y][x];
  }
}
// 수정 후
BYTE **tempImg;

tempImg = cmatrix(Ymax - Ymin, Xmax - Xmin);
for (y= Ymin ; y<Ymax; y++) {
  for (x=Xmin; x<Xmax; x++) {

  if(Label[y][x] == k) {
    tempImg[y - Ymin][x - Xmin] = img1[y][x];
    }

  else {
    tempImg[y - Ymin][x - Xmin] = 0;
    }
  }
}

위와 같이 tempImg에 잘린 레이블의 이미지를 넣는 과정에, 넣는 이미지가 k 레이블에 속하는지 묻는 if 문을 추가적으로 삽입하였다. k 레이블이 맞다면 이미지 픽셀 값을 img1에서 받아오고, k 레이블이 아니라면 픽셀 밝기가 0으로 처리되도록 하였다. 수정 이후 올바르게 출력된 화면은 다음과 같다.

좋은 웹페이지 즐겨찾기