FoO의 개발 블로그

[OpenCV] 채널 별 배열 구조 + 이미지 투명화 본문

Programming/Python

[OpenCV] 채널 별 배열 구조 + 이미지 투명화

FoO__511 2023. 10. 17. 14:02

계기

무작정 OpenCV 박치기를 해보니 openCV의 이미지파일이 numpy 배열이라는 것을 알게 되었다. 하지만 알아봤자 대체 rgba가 어떤 식으로 펼쳐져 있는지, 파이썬에서 어떻게 배열을 다뤄야하는지 몰라 난항을 겪고 있다. 일단 뭘 하는지 알아내기 위해 각 채널 별 배열부터 알아보자.

 

OpenCV의 채널

openCV에서 이미지 파일을 읽을 때 cv2.imread() 함수를 사용한다. 첫 번째 인자는 파일의 주소이고 두 번째 인자는 flag다. flag에 따라 이미지를 불러 올 때 채널 값이 바뀐다. 채널은 불러온 이미지에.shape를 하여 얻은 배열의 3번 째 값이다.

cv2.IMREAD_COLOR 혹은 1 : 이미지를 Color로 읽음. 투명 무시. Default.
cv2.IMREAD_GRAYSCALE 혹은 0 : 이미지를 Grayscale로 읽음.
cv2.IMREAD_UNCHANGED 혹은 -1 : 이미지파일을 alpha channel까지 포함하여 읽음. 투명 보존.

채널이 큰 숫자일수록 더 다양한 표현이 가능하다.

 

큰 이미지를 사용하니 배열을 파악하기 어려워 대충 아래같은 png파일을 만들었다. 세로 5px, 가로 5px이다. 

 

Grayscale

color이 디폴트이긴 하지만 채널이 적은 grayscale이 쉬울 것 같으니 이걸로 시작한다.

import cv2

grayscale = cv2.imread('.\\img\\cvTest.png', 0)
print("shape :",grayscale.shape)
print("arr :\n",grayscale)

grayscale_big=cv2.resize(grayscale, dsize=(0, 0), fx=100, fy=100, interpolation=cv2.INTER_NEAREST)
cv2.imwrite('.\\img\\grayscale.png', grayscale_big)

 

결과

shape : (5, 5)
arr :
 [[  0 255   0 255   0]
 [255 138 255 138 255]
 [150 255 150 255 150]
 [255 203 255 203 255]
 [144 255 144 255 144]]

채널이 없는데 그 이유는 .shape는 배열 형태를 알려주는 numpy의 메소드이기 때문이다. 밑을 보면 알겠지만 color와 unchanged는 3중 배열이기 때문에 마지막 값이 있다.

 

이중 배열 속에 위에서부터 한 줄씩 픽셀값이 담겨있다. 저장된 이미지는 다음과 같다.

 

 

지금 내가 1차로 필요한게 클립스튜디오에 있는 기능 중 하나인 휘도를 투명도로 변환인데, grayscale을 잘 활용하면 편하게 될 것 같다.

 

Color

rgb가 필요하기 때문에 color의 채널은 3이다.

import cv2

color = cv2.imread('.\\img\\cvTest.png', 1)
print("shape :", color.shape)
print("arr :\n", color)

color_big=cv2.resize(color, dsize=(0, 0), fx=100, fy=100, interpolation=cv2.INTER_NEAREST)
cv2.imwrite('.\\img\\grayscale.png', color_big)

 

결과

shape : (5, 5, 3)
arr :
 [[[  0   0   0]
  [255 255 255]
  [  0   0   0]
  [255 255 255]
  [  0   0   0]]

 [[255 255 255]
  [ 36  28 237]
  [255 255 255]
  [ 36  28 237]
  [255 255 255]]

 [[232 162   0]
  [255 255 255]
  [232 162   0]
  [255 255 255]
  [232 162   0]]

 [[255 255 255]
  [ 29 230 181]
  [255 255 255]
  [ 29 230 181]
  [255 255 255]]

 [[190 146 112]
  [255 255 255]
  [190 146 112]
  [255 255 255]
  [190 146 112]]]

 

3중 배열이다.


3중 배열의 첫 번째 2중 배열에는 5개의 배열이 들어있다. 검정색의 rgb값은 (0, 0, 0)이고 흰색은 rgb값은 (255, 255, 255)이므로, 각 2중 배열은 한 줄씩을 담당한다는 것을 알 수 있다.

 

채널에 관계없이 제일 처음에 접하는 배열 속 원소들은 각각 가로줄 하나를 뜻하는 듯 하다.

 

 

이미지에 투명 부분이 없기 때문에 결과는 그냥 똑같다.

Unchanged

드디어 alpha chennel을 다루나 했는데 아무래도 내가 그림판으로 대강 만든 이미지에는 alpha chennel이 없는 듯 하다. 플래그 이름이 unchanged이니 이미 없는건 당연히 못만들고.. 급한대로 이미 가지고 있던 투명도 있는 png파일을 작게 편집했다.

 

좀 길까봐 3*3 으로 했다.

 

unchanged = cv2.imread('.\\img\\cvTestAlpha.png', -1)
print("shape :", unchanged.shape)
print("arr :\n", unchanged)

unchanged_big=cv2.resize(unchanged, dsize=(0, 0), fx=100, fy=100, interpolation=cv2.INTER_NEAREST)
cv2.imwrite('.\\img\\unchanged.png', unchanged_big)

 

결과

shape : (3, 3, 4)
arr :
 [[[  0   0   0 255]
  [235 116 116 255]
  [  0   0   0 255]]

 [[234 107 107 255]
  [  0   0   0 255]
  [237 140 140 255]]

 [[  0   0   0 255]
  [233 123 123 255]
  [  0   0   0 255]]]

 

아까처럼 이번에도 3중 배열이며, 최상위 배열의 원소들은 각각 한 줄씩을 담당한다. 제일 하위의 배열에 원소가 하나 더 는게 color와의 차이점이다.

 

내가 투명하지 않은 부분을 잘랐는지 불투명도는 모두 255이다.

 

투명화하기

지금 필요한 것은 그래픽 툴 클립스튜디오의 휘도를 투명도로 변환와 유사한 기능이다.


특정 픽셀이 검은색과 먼만큼 그 부분을 투명도로 바꿔주는 것이다. 이렇게 하면 이미지는 흑백이 되지만 투명도가 생겨 여기저기 넣기 좋다.

과정을 생각해보면

  1. 이미지에서 픽셀별 검은 정도를 추출
  2. rgba에서 rgb는 (0, 0, 0)으로 검정색으로 만들고 a에 (255-검은 정도) 넣기

1번은 직접 추출하지 않고 이미지를 grayscale로 변환해 흑백으로 만든 뒤 그게 가지는 값을 이용한다.

실제 구현은 다음과 같다.

def change_to_transparent(img):
    channel = get_channel(img.shape)
    result = np.zeros((img.shape[0], img.shape[1], 4), dtype=np.uint8)

    if channel == 3 :
        img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    elif channel == 4 :
        img = cv2.cvtColor(img, cv2.COLOR_BGRA2GRAY)

    for i in range(0, img.shape[0]):
        for j in range(0, img.shape[1]):
            result[i, j, 3] = 255 - img[i][j]

    return result

 

코드 해설

channel = get_channel(img.shape)

 

위에서 말한 것처럼 grayscale은 2중배열이라 shape 값이 아예 비어있으므로 마냥 .shape[2]를 하면 out of index 오류가 발생한다. javascript에서처럼 ||을 통해 예외처리를 하려고 했는데 되지 않아 그냥 따로 함수로 빼서 처리했다.

// 자바스크립트로 채널을 추출할 시
const channel = img.shape[2] || 0

 

파이썬에서 위처럼 한줄로 없는 요소를 처리할 수 있는 방법이 있다면 누군가 친히 알려주세요.

 

result = np.zeros((img.shape[0], img.shape[1], 4), dtype=np.uint8)

 

np.zeros는 0으로 가득찬 배열을 생성해준다. img.shape[0]은 세로 픽셀 수, img.shape[1]은 가로 픽셀 수이다.
dtype=np.unit8은 배열 속에 들어가는 숫자의 타입을 uint8로 설정한다. 이렇게 하는 이유는 openCV의 api들 중 int타입을 지원하지 않는 경우가 있기 때문이다.

 

if channel == 3 :
    img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
elif channel == 4 :
    img = cv2.cvtColor(img, cv2.COLOR_BGRA2GRAY)

cv2.cvtColor은 색상공간을 변환해준다. 색상공간은 grayscale, rgba, rgb등이다.


채널 수에 따라 다른 플래그를 넣어준다.

 

for i in range(0, img.shape[0]):
    for j in range(0, img.shape[1]):
        result[i, j, 3] = 255 - img[i][j]

 

마지막 단계로, 처음에 만들어뒀던 0으로 가득찬 이미지 배열에서 모든 픽셀을 순회하며 투명도를 조작한다. 각 픽셀 배열(rgba이므로 length가 4인 배열이다)의 마지막 값(a)에 새로운 값을 부여하면 된다.

 

제일 위에 썼던 것처럼 grayscale로 변환했을 때의 값을 이용한다.

 

결과:

원본(좌) / 투명화된 이미지(우)

결론

처음 글을 쓰다가 다른 프로젝트들 때문에 한동안 미루었지만.. 일단 원하는 부분은 만들었다.
크기가 다른 두 이미지를 합성하기 전 단계로서 만든 기능이다.

 

결점은 이미 투명한 이미지를 넣을 경우에는 grayscale로 바꾸었을 때 모두 검정색 처리가 되어(사실 추정이다. 실험 안해봤다.) 결과도 검정색으로 나오게 된다. 따라서 투명한 이미지의 경우 뒤에 흰색 배경(numpy를 통해 생성, cv2.add()를 이용)을 깔아주고 위 함수에 넣어야 할 것이다.