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

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

실습 5 — 프레임당 처리 시간과 FPS 재기

컨베이어 동영상의 프레임마다 무거운 처리(7장의 Bilateral 필터 → 흑백 → Canny)를 걸고, 한 장에 걸리는 시간을 잽니다. 원래 크기(320×240)와 가로세로를 절반으로 줄인 크기(160×120)를 견주고, 웹캠처럼 프레임이 기다려 주지 않을 때 몇 장을 건너뛰게 되는지 흉내 냅니다. 잰 시간은 컴퓨터마다 다릅니다.

1단계. 프레임과 처리 함수 준비하기

Python
import time
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_conveyor.mp4")
frames = []
while True:
    ok, frame = cap.read()
    if not ok:
        break
    frames.append(frame)
cap.release()

def process(frame, size=None):
    """(크기를 줄이고) Bilateral 필터 → 흑백 → Canny 를 건다."""
    if size is not None:
        frame = cv2.resize(frame, size)                  # size = (가로, 세로)
    smooth = cv2.bilateralFilter(frame, 15, 75, 75)      # 7차시: 경계를 지키는 무거운 필터
    gray = cv2.cvtColor(smooth, cv2.COLOR_BGR2GRAY)
    return cv2.Canny(gray, 50, 150)

print("준비한 프레임:", len(frames), "장 · 원래 크기:", frames[0].shape)
print("절반 크기의 결과 모양:", process(frames[0], (160, 120)).shape)
출력
준비한 프레임: 80 장 · 원래 크기: (240, 320, 3)
절반 크기의 결과 모양: (120, 160)

2단계. 원래 크기로 한 장 처리 시간 재기

Python
def measure(size=None):
    """프레임마다 처리 시간(ms)을 재어 목록으로 돌려준다."""
    times = []
    for frame in frames:
        start = time.perf_counter()                       # 처리 직전 시각(초)
        process(frame, size)
        times.append((time.perf_counter() - start) * 1000)   # 걸린 시간을 ms 로
    return times

full = measure()
full_avg = sum(full) / len(full)
print(f"원래 크기 320×240: 한 장 평균 {full_avg:.1f}ms → 1초에 {1000 / full_avg:.1f}장 처리 가능")
for fps in (10, 30):
    budget = 1000 / fps                                   # 프레임 간격(ms)
    verdict = "제때 끝남" if full_avg <= budget else "늦음"
    print(f"  {fps}fps 의 프레임 간격 {budget:.1f}ms → {verdict}")
출력
원래 크기 320×240: 한 장 평균 35.4ms → 1초에 28.3장 처리 가능
  10fps 의 프레임 간격 100.0ms → 제때 끝남
  30fps 의 프레임 간격 33.3ms → 늦음

3단계. 크기를 절반으로 줄여 다시 재기

Python
half = measure((160, 120))
half_avg = sum(half) / len(half)
print(f"절반 크기 160×120: 한 장 평균 {half_avg:.1f}ms → 1초에 {1000 / half_avg:.1f}장 처리 가능")
for fps in (10, 30):
    budget = 1000 / fps
    verdict = "제때 끝남" if half_avg <= budget else "늦음"
    print(f"  {fps}fps 의 프레임 간격 {budget:.1f}ms → {verdict}")
# 칸 수는 4분의 1 로 줄었다. 시간은 몇 배 줄었는지 본다
print(f"칸 수: {320 * 240:,} → {160 * 120:,} (4분의 1)")
print(f"빨라진 정도: {full_avg / half_avg:.1f}배")
출력
절반 크기 160×120: 한 장 평균 9.0ms → 1초에 111.0장 처리 가능
  10fps 의 프레임 간격 100.0ms → 제때 끝남
  30fps 의 프레임 간격 33.3ms → 제때 끝남
칸 수: 76,800 → 19,200 (4분의 1)
빨라진 정도: 3.9배

4단계. 프레임이 기다려 주지 않으면

Python
def simulate(times, fps):
    """프레임이 1000/fps ms 마다 들어올 때, 처리 중에 온 프레임은 건너뛴다고 보고 센다."""
    gap = 1000 / fps
    busy_until, done, skipped = 0.0, 0, 0
    for i, t in enumerate(times):
        arrive = i * gap                     # i 번 프레임이 들어오는 시각(ms)
        if arrive < busy_until:              # 앞 프레임을 아직 처리하는 중이면 건너뛴다
            skipped += 1
            continue
        busy_until = arrive + t              # 이 프레임의 처리가 끝나는 시각
        done += 1
    return done, skipped

rows = []
for name, times in (("원래 크기", full), ("절반 크기", half)):
    for fps in (10, 30):
        done, skipped = simulate(times, fps)
        rows.append((name, fps, done, skipped))
        print(f"{name} · {fps}fps: {len(times)}장 가운데 처리 {done}장, 건너뜀 {skipped}장")

fig, ax = plt.subplots(figsize=(7, 3.4))
ax.bar(["원래 크기\n320×240", "절반 크기\n160×120"], [full_avg, half_avg])
ax.axhline(100, linestyle="--", color="gray")
ax.axhline(1000 / 30, linestyle=":", color="gray")
ax.text(1.45, 100, "10fps 간격 100ms", va="bottom", ha="right")
ax.text(1.45, 1000 / 30, "30fps 간격 33.3ms", va="bottom", ha="right")
ax.set_ylabel("한 장 평균 처리 시간 (ms)")
ax.set_ylim(0, 115)
ax.set_title("크기에 따른 프레임 하나의 처리 시간")
plt.tight_layout()
plt.show()
출력
원래 크기 · 10fps: 80장 가운데 처리 80장, 건너뜀 0장
원래 크기 · 30fps: 80장 가운데 처리 58장, 건너뜀 22장
절반 크기 · 10fps: 80장 가운데 처리 80장, 건너뜀 0장
절반 크기 · 30fps: 80장 가운데 처리 80장, 건너뜀 0장
원래 크기와 절반 크기의 한 장 평균 처리 시간을 막대로 그리고 10fps 와 30fps 의 프레임 간격을 가로선으로 그은 그래프

코드 설명

  • time.perf_counter() 를 처리 앞뒤에서 불러 그 차이에 1000 을 곱하면 ms 입니다. 프레임을 미리 읽어 두어 읽는 시간은 섞지 않았습니다.
  • 교안 PC 에서 원래 크기는 한 장 평균 35.4ms 로, 1초에 28.3장을 처리할 수 있었습니다. 10fps(간격 100ms)에는 제때 끝나지만 30fps(간격 33.3ms)에는 조금 늦습니다.
  • 가로세로를 절반으로 줄이면 칸 수는 4분의 1 이 되고, 처리 시간은 9.0ms 로 3.9배 빨라져 30fps 에도 제때 끝났습니다. 칸 수가 줄어든 만큼 거의 그대로 빨라진 셈입니다. 숫자는 컴퓨터마다 다르지만 "크기를 줄이면 몇 배 빨라진다"는 관계는 같습니다.
  • simulate 는 웹캠처럼 프레임이 정해진 간격으로 들어온다고 보고, 앞 장을 처리하는 동안 들어온 장은 건너뛴 것으로 셉니다. 교안 PC 에서는 원래 크기·30fps 일 때만 80장 가운데 58장을 처리하고 22장을 건너뛰었습니다. 한 장 처리(35.4ms)가 간격(33.3ms)보다 조금 길어 몇 장마다 한 장씩 밀려난 것입니다. 동영상 파일은 실제로는 기다려 주지만, 생방송이라면 이렇게 장이 빠집니다.
← 이전 절15장 · 실습 4 — 배경 빼기로 움직이는 물체에 상자 그리기다음 절 →15장 · 프로젝트 1 — 주차장 입구 동영상 요약 사진 만들기