이미지 비교와 차영상
실습 준비
import cv2 as cv
import numpy as np
before = cv.imread("before.png")
after = cv.imread("after.png")
if before is None or after is None:
raise FileNotFoundError("비교할 이미지를 읽을 수 없습니다")
이미지 표시
show() 함수는 OpenCV와 이미지·영상 입출력에서 정의한 함수를 사용함.
두 이미지를 픽셀별로 비교하려면 높이, 너비, 채널 수가 같아야 함.
if before.shape != after.shape:
raise ValueError("두 이미지의 크기와 채널 수가 다릅니다")
before.shape, after.shape
((420, 640, 3), (420, 640, 3))
실습 이미지에서는 같은 책상 장면을 유지하고 after.png에만 빨간 컵을 추가함.

이미지가 같은지 확인
np.array_equal()은 두 배열의 모양과 모든 원소가 같은지 확인하여 하나의 불리언 값을 반환함.
np.array_equal(before, after)
== 연산자는 같은 위치의 픽셀값을 하나씩 비교하므로 이미지와 모양이 같은 불리언 배열을 만듦.
pixel_comparison = before == after
pixel_comparison.shape, pixel_comparison.dtype
모든 위치가 같은지 확인하려면 all()을 적용함.
pixel_comparison.all()
두 방법은 이미지가 완전히 같은지는 알려 주지만 값이 얼마나 다르고 어디가 달라졌는지는 보여 주지 않음.
절대 차영상
차영상(difference image)은 같은 위치의 픽셀 차이를 영상으로 나타낸 결과임. cv.absdiff()는 두 픽셀 값의 차이를 절댓값으로 계산함.
difference = cv.absdiff(before, after)
show(difference)

- 검거나 어두운 영역: 두 이미지의 픽셀값이 같거나 차이가 작음
- 밝은 영역: 두 이미지의 픽셀값 차이가 큼
- 절댓값을 사용하므로 두 이미지의 입력 순서를 바꾸어도 결과가 같음
reverse_difference = cv.absdiff(after, before)
np.array_equal(difference, reverse_difference)
차영상을 그레이스케일로 변환
컬러 차영상은 BGR 채널마다 차이를 저장함. 하나의 변화 마스크를 만들기 위해 그레이스케일로 변환함.
gray_difference = cv.cvtColor(difference, cv.COLOR_BGR2GRAY)
show(gray_difference)

gray_difference.min(), gray_difference.max()
변화 마스크
두 이미지에는 조명, 센서 잡음, 압축으로 인한 작은 픽셀 차이가 생길 수 있음. Thresholding으로 일정 크기보다 큰 차이만 선택함.
difference_threshold = 30
_, change_mask = cv.threshold(
gray_difference,
difference_threshold,
255,
cv.THRESH_BINARY,
)
show(change_mask)

- 0: 차이가 문턱값 이하인 위치
- 255: 차이가 문턱값보다 큰 위치
문턱값이 낮으면 작은 변화까지 선택하고, 높으면 큰 변화만 선택함.
threshold_values = [10, 30, 60]
change_masks = [
cv.threshold(gray_difference, value, 255, cv.THRESH_BINARY)[1]
for value in threshold_values
]
show(cv.hconcat(change_masks))
왼쪽부터 문턱값 10, 30, 60의 결과임.

변화 위치 표시
변화하지 않은 영역을 그레이스케일로 바꾸고 변화 마스크에 해당하는 영역만 원래 색으로 남김.
gray_after = cv.cvtColor(after, cv.COLOR_BGR2GRAY)
highlighted = cv.cvtColor(gray_after, cv.COLOR_GRAY2BGR)
highlighted[change_mask > 0] = after[change_mask > 0]
show(highlighted)

변화량 수치화
cv.countNonZero()로 변화 마스크에서 0이 아닌 픽셀 수를 셈.
changed_pixels = cv.countNonZero(change_mask)
total_pixels = change_mask.size
change_ratio = changed_pixels / total_pixels
{
"변화 픽셀 수": changed_pixels,
"전체 픽셀 수": total_pixels,
"변화 비율(%)": round(change_ratio * 100, 2),
}
{'변화 픽셀 수': 17618, '전체 픽셀 수': 268800, '변화 비율(%)': 6.55}
문턱값 30에서 컵과 그림자가 차지하는 17,618픽셀, 전체의 6.55%가 변화로 선택됨. 변화 비율은 같은 장면을 반복 촬영하거나 제품의 전후 상태를 비교할 때 기준값으로 사용 가능함.
변화 비율은 의미를 직접 판단하지 않음
변화 비율이 크다는 사실만으로 중요한 객체가 바뀌었다고 단정할 수 없음. 조명 변화나 카메라 흔들림도 많은 픽셀을 변화시킬 수 있음.
차영상의 한계
픽셀 위치를 직접 비교하므로 촬영 조건이 조금만 달라져도 차이가 발생함.
- 카메라 이동: 물체의 모든 경계가 변화로 나타남
- 조명 변화: 장면 전체의 밝기가 달라짐
- 자동 노출과 화이트 밸런스: 프레임마다 색과 밝기가 달라질 수 있음
- 압축 잡음: 실제 장면이 같아도 작은 픽셀 차이 발생
정확한 비교에는 카메라를 고정하고 같은 해상도와 촬영 조건을 유지해야 함. 시점이 다른 이미지를 맞추는 방법은 2일차 이미지 매칭과 기하학적 변환에서 다룸.
연속 프레임 비교
동영상은 이미지 프레임의 연속이므로 이전 프레임과 현재 프레임의 차영상으로 움직임을 확인 가능함.
이전 프레임 ─┐
├─ absdiff → Thresholding → 움직임 마스크
현재 프레임 ─┘
웹캠의 움직임 감지
웹캠에서 프레임을 연속으로 받아 차영상을 계산함. 0은 기본 웹캠을 의미함.
camera = cv.VideoCapture(0)
if not camera.isOpened():
raise RuntimeError("웹캠을 열 수 없습니다")
ret, previous_frame = camera.read()
if not ret:
camera.release()
raise RuntimeError("웹캠 프레임을 읽지 못했습니다")
previous_gray = cv.cvtColor(previous_frame, cv.COLOR_BGR2GRAY)
현재 프레임과 이전 프레임을 비교하고 전체 픽셀의 1% 이상이 달라지면 움직임이 있다고 표시함.
motion_threshold = 30
minimum_motion_ratio = 0.01
try:
while True:
ret, frame = camera.read()
if not ret:
break
gray = cv.cvtColor(frame, cv.COLOR_BGR2GRAY)
frame_difference = cv.absdiff(previous_gray, gray)
_, motion_mask = cv.threshold(
frame_difference,
motion_threshold,
255,
cv.THRESH_BINARY,
)
motion_ratio = cv.countNonZero(motion_mask) / motion_mask.size
motion_detected = motion_ratio >= minimum_motion_ratio
label = "Motion" if motion_detected else "Still"
color = (0, 0, 255) if motion_detected else (0, 255, 0)
cv.putText(
frame,
label,
(20, 40),
cv.FONT_HERSHEY_SIMPLEX,
1,
color,
2,
cv.LINE_AA,
)
cv.imshow("frame", frame)
cv.imshow("motion mask", motion_mask)
previous_gray = gray
key = cv.waitKey(1) & 0xFF
if key == ord("q"):
break
finally:
camera.release()
cv.destroyAllWindows()
previous_gray = gray로 기준 프레임을 계속 갱신하므로 연속한 두 웹캠 프레임 사이의 움직임을 감지함. q 키를 누르면 실행을 종료함.
실행 환경
cv.imshow()와 웹캠은 데스크톱 GUI와 카메라 권한이 필요함. 원격 서버나 일반적인 Colab 환경에서는 같은 방식으로 실행되지 않을 수 있음.