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

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

실습 3 — 프레임 차이로 움직임 찾기

마당 동영상(ch15_yard.mp4)은 15번 프레임부터 빨간 공이 왼쪽에서, 30번 프레임부터 파란 수레가 오른쪽에서 들어오는 가상 동영상입니다. 이웃한 두 프레임의 차이로 움직인 곳을 찾고, 프레임마다 움직인 칸의 비율을 그래프로 그립니다.

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")
colors, grays = [], []
while True:
    ok, frame = cap.read()
    if not ok:
        break
    colors.append(frame)
    gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
    # 블러를 먼저 걸어 카메라 잡음과 압축 흔적이 차이로 잡히지 않게 한다(6차시)
    grays.append(cv2.GaussianBlur(gray, (5, 5), 0))
cap.release()
print("프레임:", len(grays), "장 · 한 장의 모양:", grays[0].shape)
출력
프레임: 60 장 · 한 장의 모양: (180, 240)

2단계. 두 프레임의 차이 보기

Python
prev, cur = grays[34], grays[35]
# cv2.absdiff: absolute difference. 칸마다 |지금 − 앞| 을 구한다(uint8 빼기의 넘침이 없다)
diff = cv2.absdiff(cur, prev)
print("차이의 가장 큰 값:", int(diff.max()), "· 평균:", round(float(diff.mean()), 3))

# 8차시의 이진화: 차이가 25 보다 큰 칸만 움직임(255)으로 본다
_, mask = cv2.threshold(diff, 25, 255, cv2.THRESH_BINARY)
print("이진화 뒤 움직인 칸 비율:", round(float((mask > 0).mean()), 3))
# 9차시의 팽창: 초승달처럼 갈라진 조각을 부풀려 잇는다
kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5))
mask = cv2.dilate(mask, kernel, iterations=2)
print("팽창 뒤 움직인 칸 비율:", round(float((mask > 0).mean()), 3))

fig, axes = plt.subplots(1, 4, figsize=(12, 2.8))
panels = [("34번 프레임", cv2.cvtColor(colors[34], cv2.COLOR_BGR2RGB), None),
          ("35번 프레임", cv2.cvtColor(colors[35], cv2.COLOR_BGR2RGB), None),
          ("차이 (absdiff)", diff, "gray"),
          ("이진화 + 팽창", mask, "gray")]
for ax, (title, img, cmap) in zip(axes, panels):
    ax.imshow(img, cmap=cmap)
    ax.set_title(title)
    ax.axis("off")
plt.tight_layout()
plt.show()
출력
차이의 가장 큰 값: 113 · 평균: 1.661
이진화 뒤 움직인 칸 비율: 0.024
팽창 뒤 움직인 칸 비율: 0.056
마당 동영상의 34번과 35번 프레임, 두 장의 차이, 이진화와 팽창으로 만든 움직임 마스크를 가로로 놓은 격자

3단계. 프레임마다 움직인 칸 비율 재기

Python
ratios = [0.0]          # 0번 프레임은 앞 프레임이 없으므로 0 으로 둔다
for i in range(1, len(grays)):
    d = cv2.absdiff(grays[i], grays[i - 1])
    _, m = cv2.threshold(d, 25, 255, cv2.THRESH_BINARY)
    ratios.append(float((m > 0).mean()))

moving = [i for i, r in enumerate(ratios) if r > 0]
print("움직임이 잡힌 프레임 수:", len(moving), "/", len(ratios))
print("움직임이 처음 잡힌 프레임:", moving[0])
best = int(np.argmax(ratios))       # argmax: 가장 큰 값의 자리(번호)
print("가장 많이 움직인 프레임:", best, "· 비율:", round(ratios[best], 3))
print("0~14번의 비율 합:", round(sum(ratios[:15]), 3))
출력
움직임이 잡힌 프레임 수: 44 / 60
움직임이 처음 잡힌 프레임: 16
가장 많이 움직인 프레임: 37 · 비율: 0.025
0~14번의 비율 합: 0.0

4단계. 비율을 그래프로 그리기

Python
fig, ax = plt.subplots(figsize=(9, 3.2))
ax.plot(ratios, marker="o", markersize=3)
ax.axvline(15, linestyle="--", color="gray")        # 공이 들어오는 프레임
ax.axvline(30, linestyle=":", color="gray")         # 수레가 들어오는 프레임
ax.text(15.5, max(ratios) * 0.9, "공 등장")
ax.text(30.5, max(ratios) * 0.3, "수레 등장")
ax.set_xlabel("프레임 번호")
ax.set_ylabel("움직인 칸 비율")
ax.set_title("프레임 차이로 잰 움직임")
plt.tight_layout()
plt.show()
마당 동영상의 프레임마다 움직인 칸 비율을 선으로 그리고 공과 수레가 들어오는 15번, 30번에 점선을 그은 그래프

코드 설명

  • cv2.absdiff(cur, prev) 는 칸마다 차이의 절댓값입니다. 34번과 35번의 차이는 가장 큰 값이 113 인데 평균은 1.661 로, 대부분의 칸(배경)은 거의 0 이고 물체가 지나간 곳만 큽니다.
  • cv2.threshold(diff, 25, 255, cv2.THRESH_BINARY) 로 작은 차이(잡음)를 버리고, cv2.dilate 로 갈라진 조각을 부풀려 잇습니다.
  • 이진화만 하면 움직인 칸이 0.024 인데 팽창 두 번 뒤에는 0.056 입니다. 그림처럼 공은 앞뒤 장이 겹치는 가운데가 비어 초승달 두 개로 잡히고, 팽창이 이를 하나로 이었습니다. 대신 덩어리가 실제보다 커집니다.
  • 0~14번의 비율 합은 0.0 으로, 블러와 기준값 25 덕분에 가만있는 배경의 잡음은 움직임으로 잡히지 않았습니다. 공은 15번 프레임에 화면 왼쪽 밖에서 출발하므로 처음 잡힌 것은 16번입니다. 그래프에서 공만 있을 때는 0.0075 안팎, 수레가 들어온 30번부터는 0.015 를 넘고, 가장 큰 값은 37번의 0.025 입니다. 프레임 차이는 물체가 몇 개인지 알려 주지 않고 "움직임이 얼마나 있나"만 알려 줍니다.
← 이전 절15장 · 실습 2 — 프레임마다 경계 찾고 새 동영상으로 저장하기다음 절 →15장 · 실습 4 — 배경 빼기로 움직이는 물체에 상자 그리기