15장. 비디오 처리와 실시간 영상처리
실습 4 — 배경 빼기로 움직이는 물체에 상자 그리기
같은 마당 동영상에서 배경을 만들어 빼고, 움직이는 물체마다 상자를 그립니다. 60장의 중앙값으로 만든 배경과, 배경을 스스로 배우는 MOG2 를 견줍니다. 상자는 12장에서 본 윤곽선과 Bounding Box 로 그립니다.
1단계. 중앙값으로 배경 만들기
import cv2
import numpy as np
import matplotlib.pyplot as plt
plt.rcParams["font.family"] = "Malgun Gothic"
plt.rcParams["axes.unicode_minus"] = False
cap = cv2.VideoCapture("ch15_yard.mp4")
frames = []
while True:
ok, frame = cap.read()
if not ok:
break
frames.append(frame)
cap.release()
# np.stack: 프레임 60장을 (60, 세로, 가로, 3) 하나로 쌓는다
stack = np.stack(frames)
# axis=0(프레임 방향)으로 칸마다 중앙값을 낸다. 잠깐 지나간 공과 수레는 지워진다
background = np.median(stack, axis=0).astype(np.uint8)
print("쌓은 모양:", stack.shape, "→ 배경 모양:", background.shape)
print("배경 저장:", cv2.imwrite("out_ch15_background.png", background))쌓은 모양: (60, 180, 240, 3) → 배경 모양: (180, 240, 3)
배경 저장: True2단계. 배경을 빼고 상자 그리기
bg_gray = cv2.cvtColor(background, cv2.COLOR_BGR2GRAY)
kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5))
def find_boxes(frame, min_area=100):
"""배경과 다른 흰 덩어리를 찾아 (마스크, 상자 목록)을 돌려준다."""
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
diff = cv2.absdiff(gray, bg_gray) # 배경과 다른 정도
_, mask = cv2.threshold(diff, 30, 255, cv2.THRESH_BINARY) # 8차시: 30 넘으면 전경
mask = cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel) # 9차시: 작은 잡티 지우기
# 12차시: 흰 덩어리의 바깥 윤곽선만(RETR_EXTERNAL) 꼭짓점만 줄여(CHAIN_APPROX_SIMPLE) 찾는다
contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
boxes = []
for c in contours:
if cv2.contourArea(c) >= min_area: # 넓이가 작은 덩어리는 버린다
boxes.append(cv2.boundingRect(c)) # (x, y, 가로, 세로)
return mask, sorted(boxes)
drawn = {}
for no in (10, 20, 35, 40, 55):
mask, boxes = find_boxes(frames[no])
view = frames[no].copy()
for x, y, bw, bh in boxes:
cv2.rectangle(view, (x, y), (x + bw, y + bh), (38, 93, 254), 2) # 다홍(BGR) 상자
drawn[no] = view
print(f"{no:2}번 프레임: 물체 {len(boxes)}개 {boxes}")10번 프레임: 물체 0개 []
20번 프레임: 물체 1개 [(12, 128, 23, 25)]
35번 프레임: 물체 2개 [(117, 128, 23, 25), (188, 145, 45, 30)]
40번 프레임: 물체 1개 [(148, 128, 45, 47)]
55번 프레임: 물체 1개 [(28, 145, 45, 30)]3단계. MOG2 로 배경을 배우며 빼기
# createBackgroundSubtractorMOG2: 칸마다 자주 나오는 밝기를 배우는 배경 빼기 도구
mog = cv2.createBackgroundSubtractorMOG2(
history=30, # 최근 30장을 기억해 배경을 배운다
varThreshold=25, # 배운 밝기에서 이만큼 넘게 벗어나면 전경
detectShadows=False, # 그림자를 따로 표시하지 않는다(전경 255, 배경 0 만)
)
mog_masks, mog_ratios = [], []
for frame in frames: # 반드시 순서대로 넣는다
fg = mog.apply(frame) # apply: 이 장으로 배경을 고치고 전경 마스크를 돌려준다
mog_masks.append(fg)
mog_ratios.append(float((fg > 0).mean()))
for no in (0, 1, 2, 10, 20, 35):
print(f"{no:2}번 프레임의 전경 비율: {mog_ratios[no]:.3f}") 0번 프레임의 전경 비율: 1.000
1번 프레임의 전경 비율: 0.000
2번 프레임의 전경 비율: 0.000
10번 프레임의 전경 비율: 0.000
20번 프레임의 전경 비율: 0.013
35번 프레임의 전경 비율: 0.0324단계. 두 방법의 결과 견주기
fig, axes = plt.subplots(2, 3, figsize=(11, 5.4))
top = [("중앙값 배경", background), ("35번: 배경 빼기 + 상자", drawn[35]), ("40번: 배경 빼기 + 상자", drawn[40])]
for ax, (title, img) in zip(axes[0], top):
ax.imshow(cv2.cvtColor(img, cv2.COLOR_BGR2RGB))
ax.set_title(title)
for ax, no in zip(axes[1], (0, 20, 35)):
ax.imshow(mog_masks[no], cmap="gray")
ax.set_title(f"MOG2 {no}번 (전경 {mog_ratios[no]:.3f})")
for ax in axes.flat:
ax.axis("off")
plt.tight_layout()
plt.show()
코드 설명
np.median(stack, axis=0)은 60장을 겹쳐 칸마다 가운데 밝기를 고릅니다. 공과 수레는 어느 칸에서나 몇 장만 지나가므로 결과 그림의 첫째 칸처럼 깨끗한 마당만 남았습니다.find_boxes는 배경 빼기 → 이진화(8장) → 열기(9장) → 윤곽선과 Bounding Box(12장)를 한 함수로 묶었습니다.contourArea로 넓이 100칸보다 작은 덩어리는 버립니다.- 물체가 없는 10번은 0개, 공만 있는 20번은 1개, 공과 수레가 떨어져 있는 35번은 2개를 찾았습니다. 40번에서는 공과 수레가 겹쳐 흰 덩어리가 하나로 붙어 상자도 하나
(148, 128, 45, 47)뿐입니다. 배경 빼기는 "움직이는 칸"을 찾을 뿐, 겹친 물체를 나누지는 못합니다. - MOG2 는 0번 프레임에서 아직 배운 배경이 없어 전경 비율이 1.000(화면 전체)이었고, 1번부터는 0.000 으로 바로 배경을 잡았습니다. 이 동영상은 배경이 고르고 첫 장면에 물체가 없어 배우는 기간이 한 장으로 끝났습니다. 20번(0.013), 35번(0.032)에서는 물체를 잡았지만, 아래 줄 그림처럼 가장자리에 자잘한 점이 붙어 중앙값 배경의 마스크보다 거칩니다.