마스크와 색상 기반 탐지
실습 준비
import cv2 as cv
import numpy as np
image = cv.imread("balloon.webp")
if image is None:
raise FileNotFoundError("이미지를 읽을 수 없습니다")
이미지 표시
show() 함수는 OpenCV와 이미지·영상 입출력에서 정의한 함수를 사용함.
마스크
마스크(mask)는 이미지에서 처리할 픽셀을 표시하는 1채널 배열임. 원본 이미지와 높이·너비가 같으며 일반적으로 uint8 자료형의 0과 255를 사용함.
| 마스크 값 | 의미 |
|---|---|
| 0 | 선택하지 않은 영역 |
| 255 | 선택한 영역 |
mask_example = np.array(
[
[0, 0, 255, 255],
[0, 255, 255, 0],
],
dtype=np.uint8,
)
mask_example
마스크 자체에는 원본의 색이 들어 있지 않음. 각 위치를 사용할지 선택하는 역할만 함.
색상 범위로 마스크 만들기
HSV 색 공간은 색상 H와 밝기 V가 분리되어 있어 색 범위를 지정하기 편리함.
hsv = cv.cvtColor(image, cv.COLOR_BGR2HSV)
lower = np.array([40, 50, 50], dtype=np.uint8)
upper = np.array([80, 255, 255], dtype=np.uint8)

cv.inRange()는 각 채널이 하한과 상한 사이에 들어오는 픽셀을 255, 나머지를 0으로 만듦. 하한과 상한은 범위에 포함됨.
mask = cv.inRange(hsv, lower, upper)
mask.shape, mask.dtype, np.unique(mask)
show(mask)

마스크 적용
cv.bitwise_and()에 같은 이미지를 두 번 전달하고 mask를 지정하면 선택한 픽셀만 원래 값을 유지함. 선택하지 않은 영역은 0이 되어 검게 표시됨.
selected = cv.bitwise_and(image, image, mask=mask)
show(selected)
같은 이미지를 AND하면 픽셀값은 그대로 유지됨. mask 인자가 연산을 적용할 위치를 제한함.
OpenCV 마스크의 조건
마스크는 원본 이미지와 높이·너비가 같은 1채널 uint8 배열이어야 함. OpenCV는 0이 아닌 위치를 선택하며 이진 마스크에는 관례적으로 255를 사용함.
마스크 반전
cv.bitwise_not()은 각 비트를 반전함. 0과 255로 구성된 마스크에서는 선택 영역과 제외 영역이 서로 바뀜.
inverse_mask = cv.bitwise_not(mask)
show(inverse_mask)
excluded = cv.bitwise_and(image, image, mask=inverse_mask)
show(excluded)
원래 마스크와 반전 마스크는 서로 겹치지 않으며 두 마스크를 합치면 전체 영역이 됨.
비트 연산
비트 연산(bitwise operation)은 정수를 구성하는 각 비트를 같은 위치끼리 계산함.
| 연산 | 결과가 1이 되는 조건 |
|---|---|
| AND | 두 비트가 모두 1 |
| OR | 하나 이상의 비트가 1 |
| XOR | 두 비트가 서로 다름 |
| NOT | 입력 비트를 반전 |
10과 12를 4비트 이진수로 표현하면 각각 1010, 1100임.
a = np.array([10], dtype=np.uint8)
b = np.array([12], dtype=np.uint8)
{
"AND": cv.bitwise_and(a, b).item(),
"OR": cv.bitwise_or(a, b).item(),
"XOR": cv.bitwise_xor(a, b).item(),
}
AND: 1010 & 1100 = 1000 → 8
OR: 1010 | 1100 = 1110 → 14
XOR: 1010 ^ 1100 = 0110 → 6
크로마키 합성 준비
크로마키(chroma key)는 일정한 배경색을 마스크로 선택한 뒤 다른 배경으로 교체하는 합성 방법임.
foreground = cv.imread("chroma-key.jpg")
background = cv.imread("desert.jpg")
if foreground is None or background is None:
raise FileNotFoundError("합성할 이미지를 읽을 수 없습니다")
두 이미지를 픽셀별로 합성하려면 높이와 너비가 같아야 함. 배경을 전경의 크기에 맞춤.
height, width = foreground.shape[:2]
background = cv.resize(background, (width, height))


배경과 피사체 마스크
전경 이미지를 HSV로 변환하고 초록색 범위를 선택함.
foreground_hsv = cv.cvtColor(foreground, cv.COLOR_BGR2HSV)
lower_green = np.array([40, 100, 100], dtype=np.uint8)
upper_green = np.array([60, 255, 255], dtype=np.uint8)
green_mask = cv.inRange(foreground_hsv, lower_green, upper_green)
green_mask: 교체할 초록 배경 영역
show(green_mask)

subject_mask: 남길 피사체 영역
subject_mask = cv.bitwise_not(green_mask)
show(subject_mask)

전경과 배경 합성
두 마스크로 전경과 새 배경에서 필요한 영역만 남김.
subject = cv.bitwise_and(
foreground,
foreground,
mask=subject_mask,
)
replacement = cv.bitwise_and(
background,
background,
mask=green_mask,
)
두 영역은 서로 겹치지 않으므로 픽셀별로 더하면 최종 합성 이미지가 됨.
composite = cv.add(subject, replacement)
show(composite)

색상 범위 조정
적절한 HSV 범위는 조명, 카메라, 배경색에 따라 달라짐.
| 조정값 | 영향 |
|---|---|
| H 하한·상한 | 선택할 색상의 종류 |
| S 하한 | 회색이나 흰색처럼 채도가 낮은 영역의 포함 여부 |
| V 하한 | 그림자처럼 어두운 영역의 포함 여부 |
배경이 남으면 범위를 넓히고 피사체까지 사라지면 범위를 좁히며 결과를 확인함.
하나의 색상 범위가 항상 맞지는 않음
조명과 그림자로 배경의 HSV 값이 달라질 수 있음. 실제 이미지의 픽셀값을 확인하며 하한과 상한을 조정해야 함.