내가 보려고 정리한 OpenCV 정리 (2) with GPT

1. 픽셀 단위 덧셈, 뺄셈, 곱셈, 나눗셈의 의미

픽셀 단위 산술 연산은 영상 처리의 가장 기본적인 출발점이다. 이미지의 각 픽셀은 정수값으로 저장되며, 그 값에 일정한 수를 더하거나 빼면 밝기와 대비가 달라진다. 흑백 영상에서는 하나의 채널 값만 바뀌고, 컬러 영상에서는 B, G, R 각 채널이 독립적으로 바뀐다.

이 연산이 중요한 이유는 대부분의 전처리가 결국 픽셀값을 재배치하는 과정이기 때문이다. 밝기 보정, 색 보정, 마스킹, 합성, 명암 대비 향상은 표현 방식만 다를 뿐 결국 픽셀값 조작으로 귀결된다.

다음 구성은 그 기초를 가장 직접적으로 보여준다. 하나의 흑백 이미지와 하나의 컬러 이미지를 읽은 뒤, 밝기 증가, 채널별 가감산, 배율 확대와 축소를 수행한다.

import cv2

img1 = cv2.imread("./images/dog.bmp", cv2.IMREAD_GRAYSCALE)
img2 = cv2.imread("./images/dog.bmp")

dst1 = cv2.add(img1, 100)
dst2 = cv2.add(img2, (50, 100, 150, 0))
dst3 = cv2.subtract(img2, (50, 100, 150, 0))
dst4 = cv2.multiply(img2, 1.5)
dst5 = cv2.divide(img2, 1.5)

# img1 shape: (H, W)              -> grayscale
# img2 shape: (H, W, 3)           -> BGR color
# dst1 shape: (H, W)              -> grayscale + scalar
# dst2 shape: (H, W, 3)           -> channel-wise add
# dst3 shape: (H, W, 3)           -> channel-wise subtract
# dst4 shape: (H, W, 3)           -> scaled brighter
# dst5 shape: (H, W, 3)           -> scaled darker

무엇인가

cv2.add, cv2.subtract, cv2.multiply, cv2.divide는 영상 산술 전용 함수다. 일반 파이썬 연산자와 비슷해 보이지만, 영상 데이터에 맞는 예외 처리를 포함하고 있다는 점이 핵심이다.

왜 필요한가

영상은 보통 uint8 형식으로 저장되며 픽셀 범위는 0~255다. 이 범위를 넘는 값이 생기면 처리 방식이 중요해진다. 밝기를 100 올릴 때 원래 값이 220인 픽셀은 320이 되는데, 이 값을 그대로 저장할 수는 없다. 영상 처리 함수는 이런 경우 255로 잘라내는 saturation 방식을 쓴다. 이 방식은 밝기 증가를 직관적으로 만든다.

반대로 일반 넘파이 덧셈은 uint8 오버플로우에 의해 값이 순환할 수 있다. 예를 들어 250 + 10이 4가 되는 wrap-around가 발생할 수 있다. 영상 처리에서는 대개 이것이 원하지 않는 결과다.

어떻게 구현되는가

cv2.imread

이미지를 메모리로 읽는다.

  • cv2.IMREAD_GRAYSCALE을 주면 단일 채널 영상이 된다.
  • 옵션을 생략하면 기본적으로 BGR 3채널 컬러 이미지로 읽는다.

cv2.add

두 입력의 같은 위치 픽셀을 더한다. 스칼라를 주면 모든 픽셀에 동일하게 더하고, 튜플을 주면 채널별로 다른 값을 더할 수 있다.

[
dst(x, y) = saturate(src(x, y) + value)
]

예를 들어 컬러 영상에서 (50, 100, 150, 0)을 더하면 B 채널에는 50, G 채널에는 100, R 채널에는 150이 더해진다. 마지막 0은 알파 채널 자리까지 맞춘 형식이지만, BGR 3채널 이미지에서는 사실상 사용되지 않는다.

cv2.subtract

채널별 또는 픽셀별 감산을 수행한다.

[
dst(x, y) = saturate(src(x, y) - value)
]

음수가 되면 0으로 잘린다.

cv2.multiply

모든 픽셀에 배율을 적용한다. 1.5를 곱하면 전반적으로 밝아진다. 다만 이미 밝은 픽셀은 255에서 포화된다. 그래서 단순 곱셈은 밝기 증가이면서 동시에 하이라이트 손실을 만들 수 있다.

cv2.divide

모든 픽셀을 나눈다. 1.5로 나누면 어두워진다. 단순 감산과 달리, 원래 값이 클수록 더 많이 감소하는 비례형 변화가 발생한다.

자주 헷갈리는 포인트

밝기 증가와 대비 증가는 다르다.

  • 일정한 값을 더하는 것은 전체를 위로 이동시키는 연산이다.
  • 일정한 값을 곱하는 것은 분포를 늘리는 연산에 가깝다.

예를 들어 50과 100이 있으면,

  • 100을 더하면 150, 200이 된다.
  • 1.5를 곱하면 75, 150이 된다.

두 경우 모두 밝아지지만 상대적 간격 변화는 다르다.

AI가 추천하는 심화예제

감마 보정까지 확장하면 곱셈보다 더 자연스러운 밝기 조절을 구현할 수 있다. 특히 사람이 체감하는 밝기 변화는 선형이 아니므로, 단순 multiply보다 감마 보정이 더 실제 화면 조절에 가깝다.


2. 일반 덧셈과 cv2.add의 차이

두 이미지를 더하는 작업은 블렌딩, 합성, 노이즈 주입, 배경 혼합의 기초가 된다. 그런데 여기서 가장 먼저 구분해야 할 것은 파이썬 연산자 + 와 cv2.add의 차이다. 겉보기에는 같은 덧셈이지만 결과는 다를 수 있다.

import cv2
import matplotlib.pyplot as plt

img1 = cv2.imread("./images/man.jpg")
img2 = cv2.imread("./images/turkey.jpg")

dst1 = img1 + img2
dst2 = cv2.add(img1, img2)

# img1 shape: (H, W, 3)
# img2 shape: (H, W, 3)
# dst1 shape: (H, W, 3)   # numpy uint8 wrap-around 가능
# dst2 shape: (H, W, 3)   # OpenCV saturation add

무엇인가

  • img1 + img2는 넘파이 배열의 일반 덧셈이다.
  • cv2.add(img1, img2)는 OpenCV의 포화 덧셈이다.

왜 필요한가

영상 합성에서는 픽셀값이 255를 초과하는 순간이 매우 많다. 이때 일반 배열 덧셈은 overflow가 날 수 있고, 그 결과 원래 밝아져야 할 부분이 오히려 어두워지는 비정상적인 결과가 나올 수 있다. 반면 cv2.add는 255를 넘으면 255에 고정하므로 직관적인 합성 결과를 만든다.

예를 들어 다음과 같은 픽셀이 있다고 하자.

[
250 + 10 = 260
]

uint8에서 일반 연산은 이 값을 4로 순환시킬 수 있다. 하지만 포화 연산은 255로 고정한다.

[
saturate(260) = 255
]

어떻게 구현되는가

이미지 로드

두 이미지는 같은 크기와 같은 채널 수를 가져야 한다. 그렇지 않으면 대응 픽셀끼리 계산할 수 없기 때문이다.

덧셈 결과 시각화

matplotlib를 사용해 결과를 한 번에 비교한다. 여기서 주의할 점은 OpenCV는 BGR 순서, matplotlib는 RGB 순서를 기대한다는 점이다. 그래서 value[:, :, ::-1]로 채널 순서를 뒤집는다.

plt.imshow(value[:, :, ::-1])

# value shape: (H, W, 3)      # BGR
# value[:, :, ::-1] shape: (H, W, 3)  # RGB로 변환

사용된 함수와 메서드의 역할

plt.subplot

여러 결과 이미지를 하나의 그림에 배치한다.

plt.imshow

배열을 이미지로 보여준다. 컬러 채널 순서가 맞지 않으면 색이 틀어지므로 OpenCV 이미지는 순서 변환이 필요하다.

dict.items 와 enumerate

이미지와 제목을 반복 처리하기 위한 구조다. 영상 처리와 직접 관련된 함수는 아니지만, 결과 비교를 자동화하는 데 유용하다.

AI가 추천하는 심화예제

동일한 두 이미지에 대해

  • 넘파이 덧셈
  • cv2.add
  • cv2.addWeighted

세 가지를 한 화면에 배치하면 saturation, overflow, 가중 평균의 차이를 한 번에 이해하기 좋다.


3. 이미지 산술 연산과 가중 합성

단순 덧셈만으로는 원하는 합성 결과를 얻기 어렵다. 두 이미지를 자연스럽게 섞으려면 각 이미지에 가중치를 부여해야 한다. 이 목적에 맞는 함수가 cv2.addWeighted다. 같은 구간에서는 뺄셈과 절대차 연산도 함께 자주 사용된다.

import cv2
import matplotlib.pyplot as plt

img1 = cv2.imread("./images/dog.jpg")
img2 = cv2.imread("./images/square.bmp")

dst1 = cv2.add(img1, img2)
dst2 = cv2.addWeighted(img1, 1, img2, 0.5, 0)
dst3 = cv2.subtract(img1, img2)
dst4 = cv2.absdiff(img1, img2)

# img1 shape: (H, W, 3)
# img2 shape: (H, W, 3)
# dst1 shape: (H, W, 3)   # saturated addition
# dst2 shape: (H, W, 3)   # weighted blend
# dst3 shape: (H, W, 3)   # saturated subtraction
# dst4 shape: (H, W, 3)   # absolute difference

무엇인가

  • cv2.add: 포화 덧셈
  • cv2.addWeighted: 가중 합성
  • cv2.subtract: 포화 뺄셈
  • cv2.absdiff: 절대 차이 계산

왜 필요한가

cv2.addWeighted가 필요한 이유

두 이미지를 반반 섞고 싶을 때 단순 덧셈은 너무 밝아진다. 두 이미지를 적절한 비율로 합치려면 선형 결합이 필요하다.

[
dst = \alpha img1 + \beta img2 + \gamma
]

여기서 $\alpha$ 와 $\beta$ 는 각 이미지의 반영 비율이고, $\gamma$ 는 전체 밝기를 미세 조정하는 bias다.

이 구성에서는 img1에 1, img2에 0.5를 곱하므로 첫 번째 이미지가 더 강하게 유지되고, 두 번째 이미지는 반투명 오버레이처럼 얹힌다.

cv2.absdiff가 필요한 이유

두 프레임 또는 두 이미지의 차이만 뽑아내고 싶을 때 유용하다. 움직임 감지, 배경 차분, 변경 구역 강조의 기초가 된다.

[
dst(x, y) = |img1(x, y) - img2(x, y)|
]

부호를 버리고 차이의 크기만 보존하므로, 어느 쪽이 더 밝은지보다 얼마나 다른지가 중요할 때 적합하다.

어떻게 구현되는가

cv2.addWeighted

dst2 = cv2.addWeighted(img1, 1, img2, 0.5, 0)

# input:
# img1: (H, W, 3)
# img2: (H, W, 3)
# output:
# dst2: (H, W, 3)

이 함수는 두 입력의 shape가 완전히 같아야 한다. 채널 수가 다르거나 크기가 다르면 픽셀별 선형 결합을 할 수 없다.

cv2.absdiff

dst4 = cv2.absdiff(img1, img2)

# input:
# img1: (H, W, 3)
# img2: (H, W, 3)
# output:
# dst4: (H, W, 3)
# each pixel = abs(img1 - img2)

사용된 함수와 메서드의 역할

cv2.addWeighted(src1, alpha, src2, beta, gamma)

실무에서 가장 자주 쓰이는 합성 함수 중 하나다.

  • alpha: 첫 번째 이미지 비중
  • beta: 두 번째 이미지 비중
  • gamma: 밝기 오프셋

페이드 인/아웃, 투명도 조절, 워터마크 합성 등에 그대로 연결된다.

cv2.absdiff

차이 강조용 함수다. 보통 단독으로 끝내지 않고, 이후 threshold나 morphology와 연결한다.

자주 헷갈리는 포인트

cv2.subtract와 cv2.absdiff는 완전히 다르다.

  • subtract는 음수를 0으로 자른다.
  • absdiff는 절대값을 취해 양수 차이만 남긴다.

즉, subtract는 방향성이 있고, absdiff는 방향성이 없다.

AI가 추천하는 심화예제

연속 프레임 두 장에 absdiff를 적용한 뒤 grayscale 변환과 threshold를 붙이면 간단한 모션 검출기를 만들 수 있다.


4. 히스토그램과 밝기 분포 해석

히스토그램은 이미지를 직접 보지 않고도 밝기 분포를 읽을 수 있게 해준다. 영상 처리에서 히스토그램은 단순한 시각화 도구가 아니라, 노출 상태와 대비 상태를 진단하는 수단이다.

import cv2
import matplotlib.pyplot as plt

img1 = cv2.imread("./images/candies.png", cv2.IMREAD_GRAYSCALE)
hist1 = cv2.calcHist([img1], [0], None, [256], [0, 256])

img2 = cv2.imread("./images/candies.png")
channels = cv2.split(img2)

# img1 shape: (H, W)
# hist1 shape: (256, 1)        # grayscale intensity histogram
# img2 shape: (H, W, 3)
# channels[0].shape: (H, W)    # B
# channels[1].shape: (H, W)    # G
# channels[2].shape: (H, W)    # R

무엇인가

히스토그램은 각 밝기값이 영상에 몇 번 나타나는지 세는 분포다.

  • 흑백 영상에서는 0~255 밝기 각각의 빈도를 셀 수 있다.
  • 컬러 영상에서는 각 채널마다 별도의 히스토그램을 구한다.

왜 필요한가

이미지가 너무 어두운지, 너무 밝은지, 명암 범위가 좁은지, 특정 채널로 치우쳤는지를 사람이 직관에만 의존하면 놓치기 쉽다. 히스토그램은 그런 상태를 수치적으로 보여준다.

예를 들어,

  • 왼쪽 구간에 몰리면 어두운 영상
  • 오른쪽 구간에 몰리면 밝은 영상
  • 중간 좁은 구간에 몰리면 저대비 영상
  • 넓게 퍼지면 상대적으로 명암 대비가 좋은 영상

으로 해석할 수 있다.

어떻게 구현되는가

cv2.calcHist

hist1 = cv2.calcHist([img1], [0], None, [256], [0, 256])

# [img1]      : 입력 이미지 리스트
# [0]         : 계산할 채널 인덱스
# None        : 마스크 없음
# [256]       : bin 개수
# [0, 256]    : 픽셀값 범위
# output shape: (256, 1)

이 함수는 다음 구조를 갖는다.

  • 첫 번째 인자는 리스트다. 여러 이미지를 동시에 처리할 수도 있기 때문이다.
  • 두 번째 인자는 채널 번호다.
  • 세 번째 인자는 마스크다. 특정 영역만 분석하고 싶다면 None 대신 mask를 넣는다.
  • 네 번째는 histogram bin 개수다.
  • 다섯 번째는 값의 범위다.

cv2.split

컬러 이미지를 채널별 단일 행렬로 분리한다.

channels = cv2.split(img2)

# img2: (H, W, 3)
# channels: tuple of 3 arrays
# each channel shape: (H, W)

이후 각 채널에 대해 따로 cv2.calcHist를 적용하면 B, G, R 분포를 확인할 수 있다.

사용된 함수와 메서드의 역할

cv2.split

채널 분리를 수행한다. HSV 변환 전후 분석이나 채널별 보정에서도 자주 등장한다.

plt.plot

히스토그램 배열을 선 그래프로 그린다. bin이 256개이므로 x축은 밝기값, y축은 빈도다.

자주 헷갈리는 포인트

히스토그램이 넓게 퍼졌다고 무조건 좋은 영상은 아니다. 노이즈가 많아도 분포는 넓어질 수 있다. 히스토그램은 영상의 질을 완전하게 평가하는 기준이 아니라, 밝기 분포를 보는 도구다.

AI가 추천하는 심화예제

마스크를 사용해 관심 영역만 따로 히스토그램을 구해보면 전체 배경이 아닌 객체 자체의 밝기 분포를 분석할 수 있다.


5. 히스토그램 평활화와 정규화

히스토그램을 본 다음 자연스럽게 이어지는 단계는 분포를 개선하는 것이다. 그 대표적인 방법이 히스토그램 평활화다. 어두운 영역과 밝은 영역이 좁은 범위에 몰려 있을 때, 이를 더 넓게 펼쳐서 대비를 높인다. 함께 등장하는 cv2.normalize는 비슷해 보여도 목적이 다르다.

import cv2
import matplotlib.pyplot as plt

img1 = cv2.imread("./images/Hawkes.jpg", cv2.IMREAD_GRAYSCALE)
img2 = cv2.imread("./images/field.bmp")

dst1 = cv2.equalizeHist(img1)

dst2 = cv2.cvtColor(img2, cv2.COLOR_BGR2YCrCb)
dst2[:, :, 0] = cv2.equalizeHist(dst2[:, :, 0])
dst2 = cv2.cvtColor(dst2, cv2.COLOR_YCrCb2BGR)

dst3 = cv2.normalize(img1, None, 0, 255, cv2.NORM_MINMAX)

# img1 shape: (H, W)
# dst1 shape: (H, W)
# img2 shape: (H, W, 3)
# dst2 shape: (H, W, 3)
# dst3 shape: (H, W)

무엇인가

  • cv2.equalizeHist: 밝기 히스토그램을 재분배해 명암 대비를 높인다.
  • cv2.normalize: 최소값과 최대값을 원하는 구간으로 선형 스케일링한다.

왜 필요한가

영상이 전체적으로 회색빛으로 눌려 있거나, 밝기 범위가 좁아서 디테일이 묻히는 경우가 있다. 이때 평활화는 누적 분포를 이용해 픽셀값을 다시 매핑한다.

히스토그램 평활화의 핵심은 픽셀값을 단순히 늘리거나 줄이는 것이 아니라, 누적분포함수 CDF를 기반으로 재배치하는 데 있다.

[
s_k = \left\lfloor (L-1)\sum_{j=0}^{k} p(r_j) \right\rfloor
]

  • $r_j$: 원래 밝기값
  • $p(r_j)$: 그 밝기값의 확률
  • $L$: 전체 밝기 단계 수, 일반적으로 256
  • $s_k$: 변환된 밝기값

즉, 자주 등장하는 구간은 더 넓게, 드물게 등장하는 구간은 덜 넓게 배치해 전체 분포를 평탄하게 만든다.

어떻게 구현되는가

cv2.equalizeHist

dst1 = cv2.equalizeHist(img1)

# input : (H, W) grayscale only
# output: (H, W)

이 함수는 단일 채널 영상에만 직접 적용할 수 있다. 컬러 영상 전체에 그대로 적용하면 채널별 균형이 깨져 색이 이상해질 수 있다.

컬러 영상에서의 적용 방식

그래서 컬러 영상은 먼저 YCrCb 색공간으로 바꾼 뒤, 밝기 정보인 Y 채널에만 평활화를 적용한다.

dst2 = cv2.cvtColor(img2, cv2.COLOR_BGR2YCrCb)
dst2[:, :, 0] = cv2.equalizeHist(dst2[:, :, 0])
dst2 = cv2.cvtColor(dst2, cv2.COLOR_YCrCb2BGR)

# img2: (H, W, 3) BGR
# after YCrCb: (H, W, 3)
# dst2[:, :, 0]: (H, W)  # Y channel only
# final dst2: (H, W, 3) BGR

이 방식은 색상 정보 Cr, Cb는 유지하고 밝기만 보정하므로, 컬러 왜곡을 줄일 수 있다.

cv2.normalize

dst3 = cv2.normalize(img1, None, 0, 255, cv2.NORM_MINMAX)

# input : (H, W)
# output: (H, W)
# effect: min -> 0, max -> 255로 선형 매핑

정규화는 현재 영상의 최소 픽셀을 0으로, 최대 픽셀을 255로 맞춘다. 이는 평활화처럼 분포 전체의 모양을 재구성하는 것이 아니라 선형 스케일 변경이다.

사용된 함수와 메서드의 역할

cv2.cvtColor

색공간 변환 함수다. 컬러 영상 처리에서 가장 중요한 도구 중 하나다.

  • BGR -> YCrCb: 밝기와 색차를 분리
  • YCrCb -> BGR: 다시 화면 출력 가능한 형식으로 복원

cv2.calcHist

원본과 변환 결과의 히스토그램을 비교하는 데 쓰인다. 이 비교를 통해 평활화가 분포를 넓혔는지 확인할 수 있다.

자주 헷갈리는 포인트

정규화와 평활화는 다르다.

  • 정규화는 최소값과 최대값을 기준으로 선형 확대/축소한다.
  • 평활화는 전체 분포를 기준으로 비선형 재매핑한다.

둘 다 대비가 좋아질 수 있지만 원리는 완전히 다르다.

AI가 추천하는 심화예제

전역 히스토그램 평활화 대신 CLAHE를 적용하면, 지역 대비를 보존하면서 과도한 밝기 왜곡을 줄일 수 있다.


6. HSV 색공간과 inRange 기반 마스킹

BGR 공간에서 색상을 직접 분리하는 것은 직관적이지 않다. 같은 초록색도 조명에 따라 B, G, R 값이 크게 달라질 수 있기 때문이다. 이 문제를 줄이기 위해 색상, 채도, 명도를 분리한 HSV 공간을 사용한다.

import cv2

img = cv2.imread("./images/candies.png")
hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)

dst = cv2.inRange(hsv, (90, 150, 0), (130, 255, 255))

# img shape: (H, W, 3)      # BGR
# hsv shape: (H, W, 3)      # HSV
# dst shape: (H, W)         # binary mask
# dst values: 0 or 255

무엇인가

HSV는 다음 세 요소로 색을 표현한다.

  • H: 색상
  • S: 채도
  • V: 명도

cv2.inRange는 지정한 범위 안에 들어오는 픽셀은 255, 아니면 0으로 만드는 이진 마스크 생성 함수다.

왜 필요한가

색상 분리는 객체 추출, 크로마키, 특정 색 강조, 색 기반 추적의 핵심이다. BGR에서는 한 색을 범위로 잡기가 어렵지만, HSV에서는 H 값 중심으로 비교적 안정적으로 선택할 수 있다.

예를 들어 초록 배경을 제거하려면 초록 계열 H 범위를 잡고, 채도와 명도 조건도 함께 주면 된다.

어떻게 구현되는가

cv2.cvtColor(img, cv2.COLOR_BGR2HSV)

hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)

# input : (H, W, 3) BGR
# output: (H, W, 3) HSV

색공간을 바꾸는 이유는, 색과 밝기를 분리해 특정 색만 쉽게 선택하기 위해서다.

cv2.inRange

dst = cv2.inRange(hsv, (90, 150, 0), (130, 255, 255))

# input : hsv (H, W, 3)
# output: mask (H, W)
# condition:
# 90 <= H <= 130
# 150 <= S <= 255
# 0 <= V <= 255
# result pixel:
# inside range  -> 255
# outside range -> 0

출력은 3채널 컬러 이미지가 아니라 단일 채널 마스크다. 이 점이 중요하다. 이 마스크는 이후 copyTo, bitwise_and, morphology 연산의 입력으로 연결된다.

사용된 함수와 메서드의 역할

cv2.inRange

범위 필터링 함수다. 임계값 기반 분리의 핵심이며, segmentation의 가장 기초적인 형태라고 볼 수 있다.

cv2.imshow

마스크와 원본을 동시에 띄워 분리 결과를 즉시 확인한다.

자주 헷갈리는 포인트

HSV에서 H 범위는 일반적인 0

360도가 아니라 OpenCV에서 보통 0

179 스케일을 사용한다. 따라서 인터넷 자료의 HSV 범위를 그대로 가져오면 맞지 않을 수 있다.

AI가 추천하는 심화예제

trackbar를 붙여 H, S, V 하한과 상한을 실시간으로 조절하면, 마스크 범위를 인터랙티브하게 찾는 도구를 만들 수 있다.


7. 마스크를 이용한 copyTo 합성

마스크가 준비되면 특정 영역만 다른 이미지로 덮어쓰는 작업이 가능해진다. 이것이 copyTo 기반 합성이다. 단순 복사와 다른 점은, 마스크가 255인 위치만 복사된다는 것이다.

airplane = cv2.imread("./images/airplane.bmp")
mask = cv2.imread('./images/mask_plane.bmp')
field = cv2.imread('./images/field.bmp')

temp = cv2.copyTo(airplane, mask)
cv2.copyTo(airplane, mask, field)

# airplane shape: (H, W, 3)
# mask shape: (H, W) or (H, W, 3) depending on file
# field shape: (H, W, 3)
# result shape after copyTo: (H, W, 3)

무엇인가

cv2.copyTo는 마스크가 참인 영역만 복사하는 선택적 복사 함수다. 일반 배열 대입보다 훨씬 안전하고 간결하다.

왜 필요한가

객체 합성에서 중요한 것은 전체 이미지를 덮는 것이 아니라 객체 부분만 옮기는 것이다. 비행기 이미지를 배경 이미지에 붙일 때, 직사각형 전체를 복사하면 검은 배경이나 불필요한 영역까지 함께 복사된다. 마스크를 쓰면 실제 객체 실루엣만 옮길 수 있다.

어떻게 구현되는가

cv2.copyTo(src, mask)

temp = cv2.copyTo(airplane, mask)

# input :
# src  = airplane (H, W, 3)
# mask = selected region
# output:
# temp = only masked region copied

mask가 없는 부분은 기본적으로 0 또는 검은색으로 남는다.

cv2.copyTo(src, mask, dst)

cv2.copyTo(airplane, mask, field)

# src  : airplane (H, W, 3)
# mask : object region
# dst  : field (H, W, 3)
# effect:
# field의 mask 영역에만 airplane 픽셀 복사

이 방식은 dst를 직접 수정한다. 즉, 반환값을 따로 받지 않아도 field 자체가 합성 결과로 바뀐다.

사용된 함수와 메서드의 역할

cv2.copyTo

실무에서 ROI 복사, 객체 합성, 마스킹 기반 삽입에 매우 자주 쓰인다. bitwise 연산 조합보다 코드가 간단하고 의도가 명확하다.

자주 헷갈리는 포인트

마스크는 보통 단일 채널 binary 형태가 가장 명확하다. 마스크 이미지가 컬러로 읽히면 기대와 다르게 동작할 수 있으므로, 실제 프로젝트에서는 보통 grayscale로 읽고 threshold를 거쳐 사용하는 편이 안정적이다.

AI가 추천하는 심화예제

copyTo 이전에 mask를 erosion, dilation으로 정제하면 객체 가장자리 잡음이 줄어든다. 특히 크로마키 합성에서는 테두리 깨짐 완화에 효과적이다.


8. 비디오 크로마키 합성의 구조

정적 이미지 합성이 끝나면 바로 비디오 합성으로 확장할 수 있다. 구조는 같다. 프레임을 한 장씩 읽고, 색공간을 바꾸고, 마스크를 만든 다음, 배경 프레임을 현재 프레임에 복사한다. 차이는 이 과정을 반복문 안에서 매 프레임 수행한다는 점이다.

import cv2

cap1 = cv2.VideoCapture("./movies/woman.mp4")
cap2 = cv2.VideoCapture("./movies/sea.mp4")

w = int(cap1.get(cv2.CAP_PROP_FRAME_WIDTH))
h = int(cap1.get(cv2.CAP_PROP_FRAME_HEIGHT))

frame_count1 = int(cap1.get(cv2.CAP_PROP_FRAME_COUNT))
frame_count2 = int(cap2.get(cv2.CAP_PROP_FRAME_COUNT))

fps = cap2.get(cv2.CAP_PROP_FPS)

while True:
    ret1, frame1 = cap1.read()
    if not ret1:
        break

    rec2, frame2 = cap2.read()
    if not rec2:
        break

    hsv = cv2.cvtColor(frame1, cv2.COLOR_BGR2HSV)
    mask = cv2.inRange(hsv, (50, 150, 0), (70, 255, 255))
    cv2.copyTo(frame2, mask, frame1)

    cv2.imshow("frame1", frame1)

# frame1 shape: (H, W, 3)
# frame2 shape: (H, W, 3)
# hsv shape: (H, W, 3)
# mask shape: (H, W)
# final displayed frame1: (H, W, 3)

무엇인가

이 구조는 녹색 배경 영상에서 녹색 부분을 검출한 뒤, 그 자리에 다른 배경 영상의 프레임을 덮어쓰는 크로마키 합성이다.

왜 필요한가

정적 배경 위에 사람이나 객체를 자연스럽게 분리해 넣는 작업은 영상 편집, 방송, 가상 배경, 콘텐츠 제작의 기본 기술이다. 복잡한 세그멘테이션 모델 없이도 단색 배경만 확보되면 비교적 단순한 규칙으로 구현할 수 있다.

어떻게 구현되는가

cv2.VideoCapture

영상 파일 또는 카메라 스트림을 여는 클래스다.

cap1 = cv2.VideoCapture("./movies/woman.mp4")
cap2 = cv2.VideoCapture("./movies/sea.mp4")

get 메서드

비디오 메타데이터를 가져온다.

w = int(cap1.get(cv2.CAP_PROP_FRAME_WIDTH))
h = int(cap1.get(cv2.CAP_PROP_FRAME_HEIGHT))
frame_count1 = int(cap1.get(cv2.CAP_PROP_FRAME_COUNT))
fps = cap2.get(cv2.CAP_PROP_FPS)

# w, h          : frame size
# frame_count1  : total frames
# fps           : frames per second

이 값들은 출력 영상 저장, 프레임 동기화, 리사이즈 판단 등에 필요하다. 현재 구성에서는 직접 저장하지는 않지만, 비디오 처리 구조를 이해하는 데 중요한 메서드다.

read 메서드

가장 중요한 비디오 입력 메서드다.

ret1, frame1 = cap1.read()

# ret1   : bool, 프레임 읽기 성공 여부
# frame1 : (H, W, 3) BGR image if success

프레임이 끝나면 ret1이 False가 되므로 반복을 종료한다. 이 구조는 비디오 처리 루프의 기본 패턴이다.

프레임 단위 색 분리와 합성

hsv = cv2.cvtColor(frame1, cv2.COLOR_BGR2HSV)
mask = cv2.inRange(hsv, (50, 150, 0), (70, 255, 255))
cv2.copyTo(frame2, mask, frame1)

# frame1: foreground video frame
# frame2: background video frame
# mask  : green-screen region
# result: frame1의 녹색 부분이 frame2로 대체됨

여기서 중요한 점은 copyTo(frame2, mask, frame1)이라는 호출이다. 즉, 배경 영상 frame2를 소스로 사용하고, 녹색 영역 mask가 참인 위치에 한해서 현재 foreground 프레임 frame1에 복사한다. 결과적으로 사람은 남고 배경은 바뀐다.

waitKey와 재생 제어

if cv2.waitKey(10) == ord(" "):
    cv2.waitKey(0)
elif cv2.waitKey(10) == 27:
    break

의도는 다음과 같다.

  • 스페이스바를 누르면 일시정지
  • ESC를 누르면 종료

다만 이 구현은 같은 루프 안에서 waitKey를 두 번 호출하고 있어 키 입력이 분산될 수 있다. 실무에서는 보통 한 번만 호출하고 그 값을 변수에 저장해 분기한다.

key = cv2.waitKey(10)
if key == ord(" "):
    cv2.waitKey(0)
elif key == 27:
    break

이 방식이 더 안정적이다.

release 와 destroyAllWindows

cap1.release()
cap2.release()
cv2.destroyAllWindows()

입력 스트림과 창 자원을 정리한다. 비디오 처리에서는 종료 정리가 중요하다.

사용된 함수와 메서드의 역할

VideoCapture.read

프레임 기반 처리를 가능하게 하는 핵심 메서드다. 딥러닝 추론, 객체 검출, 트래킹, 필터링도 모두 이 패턴 위에 올라간다.

cv2.inRange + cv2.copyTo

이 둘의 조합이 크로마키의 핵심이다.

  • inRange: 배경 색 추출
  • copyTo: 해당 위치에 새로운 배경 삽입

cv2.waitKey

화면 갱신과 입력 처리를 동시에 맡는다. OpenCV 창을 다룰 때 거의 필수다.

자주 헷갈리는 포인트

foreground와 background의 source, destination 방향을 반대로 잡으면 결과가 완전히 달라진다. 지금 구조는 frame1의 녹색 부분을 frame2로 대체하는 방식이다. 반대로 쓰면 사람 대신 배경만 남을 수 있다.

AI가 추천하는 심화예제

  • mask를 바로 쓰지 말고 medianBlur나 morphology로 정제
  • frame2를 frame1 크기에 맞춰 resize
  • 합성 결과를 VideoWriter로 저장
  • 가장자리 feathering을 넣어 자연스러운 경계 처리

9. 주요 메서드 중심으로 다시 정리한 흐름

여기까지의 내용을 메서드 기준으로 다시 정리하면 다음과 같다.

cv2.imread

이미지를 배열로 읽는다.

img = cv2.imread(path, flag)

# grayscale -> (H, W)
# color     -> (H, W, 3)

영상 처리의 모든 시작점이다. 읽을 때 채널 수가 결정되므로 이후 shape 흐름이 달라진다.

cv2.add / cv2.subtract / cv2.multiply / cv2.divide

픽셀 단위 수치 변환을 수행한다.

dst = cv2.add(img, value)
dst = cv2.subtract(img, value)
dst = cv2.multiply(img, alpha)
dst = cv2.divide(img, alpha)

# input  : (H, W) or (H, W, 3)
# output : same shape

기본 밝기 조절과 채널 보정의 기초다.

cv2.addWeighted

가중 합성의 표준 함수다.

dst = cv2.addWeighted(img1, a, img2, b, g)

# input  : img1 (H, W, 3), img2 (H, W, 3)
# output : (H, W, 3)

페이드, 오버레이, 반투명 합성의 핵심이다.

cv2.calcHist

밝기 분포를 수치로 추출한다.

hist = cv2.calcHist([img], [0], None, [256], [0, 256])

# input image: (H, W)
# output hist: (256, 1)

평활화 전후 비교, 노출 진단, 색 분석의 출발점이다.

cv2.equalizeHist

단일 채널 대비 향상 함수다.

dst = cv2.equalizeHist(gray)

# input  : (H, W)
# output : (H, W)

컬러에 직접 쓰지 않고 밝기 채널에만 적용해야 한다는 점이 중요하다.

cv2.cvtColor

색공간을 바꾼다.

hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
ycc = cv2.cvtColor(img, cv2.COLOR_BGR2YCrCb)

# input  : (H, W, 3)
# output : (H, W, 3)

색 기반 분리와 밝기 기반 보정을 가능하게 만든다.

cv2.inRange

색 또는 값 범위를 만족하는 위치만 255로 만드는 이진 마스크 생성기다.

mask = cv2.inRange(img, lower, upper)

# input  : (H, W, 3) or (H, W)
# output : (H, W)
# values : 0 or 255

색 분리, 임계값 기반 추출, 크로마키에 필수다.

cv2.copyTo

마스크 위치에만 선택적으로 복사한다.

cv2.copyTo(src, mask, dst)

# src   : (H, W, 3)
# mask  : (H, W)
# dst   : (H, W, 3)
# result: dst updated in masked region

객체 합성, ROI 복사, 배경 대체의 핵심 메서드다.

VideoCapture.read

비디오를 프레임 스트림으로 바꾸는 핵심 메서드다.

ret, frame = cap.read()

# ret   : bool
# frame : (H, W, 3) if ret is True

정지 이미지를 다루던 구조를 시간축으로 확장하는 순간 이 메서드가 중심이 된다.


10. 수정하거나 확장할 때 먼저 이해해야 할 지점

이 문서에 등장한 예제들을 실제 프로젝트로 확장하려면 다음 지점을 먼저 이해해야 한다.

1) shape와 채널 수

대부분의 오류는 shape 불일치에서 시작된다.

  • grayscale: (H, W)
  • color: (H, W, 3)
  • mask: (H, W)

특히 copyTo, addWeighted, absdiff, add는 입력 shape가 맞아야 한다.

2) OpenCV의 기본 색 순서

OpenCV는 RGB가 아니라 BGR을 쓴다. matplotlib로 시각화할 때 색이 이상하면 거의 항상 이 문제다.

3) saturation과 overflow의 차이

영상에서는 단순 배열 연산보다 OpenCV 전용 산술 함수를 쓰는 것이 안전하다. 이 차이를 이해하지 못하면 예상과 다른 합성 결과가 나온다.

4) 컬러 보정은 밝기 채널만 다루는 편이 안전함

equalizeHist를 컬러 전체에 직접 적용하면 색이 깨질 수 있다. YCrCb나 HSV처럼 밝기 성분이 분리된 색공간으로 옮겨 처리하는 이유를 이해해야 한다.

5) 크로마키는 마스크 품질이 결과를 결정함

inRange 자체보다도, 얼마나 정확한 범위를 잡았는지와 마스크를 얼마나 잘 정제했는지가 합성 품질을 좌우한다. 실제 응용에서는 morphology, blur, edge refinement가 자주 추가된다.


11. 마무리

여기서 다룬 구성은 모두 서로 이어진다. 픽셀 단위 산술 연산은 밝기와 색 변화의 기초가 되고, 히스토그램은 그 변화가 분포 차원에서 어떤 의미인지 보여준다. 평활화는 분포를 개선하는 단계이고, HSV와 inRange는 특정 색만 골라내는 단계다. 마지막으로 copyTo와 비디오 프레임 루프가 결합되면 정적 이미지 처리에서 실제 영상 합성으로 확장된다.

핵심은 함수를 외우는 것이 아니라, 각 메서드가 입력 배열을 어떤 shape의 출력으로 바꾸며, 그 과정에서 픽셀값을 어떤 규칙으로 재해석하는지 이해하는 데 있다. 그 구조를 이해하면 이 예제들은 밝기 보정, 객체 추출, 배경 제거, 동영상 합성 같은 더 큰 작업으로 자연스럽게 확장된다.