Framework/OpenCV

[3D LUT] 에 대해 이해한 내용 적어보기

깜태 2023. 8. 14. 16:35
728x90

최근에 이미지 보정 관련하여 일이 있어서 조사해보던 중에

LUT 에 대해 알게 되었고, 궁금했던 부분과 공부한 내용에 대해 짧게 적어보려고 한다.

내가 궁금했던 내용들은 이렇다.

 

  • LUT가 뭐지??
  • 3차원의 RGB 픽셀을 어떻게 LUT를 적용시키는지, 적용 방법
  • 3D Cube 파일 LUT 적용 방법
  • CUBE 파일의 LUT_SIZE는 왜 홀수인지??

 

LUT(Look Up Table)가 뭐지?

 

LUT는 Look Up Table의 약자이다.

LUT는 말 그대로 룩업 테이블, 찾아 보는 테이블이라는 뜻으로

엑셀에서의 LOOKUP 함수를 안다면 이해하기 쉬운데, 더 이해하기 쉽게 이미지를 가져왔다.

참고: https://hotcoffee.tistory.com/38

그림을 참고해서 왼쪽의 테이블(Table)이 제공된 상황에서는

A07이라는 부품을 알기 위해선 찾아보기만 하면(Look Up)

쉽게 부품명과 재고상태, 부품가격을 알 수 있다.

 

영상처리에서는 포토샵이나 사진 보정 어플에서 보정이 필요할 때

누군가가 정의해놓은 픽셀 공간들을 참고하라는 방식으로 3D LUT 파일을 사용한다. 

이 과정을 1채널인 흑백 기준으로부터 천천히 이해해보았다.

 

LUT 적용 과정

1채널로 이해하기 쉽게 흑백기준으로는 이렇다.

0~255 값에 해당하는 값 범위 중에서 어두운 부분은 더 어둡게, 밝은 부분은 더 밝게 하고 싶다면

아마 이런 방식의 테이블이 그려질 것이다.

 

0~255 값의 픽셀값이 1:1로 매칭된다면 LUT는 256개의 픽셀값을 저장한 테이블이 되고,

Original Pixel Value * LUT계수 를 통해 보정된 값이 생성된다.

 

그렇다면 RGB 경우인 3D의 경우에는 어떻게 될까??

3D로 진행된다면 위의 적용한 방법이 채널 별로 가능해진다.

예를 들어 R 채널은 기울기를 더 가파르게, G채널은 기울기를 낮게, B 채널에는 곡선의 기울기를 적용할 수 있다.

 

이렇게 한다고 하면, RGB 픽셀 값 하나하나가 매칭이 되서 쉽게 픽셀이 바뀐다면 

r = max(1.2x) ,  g = 0.8x, b = (5/6 *x^2) 이 될것이다.

[128, 128, 128] 픽셀이 [150, 110, 70] 이런식으로 바뀔 수 있을 것 같다.

 

그런데 궁금한 건 포토샵에서 적용하는 LUT 테이블을 보면 3D 방식을 지닌다.

이 부분에서 왜 그런지 생각해보고 이해해보려고 해보았다.

 

RGB 룩업 테이블을 만든다면 어떻게 만들어야 될까??

1:1로 매핑된다면 [256, 256, 256] => 256 * 3의 크기의 테이블이 존재해야 한다.

 

근데 이런 생각이 들었다.

256, 256, 256 크기 하나는 RGB 스케일 1개에 대해서만 존재한다. (1D LUT)

룩업 테이블 1개라면 무슨 값을 적용해도 값이 고정적으로 진행될 수 밖에 없다.

무슨 말이냐면 R값은 어떻게 해도 1.2x 필터 계수 값을 벗어날 수가 없다.

 

 

보정이 필요한 입장에서 다시 생각해보면,

RGB란 3차원 구조의 공간 대비 사용 방식이 너무 단순해보였다.

R값은 고정일 때, G와 B에 대한 변화량도 더 다양하게 그릴 수 있어야 하지 않을까?

 

이런 필터를 갖도록 필터를 3차원으로 만들면 될 것 같다.

그러면 1차원 LUT에 비해 3차원 LUT는 위의 그림과 같이 한 축을 고정시켜도 더 다양한 값들이 존재할 수 있다.

검색해보니, HSV 색상 공간을 Cube형태로 만들어 둔 것으로 볼 수도 있을 것 같다.

위에서 언급된 다양한 색감을 표현할 수 있는 이유로 LUT는 3D LUT를 더 많이 이용된다고 한다.

3D Cube 파일 LUT 적용 방법

코드적으로 어떻게 LUT를 적용시키는 지에 대해서도 검색해보았다.

LUT파일을 윗줄에는 LUT_SIZE에 대한 크기가 나와있고,

아래로는 소수점의 숫자들이 주루룩 나열되어있다.

이는 [0,0,0]~[255,255,255] 범위 내의 픽셀 하나당 어떻게 적용시켜야되는지에 대한 내용으로,

LUT_SIZE * LUT_SIZE * LUT_SIZE * 3 의 길이를 갖는다.

 

# LUT 파일 예시
#LUT size
LUT_3D_SIZE 32

#data domain
DOMAIN_MIN 0.0 0.0 0.0
DOMAIN_MAX 1.0 1.0 1.0

#LUT data points
0.013214 0.010803 0.008881
0.040009 0.015350 0.017090
0.068207 0.027527 0.027527
0.090179 0.033844 0.033844
0.116791 0.031860 0.031860
0.148682 0.029510 0.029510
0.180481 0.027252 0.027252
0.212372 0.025024 0.025024
0.244446 0.022919 0.022888
...(생략)

 

여기서 LUT_SIZE는 1개 색상공간 당 크기로 LUT_SIZE가 32면, 8개 픽셀 단위로 테이블 값이 변한다는 의미다.

LUT_SIZE는 실제 색상공간만큼 1:1 매칭을 시킨다면 모든 픽셀 수(256 * 256 * 256 ) * 매핑 RGB 픽셀 (RGB = 3)으로 한 픽셀당 매칭시키는 연산이 매우 크기 때문에 약간의 양자화를 통해서 색상공간을 약간 줄여준 것이라고 한다.

 

이제 실제로 LUT 테이블을 이미지 하나에 적용시켜보자.

 

import cv2

LUT_SIZE=32
LUT_TABLE=[[r,g,b], [r,g,b], ...]
def convert_pixel(pixel):
    r,g,b = pixel
    return LUT_TABLE[r//LUT_SIZE][g//LUT_SIZE][b//LUT_SIZE]

img = cv2.imread('test.img') // H x W x 3
img_lut = img.copy()
for height in img.shape[0]:
    for width in img.shape[1]:
        img_lut[height][width] = convert_pixel(img[height][width])

 

이렇게 .CUBE 형태의 LUT 테이블을 읽은 뒤, 이미지 픽셀을 하나씩 매칭시켜서 변환시켜보면

다음과 같은 이미지가 나오는 것을 볼 수 있다.

근데 해보고 나니, 이미지 픽셀이 깨지는 것을 보면서 이 부분이 이상하다 느꼈다.

픽셀이 깨지는 원인을 생각해보니 256에서 저차원으로 매핑하는 부분이 깨졌을거라 생각이 들었고,

그에 대해 어떻게 보간(interpolation)을 진행할지 생각했다.

3D LUT인만큼 3차원의 보간이 필요했는데, 

검색해보니 쉽게 3차원 보간에 관한 코드를 얻을 수 있어서 적용하였더니 픽셀 깨짐 현상을 해결했다.

 

def trilinear(xyz, data):
    '''
    xyz: array with coordinates inside data
    data: 3d volume
    returns: interpolated data values at coordinates
    '''
    ijk = xyz.astype(np.int32)
    i, j, k = ijk[:,0], ijk[:,1], ijk[:,2]
    V000 = data[ i   , j   ,  k   ].astype(np.int32)
    V100 = data[(i+1), j   ,  k   ].astype(np.int32)
    V010 = data[ i   ,(j+1),  k   ].astype(np.int32)
    V001 = data[ i   , j   , (k+1)].astype(np.int32)
    V101 = data[(i+1), j   , (k+1)].astype(np.int32)
    V011 = data[ i   ,(j+1), (k+1)].astype(np.int32)
    V110 = data[(i+1),(j+1),  k   ].astype(np.int32)
    V111 = data[(i+1),(j+1), (k+1)].astype(np.int32)
    xyz = xyz - ijk
    x, y, z = xyz[:,0], xyz[:,1], xyz[:,2]
    Vxyz = (V000 * (1 - x)*(1 - y)*(1 - z)
            + V100 * x * (1 - y) * (1 - z) +
            + V010 * (1 - x) * y * (1 - z) +
            + V001 * (1 - x) * (1 - y) * z +
            + V101 * x * (1 - y) * z +
            + V011 * (1 - x) * y * z +
            + V110 * x * y * (1 - z) +
            + V111 * x * y * z)
    return Vxyz
 
적용 결과

 

적용하고 나니 픽셀이 깨짐없이 자연스럽게 이어지는 걸 볼 수 있다.

 

오늘은 이렇게 글을 쓰고 정리해보면서

LUT가 무엇이고, LUT를 어떻게 적용시키는지,

3D LUT 테이블을 적용시키는 방법에 대해 알아보았다.

 

추가로 그리고 궁금한 것 중 하나가 왜 필터가 17, 33, 65일까가 궁금했다.

일반적으로 영상처리는 8비트를 상정하고 시작하지만, 

특수한 상황에서는 10비트와 같은 다양한 표현이 하고 싶을 수도 있다.

+1을 시킨 이유는 여유를 두어 위와 같은 상황일 때 더 다양한 표현을 할 수도 있고,

오차값을 허용하는 건지 실제로 연산할 때는 -1을 빼고 진행하는 것도 있는 것 같다.

(이건 미정확해서 다시 알아보고 수정하겠다)

 

 

 

 

참고

https://www.bromptontech.com/what-is-a-3d-lut/

https://gist.github.com/arsenyinfo/74e42b41749cf29a7bbb69ed839bff1a

https://jianyucheng.medium.com/color-management-in-vfx-lut-a6928fa20fef

https://neurostars.org/t/trilinear-interpolation-in-python/18019

728x90