본문 바로가기

OpenCV

이미지 프로세싱 (2)

1. 이미지 연산

■ OpenCV에서 한 픽셀이 가질 수 있는 값의 범위는 0~255이므로 이미지 연산 결과, 값이 0보다 작거나 255보다 큰 경우 값을 0~255 범위 내로 제한해야 한다.

■ OpenCV에서는 값의 범위를 제한해 주는 사칙 연산 함수를 제공하며, 해당 함수들은 결괏값을 0보다 작은 값은 0으로, 255보다 큰 값은 255로 처리해서 0~255로 값의 범위를 제한하고 정숫값만 사용한다.

OpenCV 사칙 연산 함수는 다음과 같다.

(1) cv2.add(src1, src2, dest, mask, dtype)

- 입력으로 들어오는 두 배열 혹은 배열과 스칼라의 각 원소 간 합을 계산한다.

- scr1과 src2는 첫 번째 입력 이미지, 두 번째 입력 이미지이며,

- dest는 계산된 결과의 출력 이미지 (배열)

- 여기서 scr1, scr2만 필수, dest, mask, dtype 파라미터는 선택 사항이다.

(2) cv2.subtract(src1, src2, dest, mask, dtype)

- 입력으로 들어오는 두 배열 혹은 배열과 스칼라의 각 원소 간 차를 계산한다.

- 모든 파라미터는 cv2.add( )와 동일하다.

(3) cv2.multiply(src1, src2, dest, scale, dtype)

- 입력으로 들어오는 두 배열 혹은 배열과 스칼라의 각 원소 간 곱을 계산한다.
- scale은 src1과 src2의 원소 간 곱을 계산할 때 추가로 곱할 값이다.

(4) cv2.divide(src1, src2, dest, scale, dtype)

- 입력으로 들어오는 두 배열 혹은 배열과 스칼라의 각 원소 간 나눗셈을 계산한다.
- 모든 파라미터는 cv2.multiply( )과 동일하다.

import cv2
import numpy as np

## 연산에 사용할 배열 a와 b, 0~255는 256개 값으로 8비트이므로 a, b는 8비트로 설정
a = np.uint8([[200, 50]]) 
b = np.uint8([[100, 100]])

## numpy 사칙 연산
numpy_add = a + b
numpy_sub = a - b
numpy_mul = a * b
numpy_div = a / b

## OpenCV 사칙 연산
cv2_add = cv2.add(a, b)
cv2_sub = cv2.subtract(a, b)
cv2_mul = cv2.multiply(a , b)
cv2_div = cv2.divide(a, b)

## 넘파이 사칙 연산 vs. OpenCV 사칙 연산
print(f'add) Numpy: {numpy_add}\t| OpenCV: {cv2_add}')
print(f'sub) Numpy: {numpy_sub}\t| OpenCV: {cv2_sub}')
print(f'mul) Numpy: {numpy_mul}\t| OpenCV: {cv2_mul}')
print(f'div) Numpy: {numpy_div}\t| OpenCV: {cv2_div}')
```#결과#```
add) Numpy: [[ 44 150]]	| OpenCV: [[255 150]]
sub) Numpy: [[100 206]]	| OpenCV: [[100   0]]
mul) Numpy: [[ 32 136]]	| OpenCV: [[255 255]]
div) Numpy: [[2.  0.5]]	| OpenCV: [[2 0]]
````````````

먼저 덧셈 결과부터 보면, 넘파이의 경우 200 + 100 = 300으로 255를 초과해서 255를 넘는 값은 다시 0부터 카운팅을 한다. 300 - 255 - 1 = 44

- 반면, OpenCV는 255를 초과한 값 300을 255로 처리하는 것을 확인할 수 있다.

- 뺄셈의 경우 넘파이에서는 50 - 100 = -50이므로 255 - 50 + 1 = 206이 된다. 반면, OpenCV는 0보다 작은 값 -50을 0으로 처리하는 것을 확인할 수 있다.

- 또한, 나눗셈 결과를 보면 넘파이와 달리 OpenCV는 정숫값만 사용하는 것을 볼 수 있다.

## mask
mask = np.uint8([[1, 0]])

cv2.add(a, b, None, mask)
```#결과#```
array([[255,   0]], dtype=uint8)
````````````

- cv2.add(a, b, None)의 결과는 [[255 150]]이어야 하지만, mask 배열의 index 0이 1, index 1이 0이기 때문에 index 1에 해당되는 연산 50 + 100은 수행하지 않는다. 

1.1 알파 블렌딩(합성)

■ 알파 블렌딩(alpha blending)은 여러 이미지를 합성할 때 투명도(\( \alpha \)) 값을 사용해서 합성된 이미지에 부분적 또는 전체 투명도를 만드는 fade-in/fade-out 기법으로 흔히 영상 전환에 많이 사용된다. 

■ OpenCV에서 cv2.addWeighted(img1, alpha, img2, gamma) 함수로 알파 블렌딩을 적용할 수 있다. cv2.addWeighted( ) 함수는 각 픽셀의 합이 255를 넘지 않도록 투명도(\( \alpha \)) 값을 가중치로 사용한다.

- img1, img2는 합성할 두 영상 혹은 이미지이며,
- alpha는 img1에 적용할 가중치(투명도)
- beta는 img2에 적용할 가중치(투명도)이며 주로 (1-alpha) 값을 사용한다.
- 이 alpha, beta 값을 통해 어떤 이미지를 더 강하게 드러내고, 어떤 이미지를 더 약하게 드러낼지 결정한다.
- gamma는 선택 사항으로 연산 결과에 가감할 상수로 주로 0을 사용한다.
- 단, cv2.addWeighted( ) 함수로 img1과 img2를 합성할 때 두 이미지의 크기가 같아야 한다.

■ 두 이미지의 픽셀 값을 \( f_0(x), f_1(x) \)라고 했을 때, 투명도(알파 기호) 값을 사용하여 합성된 픽셀 값 \( g(x) \)를 구하는 공식은 다음과 같다. \[ g(x) = \beta f_0(x) + \alpha f_1(x) \] - \( f_0(x), f_1(x) \)는 첫 번째 이미지의 픽셀 값, 두 번째 이미지의 픽셀 값

- \( \beta \) = \( (1 - \alpha) \)

img1 = cv2.imread('1280_720_1.png')
img2 = cv2.imread('1280_720_2.jpg')

print(img1.shape); print(img1.shape == img2.shape)
```#결과#```
(720, 1280, 3)
True
````````````

alpha = 0.5 # 이미지 합성 시 투명도 0.5 사용
beta = 1 - alpha
alpha_blended = cv2.addWeighted(img1, alpha, img2, beta, 0)

-alpha = 0.5로 설정했기 때문에 두 이미지에 동일한 투명도를 적용해 합성을 수행한다. 

titles = ['img1', 'img2', 'cv2.addWeighted']
images = [img1, img2, alpha_blended]

for i in range(3):
    plt.subplot(1, 3, i+1)
    plt.imshow(images[i][:,:,::-1])
    plt.title(titles[i])
    plt.xticks([]); plt.yticks([])
plt.show()

cf) 이미지에 8비트를 적용하면 넘파이를 이용해서도  cv2.addWeighted( ) 함수와 동일한 작업을 수행할 수 있다.

numpy_alpha_blended = cv2.addWeighted(img1, alpha, img2, beta, 0) 
numpy_alpha_blended = numpy_alpha_blended.astype(np.uint8)
plt.imshow(numpy_alpha_blended[:,:,::-1])
plt.xticks([]); plt.yticks([])

 

1.2 비트와이즈 연산

■ 비트와이즈 연산은 두 이미지의 각 픽셀 단위로 연산을 수행하여 두 영상/이미지를 합성할 때 특정 영역만 선택하거나 제외하는 데 사용된다.

[출처] https://www.xsharp.eu/help/bitwise_operators.html

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

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

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

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

- 이때, img1과 img2의 크기는 동일해야 하며 mask를 지정할 경우 0이 아닌 픽셀만 연산을 수행한다.

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)

plt.subplot(1, 2, 1)
plt.title('img1')
plt.imshow(img1, cmap = 'gray')
plt.xticks([]); plt.yticks([])

plt.subplot(1, 2, 2)
plt.title('img2')
plt.imshow(img2, cmap = 'gray')
plt.xticks([]); plt.yticks([])

-  여기서 흰색을 1(True), 검은색을 0(False)으로 보자.

## 비트와이즈 연산
bitwise_and = cv2.bitwise_and(img1, img2)
bitwise_or = cv2.bitwise_or(img1, img2)
bitwise_xor = cv2.bitwise_xor(img1, img2)
bitwise_not = cv2.bitwise_not(img1)

titles = ['AND', 'Or', 'XOR', 'NOT']
images = [bitwise_and, bitwise_or, bitwise_xor, bitwise_not]

for i in range(4):
    plt.subplot(2, 2, i+1)
    plt.imshow(images[i], cmap = 'gray')
    plt.title(titles[i])
    plt.xticks([]); plt.yticks([])
plt.show()

■ 이렇게 비트와이즈 연산을 수행하면 True가 되는 영역은 흰색, False가 되는 영역은 검은색으로 만들 수 있기 때문에 이미지의  특정 영역만 선택/제외가 가능하다.

img = cv2.imread('1280_720_2.jpg')

## 마스크 만들기
mask = np.zeros(img.shape[:2], dtype=np.uint8) # 마스킹하기 위해 원본 이미지와 똑같은 크기의 배열을 만들어야 한다.
cv2.circle(mask, (740,360), 150, (255), -1) # AND 연산을 적용할 것이기 때문에 이미지를 나타낼 부분의 색상을 255 = 흰색 = True로 

print(img.shape);print(mask.shape);print(img.shape[:2] == mask.shape)
```#결과#```
(720, 1280, 3)
(720, 1280)
True
````````````

## 마스킹 적용 - AND 연산
masked = cv2.bitwise_and(img, img, mask = mask)

cv2.imshow('original', img)
cv2.imshow('mask', mask)
cv2.imshow('masked', masked)
cv2.waitKey()
cv2.destroyAllWindows()

■ 원본 이미지는 완전한 검은색 부분이 아니라면 0보다 큰 픽셀 값을 가지고, 마스크 이미지는 흰색 부분만 255의 픽셀 값을 가지고 나머지 부분은 픽셀 값 0을 가지기 때문에 두 이미지에 대하여 AND 연산을 수행하면, 다음과 같이 두 이미지의 True(0이 아닌 값) 부분만 출력된다. 즉, True 부분을 제외한 부분을 0(검은색)으로 마스킹한 것이다.

cf) 위에서는 cv2.bitwise_and( ) 함수의 mask 인수에 mask 이미지를 지정하여 사용하기 때문에 2차원 배열(가로 x 세로)로 mask 변수를 정의해도 된다. 만약, mask 인수를 사용하지 않을 경우 다음과 같이 mask 변수와 cv2.circle( )을 정의해서 마스킹을 수행해야 한다.

mask = np.zeros_like(img)
cv2.circle(mask, (740,360), 150, (255,255,255), -1)

mask = np.zeros_like(img)
print(mask.shape)
```#결과#```
(720, 1280, 3)
````````````

masked = cv2.bitwise_and(img, mask)

 

1.3 차영상

차영상(image differencing)은 영상/이미지에서 영상/이미지의 뺄셈 연산으로 두 영상/이미지의 변화를 확인하기 위해 사용한다. 

■ 예를 들어 동영상에서 동일한 장면을 빼면 0이 되지만, 움직임이 있다면 장면의 특정 부분(픽셀)이 달라지므로, 움직임이 있는 장면과 없는 장면을 빼면 움직임을 감지할 수 있다.

단, 두 이미지의 픽셀에 대해 뺄셈 연산을 하게 되면 음수가 나올 수 있기 때문에 절대값을 취해야 한다.

■ OpenCV에서 픽셀 값의 차를 구하는 함수는 cv2.absdiff(img1, img2)이다.

img1 = cv2.imread('before.png')
img2 = cv2.imread('after.png')

plt.subplot(1, 2, 1)
plt.title('before')
plt.imshow(img1)
plt.xticks([]); plt.yticks([])

plt.subplot(1, 2, 2)
plt.title('after')
plt.imshow(img2)
plt.xticks([]); plt.yticks([])

## 3차원 -> 2차원 배열
img1_gray = cv2.cvtColor(img1, cv2.COLOR_BGR2GRAY)
img2_gray = cv2.cvtColor(img2, cv2.COLOR_BGR2GRAY)

## 두 이미지에 cv2.absdiff() 함수를 적용해 절대값 차 연산
diff = cv2.absdiff(img1_gray, img2_gray)

## 차 영상을 극대화 하기 위해 스레시홀딩 적용 
_, diff = cv2.threshold(diff, 1, 255, cv2.THRESH_BINARY)

## 다른 부분을 컬러로 변환 (2차원 -> 3차원 배열)
diff_green = cv2.cvtColor(diff, cv2.COLOR_GRAY2BGR)
diff_green[:,:,1] = 0 # BGR에서 Green은 배열의 1번 축 # 여기에서는 R=255, G=0, B=255이므로 보라색

## after 이미지에 변화 부분 표시
spot = cv2.bitwise_xor(img2, diff_green) # img2와 dif_green의 xor연산을 통해 변화 부분이 초록색으로 표시

cv2.imshow('diff', diff_green)
cv2.imshow('spot', spot)
cv2.waitKey()
cv2.destroyAllWindows()

- diff_green[:,:,1] = 0을 적용하면 R = 255, G = 0, B = 255라서 보라색이 된다. 

- 그러므로 img2와 dif_green의 XOR 연산 결과는 R = 0, G = 255, B = 0이 되므로 spot은 초록색이 된다.

 

1.4 이미지 합성과 마스킹

■ 여러 영상/이미지에서 특정 영역끼리 합성하려면, 합성을 원하는 영역을 떼어낼 때 마스크를 사용해야 한다.

■ 예를 들어 두 이미지를 합성할 때, 합성에 직접적으로 사용할 이미지를 A, 배경으로 사용할 이미지를 B, 합성된 결과를 A + B라고 하자.

■ 두 이미지가 자연스럽게 합성되려면 A의 배경은 B의 배경과 동일한 상태에서 A의 전경(배경이 아닌 실제 이미지)이 A + B에 나타나야 할 것이다. 그러므로 A의 전경 영역과 배경 영역을 따로 분리해야 하며, 이를 위해서 마스크를 사용한다.

■ 예를 들어, 다음 그림과 같이 하나는 투명도가 포함된 png 이미지이고 다른 하나는 일반적인 jpg 이미지일 때, 두 이미지를 합성하는 방법은 다음과 같다.

img_fg = cv2.imread('alpha_channel_cat1.png', cv2.IMREAD_UNCHANGED) # 합성에 사용할 전경은 4채널 png 파일
img_bg = cv2.imread('666.jpg') # 합성에 사용할 배경

img_bg_copy = img_bg.copy() # 비교를 위해 img_bg copy 생성

img_fg.shape, img_bg.shape
```#결과#```
((145, 216, 4), (571, 1019, 3))
````````````

이 예시에서 이미지 A의 배경은 알파 채널이 되므로 다음과 같이 알파 채널을 이용해서 마스크와 역마스크를 생성한다.

plt.imshow(img_fg[:,:,3], cmap = 'gray')
plt.xticks([]); plt.yticks([])

- 알파 채널만 사용할 경우 전경의 \( \alpha \) 값은 255(흰색), 배경의 \( \alpha \) 값은 0(검은색)을 가지기 때문에 전경과 배경을 쉽게 구분할 수 있다.

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

## A의 전경 크기로 B의 배경에서 ROI 추출
img_fg = cv2.cvtColor(img_fg, cv2.COLOR_BGRA2BGR) # 전경 부분 BFRA에서 BGR로 변환
h, w = img_fg.shape[:2] # h = 145, w = 216
roi = img_bg[10:10+h, 10:10+w]

■ alpha_channel_cat1.png의 배경은 투명하므로 이 이미지의 배경 부분은 BGRA의 A(\( \alpha \) 채널)의 값은 0이지만, 전경 부분의 A(\( \alpha \) 채널)의 값은 0이 아니다. 

■ 그러므로 mask는 전경의 알파 채널에서 픽셀 값이 1 이상이면 255, 그렇지 않으면 0으로 변환하는 스레시홀딩을 수행한 결과이므로 전경은 255(흰색), 배경은 0(검은색)이 된다.

■ 이때, mask_inv는 mask에 NOT 연산을 적용한 결과이므로 전경은 검은색, 배경은 흰색이 된다.

■ 이제 이 두 개의 마스크를 다음과 같이 활용하여 영상/이미지의 특정 영역끼리 합성을 수행할 수 있다.

## 전경 크기로 배경에서 ROI 추출
img_fg = cv2.cvtColor(img_fg, cv2.COLOR_BGRA2BGR) # 전경 부분 BFRA에서 BGR로 변환
h, w = img_fg.shape[:2] # h = 145, w = 216
roi = img_bg[10:10+h, 10:10+w] # img_bg에서 (10, 10) 위치를 시작으로 (145, 216) 크기의 부분 이미지를 떼어냄

## 마스크 이용해서 오려내기 전 각 이미지의 크기가 동일한지 확인
roi.shape[:2], mask_inv.shape
```#결과#```
((145, 216), (145, 216))
````````````

img_fg.shape[:2], mask.shape
```#결과#```
((145, 216), (145, 216))
````````````

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

■ 여기까지의 연산 과정을 그림으로 나타내면 다음과 같다.

- 두 이미지를 자연스럽게 합성하기 위해서는 A에서 전경과 배경을 분리한 다음, A의 전경은 그대로 가져가되 A의 배경은 B의 배경으로 교체해 새로운 A를 만들어야 한다.

- 그러므로 먼저 A 이미지의 전경에 대한 마스크(mask)와 배경에 대한 마스크(mask_inv)를 생성한다.

- 그다음, 각각 비트와이즈 AND 연산을 수행한다. mask의 전경은 True, 배경은 Flase이고 img_fg의 전경은 True, 배경은 False이므로 AND 연산 결과인 masked_fg의 전경은 True, 배경은 False가 된다.

- mask_inv는 B의 배경(정확히는 새로운 A의 배경으로 사용할 영역)과 AND 연산을 수행하기 위해 만든 것으로, mask_inv의 전경은 False, 배경은 True, roi는 일부 검은색 포인트를 제외한 나머지 영역 전체가 True이므로 mask_inv와 roi의 AND 연산을 수행하면 masked_bg의 전경은 False, 배경은 True가 된다.

- masked_fg의 전경의 픽셀 값은 0이 아닌 값으로 구성되어 있고, 배경의 픽셀 값은 0

- masked_bg의 전경의 픽셀 값은 모두 0, 배경의 픽셀 값은 0이 아닌 값으로 구성되어 있기 때문에 두 이미지 (배열)을 더하면 위와 같이 added라는 B 배경에 A 전경이 합성된 이미지가 산출된다.

■ 이제 다음과 같이 배경 이미지 B에 added 이미지를 합성하면 된다.

##이미지 합성
added = masked_fg + masked_bg
img_bg[10:10+h, 10:10+w] = added # 배경 이미지에 added 합성

■ B 이미지의 img_bg[10:10+h, 10:10+w] 영역에 합성 전, 합성 후의 결과를 확인하면 added 이미지가 img_bg[10:10+h, 10:10+w]의 영역에 잘 안착된 것을 확인할 수 있다.

plt.subplot(1, 2, 1)
plt.title('before')
plt.imshow(img_bg_copy[10:10+h, 10:10+w][:,:,::-1])
plt.xticks([]); plt.yticks([])

plt.subplot(1, 2, 2)
plt.title('after')
plt.imshow(img_bg[10:10+h, 10:10+w][:,:,::-1])
plt.xticks([]); plt.yticks([])

plt.subplot(1, 3, 1)
plt.title('added')
plt.imshow(added[:,:,::-1])
plt.xticks([]); plt.yticks([])

plt.subplot(1, 3, 2)
plt.title('image background')
plt.imshow(img_bg_copy[:,:,::-1])
plt.xticks([]); plt.yticks([])

plt.subplot(1, 3, 3)
plt.title('result')
plt.imshow(img_bg[:,:,::-1])
plt.xticks([]); plt.yticks([])

 

1.5 HSV 컬러 마스크

■ 어떤 특정 색만 추출할 때 HSV를 이용한 마스크를 사용한다. 이때 사용되는 OpenCV 함수는 cv2.inRange(img, from, to)로 이미지(img)의 특정 범위(from ~ to) 내 속하는 픽셀만 가져오는 역할을 수행한다.

■ RGB(또는 BGR)가 아닌 HSV를 이용해 컬러 마스크를 하는 이유는 RGB는 R, G, B 세 가지 채널의 조합으로 색을 표현하지만, HSV는 색조(H)만으로도 색을 표현할 수 있기 때문에 HSV를 이용하는 것이 더 간단하다.

img = cv2.imread('888.jpg')
hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)

## 색상별 영역 지정 # 채도와 명도는 모두 50에서 255으로 지정

# blue의 시작 H = 180도 ~  끝 H = 240도로 지정 (360도 기준)
blue1 = np.array([90, 50, 50])
blue2 = np.array([120, 255,255])

# green의 시작 H = 90도 ~  끝 H = 150도로 지정
green1 = np.array([45, 50,50])
green2 = np.array([75, 255,255])

## 빨간색은 두 범위에서 존재
# 첫 번째 빨간색의 시작 H = 0도 ~ 끝 H = 30도
red1 = np.array([0, 50,50])
red2 = np.array([15, 255,255])
# 두 번째 빨간색의 시작 H = 330도 ~ 끝 H = 360도
red3 = np.array([165, 50,50])
red4 = np.array([180, 255,255])

# yellow 시작 H = 40도 ~  끝 H = 70도로 지정
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) # or 연산으로 두 개로 나눈 빨간색을 통합
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(1,5, i+1)
    plt.title(k)
    plt.imshow(v[:,:,::-1])
    plt.xticks([]); plt.yticks([])
    
plt.show()

■ 이렇게 색상을 이용한 마스크 원리가 바로 크로마키(chroma key)의 원리이다.

 

 

 

 

 

'OpenCV' 카테고리의 다른 글

기하학적 변환 (1)  (0) 2024.12.16
이미지 프로세싱 (3)  (0) 2024.12.15
이미지 프로세싱 (1)  (0) 2024.12.13
NumPy와 Matplotlib  (0) 2024.12.13
기본 입출력  (1) 2024.12.12