해당 포스트는 "열혈강의 영상처리 프로그래밍" 책의 내용을 요약한 것이다.



※ 감마 보정

항상 영상의 밝기는 0에서 255사이의 값을 가진다. 그러면 밝기값이 100인 픽셀은 밝기값이 200인 픽셀보다 정확히 절반의 밝기로 보일까? 그건 모니터나 프린터 등 디스플레이 종류에 따라 달라진다. 즉, 디스플레이 장치는 입력신호에 따른 출력 밝기가 비선형적이다. 그래서 입력 신호(모니터에 가해지는 전압)에 100을 넣으면 100이 나오고 200을 넣으면 200이 나오는 선형적 관계가 아니다.



위 그래프는 디스플레이의 입력신호마다 실제 모니터가 출력해 우리 눈에 보이는 밝기를 나타낸다. 일반 LCD 모니터는 위 그래프와 같이 입력 신호의 세기를 올려도 밝기가 천천히 증가하다가 신호의 세기가 커질수록 밝기가 급격하게 증가한다. 그래서 밝은 부분은 더 밝게, 어두운 부분은 더 어둡게 보여 전체적으로 영상이 명암은 높되 밝기는 어두워지는 효가가 있다. 따라서 우리 눈은 위에서 보이는 선형 직선 함수와 같이 왜곡이 없는 영상에 익숙 하기 때문에 모니터의 출력을 보정해 줘야 한다. 


위 그래프는 다음과 같은 지수 함수로 나타낼 수 있다.


(출력 밝기) = (상수) * (입력 밝기)


지수 r(감마)가 입출력 곡선의 특징을 결정하는 값으로 위 곡선을 감마 곡선이라고 부른다. 일반 LCD 모니터의 경우 감마 값이 2.5정도 되고 우리 눈에 가장 보기 좋은 감마 값은 직선 형태를 갖도록 하는 감마값 1이다. 



- 감마 보정 방법


예를 들어 LCD 모니터의 감마 값이 2.5다면 감마 보정 후에 감마 값이 1이 되도록 하면 되기 때문에 감마 값에 2.5를 나누어 주면 된다. 즉, 아래와 같은 식이 된다.


(보정 밝기) = (입력 밝기)


따라서 주어진 디스플레이 장치의 감마 값을 나타내는 변수 dGamma가 주어졌을 때 디스플레이 장치의 감마 보정을 수행하는 코드는 다음과 같다.

 pOut[c] = (BYTE)(pow(pIn[c]/255.0 , 1/dGamma)*255 + 0.5);

pIn 과 pOut은 입력 영상과 출력 영상의 픽셀 포인터를 나타낸다. pow는 지수함수인데 입력 픽셀 값을 255.0으로 나누고 있다. 그 이유는 감마 곡선이 실수 값 0에서 1 사이의 영역에서 정의 되기 때문이다. 입력 픽셀 값을 255로 나눴기 때문에 마지막에 다시 255를 곱해주었고 거기에 0.5를 더하고 BYTE 형변환으로 반올림 계산을 하였다. 여기서 앞 포스트에서 배운 BYTE형 값 범위를 조정해주는 CLIP 매크로를 사용하지 않았는 데 그 이유는 1/dGamma 값 즉, 지수가 0에서 1사이 값이여서 원래 픽셀 이상의 값을 가질 수 없기 때문이다. 



- 룩업 테이블로 감마 보정 속도 올리기


바로 위에서 구현한 감마 보정 코드는 영상의 모든 픽셀에 대해서 pow 메서드를 실행한다. 만약 입력 영상이 가로 640, 세로 480, 컬러 영상이라면 pow 연산을 640*480*3 즉, 921600번 해야 한다. 실제 픽셀 데이터는 0에서 255사이 즉, 256개이기 때문에 pow 메서드의 매개변수로 최대 256 가지가 들어갈 수 있다. 하지만 앞에서 말한 640*480 입력 영상에서의 921600개의 매개변수 전달에 의한  pow 연산은 매우 중복된다는 것을 알 수 있다. 이러한, 비효율을 해결하는 하나의 방법이 룩업 테이블(lookup table)을 이용한 연산이다. 룩업 테이블이란 미리 계산된 결과를 저장한 테이블로, 데이터마다 직접 계산하는 것보다 미리 계산된 결과를 저장된 메모리에서 읽어오는 것이 더 효율적이라고 판단될 때 사용하는 방법이다. 이는 영상 처리에서도 많이 활용된다. 미리 0에서 255까지의 정수 값에 대해서 pow 메서드의 결과를 배열에 저장해 꺼내서 사용하면 중복된 pow 연산 없앨 수 있다. 밑의 코드가 룩업 테이블을 구현한 감마 보정 구현한 예이다. 

CByteImage& GammaImage(CByteImage& inImage, CByteImage& outImage)
{
  double gamma = 1.0 / m_dGamma;
  
  BYTE arrPow[255];
  for(int n=0; n<255; n++)
  {
    arrPow[n] = (BYTE)(pow(n/255.0, gamma)*255 + 0.5);
  }
  int nWStep = inImage.GetWidth() * inImage.GetChannel();
  int nHeight = inImage.GetHeight();
 
  int r,c;
  for(r=0; r<nHeight ; r++)
  {
    BYTE * pIn = inImage.GetPtr(r);
    BYTE * pOut = outImage.GetPtr(r);
    for(c=0; c<nWStep; c++)
    {
      pOut[c] = arrPow[pIn[c]];
    }
  }
  return outImage;
}


+ Recent posts