OpenCV 비전 실무15장 · 비디오 처리와 실시간 영상처리18 / 22← 이전목차다음 →MetaContents

15장. 비디오 처리와 실시간 영상처리

실습 4 — 배경 빼기로 움직이는 물체에 상자 그리기

같은 마당 동영상에서 배경을 만들어 빼고, 움직이는 물체마다 상자를 그립니다. 60장의 중앙값으로 만든 배경과, 배경을 스스로 배우는 MOG2 를 견줍니다. 상자는 12장에서 본 윤곽선과 Bounding Box 로 그립니다.

1단계. 중앙값으로 배경 만들기

Python
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)
배경 저장: True

2단계. 배경을 빼고 상자 그리기

Python
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 로 배경을 배우며 빼기

Python
# 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.032

4단계. 두 방법의 결과 견주기

Python
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()
위 줄에 중앙값 배경과 35번, 40번 프레임에 그린 상자, 아래 줄에 MOG2 의 0번, 20번, 35번 전경 마스크를 놓은 격자

코드 설명

  • 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)에서는 물체를 잡았지만, 아래 줄 그림처럼 가장자리에 자잘한 점이 붙어 중앙값 배경의 마스크보다 거칩니다.
← 이전 절15장 · 실습 3 — 프레임 차이로 움직임 찾기다음 절 →15장 · 실습 5 — 프레임당 처리 시간과 FPS 재기