15장. 비디오 처리와 실시간 영상처리
실습 3 — 프레임 차이로 움직임 찾기
마당 동영상(ch15_yard.mp4)은 15번 프레임부터 빨간 공이 왼쪽에서, 30번 프레임부터 파란 수레가 오른쪽에서 들어오는 가상 동영상입니다. 이웃한 두 프레임의 차이로 움직인 곳을 찾고, 프레임마다 움직인 칸의 비율을 그래프로 그립니다.
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")
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단계. 두 프레임의 차이 보기
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
3단계. 프레임마다 움직인 칸 비율 재기
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.04단계. 비율을 그래프로 그리기
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()
코드 설명
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 입니다. 프레임 차이는 물체가 몇 개인지 알려 주지 않고 "움직임이 얼마나 있나"만 알려 줍니다.