본문 바로가기
ML & DL/OpenCV

[OpenCV] 8. 이미지 연산

by 홍월이_ 2023. 2. 27.

아래 내용은 파이썬으로 만드는 OpenCV 프로젝트(이세우 저) 를 공부하며 정리한 내용들입니다.

모든 소스 코드를 확인하고 싶으시다면 제일 하단의 저자 GitHub 주소를 참고하시기 바랍니다.


이미지와 영상에서 연산하는 방법을 알아보자.

연산 결과는 새로운 영상을 만들어 내므로 그 자체가 목적이 될 수도 있고, 정보를 얻기 위한 과정일 수도 있다.

8.1 영상과 영상의 연산

영상에 연산을 할 수 있는 방법은 Numpy의 브로드캐스팅 연산을 직접 적용하는 방법과 OpenCV에서 제공하는 함수를 사용하는 방법이 있다.

영상에서 한 픽셀이 가질 수 있는 값의 범위는 0~255인데, OpenCV의 함수를 사용하면 그 범위를 초과한 결과에 대해서도 안전하게 처리가 가능하다.

  • dest = cv2.add(src1, src2[, dest, mask, dtype]) : src1과 src2 더하기
    • src1 : 입력 영상 1 또는 수
    • src2 : 입력 영상 2 또는 수
    • dest : 출력 영상
    • mask : 0이 아닌 픽셀만 연산
    • dtype : 출력 dtype
  • dest = cv2.substract(src1, src2[, dest, mask, dtype]) : src1 에서 src2를 빼기
    • 모든 인자는 cv2.add() 함수와 동일
  • dest = cv2.multiply(src1, src2[, dest, scale, dtype]) : src1과 src2를 곱하기
    • scale : 연산 결과에 추가 연산할 값
  • dest = cv2.divide(src1, src2[, dest, scale, dtype]) : src1을 src2로 나누기
    • 모든 인자는 cv2.multiply()와 동일

 

영상의 사칙 연산 실습

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를 이용한 연산
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)

>>> [[ 44 150]] [[255 150]]
>>> [[100 206]] [[100   0]]
>>> [[144 100]] [[255 100]]
>>> [[66.66666667 16.66666667]] [[67 17]]

출력 결과를 보면, 직접 더한 결과는 255를 초과하여 44이고, cv2.add() 함수의 결과는 최대 값인 255이다. 빼기 연산의 결과도 마찬가지로 206으로 정상적이지 않지만, cv2.subtract() 함수의 결과는 최소값인 0이 나온다. 곱하기와 나누기도 255를 초과하지 않고 소수점을 갖지 않는 결과가 나온다.

 

OpenCV의 네 가지 연산을 조금 더 자세히 알아보자. 앞에서 함수에 연산의 대상으로 두개의 인자를 전달해 주었다. 그 두 인자의 연산 결과를 세 번째 인자로 전달한 배열에 할당하고 결과값으로 반환 받을 수 있다. 만약 c = a + b와 같은 연산이 필요하다면 아래와 같은 코드로 적용할 수 있다. 세 코드의 결과는 모두 같다.

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

 

만약 b += a 와 같이 두 입력의 합산 결과를 입력 인자의 하나에 재할당 하고싶을 때는 아래 코드로 작성 할 수 있다.

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

 

만약 네 번째 인자인 mask를 지정하는 경우에, 네 번째 인자에 전달한 배열에 어떤 요소 값이 0이면 그 위치의 픽셀은 연산을 하지 않는다.

 

mask와 누적 할당 연산 실습

import cv2
import numpy as np

# 연산에 사용할 배열 생성
a = np. array([[1, 2]], dtype=np.uint8)
b = np. array([[10, 20]], dtype=np.uint8)

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

# 두 번째 요소가 0인 마스크 생성
mask = np.array([[1, 0]], dtype=np.uint8)

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

>>> [[11  0]]
>>> [[11 20]]

위 예제에서 보면 a와 b의 더하기 연산은 1+10, 2+20 연산이 각각 이뤄져야 하지만, mask의 두번째 요소의 값이 0이므로 2+20의 연산이 이루어지지 않는다. 따라서 c1의 결과는 [11, 0] 이 된다.

 

누적 할당을 적용한 c2는 기존의 b에 a가 더해지는데 두 번째 요소는 연산이 무시 되므로 20을 그대로 가져서 [11, 20]이 된다. 이때 주의할 점은 b 자체도 c2와 동일하게 연산의 결과를 갖게 된다. b를 그대로 유지하고 싶다면 아래 코드로 사용할 수 있다.

cv2 = cv2.add(a, b, b.copy(), mask)

 

8.2 알파 블렌딩

두 영상을 합성하려고 할 때, 더하기 연산이나 cv2.add() 함수만으로는 좋은 결과를 얻기가 어렵다.

직접 더하기 연산을 하면 255를 넘는 경우 초과 값만을 가지게 되므로 영상에서 튀는 색상들이 나타나거나 거뭇거뭇하게 나타나고, cv2.add() 연산을 하면 초과값이 255가 되므로 픽셀 값이 255 가까이 몰리는 현상이 일어나서 하얗게 날아간 것처럼 보인다.

 

두 영상을 적절하게 합성하려면 각 픽셀의 합이 255가 되지 않게 각각의 영상에 가중치를 줘서 계산을 해야한다. 예를 들어, 두 영상이 정확히 절반씩 반영된 결과 영상을 원한다면 각 영상의 픽셀 값에 각각 50%씩 곱해서 새로운 영상을 생성하면 된다. 각 영상에 적용할 가중치를 알파(alpha)값이라고 부른다.

$$ g(x) = (1 - \alpha)f_0(x)\ + \ \alpha f_1(x) $$

  • $f_0(x)$ : 첫 번째 이미지 픽셀 값
  • $f_1(x)$ : 두 번째 이미지 픽셀 값
  • $\alpha$ : 가중치(알파)
  • $g(x)$ : 합성 결과 픽셀 값

OpenCV에서는 이것을 구현한 함수를 제공한다.

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

알파블렌딩이 없는 단순한 이미지 합성
알파블렌딩 적용 이미지 합성

 

8.3 비트와이즈 연산

OpenCV는 두 영상의 각 픽셀에 대한 비트와이즈(bitwise, 비트 단위) 연산 기능을 제공한다. 비트와이즈 연산은 두 영상을 합성할 때 특정 영역만 선택하거나 특정 영역만 제외하는 등의 선별적인 연산에 도움이 된다.

  • bitwise_and(img1, img2, mask=None) : 각 픽셀에 대해 비트와이즈 AND 연산
  • bitwise_or(img1, img2, mask=None) : 각 픽셀에 대해 비트와이즈 OR 연산
  • bitwise_xor(img1, img2, mask=None) : 각 픽셀에 대해 비트와이즈 XOR 연산
  • bitwise_not(img1, mask=None) : 각 픽셀에 대해 비트와이즈 NOT 연산
    • img1, img2 : 연산 대상 영상, 동일한 shape
    • mask : 0이 아닌 픽셀만 연산, 바이너리 이미지

비트와이즈 연산 예시

 

8.4 차영상

영상에서 영상을 빼기 연산하면 두 영상의 차이, 즉 변화를 알 수 있다.

틀린 그림 찾기 같은 문제 풀이시 손쉽게 답을 찾을 수 있다. 산업현장에서 도면의 차이를 찾거나, PCB 회로의 오류를 찾는데도 사용 가능하고 카메라 촬영 영상에서 실시간으로 움직임이 있는지를 알아내는데도 유용하다.

차영상에서 무턱대고 빼기 연산을 하면 음수가 나올 수 있으므로 절대값을 구해줘야 한다.

  • diff = cv2.absdiff(img1, img2)
    • img1, img2 : 입력 영상
    • diff : 두 영상의 차의 절대값 반환

 

8.5 이미지 합성과 마스킹

두 개의 이미지를 특정 영역끼리 합성하기 위해서 전경이 될 영상과 배경이 될 영상에서 합성하고자 하는 영역만 떼어내는 작업과 그것을 합하는 작업으로 나눌 수 있다. 원하는 영역을 떼어내는 데 꼭 필요한 것은 마스크(mask)인데, 이 작업은 객체 인식과 분리라는 컴퓨터 비전 분야의 한 종류이다.

마스크를 이용하여 전경과 배경을 나누는 것은 cv2.bitwise_and() 연산을 이용하면 쉽다.

 

색상에 따라 영역을 떼어내야 하는 경우도 있을 수 있다. 이때는 색을 가지고 마스크를 만들어야 하는데, HSV로 변환하면 원하는 색상 범위의 것만 골라 낼 수 있다.

OpenCV에서는 특정 범위에 속하는지 여부에 대한 함수를 제공한다.

  • dst = cv2.inRange(img, from, to) : 범위에 속하지 않은 픽셀 판단
    • img : 입력 영상
    • from : 범위의 시작 배열
    • to : 범위의 끝 배열
    • dst : img가 from ~ to 에 포함되면 255, 아니면 0을 픽셀 값으로 하는 배열

HSV 색상으로 마스킹 실습

import cv2
import numpy as np
import matplotlib.pyplot 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()

cv2.inRange() 함수를 호출해서 각 색상 범위별 마스크를 만든다. 함수의 반환 결과는 바이너리 스케일이 되어 cv2.bitwise_and() 함수의 mask로 사용하기 적합하다.

위와 같이 색상을 이용한 마스크를 사용하는 것은 크로마 키(chroma key)의 원리와 같다. 초록색 또는 파란색 배경을 두고 찍어서 나중에 원하는 배경과 합성할 때 그 배경을 크로마 키라고 한다.

크로마 키를 배경으로 한 이미지에서 크로마 키 색상으로 마스크를 만들어서 합성을 해보자.

 

크로마 키 마스킹과 합성 실습

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

# 크로마키 이미지와 합성할 이미지 불러오기
img1 = cv2.imread('./img/man_chromakey.jpg')
img2 = cv2.imread('./img/street.jpg')

# ROI 선택을 위한 좌표 계산
h1, w1 = img1.shape[:2]
h2, w2 = img2.shape[:2]
x = (w2 - w1) // 2
y = h2 - h1
w = x + w1
h = y + h1

# 크로마 키 배경 이미지에서 크로마 키가 있을 법한 영역을 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]
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

print(img1.shape)

>>> (400, 314, 3)

# 결과 출력
plt.title('img1')
plt.imshow(img1[:,:,::-1])
plt.xticks([])
plt.yticks([])
plt.show()

plt.title('img2')
plt.imshow(img2[:,:,::-1])
plt.xticks([])
plt.yticks([])

plt.show()

이미지 합성에는 대부분 알파 블렌딩 또는 마스킹이 필요하다. 하지만 이런 작업들은 적절한 알파 값 선택과 마스킹을 위한 좌표나 색상값 선택에 많은 노력과 시간이 필요하다.

OpenCV는 3 버전에서 새로운 함수를 추가했는데, 알아서 두 영상의 특징을 살려 합성하는 기능이다.

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

 

REFERENCE

  • 소스 코드 참고(저자 GitHub 주소)
 

GitHub - dltpdn/insightbook.opencv_project_python

Contribute to dltpdn/insightbook.opencv_project_python development by creating an account on GitHub.

github.com

 

  • OpenCV 공식문서
 

OpenCV: OpenCV modules

OpenCV  4.7.0 Open Source Computer Vision

docs.opencv.org

 

'ML & DL > OpenCV' 카테고리의 다른 글

[OpenCV] 10. 이동, 확대/축소, 회전  (0) 2023.03.03
[OpenCV] 9. 히스토그램  (0) 2023.03.02
[OpenCV] 7. 스레시홀딩  (0) 2023.02.22
[OpenCV] 6. 컬러 스페이스  (0) 2023.02.22
[OpenCV] 5. 관심영역(ROI)  (0) 2023.02.21

댓글