이미지 연산
이 문서는 OpenCV를 활용한 이미지 연산의 핵심 개념과 실습 방법을 다룹니다. 이미지 합성, 알파 블렌딩, 마스킹 등의 기술을 통해 다양한 이미지 처리 작업을 수행할 수 있습니다.
기본 이미지 연산
사칙연산의 차이점
OpenCV에서 이미지 연산을 위한 전용 함수를 제공하는 이유는 픽셀 값의 범위 제한 때문입니다. 한 픽셀이 가질 수 있는 값의 범위는 0~255인데, 연산 결과가 이 범위를 벗어날 수 있어서 결과 값을 0~255로 제한할 필요가 있습니다.
OpenCV 사칙연산 함수
# 덧셈
cv2.add(src1, src2, dest, mask, dtype)
# 뺄셈
cv2.subtract(src1, src2, dest, mask, dtype)
# 곱셈
cv2.multiply(src1, src2, dest, scale, dtype)
# 나눗셈
cv2.divide(src1, src2, dest, scale, dtype)
주요 매개변수:
-
src1: 첫 번째 입력 이미지 -
src2: 두 번째 입력 이미지 -
dest: 출력 영상 (선택사항) -
mask: 마스크 (0이 아닌 픽셀만 연산) -
dtype: 출력 데이터 타입
Numpy vs OpenCV 연산 비교
import cv2
import numpy as np
# 연산에 사용할 배열 생성
a = np.uint8([[200, 50]])
b = np.uint8([[100, 100]])
# NumPy 배열 직접 연산
add1 = a + b # [[44, 150]]
sub1 = a - b # [[206, 206]]
# OpenCV API를 이용한 연산
add2 = cv2.add(a, b) # [[255, 150]]
sub2 = cv2.subtract(a, b) # [[100, 0]]
print("NumPy 덧셈:", add1, "OpenCV 덧셈:", add2)
print("NumPy 뺄셈:", sub1, "OpenCV 뺄셈:", sub2)
핵심 차이점:
-
NumPy: 255 초과 시 순환 (300 → 44), 0 미만 시 순환 (-50 → 206)
-
OpenCV: 255 초과 시 255로 고정, 0 미만 시 0으로 고정
마스크를 활용한 선택적 연산
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)
# 마스크 적용 연산
result = cv2.add(a, b, None, mask)
print(result) # [[11, 0]] - 첫 번째 요소만 연산됨
이미지 합성 기법
단순 합성의 문제점
단순히 두 이미지를 더하면 다음과 같은 문제가 발생합니다:
import cv2
import numpy as np
import matplotlib.pylab as plt
# 이미지 읽기
img1 = cv2.imread('image1.jpg')
img2 = cv2.imread('image2.jpg')
# 문제가 있는 합성 방법들
img3 = img1 + img2 # numpy 덧셈 - 이상한 색상
img4 = cv2.add(img1, img2) # OpenCV 덧셈 - 과도하게 밝음
문제점:
-
Numpy 덧셈: 255 초과 시 순환으로 인한 이상한 색상
-
OpenCV 덧셈: 대부분 픽셀이 255에 몰려 전체적으로 하얗게 됨
알파 블렌딩 (Alpha Blending)
두 이미지를 자연스럽게 합성하는 핵심 기법입니다.
수식: result = img1 × alpha + img2 × (1-alpha)
import cv2
import numpy as np
# 알파 블렌딩 함수
cv2.addWeighted(img1, alpha, img2, beta, gamma)
매개변수:
-
img1,img2: 합성할 두 이미지 -
alpha: img1에 지정할 가중치 (0.0 ~ 1.0) -
beta: img2에 지정할 가중치 (보통 1-alpha) -
gamma: 연산 결과에 가감할 상수 (보통 0)
실제 알파 블렌딩 구현
import cv2
import numpy as np
alpha = 0.5 # 두 이미지에 동일한 가중치
# 합성할 이미지 읽기
img1 = cv2.imread('background.jpg')
img2 = cv2.imread('foreground.jpg')
# 방법 1: NumPy로 직접 구현
blended = img1 * alpha + img2 * (1-alpha)
blended = blended.astype(np.uint8)
# 방법 2: OpenCV 함수 사용 (권장)
result = cv2.addWeighted(img1, alpha, img2, (1-alpha), 0)
cv2.imshow('Alpha Blending Result', result)
cv2.waitKey(0)
cv2.destroyAllWindows()
트랙바를 활용한 동적 블렌딩
import cv2
import numpy as np
def onChange(x):
alpha = x / 100
dst = cv2.addWeighted(img1, 1-alpha, img2, alpha, 0)
cv2.imshow('Alpha Blending', dst)
# 이미지 읽기
img1 = cv2.imread('image1.jpg')
img2 = cv2.imread('image2.jpg')
# 창과 트랙바 생성
cv2.imshow('Alpha Blending', img1)
cv2.createTrackbar('Alpha', 'Alpha Blending', 0, 100, onChange)
cv2.waitKey()
cv2.destroyAllWindows()
비트와이즈 연산
기본 비트와이즈 함수들
# AND 연산 - 두 이미지의 공통 영역만 표시
cv2.bitwise_and(img1, img2, mask=None)
# OR 연산 - 두 이미지의 모든 영역 표시
cv2.bitwise_or(img1, img2, mask=None)
# XOR 연산 - 서로 다른 영역만 표시
cv2.bitwise_xor(img1, img2, mask=None)
# NOT 연산 - 이미지 반전
cv2.bitwise_not(img1, mask=None)
비트와이즈 연산 예제
import numpy as np
import 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 # 왼쪽 흰색, 오른쪽 검정색
img2[100:200, :] = 255 # 위쪽 검정색, 아래쪽 흰색
# 비트와이즈 연산 수행
bitAnd = cv2.bitwise_and(img1, img2) # 겹치는 부분만
bitOr = cv2.bitwise_or(img1, img2) # 모든 흰색 부분
bitXor = cv2.bitwise_xor(img1, img2) # 겹치지 않는 부분만
bitNot = cv2.bitwise_not(img1) # 반전
# 결과 시각화
images = {'img1':img1, 'img2':img2, 'AND':bitAnd,
'OR':bitOr, 'XOR':bitXor, 'NOT':bitNot}
for i, (title, img) in enumerate(images.items()):
plt.subplot(2, 3, i+1)
plt.title(title)
plt.imshow(img, 'gray')
plt.xticks([]); plt.yticks([])
plt.show()
마스킹을 활용한 영역 추출
import numpy as np
import cv2
# 원본 이미지 읽기
img = cv2.imread('landscape.jpg')
# 원형 마스크 생성
mask = np.zeros_like(img)
cv2.circle(mask, (260, 210), 100, (255, 255, 255), -1)
# 마스킹 수행 - 원형 영역만 추출
masked = cv2.bitwise_and(img, mask)
cv2.imshow('Original', img)
cv2.imshow('Mask', mask)
cv2.imshow('Masked Result', masked)
cv2.waitKey()
cv2.destroyAllWindows()
이미지 차이 검출
절대차를 이용한 변화 검출
# 두 이미지의 차이 계산
diff = cv2.absdiff(img1, img2)
실제 변화 검출 예제
import numpy as np
import cv2
# 비교할 두 이미지 읽기 (회색조 변환)
img1 = cv2.imread('before.jpg')
img2 = cv2.imread('after.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 # 빨간색 채널만 유지
# 원본에 변화 부분 표시
result = cv2.bitwise_xor(img2, diff_red)
cv2.imshow('Difference Detection', result)
cv2.waitKey()
cv2.destroyAllWindows()
활용 사례:
-
보안 시스템의 움직임 감지
-
제품 품질 검사에서 결함 탐지
-
의료 영상에서 변화 추적
고급 마스킹 기법
BGRA를 활용한 투명 배경 합성
import cv2
import numpy as np
# 투명 배경 PNG 파일과 배경 이미지 읽기
img_fg = cv2.imread('logo.png', cv2.IMREAD_UNCHANGED) # 4채널
img_bg = cv2.imread('background.jpg')
# 알파 채널로 마스크 생성
_, mask = cv2.threshold(img_fg[:, :, 3], 1, 255, cv2.THRESH_BINARY)
mask_inv = cv2.bitwise_not(mask)
# 전경 이미지를 BGR로 변환
img_fg = cv2.cvtColor(img_fg, cv2.COLOR_BGRA2BGR)
h, w = img_fg.shape[:2]
# 배경에서 ROI 영역 설정
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)
# 최종 합성
result = masked_fg + masked_bg
img_bg[10:10+h, 10:10+w] = result
cv2.imshow('Transparent Background Compositing', img_bg)
cv2.waitKey()
cv2.destroyAllWindows()
HSV 색상 기반 마스킹
import cv2
import numpy as np
# 이미지를 HSV로 변환
img = cv2.imread('colorful_image.jpg')
hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
# 색상별 범위 정의 (HSV 값)
blue_lower = np.array([90, 50, 50])
blue_upper = np.array([120, 255, 255])
green_lower = np.array([45, 50, 50])
green_upper = np.array([75, 255, 255])
red_lower1 = np.array([0, 50, 50])
red_upper1 = np.array([15, 255, 255])
red_lower2 = np.array([165, 50, 50])
red_upper2 = np.array([180, 255, 255])
# 색상별 마스크 생성
mask_blue = cv2.inRange(hsv, blue_lower, blue_upper)
mask_green = cv2.inRange(hsv, green_lower, green_upper)
mask_red1 = cv2.inRange(hsv, red_lower1, red_upper1)
mask_red2 = cv2.inRange(hsv, red_lower2, red_upper2)
mask_red = cv2.bitwise_or(mask_red1, mask_red2)
# 색상별 추출 결과
blue_only = cv2.bitwise_and(img, img, mask=mask_blue)
green_only = cv2.bitwise_and(img, img, mask=mask_green)
red_only = cv2.bitwise_and(img, img, mask=mask_red)
cv2.imshow('Blue Objects', blue_only)
cv2.imshow('Green Objects', green_only)
cv2.imshow('Red Objects', red_only)
cv2.waitKey()
cv2.destroyAllWindows()
크로마키 (Chroma Key) 합성
크로마키는 특정 색상(보통 녹색 또는 파란색) 배경을 다른 이미지로 교체하는 기술입니다.
import cv2
import numpy as np
# 크로마키 배경 영상과 새 배경 읽기
foreground = cv2.imread('person_greenscreen.jpg')
background = cv2.imread('new_background.jpg')
# ROI 좌표 계산
h1, w1 = foreground.shape[:2]
h2, w2 = background.shape[:2]
x = (w2 - w1) // 2
y = h2 - h1
roi = background[y:y+h1, x:x+w1]
# 크로마키 샘플 추출 (좌상단 10x10 픽셀)
chromakey_sample = foreground[:10, :10, :]
offset = 20
# HSV로 변환
hsv_sample = cv2.cvtColor(chromakey_sample, cv2.COLOR_BGR2HSV)
hsv_img = cv2.cvtColor(foreground, cv2.COLOR_BGR2HSV)
# 크로마키 색상 범위 설정
chroma_h = hsv_sample[:, :, 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)
# 전경과 배경 분리
fg = cv2.bitwise_and(foreground, foreground, mask=mask_inv)
bg = cv2.bitwise_and(roi, roi, mask=mask)
# 최종 합성
background[y:y+h1, x:x+w1] = fg + bg
cv2.imshow('Chroma Key Result', background)
cv2.waitKey()
cv2.destroyAllWindows()
Seamless Cloning
OpenCV의 seamlessClone() 함수는 두 이미지의 특징을 자동으로 분석하여 자연스러운 합성을 수행합니다.
SeamlessClone 함수 사용법
# SeamlessClone 함수
dst = cv2.seamlessClone(src, dst, mask, coords, flags, output)
매개변수:
-
src: 합성할 전경 이미지 -
dst: 배경 이미지 -
mask: 마스크 (합성 영역은 255, 나머지는 0) -
coords: 전경이 배치될 배경의 중심 좌표 -
flags: 합성 방식
* cv2.NORMAL_CLONE: 원본 유지
* cv2.MIXED_CLONE: 원본과 배경 혼합
실제 SeamlessClone 예제
import cv2
import numpy as np
# 합성할 이미지들 읽기
foreground = cv2.imread('object.jpg') # 합성할 객체
background = cv2.imread('scene.jpg') # 배경 장면
# 전체 영역을 마스크로 설정
mask = np.full_like(foreground, 255)
# 배경의 중심 좌표 계산
height, width = background.shape[:2]
center = (width // 2, height // 2)
# SeamlessClone으로 합성
normal_clone = cv2.seamlessClone(foreground, background, mask, center, cv2.NORMAL_CLONE)
mixed_clone = cv2.seamlessClone(foreground, background, mask, center, cv2.MIXED_CLONE)
cv2.imshow('Normal Clone', normal_clone)
cv2.imshow('Mixed Clone', mixed_clone)
cv2.waitKey()
cv2.destroyAllWindows()
두 방식의 차이점:
-
NORMAL_CLONE: 전경 객체가 선명하지만 경계가 다소 부자연스러울 수 있음
-
MIXED_CLONE: 전경과 배경이 자연스럽게 혼합되지만 객체가 다소 흐려질 수 있음
실무 활용 팁
이미지 크기 맞추기
합성 전에 이미지 크기를 맞춰야 합니다:
# 이미지 크기 조정
def resize_images(img1, img2):
h1, w1 = img1.shape[:2]
h2, w2 = img2.shape[:2]
# 더 작은 크기에 맞춤
min_h, min_w = min(h1, h2), min(w1, w2)
img1_resized = cv2.resize(img1, (min_w, min_h))
img2_resized = cv2.resize(img2, (min_w, min_h))
return img1_resized, img2_resized
마스크 품질 개선
# 마스크 스무딩으로 경계 개선
def improve_mask(mask):
# 가우시안 블러로 경계 부드럽게
smooth_mask = cv2.GaussianBlur(mask, (5, 5), 0)
# 모폴로지 연산으로 노이즈 제거
kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5))
clean_mask = cv2.morphologyEx(smooth_mask, cv2.MORPH_CLOSE, kernel)
return clean_mask
실시간 합성 최적화
# 실시간 처리를 위한 최적화된 블렌딩
def fast_blend(img1, img2, alpha):
# 데이터 타입 최적화
if img1.dtype != np.float32:
img1 = img1.astype(np.float32)
if img2.dtype != np.float32:
img2 = img2.astype(np.float32)
# 벡터화된 연산 사용
result = cv2.addWeighted(img1, alpha, img2, 1-alpha, 0)
return result.astype(np.uint8)