귀퉁이 서재

OpenCV - 9. 이미지 연산 (합성, 알파 블렌딩, 마스킹) 본문

OpenCV

OpenCV - 9. 이미지 연산 (합성, 알파 블렌딩, 마스킹)

Baek Kyun Shin 2020. 9. 20. 15:11

이번 포스팅에서는 이미지 연산에 대해 알아보겠습니다. 이번 포스팅 역시 '파이썬으로 만드는 OpenCV 프로젝트(이세우 저)'를 정리한 것임을 밝힙니다.

코드: github.com/BaekKyunShin/OpenCV_Project_Python/tree/master/04.img_processing

이미지 연산

이미지 연산을 위해서 numpy 연산을 활용하는 방법이 있습니다. 하지만 OpenCV에서도 이미지 연산을 위한 함수를 제공합니다. OpenCV에서 굳이 연산을 위한 함수를 제공하는 이유는 값의 범위 때문입니다. 한 픽셀이 가질 수 있는 값의 범위는 0~255인데, 연산의 결과가 255보다 크거나 0보다 작을 수 있어서 결과 값을 0~255로 제한할 필요가 있습니다. OpenCV에서 제공하는 사칙연산 함수는 다음과 같습니다.

  • cv2.add(src1, src2, dest, mask, dtype): src1과 src2 더하기
    src1: 첫 번째 입력 이미지
    src2: 두 번째 입력 이미지
    dest(optional): 출력 영상
    mask(optional): mask 값이 0이 아닌 픽셀만 연산
    dtype(optional): 출력 데이터 타입(dtype)
  • cv2.subtract(src1, src2, dest, mask, dtype): src1에서 src2 빼기
    모든 파라미터는 cv2.add()와 동일
  • cv2.multiply(src1, src2, dest, scale, dtype): src1과 src2 곱하기
    scale(optional): 연산 결과에 추가 연산할 값
  • cv2.divide(src1, src2, dest, scale, dtype): src1을 src2로 나누기
    모든 파라미터는 cv2.multiply()와 동일

아래는 numpy 연산과 OpenCV 연산 함수를 이용한 사칙 연산 예제 코드입니다.

# 이미지의 사칙 연산 (arithmatic.py)

import cv2
import numpy as np

# ---① 연산에 사용할 배열 생성
a = np.uint8([[200, 50]]) 
b = np.uint8([[100, 100]])

#---② NumPy 배열 직접 연산
add1 = a + b
sub1 = a - b
mult1 = a * 2
div1 = a / 3

# ---③ OpenCV API를 이용한 연산
add2 = cv2.add(a, b)
sub2 = cv2.subtract(a, b)
mult2 = cv2.multiply(a , 2)
div2 = cv2.divide(a, 3)

#---④ 각 연산 결과 출력
print(add1, add2)
print(sub1, sub2)
print(mult1, mult2)
print(div1, div2)

numpy 연산 결과와 OpenCV 연산 함수를 활용한 연산 결과가 서로 다르다는 걸 볼 수 있습니다. 좌측이 numpy로 연산한 결과고, 우측이 OpenCV 함수로 연산한 결과입니다.

200 + 100 = 300인데, 이 값은 255를 초과합니다. unit8 타입의 값의 범위는 0 ~ 255이므로 255를 넘는 값은 다시 0부터 카운팅을 합니다. 200 + 100을 numpy로 계산하면 300인데, 이는 300 - 255 - 1 = 44입니다. 반면 cv2.add() 함수를 활용하면 255를 초과하는 모든 값은 255로 반환합니다. 반면, 50 + 100 = 150인데, 이는 numpy 연산이나 cv2.add() 연산이나 결과가 동일합니다. 150은 255를 넘지 않기 때문입니다. 마찬가지로 50 - 100 = -50인데, numpy 연산에서의 결과는 206입니다. -50 + 255 + 1 = 206이기 때문입니다. cv2.subtract()로 계산한 결과는 0입니다. OpenCV에서는 0보다 작은 모든 값을 0으로 반환하기 때문입니다. 곱하기와 나누기 연산도 255를 초과하거나 0보다 작은 값을 갖지 않고, 소수점은 갖지 않습니다.

cv2.add() 함수에 세 번째 파라미터를 전달하면 첫 번째와 두 번째 파라미터의 합을 세 번째 파라미터에 할당합니다. 따라서 아래의 세 코드의 결과는 똑같습니다.

c = cv2.add(a, b)
c = cv2.add(a, b, None)
cv2.add(a, b, c)

첫 번째 코드는 a와 b를 더한 뒤 c에 할당하는 전형적인 코드입니다. 두 번째는 a와 b를 더한 뒤 None과 c에 할당합니다. 결과는 첫 번째와 동일합니다. 마지막 코드는 a와 b를 더한 뒤 세 번째 파라미터인 c에 결과를 할당합니다.

네 번째 파라미터인 mask에 numpy 배열을 전달한 경우에는 mask의 값이 0이 아닌 위치(인덱스)에 있는 픽셀만 연산을 수행합니다. 아래의 코드를 예로 들어 보겠습니다.

# mask와 누적 할당 연산 (arithmatic_mask.py)

import cv2
import numpy as np

#---① 연산에 사용할 배열 생성
a = np.array([[1, 2]], dtype=np.uint8)
b = np.array([[10, 20]], dtype=np.uint8)
#---② 2번째 요소가 0인 마스크 배열 생성 
mask = np.array([[1, 0]], dtype=np.uint8)

#---③ 누적 할당과의 비교 연산
c1 = cv2.add(a, b, None, mask)
print(c1)
c2 = cv2.add(a, b, b.copy(), mask)
print(c2, b)
c3 = cv2.add(a, b, b, mask)
print(c3, b)

만약 코드가 c1 = cv2.add(a, b, None)라면 c1은 [[11, 22]]가 되어야 합니다. 하지만 배열이 [1, 0]인 mask를 지정해주었습니다. 앞서 mask 값이 0이 아닌 위치에 있는 픽셀만 연산해준다고 했습니다. mask 배열의 0번째 index는 1이고, 1번째 index는 0입니다. 따라서 0번째 index에 해당하는 값만 연산을 수행합니다. 0번째 index에 해당하는 값만 0이 아니기 때문입니다. 즉, 0번째 index에 있는 1과 10의 합(1+10 = 11)인 연산만 수행하고, 1번째 index에 있는 2와 20의 합(2+20 = 22)인 연산은 수행하지 않습니다. 따라서 c1 = cv2.add(a, b, None, mask)를 실행했을 때 c1 = [[11, 0]]이 됩니다.

cv2.add(a, b, b.copy(), mask)를 실행하면 마찬가지로 a와 b의 첫 번째 요소만 더하고, 두 번째 요소는 더하지 않습니다. 하지만 세 번째 파라미터가 b.copy()입니다. b = [10, 20]입니다. 따라서 a와 b의 첫 번째 요소만 더하여 b의 첫 번째 요소에 적용하고, b의 두 번째 요소는 그대로 둡니다. 따라서 결과는 [[11, 20]]입니다. 

이미지 합성

두 이미지를 합성할 때 위에서 살펴본 numpy의 합이나 cv2.add() 함수만으로는 좋은 결과를 얻을 수 없습니다. numpy의 합 연산을 수행하면 픽셀 값이 255가 넘는 경우 초과 값만을 갖기 때문에 이미지가 검은색에 가깝게 됩니다. 예를 들어, 150와 180을 더하면 330이므로, 최종 결괏값은 255에서의 초과 값만큼인 74가 됩니다. 그러면 일부 영역이 거뭇거뭇하게 됩니다. 반면 cv2.add() 연산을 하면 대부분의 픽셀 값이 255 가까이 몰리는 현상이 발생합니다. 따라서 영상이 전체적으로 하얗게 됩니다. 아래 코드는 이러한 예를 보여줍니다.

# 이미지 단순 합성 (blending_simple.py)

import cv2
import numpy as np
import matplotlib.pylab as plt

# ---① 연산에 사용할 이미지 읽기
img1 = cv2.imread('../img/wing_wall.jpg')
img2 = cv2.imread('../img/yate.jpg')

# ---② 이미지 덧셈
img3 = img1 + img2  # 더하기 연산
img4 = cv2.add(img1, img2) # OpenCV 함수

imgs = {'img1':img1, 'img2':img2, 'img1+img2': img3, 'cv.add(img1, img2)': img4}

# ---③ 이미지 출력
for i, (k, v) in enumerate(imgs.items()):
    plt.subplot(2,2, i + 1)
    plt.imshow(v[:,:,::-1])
    plt.title(k)
    plt.xticks([]); plt.yticks([])

plt.show()

img1과 img2를 합성한 사진의 예시입니다. 단순히 numpy로 img1+img2를 한 경우 255를 넘는 픽셀은 이상한 색상을 보이고 있습니다. 반면 cv2.add(img1, img2)를 수행한 경우 대부분의 값이 255에 몰려 전체적으로 하얀 픽셀이 많습니다. 두 결과 모두 좋지 않습니다. 

두 이미지를 제대로 합성하려면 각각의 이미지에 가중치를 주고 합해야 합니다. 예를 들어 두 이미지의 픽셀 값에 각각 50%씩 곱해서 더하면 됩니다. 이때 가중치를 조정할 수 있는데, 이 가중치를 알파(alpha) 값이라고 합니다. 새로운 이미지 알파(alpha)를 활용하여 합성 결과 픽셀 값 g(x)를 구하는 공식은 아래와 같습니다.

  • cv2.addWeight(img1, alpha, img2, beta, gamma)
    img1, img2: 합성할 두 이미지
    alpha: img1에 지정할 가중치(알파 값)
    beta: img2에 지정할 가중치, 흔히 (1-alpha) 적용
    gamma: 연산 결과에 가감할 상수, 흔히 0 적용

아래 코드는 cv2.addWeight() 함수를 활용하여 이미지를 합성하는 예시 코드입니다.

# 알파 블렌딩 (blending_alpha.py)

import cv2
import numpy as np

alpha = 0.5 # 합성에 사용할 알파 값

#---① 합성에 사용할 영상 읽기
img1 = cv2.imread('../img/wing_wall.jpg')
img2 = cv2.imread('../img/yate.jpg')

# ---② NumPy 배열에 수식을 직접 연산해서 알파 블렌딩 적용
blended = img1 * alpha + img2 * (1-alpha)
blended = blended.astype(np.uint8) # 소수점 발생을 제거하기 위함
cv2.imshow('img1 * alpha + img2 * (1-alpha)', blended)

# ---③ addWeighted() 함수로 알파 블렌딩 적용
dst = cv2.addWeighted(img1, alpha, img2, (1-alpha), 0) 
cv2.imshow('cv2.addWeighted', dst)

cv2.waitKey(0)
cv2.destroyAllWindows()

이 코드에서는 알파를 0.5로 잡았습니다. 알파 값이 0.5라는 것은 두 이미지에 동일한 가중치를 주고 합성한다는 뜻입니다. 왼쪽은 알파를 활용한 이미지 합성을 numpy로 직접 구현한 결과고 오른쪽은 cv2.addWeight() 함수를 활용한 결과입니다. 결과가 똑같은 것을 볼 수 있습니다.

좀 더 나아가, 트랙바를 움직여 알파 값을 조정하면서 사람이 서서히 사자로 바뀌는 것처럼 보이게 해 보겠습니다. 이는 영화에서 변신 장면을 구현할 때 쓰는 기술 중 하나이기도 합니다.

# 트랙바로 알파 블렌딩 (blending_alpha_trackbar.py)

import cv2
import numpy as np

win_name = 'Alpha blending'     # 창 이름
trackbar_name = 'fade'          # 트렉바 이름

# ---① 트렉바 이벤트 핸들러 함수
def onChange(x):
    alpha = x/100
    dst = cv2.addWeighted(img1, 1-alpha, img2, alpha, 0) 
    cv2.imshow(win_name, dst)


# ---② 합성 영상 읽기
img1 = cv2.imread('../img/man_face.jpg')
img2 = cv2.imread('../img/lion_face.jpg')

# ---③ 이미지 표시 및 트렉바 붙이기
cv2.imshow(win_name, img1)
cv2.createTrackbar(trackbar_name, win_name, 0, 100, onChange)

cv2.waitKey()
cv2.destroyAllWindows()

비트와이즈 연산

OpenCV를 활용하여 두 이미지의 비트 단위 연산을 할 수도 있습니다. 비트와이즈 연산은 두 이미지를 합성할 때 특정 영역만 선택하거나 특정 영역만 제외하는 등의 선별적인 연산에 도움이 됩니다. 비트와이즈 연산에 대해서 공간 제약상 자세하게 설명하지는 않겠습니다. 아래 그림을 보면 어느 정도 이해가 되실 겁니다.

출처: https://slideplayer.com/slide/5378944/

OpenCV에서 제공하는 비트와이즈 연산 함수는 아래와 같습니다.

cv2.bitwise_and(img1, img2, mask=None): 각 픽셀에 대해 AND 연산

cv2.bitwise_or(img1, img2, mask=None): 각 픽셀에 대해 OR 연산

cv2.bitwise_xor(img1, img2, mask=None): 각 픽셀에 대해 XOR 연산

cv2.bitwise_not(img1, img2, mask=None): 각 픽셀에 대해 NOT 연산

img1, img2는 연산을 할 이미지이며, 두 이미지는 동일한 shape를 가져야 합니다. mask는 0이 아닌 픽셀만 연산하게 합니다. 아래는 비트와이즈 연산의 예시 코드입니다.

# 비트와이즈 연산 (bitwise.py)

import numpy as np, cv2
import matplotlib.pylab as plt

#--① 연산에 사용할 이미지 생성
img1 = np.zeros( ( 200,400), dtype=np.uint8)
img2 = np.zeros( ( 200,400), dtype=np.uint8)
img1[:, :200] = 255         # 왼쪽은 흰색(255), 오른쪽은 검정색(0)
img2[100:200, :] = 255      # 위쪽은 검정색(0), 아래쪽은 흰색(255)

#--② 비트와이즈 연산
bitAnd = cv2.bitwise_and(img1, img2)
bitOr = cv2.bitwise_or(img1, img2)
bitXor = cv2.bitwise_xor(img1, img2)
bitNot = cv2.bitwise_not(img1)

#--③ Plot으로 결과 출력
imgs = {'img1':img1, 'img2':img2, 'and':bitAnd, 
          'or':bitOr, 'xor':bitXor, 'not(img1)':bitNot}
for i, (title, img) in enumerate(imgs.items()):
    plt.subplot(3,2,i+1)
    plt.title(title)
    plt.imshow(img, 'gray')
    plt.xticks([]); plt.yticks([])

plt.show()

0은 검은색, 255은 흰색입니다. 불리언(boolean) 값으로 치환하면 0은 False, 0이 아닌 값은 True입니다. 따라서 검은색은 0이므로 False를 의미하고, 흰색은 255이므로 True를 의미합니다. 두 값의 AND 연산 결과 True가 되기 위해서는 두 값 모두 True여야 합니다. 따라서 AND 연산 후에는 img1과 img2의 흰색 부분이 겹치는 곳만 흰색으로 표시됩니다.

아래는 비트와이즈 연산으로 이미지 일부분을 원하는 모양으로 떼어내는 예제 코드입니다.

# bitwise_and 연산으로 마스킹하기 (bitwise_masking.py)

import numpy as np, cv2
import matplotlib.pylab as plt

#--① 이미지 읽기
img = cv2.imread('../img/yeosu_small.jpg')

#--② 마스크 만들기
mask = np.zeros_like(img)
cv2.circle(mask, (260,210), 100, (255,255,255), -1)
#cv2.circle(대상이미지, (원점x, 원점y), 반지름, (색상), 채우기)

#--③ 마스킹
masked = cv2.bitwise_and(img, mask)

#--④ 결과 출력
cv2.imshow('original', img)
cv2.imshow('mask', mask)
cv2.imshow('masked', masked)
cv2.waitKey()
cv2.destroyAllWindows()

원본 이미지에서 완벽한 검은색 부분만 빼고는 모두 0보다 큰 픽셀 값을 가집니다. 두 번째 이미지인 흰색 원에서는 흰색 원 부분만 255의 값을 가지고 나머지는 0의 값을 가집니다. 따라서 원본 이미지와 원 이미지를 AND 연산하면 세 번째 이미지가 구해집니다. AND 연산은 True(0이 아닌 값)와 True(0이 아닌 값)가 합해진 부분만 True가 출력되기 때문입니다.

두 이미지의 차이

이제 두 이미지의 차를 구하는 방법에 대해 알아보겠습니다. 무작정 두 이미지의 픽셀 값을  빼면 음수가 나오므로 차의 절댓값을 취해야 합니다. 픽셀 값의 차를 구하는 함수는 다음과 같습니다.

  • diff = cv2.absdiff(img1, img2)
    img1, img2: 입력 이미지
    diff: 두 이미지의 차의 절대 값

이 함수를 활용한 예제를 들어보겠습니다.

# 두 이미지의 차를 통해 도면의 차이 찾아내기 (diff_absolute.py)

import numpy as np, cv2

#--① 연산에 필요한 영상을 읽고 그레이스케일로 변환
img1 = cv2.imread('../img/robot_arm1.jpg')
img2 = cv2.imread('../img/robot_arm2.jpg')
img1_gray = cv2.cvtColor(img1, cv2.COLOR_BGR2GRAY)
img2_gray = cv2.cvtColor(img2, cv2.COLOR_BGR2GRAY)

#--② 두 영상의 절대값 차 연산
diff = cv2.absdiff(img1_gray, img2_gray)

#--③ 차 영상을 극대화 하기 위해 쓰레시홀드 처리 및 컬러로 변환
_, diff = cv2.threshold(diff, 1, 255, cv2.THRESH_BINARY)
diff_red = cv2.cvtColor(diff, cv2.COLOR_GRAY2BGR)
diff_red[:,:,2] = 0

#--④ 두 번째 이미지에 변화 부분 표시
spot = cv2.bitwise_xor(img2, diff_red)

#--⑤ 결과 영상 출력
cv2.imshow('img1', img1)
cv2.imshow('img2', img2)
cv2.imshow('diff', diff)
cv2.imshow('spot', spot)
cv2.waitKey()
cv2.destroyAllWindows()

img1과 img2를 딱 봤을 때는 거의 똑같은 이미지입니다. 틀린 그림 찾기처럼 다른 부분을 찾기가 쉽지 않습니다. 하지만 위 코드를 실행하면 어떤 부분이 다른지 한 번에 알 수 있습니다.

우선, 두 이미지를 읽어온 뒤 회색조로 변환합니다. 회색조로 변환된 두 이미지의 차를 구합니다. 그 차이를 뚜렷하게 하기 위해 cv2.threshold() 함수를 활용하여 픽셀 값이 1보다 큰 값은 255로 바꾸고 그렇지 않은 픽셀 값은 0으로 바꿨습니다. 색상을 표현하기 위해 cv2.cvtColor() 함수를 활용하여 색상 스케일을 맞추기 위해 차원을 변경해준 뒤, 빨간색으로 변경해줬습니다. 마지막으로 cv2.bitwise_xor() 연산을 해줬습니다. 차이가 있는 곳이 빨간색 영역으로 표시가 되었습니다.

이미지 합성과 마스킹

이제, 두 이미지를 합성하는 방법에 대해 배워보겠습니다. 일반적으로 하나의 이미지는 배경과 전경(배경이 아닌 실제 이미지)으로 나뉩니다. 예를 들어 푸른 잔디에 강아지가 있는 이미지를 생각해봅시다. 푸른 잔디는 배경이고, 강아지는 전경입니다. 우리가 원하는 게 푸른 잔디가 아닌 강아지라면 어떻게 해야 할까요? 이미지에서 강아지만을 추출해야 합니다. 하지만 이 작업은 컴퓨터 비전의 정점과도 같다고 합니다. 그만큼 쉽지만은 않다는 것이겠죠. 여기서는 우선 배경이 투명한 이미지를 활용하여 합성해보겠습니다. BGRA 색상 형식으로 표현할 때, 배경은 A(알파, alpha)가 0이고, 전경은 A가 255입니다. A가 0이면 투명하고, 255면 불투명하기 때문입니다. BGRA에 대해 모르시는 분은 OpenCV - 7. 이미지 색상 표현  방식(BGR, HSV, YUV)을 참고해주시기 바랍니다. BGRA를 활용하면 배경을 손쉽게 오려낼 수 있습니다. 아래 예를 들어보겠습니다.

# 투명 배경 PNG 파일을 이용한 합성 (addition_rgba_mask.py)

import cv2
import numpy as np

#--① 합성에 사용할 영상 읽기, 전경 영상은 4채널 png 파일
img_fg = cv2.imread('../img/opencv_logo.png', cv2.IMREAD_UNCHANGED)
img_bg = cv2.imread('../img/girl.jpg')

#--② 알파채널을 이용해서 마스크와 역마스크 생성
_, mask = cv2.threshold(img_fg[:,:,3], 1, 255, cv2.THRESH_BINARY)
mask_inv = cv2.bitwise_not(mask)

#--③ 전경 영상 크기로 배경 영상에서 ROI 잘라내기
img_fg = cv2.cvtColor(img_fg, cv2.COLOR_BGRA2BGR)
h, w = img_fg.shape[:2]
roi = img_bg[10:10+h, 10:10+w ]

#--④ 마스크 이용해서 오려내기
masked_fg = cv2.bitwise_and(img_fg, img_fg, mask=mask)
masked_bg = cv2.bitwise_and(roi, roi, mask=mask_inv)

#--⑥ 이미지 합성
added = masked_fg + masked_bg
img_bg[10:10+h, 10:10+w] = added

cv2.imshow('mask', mask)
cv2.imshow('mask_inv', mask_inv)
cv2.imshow('masked_fg', masked_fg)
cv2.imshow('masked_bg', masked_bg)
cv2.imshow('added', added)
cv2.imshow('result', img_bg)
cv2.waitKey()
cv2.destroyAllWindows()

우선, 합성할 두 이미지를 읽습니다. 

_, mask = cv2.threshod(img_fg[:, :, 3], 1, 255, cv2.THRESH_BINARY)를 호출하여 배경과 전경을 분리하는 마스크를 만듭니다. OpenCV라고 쓰여있는 이미지는 배경이 투명합니다. 따라서 배경 부분은 BRGA의 A값이 0입니다. 반면 배경이 아닌 전경 부분은 A가 0이 아닙니다. 따라서 A가 1 이상이면 255, 1 미만이면 0으로 바꾸어주면 배경은 검은색, 전경은 흰색이 됩니다. mask_inv = cv2.bitwise_not(mask)이므로 mask_inv는 mask의 반대입니다. 즉, 배경은 흰색, 전경은 검은색입니다. 이 두 mask를 활용하여 여수 이미지와 OpenCV 이미지를 합성했습니다.

아래 예제는 색상 별로 추출하는 코드입니다. 참고로 cv2.inRange(hsv, lower, upper) 함수는 hsv의 모든 값 중 lower와 upper 범위 사이에 있는 값은 255, 나머지 값은 0으로 변환합니다.

# HSV 색상으로 마스킹 (hsv_color_mask.py)

import cv2
import numpy as np
import matplotlib.pylab as plt

#--① 큐브 영상 읽어서 HSV로 변환
img = cv2.imread("../img/cube.jpg")
hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)

#--② 색상별 영역 지정
blue1 = np.array([90, 50, 50])
blue2 = np.array([120, 255,255])
green1 = np.array([45, 50,50])
green2 = np.array([75, 255,255])
red1 = np.array([0, 50,50])
red2 = np.array([15, 255,255])
red3 = np.array([165, 50,50])
red4 = np.array([180, 255,255])
yellow1 = np.array([20, 50,50])
yellow2 = np.array([35, 255,255])

# --③ 색상에 따른 마스크 생성
mask_blue = cv2.inRange(hsv, blue1, blue2)
mask_green = cv2.inRange(hsv, green1, green2)
mask_red = cv2.inRange(hsv, red1, red2)
mask_red2 = cv2.inRange(hsv, red3, red4)
mask_yellow = cv2.inRange(hsv, yellow1, yellow2)

#--④ 색상별 마스크로 색상만 추출
res_blue = cv2.bitwise_and(img, img, mask=mask_blue)
res_green = cv2.bitwise_and(img, img, mask=mask_green)
res_red1 = cv2.bitwise_and(img, img, mask=mask_red)
res_red2 = cv2.bitwise_and(img, img, mask=mask_red2)
res_red = cv2.bitwise_or(res_red1, res_red2)
res_yellow = cv2.bitwise_and(img, img, mask=mask_yellow)

#--⑤ 결과 출력
imgs = {'original': img, 'blue':res_blue, 'green':res_green, 
                            'red':res_red, 'yellow':res_yellow}
for i, (k, v) in enumerate(imgs.items()):
    plt.subplot(2,3, i+1)
    plt.title(k)
    plt.imshow(v[:,:,::-1])
    plt.xticks([]); plt.yticks([])
plt.show()

이와 같이 색상을 이용한 마스킹 방식을 크로마키(chroma key)라고 합니다. 예를 들어, 초록색 배경을 두고 배우가 촬영한 뒤 나중에 초록색 배경은 다른 멋진 배경과 합성하는 방식입니다. 아래는 크로마키의 예시입니다.

# 크로마 키 마스킹과 합성 (chromakey.py)

import cv2
import numpy as np
import matplotlib.pylab as plt

#--① 크로마키 배경 영상과 합성할 배경 영상 읽기
img1 = cv2.imread('../img/man_chromakey.jpg')
img2 = cv2.imread('../img/street.jpg')

#--② ROI 선택을 위한 좌표 계산
height1, width1 = img1.shape[:2]
height2, width2 = img2.shape[:2]
x = (width2 - width1)//2
y = height2 - height1
w = x + width1
h = y + height1

#--③ 크로마키 배경 영상에서 크로마키 영역을 10픽셀 정도로 지정
chromakey = img1[:10, :10, :]
offset = 20

#--④ 크로마키 영역과 영상 전체를 HSV로 변경
hsv_chroma = cv2.cvtColor(chromakey, cv2.COLOR_BGR2HSV)
hsv_img = cv2.cvtColor(img1, cv2.COLOR_BGR2HSV)

#--⑤ 크로마키 영역의 H값에서 offset 만큼 여유를 두어서 범위 지정
# offset 값은 여러차례 시도 후 결정
#chroma_h = hsv_chroma[0]
chroma_h = hsv_chroma[:,:,0]
lower = np.array([chroma_h.min()-offset, 100, 100])
upper = np.array([chroma_h.max()+offset, 255, 255])

#--⑥ 마스크 생성 및 마스킹 후 합성
mask = cv2.inRange(hsv_img, lower, upper)
mask_inv = cv2.bitwise_not(mask)
roi = img2[y:h, x:w]
fg = cv2.bitwise_and(img1, img1, mask=mask_inv)
bg = cv2.bitwise_and(roi, roi, mask=mask)
img2[y:h, x:w] = fg + bg

#--⑦ 결과 출력
cv2.imshow('chromakey', img1)
cv2.imshow('added', img2)
cv2.waitKey()
cv2.destroyAllWindows()

 

이렇듯 이미지 합성에는 블렌딩과 마스킹이 필요합니다. 하지만, 블렌딩을 위한 알파 값 선택과 마스킹을 위한 좌표, 색상 선택에는 많은 시간이 소요됩니다. OpenCV에서는 cv2.seamlessClone()이라는 함수가 있는데 이는 두 이미지의 특징을 살려 알아서 합성하는 기능을 합니다.

  • dst = cv2.seamlessClone(src, dst, mask, coords, flags, output)
    src: 입력 이미지, 일반적으로 전경
    dst: 대상 이미지, 일반적으로 배경
    mask: 마스크, src에서 합성하고자 하는 영역은 255, 나머지는 0
    coords: src가 놓이기 원하는 dst의 좌표 (중앙)
    flags: 합성 방식
    output(optional): 합성 결과

flags는 입력 원본을 유지하는 cv2.NORMAL_CLONE과 입력과 대상을 혼합하는 cv2.MIXED_CLONE이 있습니다. 

아래 코드는 손에 꽃 이미지를 합성하는 예제입니다.

# SeamlessClone을 활용한 이미지 합성 (seamlessclone.py)

import cv2
import numpy as np
import matplotlib.pylab as plt
 
#--① 합성 대상 영상 읽기
img1 = cv2.imread("../img/drawing.jpg")
img2= cv2.imread("../img/my_hand.jpg")

#--② 마스크 생성, 합성할 이미지 전체 영역을 255로 셋팅
mask = np.full_like(img1, 255)
 
#--③ 합성 대상 좌표 계산(img2의 중앙)
height, width = img2.shape[:2]
center = (width//2, height//2)
 
#--④ seamlessClone 으로 합성 
normal = cv2.seamlessClone(img1, img2, mask, center, cv2.NORMAL_CLONE)
mixed = cv2.seamlessClone(img1, img2, mask, center, cv2.MIXED_CLONE)

#--⑤ 결과 출력
cv2.imshow('normal', normal)
cv2.imshow('mixed', mixed)
cv2.waitKey()
cv2.destroyAllWindows()

 

원본은 꽃 이미지와 손 이미지입니다. 각각의 이미지를 cv2.seamlessClone() 함수를 활용하여 합성한 것입니다. 이 함수를 활용하면 알파 값이나 마스크를 신경 쓰지 않아도 되기 때문에 편합니다. mask는 img1의 전체 영역을 255로 채워서 해당 영역 전부가 합성의 대상임을 표현합니다. 가급적이면 합성하려는 영역을 제외하고는 0으로 채우는 것이 더 좋지만 이번 예제에서는 모든 값을 255로 채웠습니다.

왼쪽이 cv2.NORMAL_CLONE, 오른쪽이 cv2.MIXED_CLONE을 적용한 합성 이미지입니다. cv2.NORMAL_CLONE일 때는 꽃은 선명하지만 주변의 피부가 다소 뭉개진 것을 볼 수 있습니다. 반면, cv2.MIXED_CLONE일 때는 꽃은 다소 흐리지만 주변 피부와 잘 매칭이 되게 합성했습니다.

Comments