지금까지 조금씩이나마 영상 쪽 데이터를 다루는 방법을 배우면서, 많은 RGB 채널 이미지들을 grayscale로 변환하는 작업을 거쳤었다.
그런데! 정작 RGB 채널의 이미지가 어떻게 GRAYscale로 변환되는지 그 원리에 대해서 궁금해 했던 적이 없었다.. 😅😅 그냥 cv2 라이브러리 불러다가 cvtColor 코드 한 줄이면 됐었으니까...
최근 프로젝트 진행중에 교수님께서 이에 대해서 내게 여쭈어 보셨는데, 그 때의 나는 당연히 대답을 못했다. 알 리가 없었다.. 궁금해 해 본 적도 없으니까.
그동안 왜 이런 기초적인 것들을 알려고도 하지 않았는지 스스로에게 좀 화가 났다. 24년이 된 지 1주일이 넘게 지났는데, 반성 차원에서 글도 정리하고 올해에는 기초에도 충실한 사람이 되어야지 하고 다짐해 본다!
Colorimetry
란 인간의 색채 인식을 물리적으로 정량화하고 묘사하는데 사용되는 과학 기술이다. 색을 인지하는 방식에 기반하여 색상을 정의하고, 색 공간을 사용하여 수치적으로 색을 표현한다. 대표적인 colorimetry 방법은 1931년 CIE에서 처음 소개된 XYZ 색공간이다.
XYZ 색공간은 인간의 시각을 기반으로, 세 가지 주요 요소를 X, Y, Z
로 나타낸 것이다. 여기서 X는 적색, Y는 녹색, Z는 청색에 해당한다.
또한 Y 성분은 Luminance(밝기)
를 나타내며, 사람의 시각이 가장 민감한 밝기 영역(녹색에 가장 민감하다고 함)을 반영한다.
현대 디지털 이미지, 비디오는 사용되는 카메라, 센서의 표준 처리 방식에 의하여 대부분 감마 압축(gamma compression)이 적용되어 있다고 한다. 감마 압축이란, 실제 우리가 보는 자연 밝기 수준 범위를 디지털 파일이나 비디오 신호에서 제한된 수의 밝기 수준(예를 들면, 8bit 이미지에서 0~255)으로 효과적으로 매핑하기 위해 사용된다.
감마 압축에 대한 대표적인 예는 sRGB 색 공간인데, sRGB에서는 gamma=2.2의 비선형 관계를 사용하여 이미지의 밝기 정보를 인코딩한다고 한다.
이러한 감마 압축 RGB를 grayscale로 변환하기 위해서는 다음과 같은 과정이 필요하다.
1. 감마 확장
감마 압축된 RGB 값을 선형화하는 작업이다. (Nonlinear -> Linear) 각 채널값을 감마로 거듭제곱하여 원래 선형 밝기 값을 복원한다.
여기서 감마 값은 sRGB 표준에 의해 일반적으로 2.2로 가정된다.
2. 선형 밝기 계산
선형화된 RGB값에 대하여, CIE의 XYZ 색공간에서 Y성분과 유사하게 밝기를 계산하기 위해 특정 가중치를 적용한다. 이는 녹색 채널에 더 높은 가중치를, 파란색 채널에 더 낮은 가중치를 부여한다.
이 가중치는 ITU-R BT.709 표준에서 권장하는 것으로, HDTV 시스템에 사용된다.
3. 감마 압축
계산된 선형 밝기 값을 다시 감마 압축하여 비선형 그레이스케일값으로 변환한다.
다음은 Colorimetry grayscale conversion 방법에 따라, 감마 압축된 RGB 이미지를 grayscale로 변환하는 python code이다.
def gamma_decompress(value, gamma=2.2):
return value ** gamma
def gamma_compress(value, gamma=2.2):
return value ** (1/gamma)
def rgb_to_grayscale_gamma_corrected(img, gamma=2.2):
b, g, r = cv2.split(img)
# 감마 확장을 적용하여 감마 압축된 RGB 값을 선형 공간으로 변환
r_linear = gamma_decompress(r/255.0, gamma)
g_linear = gamma_decompress(g/255.0, gamma)
b_linear = gamma_decompress(b/255.0, gamma)
# 선형 RGB 값을 사용하여 선형 밝기 값을 계산
y_linear = 0.2126 * r_linear + 0.7152 * g_linear + 0.0722 * b_linear
# 필요한 경우 선형 밝기 값을 다시 감마 압축하여 그레이스케일 값으로 변환
y_nonlinear = gamma_compress(y_linear, gamma)
# 0-255 스케일로 변환
return np.rint(y_nonlinear * 255)
이러한 코드를 이미지에 적용해 보면 다음과 같이 출력된다.
gray = rgb_to_grayscale_gamma_corrected(im)
plt.imshow(gray, cmap='gray')
plt.show()
print(gray)
Luma coding을 이용한 grayscale 변환법은 색상 이미지에서 밝기 정보(Luma)만을 추출하여 grayscale image를 생성하는 방법이다. (색상 정보는 Chroma) Luma는 위에서 언급했듯, 인간의 시각이 가장 민감한 녹색을 가장 크게 반영하여 계산한다. YUV, YCbCr과 같은 컬러 영역에서 Y 성분은 Luma를 나타내며, 이러한 컬러 스페이스는 색상 정보를 효율적으로 압축하고 전송하는 데 도움이 된다.
Luma 값(Y)는 일반적으로 다음과 같은 공식을 통해 계산된다.
이 공식은 ITU-R BT.601 표준에서 비롯된 것이다.
이 외에도, 앞서 언급된 ITU-R BT.709 표준(Y=0.2126R + 0.7152G + 0.0722B)이나 ITU-R BT.2100 표준(0.2627R + 0.6780G + 0.0593B)을 사용하여 Y 값을 계산할 수 있다. (BT.2100 표준은 HDR TV에 사용된다고 한다.) Luma coding은 감마 보정된 신호에 직접 적용할 수 있고, 비디오 압축과 효율적인 전송에 적합하다.
다음은 Luma coding 방법에 따라, RGB 이미지를 grayscale로 변환하는 python code이다.
def rgb_to_grayscale(img):
b, g, r = cv2.split(img)
return np.rint(0.299 * r + 0.587 * g + 0.114 * b)
이러한 코드를 이미지에 적용해 보면 다음과 같이 출력된다.
gray = rgb_to_grayscale(im)
plt.imshow(gray, cmap='gray')
plt.show()
print(gray)
자 그럼, opencv 라이브러리 사용한 변환법과 두 가지 방법의 결과가 같은지 비교해 보자.
나는 일반적으로 cvtColor 함수를 사용해서 color 이미지를 grayscale로 변환한다.
gray = cv2.cvtColor(im, cv2.COLOR_BGR2GRAY)
plt.imshow(gray, cmap='gray')
plt.show()
print(gray)
한가지 덧붙이자면, 아예 이미지를 부를 때 grayscale로 변환하여 부를 수도 있다.
코드는 다음과 같다.
gray = cv2.imread("./choonsik.png", 0)
plt.imshow(gray, cmap='gray')
plt.show()
print(gray)
그러면 네가지 예제의 결과를 사진으로 비교해 보자.
4가지 이미지는 보기에는 똑같은데, 값이 약간 차이가 나기도 한다. 우선 Luma coding과 cvtColor 방법은 값이 완전 동일한 것으로 보이는데, 이는 Opencv의 cvtColor 방법이 Luma coding 방법을 사용했기 때문으로 보인다.
위와 같이 Opencv 공식 문서에 따르면, cvtColor 방법은 위에서 언급된 Luma coding의 ITU-R BT.601 표준 가중치 계산법을 사용하고 있는 것 같다.
흥미로운 점은 imread에서 0을 파라미터로 넣어 grayscale로 읽는 것과 cvtColor로 변환하는 방법의 값이 미세하게 다르다는 것이다.