홈 › 3장 MediaInfo와 파일 분석

3.4 프레임 단위 분석 - GOP, 필드 순서, 가변 프레임레이트

FFprobe 프레임 정보를 이용해 GOP 길이와 필드 순서, 프레임 간격(VFR)을 통계로 요약하는 방법과, 프레임 번호를 타임코드로 바꿀 때의 흔한 오류를 설명합니다.

최종 수정 2026-10-28 · 읽는 시간 약 5분 · 운영 프리즘(Prism) · 편집 원칙

#프레임 정보 뷰어의 원리

영상 파일의 모든 프레임에 대해 "이 프레임은 I/P/B 중 무엇인가, 몇 초에 표시되는가, 인터레이스인가"를 목록으로 보여 주는 도구를 프레임 정보 뷰어라고 부를 수 있습니다. 직접 만든다면 구조는 단순합니다.

ffprobe -select_streams v -show_frames -print_format ini  →  프레임별 키=값 목록
        ↓ 파싱
프레임 번호 / pict_type / pts_time / interlaced_frame / top_field_first
        ↓
리스트 뷰에 표시 (I프레임만 필터, 프레임 번호 → 타임코드 변환 열 추가)

핵심은 FFprobe의 -show_frames 출력을 읽는 것입니다. 한 프레임당 아래와 같은 항목이 나옵니다(MPEG-2 인터레이스 MXF의 첫 프레임).

[frames.frame.0]
media_type=video
key_frame=1
pts=0
pts_time=0.000000
pkt_pos=8192
pkt_size=97109
width=1920
height=1080
pix_fmt=yuv422p
pict_type=I
interlaced_frame=1
top_field_first=1
...
[frames.frame.0.side_data_list.side_data.1]
side_data_type=GOP timecode
timecode=01:00:00:00

pict_type, pts_time, interlaced_frame, top_field_first가 핵심 항목입니다. 그리고 MPEG-2 같은 코덱은 GOP timecode라는 부가 데이터(side data) 로 타임코드를 프레임 안에 담고 있는 것도 볼 수 있습니다.

#통계로 요약하기

수만 프레임을 눈으로 볼 수는 없으므로, 목록을 통계로 요약하면 파일의 성격이 한눈에 보입니다. 아래는 GOP 길이, 필드 순서, 프레임 간격의 종류를 요약하는 파이썬 스크립트입니다.

import json
import subprocess
import sys
from collections import Counter


def probe_frames(path):
    cmd = ['ffprobe', '-v', 'error', '-select_streams', 'v:0', '-show_entries',
           'frame=pict_type,pts_time,interlaced_frame,top_field_first',
           '-of', 'json', path]
    out = subprocess.run(cmd, capture_output=True, text=True, check=True).stdout
    return json.loads(out)['frames']


def summarize(frames):
    types = [f['pict_type'] for f in frames]
    i_pos = [i for i, t in enumerate(types) if t == 'I']
    gops = [b - a for a, b in zip(i_pos, i_pos[1:])]
    pts = sorted(float(f['pts_time']) for f in frames
                 if f.get('pts_time') not in (None, 'N/A'))
    deltas = Counter(round(b - a, 4) for a, b in zip(pts, pts[1:]))
    return {
        'frames': len(frames),
        'types': dict(Counter(types)),
        'gop_lengths': sorted(set(gops)),
        'interval_kinds': len(deltas),
        'intervals': dict(deltas.most_common(4)),
        'interlaced_frame': dict(Counter(f.get('interlaced_frame') for f in frames)),
        'top_field_first': dict(Counter(f.get('top_field_first') for f in frames)),
    }


if __name__ == '__main__':
    print(json.dumps(summarize(probe_frames(sys.argv[1])), indent=2))

#실행 결과 1: 방송용 MXF (MPEG-2 인터레이스)

{
  "frames": 150,
  "types": { "I": 13, "P": 137 },
  "gop_lengths": [12],
  "interval_kinds": 1,
  "intervals": { "0.0334": 149 },
  "interlaced_frame": { "1": 150 },
  "top_field_first": { "1": 150 }
}

읽는 법: B프레임이 없고, GOP가 12프레임으로 고정이며, 150프레임 모두 인터레이스·상단 필드 우선입니다. 프레임 간격이 0.0334초 한 종류뿐이므로 일정한 프레임레이트(CFR) 입니다.

#실행 결과 2: 웹 프록시 (H.264)

{
  "frames": 150,
  "types": { "I": 5, "P": 66, "B": 79 },
  "gop_lengths": [30],
  "interval_kinds": 1,
  "interlaced_frame": { "0": 150 }
}

2장에서 -g 30으로 만든 프록시이므로 GOP 길이가 정확히 30으로 나옵니다. 프록시 생성 설정이 의도대로 적용되었는지를 파일에서 검증하는 방법이기도 합니다.

#실행 결과 3: 가변 프레임레이트(VFR) 파일

프레임 하나를 일부러 빼서 만든 파일입니다.

{
  "frames": 149,
  "intervals": { "0.0334": 147, "0.0667": 1 },
  "interval_kinds": 2
}

interval_kinds가 2 이상이거나 간격 하나가 다른 값이면 VFR을 의심합니다. 단, 타임스탬프를 정수로 표현하는 단위(time_base) 때문에 정상적인 고정 프레임레이트(CFR)도 간격이 두 가지로 나올 수 있습니다. 예를 들어 24fps를 밀리초(1/1000) 단위로 표현하면 프레임 간격이 41ms와 42ms로 섞입니다. 그래서 실무에서는 간격의 종류 수가 아니라 time_base 한 칸 이내의 차이는 허용하고, 프레임 번호×기대 간격과 실제 시각의 누적 오차가 한 프레임을 넘는지로 판단합니다. 스마트폰 영상, 화면 녹화, 일부 편집 소프트웨어 출력에서 흔히 생기며, 타임코드 기반 시스템에서는 프레임 번호와 시간이 일치하지 않는 문제를 만듭니다. MAM 인제스트에서 VFR을 감지하면 CFR로 변환하는 정규화 단계를 거치게 하는 것이 안전합니다.

#통계에서 얻는 실무 판단

통계 판단
gop_lengths가 하나 고정 GOP. 구간 추출 계산이 쉬움
GOP 길이가 매우 큼(예: 250) 웹 탐색이 느림 → 프록시는 재인코딩 필요
types에 B 없음 저지연 설정이거나 MPEG-2/인트라 계열
모든 프레임이 I 인트라 코덱(프레임 정확 편집에 유리)
interlaced_frame이 섞여 있음 인터레이스/프로그레시브 혼합 소재 → 디인터레이스 정책 필요
interval_kinds ≥ 2 VFR 의심(단, time_base 양자화로 정상 CFR도 2종이 될 수 있으니 누적 오차로 확인) → CFR 변환

#프레임 번호를 타임코드로 바꿀 때의 흔한 오류

프레임 정보 목록에 "프레임 번호 → 타임코드" 열을 넣는 기능을 만들 때, 가장 흔한 실수는 29.97을 실수(float)로 나누는 것입니다.

// 잘못된 방식: 29.97로 직접 나눔
hh = frame / (29.97 * 60 * 60);
mm = (frame - hh * (29.97*60*60)) / (29.97 * 60);
ss = ...
ff = frame - (...);   // 실수 오차가 누적됨

이 방식에는 두 가지 문제가 있습니다.

  1. 29.97은 30000/1001(29.97002997…)의 근사값이라 시간이 길어질수록 오차가 쌓입니다.
  2. 드롭 프레임 규칙(매 분 00, 01 프레임 건너뜀, 10분 단위 제외)이 반영되지 않아 1분 단위 경계에서 어긋납니다.

정확한 방법은 1장 타임코드 편에서 소개한 정수 연산 기반 드롭 프레임 변환입니다. 분 경계 부근에서 위 식(소수 부분을 내림)과 정답을 비교하면 다음과 같습니다.

프레임 번호 정답(드롭 프레임) 29.97로 직접 나눈 값
1798 00:00:59;28 00:00:59;29
1799 00:00:59;29 00:01:00;00
1800 00:01:00;02 00:01:00;01
1801 00:01:00;03 00:01:00;02

1분 경계에서 이미 한 프레임이 앞서가다가, 1800번 이후에는 거꾸로 ;02가 ;01로 나옵니다. 소수 부분을 내림이 아니라 반올림하면 1798번이 00:00:59;30처럼 존재할 수 없는 값이 나옵니다. 프레임 목록 화면에서는 눈에 잘 띄지 않지만, 이 값을 편집 시스템으로 넘기면 분 경계 이후의 구간이 프레임 단위로 틀어집니다.

#정리

  • 프레임 정보는 ffprobe -show_frames로 얻고, 통계로 요약해야 실무에 쓸 수 있습니다.
  • GOP 길이, 필드 순서, 프레임 간격 종류만 봐도 파일의 성격과 문제 가능성을 알 수 있습니다.
  • 프레임 번호 ↔ 타임코드 변환은 실수 연산이 아니라 정수 기반 드롭 프레임 로직을 써야 합니다.

#참고 자료

링크는 2026-10-28에 열람이 되는 것을 확인했습니다. 규격과 도구 문서는 개정될 수 있으므로 적용하기 전에 최신 판을 확인하세요.