아무래도 이미지를 다루니까 이미지에 대해 설명하지 않을 수 없다.
이미지는 픽셀들의 집합이고, 각 픽셀들은 색을 가지고 있다. 모든 색은 빨강, 초록, 파랑의 적절한 조화로 만들 수 있음을 알고있는가? 몰랐다면 알아두자. 모든 색은 빨강, 초록, 파랑을 적절히 섞어 만들 수 있다.
따라서 하나의 픽셀은 빨강, 초록, 파랑 3개로 나누어서 볼 수 있다. 이렇게 나누면 각각의 색깔은 하나의 숫자로 표현이 된다. 이 숫자는 0~255사이의 숫자로 255에 가까울수록 밝아지고, 0에 가까울수록 어두워진다. 즉, 각각의 숫자 자체는 밝기를 나타낸다. 앞으로 사용할 이미지들은 데이터 타입이 numpy.uint8 이다. 따라서 값이 uint8 범위를 넘어가버리면 오버플로우가 일어나서 이미지가 비정상적으로 변할 수 있으니 참고하자.
이때 빨강, 초록, 파랑 각각을 이미지의 Channel이라고 부른다.
이미지의 전체적인 분위기는 Channel들이 어떤 순서로 합쳐지는 지에 따라 달라진다.
다음 코드를 통해 이미지를 불러와보자.
import numpy as np
import matplotlib.pyplot as plt
import cv2
import urllib.request
url = "https://thumbs.dreamstime.com/b/funny-lemon-character-smiling-cute-cartoon-isolated-white-background-eps-file-available-41690914.jpg"
response = urllib.request.urlopen(url)
img = cv2.imdecode(np.asarray(bytearray(response.read()), dtype='uint8'), cv2.IMREAD_COLOR)
plt.imshow(img)
url에는 인터넷에 있는 이미지의 주소를 넣어주면 된다.
위 캐릭터의 이름은 Funny Lemon Character이다. 웃긴 레몬 캐릭터인데 보다시피 노랑~주황색 느낌을 가지고 있다. 아마 위 코드를 그대로 실행하면 다음과 같이 나왔을 것이다.
이는 채널이 B, G, R 순서로 합쳐졌기 때문이다. 이는 openCV의 특징인데, ovenCV로 이미지를 불러오면 BGR 순서로 채널이 설정되어서 나온다. 그렇다면 이 순서는 어떻게 바꿔줄 수 있을까?
호기심을 잠시 접어두고 아래 글들을 더 읽어보자.
앞으로 이미지는 배열 형태로 다루게 된다. 그냥 배열은 아니고 Numpy의 ndarray형태로 다룰 것이다. 나중에 행렬연산을 해야하기 때문이다.
어떤 이미지 배열을 img라고 할 때, 각각의 차원이 의미하는 것은 다음과 같다.
img[y좌표][x좌표][채널번호]
이때 좌표는 이미지의 왼쪽 위가 (0, 0)이며, 오른쪽 아래가 (이미지 높이-1, 이미지 너비-1) 이다. 즉 좌표를 통해 하나의 픽셀을 다룰 수 있게 된다. 채널 번호는 기본적으로 빨강, 초록, 파랑 순서로 0~2이다. 따라서 img[0][0][0]이라고 하면, Red Channel의 (0, 0)에 해당하는 픽셀을 의미한다.
위에 있었던 파랑 레몬 캐릭터를 다시 노란색으로 바꾸려면, 채널의 순서를 뒤바꿔 주면 된다. for문을 통해 순서를 뒤집어줘도 되지만, img[ :, :, ::-1 ]이라고 작성해도 된다.
plt.imshow(img[:, :, ::-1])
보면 알겠지만 인덱싱을 ':'을 통해 바로 해줄 수도 있다. 3번째 자리에서 '::-1'을 해줬으니 채널의 순서를 뒤집었다고 이해할 수 있다. 이를 첫번째나 두번째에 적용하면 어떻게 될까?
첫번째에 적용하게된다면, 상하 반전을 기대할 수 있다. 따라서 이미지는 다음과 같이 나온다.
두번째에 적용하는 경우는 예상했겠지만 좌우 반전을 만들어 줄 수 있다.
채널의 순서에 의한 색상의 변화, 배열에서 순서를 뒤집었을 때 이미지가 어떻게 변하는 지에 대해 알아봤다.
이제 채널들을 분리해보려 한다.
순수하게 빨간색 채널만 보이려면, 초록과 파란색 채널들이 전부 사라져야한다. 너무 당연한 이야기지만, 이를 어떻게 해야하는 것일까?
순수한 빨간색의 RGB값은 (255, 0, 0)이다. 즉, G 채널과 B 채널의 값든 모두 0이어야 한다. 이제 이를 코드로 작성해보자.
def get_red_channel(img):
ret_img = img.copy()
ret_img[:,:,1] = 0
ret_img[:,:,2] = 0
return ret_img
이때 눈 여겨 봐야할 부분은 img.copy() 이다.
그냥 img 자체를 '=' 연산자로 넘기면 얕은 복사가 된다. 따라서 img에 빨간색만 남기면 더이상 다채로운 이미지로 돌아오지 않고, 영영 빨간색 이미지가 되버린다.
다음으로 봐야할 부분은 ret_img[:, :, 1] = 0 인데, 1번 채널은 초록색 채널을 의미하기 때문에 이 채널의 값을 모두 0으로 만든다는 의미이다. 이를 파란색 채널에도 적용하면 빨간색 채널만 분리 해낼 수 있다.
초록색, 파란색 채널도 마찬가지로 분리할 수 있다.
혹시 서로다른 두 이미지를 하나의 이미지로 합칠 수 있을까?
이미지는 배열의 형태로 있지만, 이는 행렬 연산이 가능한 numpy.ndarray 타입이다. 이는 두 행렬을 더할 수 있다는 의미다. 따라서 두 이미지를 합친다는 것은 말 그대로 '+' 연산을 하면 된다는 것이다. 이후 합쳐진 값을 0~255 사이의 숫자로 만들기 위해 2로 나누어 평균을 내주면 이미지를 합칠 수 있다.
먼저 손을 왼쪽으로 뻗고있는 레몬과 좌우반전 시킨 레몬을 합친다고 해보자.
img2 = img.copy()
img2 = img2[:,::-1,:]
img3 = ((img+img2)/2)
plt.imshow(img3)
잘 합쳐질 것이라 기대했겠지만, 이미지는 다음과 같이 나온다.
이유는 글의 상단에 말한대로 한 채널의 한 픽셀을 나타내는 데이터 타입은 uint8로 0~255를 넘어버리면 오버플로우가 난다.
따라서 합치려는 이미지의 데이터 타입을 uint8보다 큰 uint16으로 바꾼 뒤 다시 합성을 해보자.
img = img.astype(np.uint16)
img2 = img.copy()
img2 = img2[:,::-1,:]
img3 = ((img+img2)/2)
plt.imshow(img3)
astype이라는 함수에 인자로 np.uint16을 넣어주면 이미지의 데이터 타입이 변경된다.
위 코드대로 해도 결과가 달라지지 않았을텐데 이유는 이미지를 출력할 때의 데이터 타입은 uint8이어야 하기 때문이다. 따라서 img3의 데이터 타입을 다시 uint8로 변경해서 출력해보자.
plt.imshow(img3.astype(np.uint8))
그럼 다음과 같이 합쳐진 이미지를 볼 수 있다.
근데 잘 보면, 겹쳐지는 부분이 아니라 원래 그 자리에 있었던 이미지는 흐려졌다. 왤까?
하얀색 픽셀값과 겹치지 않는 부분이 더해진 후 2로 나누어졌기 때문이다. 따라서 하얀색이 섞여 흐린 것 처럼보이는 것이다.
따라서 이를 해결하기 위해서는 두 이미지를 비교하면서 합성된 이미지를 생성해줘야한다. 합쳐질 때, 두 이미지중 하나라도 하얀색 픽셀일 경우 하얀색 픽셀이 아닌 이미지를 합성될 이미지에 넣어줘야한다.
img3 = np.zeros(img.shape).astype(np.uint8)
for i in range(img.shape[0]):
for j in range(img.shape[1]):
for k in range(3):
value = (int(img[i][j][k]) + int(img2[i][j][k])) // 2
img_is_white = sum(img[i][j]) == (255 * 3) # 하얀색 픽셀은 (255, 255, 255)
img2_is_white = sum(img2[i][j]) == (255 * 3)
if img_is_white or img2_is_white:
value = min(img[i][j][k], img2[i][j][k])
img3[i][j][k] = value
위 코드를 실행하면 다음과 같이 나온다.
하지만 이는 이미지가 단순하게 하얀색과 섞인다고 가정할 때 가능한 방법이다. 위 결과도 잘 생각해보면, 제대로 합성되었다고 생각할 수 없다. 겹친다고 해서 흐려지면 안 되기 때문이다.
그렇다면 위보다 더 선명하게 합성하려면 어떻게 해야할까?
내가 생각한 방법은 두 이미지의 픽셀들 밝기를 비교해보고 밝기가 더 낮은 픽셀을 선택하는 것이었다.
for i in range(img.shape[0]):
for j in range(img.shape[1]):
for k in range(3):
value = min(img[i][j][k], img2[i][j][k])
img3[i][j][k] = value
이렇게 작성하면 다음과 같이 나온다.
두 이미지를 더한다는 아이디어부터 시작했던 합성하기는 여기서 끝이다.
'컴퓨터 비전' 카테고리의 다른 글
3D keypoints 시각화 해보기 (0) | 2024.07.13 |
---|---|
Camera calibration (0) | 2024.07.06 |
[컴퓨터 비전] Histogram Equalization (1) | 2023.03.21 |
[컴퓨터 비전] 이미지 변환(2) - Bilinear Interpolation (0) | 2023.01.21 |