비디오 컬러 그레이드 (분석 → 보정 → 룩 → 선택 → LUT + 적용)
처음부터 코드로 그레이드를 작성하고, 실제 영상에서 여러 가지 고유한 네임드 룩을 생성한 후, 명시적인 사람의 선택 또는 위임된 에이전트의 판단을 통해 하나를 골라 포터블 .cube LUT로 추출하여 전체 클립에 적용합니다. 프리셋 팩을 사용하지 않습니다.
핵심 아이디어 — 보정 후 스타일링. 모든 룩은 보정 베이스 + 창의적 레이어로 구성됩니다:
- 보정 베이스는 영상을 정직하게 만듭니다 — 평탄한 LOG/HLG의 경우 log→Rec.709 변환; 이미 Rec.709인 영상의 경우 WB 중립화 + 노출 + 약간의 대비입니다. 모든 룩이 이 베이스를 공유하므로 "중립"이 의미를 유지하고 룩은 의도에 따라 달라집니다.
- 창의적 레이어는 그 위에 쌓인 룩입니다 (웜 / 쿨 / 틸-오렌지 / 펀치 / 페이드드…).
피부가 품질 기준입니다. 사람, 특히 어두운 피부에서 teal_orange와 cool_desat은 종종 녹색/회색 기운을 만듭니다. 모든 룩을 얼굴부터 평가하고 미드톤의 온기를 유지하며, 틴트는 섀도우/하이라이트/배경에 밀어넣고, 원시 saturation보다 vibrance를 선호합니다. 화이트 밸런스를 완전히 중립으로 밀어붙이면 피부가 생기를 잃습니다.
요구 사항: ffmpeg/ffprobe가 PATH에 있어야 하며, Python에 numpy + Pillow가 설치되어 있어야 합니다.
계약
- 모든 룩 =
베이스 + 창의적 레이어; 베이스는 모든 룩이 공유합니다. - 모든 룩은 동일한 대표 프레임에 렌더링됩니다 (공정하고 통제된 비교), 레이블이 지정되고, 전체 해상도로 표시됩니다.
- 전체 적용은 **
-c:a copy**를 사용합니다 (오디오는 재인코딩되지 않음 → A/V 동기 유지)하고 소스의 지속 시간, FPS, 해상도를 유지합니다. - 전달 전에 룩 선택을 완료합니다. 사용자가 선택을 요청한 경우
human모드를 사용하고, 사용자가 선택을 위임하거나 자동 실행을 요청한 경우 중단 없이agent모드를 사용합니다. 두 모드 모두에서 근거를 지속적인 계획에 보존합니다.
프로젝트 프로토콜 워크플로우
지속적인 결정을 work/color-grade/grade-plan.json에 저장합니다:
{
"schema_version": 1,
"target": "base-video",
"base": "eq=contrast=1.05",
"looks": [{"name": "clean", "chain": "null"}],
"selected_look": "clean",
"selection_mode": "agent",
"selection_rationale": "Neutral correction preserves natural skin and source lighting.",
"selected_lut": "../../final/selected-color-look.cube",
"evidence_refs": ["media:source"]
}
기본적으로 base-video를 사용하여 콘텐츠 카드와 캡션이 재색상화되지 않도록 합니다. composite는 의도적으로 이미 합성된 픽셀을 그레이딩할 때만 유효합니다. 공유된 work/understand/media.json과 시각적 증거를 읽습니다. review/02-color-grade/choose-color-look.jpg와 review/02-color-grade/skin-tone-check.jpg를 생성하고, 선택적으로 짧은 selected-look-preview.mp4를 생성합니다. 결정과 증거를 review/02-color-grade/selected-look.md에 기록합니다.
명시적인 사용자 선택 후에는 selection_mode를 human으로 설정합니다. 사용자가 결정을 위임하거나 중단 없는 워크플로우를 요청한 경우 agent로 설정하고, 피부톤, 하이라이트 보존, 중립 밸런스를 최우선으로 하여 보수적으로 선택합니다. 항상 비어 있지 않은 selection_rationale을 작성합니다. 에이전트 모드에서는 멈추지 않습니다.
선택된 LUT를 final/selected-color-look.cube로 굽습니다. 해당 LUT가 전달될 때 selected_lut를 그레이드 계획에 설정하여 공유 렌더러가 정확한 파일을 사용하도록 합니다. 굽혀진 전체 룩 LUT는 이미 보정 베이스를 포함하고 있습니다. 렌더러는 base + lut3d가 아닌 lut3d만 적용해야 합니다.
작업의 정확한 입력 리비전을 based_on에 기록하고, 계획이나 선택된 입력이 변경될 때 정수 revision을 증가시키며, 리뷰 파일이 완료된 후에만 작업 검사를 pass로 설정하고, 전체 프로토콜 필드를 기여합니다:
{
"target": {"sequence": "main", "scope": "base-video"},
"effects": {
"changes_timeline": false,
"changes_geometry": false,
"changes_video_pixels": true,
"changes_audio": false,
"adds_track": null
},
"check": {
"status": "pass",
"report": "../review/02-color-grade/selected-look.md"
},
"render": {
"kind": "video-filter",
"target": "base-video",
"plan": "color-grade/grade-plan.json"
}
}
공유 렌더러는 selected_look이 존재하는지 확인하고, 타임라인 변환 후, 오버레이 전에 그레이드를 적용한 후 최종 비디오를 한 번 인코딩합니다:
python skills/video-understand/scripts/build_render_plan.py .
python skills/video-understand/scripts/render_project.py work/render/render-plan.json
독립 실행형 호환성 입력
- 소스 비디오.
looks.json(base+ 명명된looks);looks.example.json에서 시작하고assess.py가 보고하는 대로base를 재조정하십시오.
모든 기존 스크립트는 레거시 looks.json을 계속 수용합니다. gradelib.load_spec()은 정식 grade-plan.json도 수용하므로, 연락 시트, 클립, LUT, 독립 실행형 적용 명령은 마이그레이션 중에도 호환성을 유지합니다.
looks.json 스키마
{
"base": "<corrective base: log->Rec.709 conversion, OR WB+exposure+contrast for normal footage>",
"looks": [
{ "name": "clean_neutral", "label": "1 · CLEAN NEUTRAL", "desc": "corrected only", "chain": "eq=contrast=1.02" },
{ "name": "teal_orange", "label": "4 · TEAL & ORANGE", "desc": "cinematic", "chain": "colorbalance=...,eq=..." }
// a look's full ffmpeg filter = base + "," + chain. Set "prepend_base": false to use chain standalone.
]
}
워크플로우
-
분석 — 절대 맹목적으로 그레이딩하지 마십시오.
python scripts/assess.py SOURCE.mp4 --out work/log-vs-Rec.709, 화이트 밸런스 캐스트(signalstats U/V vs 128), 노출(Y)을 보고하고, 대표 프레임(
work/assess/frame.png)을 선택하며, 베이스 조정을 제안합니다. 그에 따라looks.json의base를 재조정하십시오(예: 웜 캐스트 → 약간의 블루 추가; log → log→709 변환을base에 넣기).- 대표 프레임은 얼굴이 아닌 중간 휘도로 선택됩니다. 단일 샷의 토킹 헤드에서는 일반적으로 좋은 얼굴 프레임이 나오지만, B-roll이 많거나 멀티 샷 영상에서는 얼굴이 없는 프레임이 선택될 수 있습니다. 그러면
face_skin_check.png가 쓸모없어집니다(피부가 품질 기준입니다).--frame-time SECONDS로 알려진 얼굴 순간을 강제 지정하십시오. - WB/노출 통계는 지속 시간의 10% 지점에서 하나의 약 10초
signalstats창에서 가져옵니다 — 일정한 조명에는 적합하지만 조명이 변하는 영상의 경우--win-start SECONDS/--win-dur SECONDS로 이동/연장하십시오.
- 대표 프레임은 얼굴이 아닌 중간 휘도로 선택됩니다. 단일 샷의 토킹 헤드에서는 일반적으로 좋은 얼굴 프레임이 나오지만, B-roll이 많거나 멀티 샷 영상에서는 얼굴이 없는 프레임이 선택될 수 있습니다. 그러면
-
선택을 위해 룩을 렌더링합니다.
python scripts/render_looks.py work/assess/frame.png looks.json --out work/lookswork/looks/looks_compare.png(원본 + 모든 룩, 레이블 지정, 전체 해상도)와face_skin_check.png(확대된 피부 스트립)를 생성합니다. 둘 다 보십시오. 피부가 기준입니다 — 피부를 회색/녹색으로 만드는 룩은 재조정하거나 버린 후 다시 렌더링하십시오. 피사체가 중앙에 없습니까?--face-crop W:H:X:Y를 추가하십시오. 해당 PNG를 작업 중간물로 취급하고, 승인된 리뷰 사본을review/02-color-grade/choose-color-look.jpg및review/02-color-grade/skin-tone-check.jpg로 게시합니다. -
(선택 사항) 상위 2~3개 룩의 모션 프리뷰를 생성하여 움직이는 모습을 확인합니다(오디오 유지):
python scripts/make_clips.py SOURCE.mp4 looks.json --out work/clips \ --pick clean_neutral,warm_filmic,teal_orange --with-original --ss 148 --t 8 -
하나의 룩을 선택합니다. 연락 시트를 제시하고 사용자가 선택을 요청한 경우에만 기다립니다. 그렇지 않으면 에이전트 모드에서 선택하고 결정 지연 없이 계속 진행합니다. 전달 전에
selected_look,selection_mode,selection_rationale을 작성합니다. -
선택 후 — LUT를 굽고 전체 클립에 적용합니다.
python scripts/bake_lut.py looks.json --name CHOSEN \ --out final/selected-color-look.cube --verify-frame work/assess/frame.png python scripts/apply_grade.py SOURCE.mp4 --looks looks.json --name CHOSEN --out out/graded.mp4 # 또는 전달된 LUT 적용: --lut final/selected-color-look.cubeapply_grade.py는 지속 시간/오디오가 유지되는지 확인하고, WB 이동(U/V가 128을 향해)을 출력하며, 확인용 스팟 프레임을 떨어뜨립니다.- 적용 과정에서 비디오를 한 번 재인코딩합니다(손실); 오디오는
-c:a copy입니다. 품질/시간은--crf(기본값 18)와--preset(기본값slow)으로 설정됩니다.slow는 작은/짧은 클립에는 괜찮지만, 1080p/4K 길이의 영상에서는 크고 조용한 시간 비용이 듭니다 — 전달용으로는--preset medium으로 낮추고(빠른 프리뷰용으로는veryfast), 최종본에만slow를 사용하십시오. - 전달된 비디오를 전달된 LUT와 일치시키십시오: 체인(
--name)과 굽혀진 33³.cube(--lut)는 급격한 곡선에서 최대 약 5/255까지 차이가 날 수 있습니다(bake_lut.py가 이max|d|를 보고합니다). 대부분의 작업에서 눈에 띄지 않지만,.cube를 전달하고 렌더링된 비디오가 정확히 일치하기를 원한다면--name이 아닌 **--lut final/selected-color-look.cube**로 적용하십시오.
- 적용 과정에서 비디오를 한 번 재인코딩합니다(손실); 오디오는
설계 노트 / 주의사항
- CLEAN 컷을 그레이딩한 후, 그레이딩된 영상 위에 합성 오버레이를 쌓으십시오. 이미 자막/카드가 박힌 비디오를 그레이딩해야 한다면
assess.py를 신뢰하지 마십시오: WB/노출 통계와 프레임 선택이 그래픽에 의해 왜곡됩니다(어두운 상단/하단 스크림이 Y를 낮추고, 밝은 캡션 텍스트/틸 악센트가 U/V를 왜곡하며, 프레임 선택이 캡션이 많은 프레임에 떨어질 수 있음) — 영상이 아닌 오버레이를 보정하게 됩니다. 클린 영상에 맞춰 조정된base를 재사용하고, 그레이드가 오버레이를 틴트할 것으로 예상되므로 미묘한/보정용 룩(clean_neutral)만 사용하십시오.teal_orange/vintage_faded는 텍스트/악센트를 눈에 띄게 재색상화합니다. - 하나의 룩/LUT가 모든 관련 클립을 커버합니다. 룩을 선택한 후에는 모든 관련 클립(예: 클린 컷 + 오버레이 버전)에 동일한
looks.json/.cube를 재사용하십시오 — 클립별로 2단계 선택을 다시 실행하지 마십시오. - 보정 후 스타일링. 베이스는 데크의 S-Log3→Rec.709 단계의 일반화입니다. 일반 영상에서는 베이스가 WB+노출+대비이고, log 영상에서는 변환을 거기에 넣으십시오.
- 피부 우선.
teal_orange/cool_desat은 어두운 피사체에서 일반적인 피부 범죄자입니다 — 미드톤을 따뜻하게 유지하고, 섀도우/하이라이트/배경을 틴트하며,vibrance를 사용하십시오. U/V를 완전히 중립화하지 마십시오. - 레이블은 ffmpeg에 의해 오버레이된 PIL PNG입니다 —
drawtext를 사용하지 마십시오. Windows에서drawtext의fontfile경로(드라이브 콜론)는 신뢰할 수 없습니다. 폰트는gradelib.py상단에 설정됩니다. - 드라이브-콜론 경로는 경로를 사용하는 ffmpeg 필터그래프 옵션(
lut3d,metadata=print:file=)을 손상시킵니다. 스크립트는 파일의 폴더를cwd로 ffmpeg를 실행하고 기본 이름으로 참조합니다. 새로운 경로를 사용하는 필터에 대해 이 패턴을 유지하십시오. - 출력물은 전달한
--out디렉토리에 저장됩니다(지속적). 시스템 임시 디렉토리가 아닙니다. - 얼굴/피부 스트립은 얼굴 감지가 아닌 고정된 중앙 상단-중간 크롭을 사용합니다. 피사체가 대략 중앙에 있지 않으면
render_looks.py에--face-crop W:H:X:Y를 전달하십시오. 그렇지 않으면 피부 스트립이 잘못된 영역을 보여줍니다. apply_grade.py의 확인용 스팟 프레임은 지속 시간의 20% 지점에 있습니다 — 룩이 선택된 프레임과 다른 프레임입니다. 해당 20% 지점에 얼굴이 없으면 대신 다른 프레임에서 피부를 육안으로 확인하십시오(또는assess.py에--frame-time을 전달하여 선택된 프레임이 알려진 얼굴 순간이 되도록 하십시오).- LUT 굽기는 정확합니다. 여기의 모든 필터는 픽셀 단위 포인트 연산이기 때문입니다:
bake_lut.py는 33³ 아이덴티티 그리드를 실제 체인을 통해 밀어내고 자체 검사합니다(mean|d|는 1/255 미만이어야 합니다; 몇 레벨의max|d|는 단순한 보간입니다). - 항상 전달 전에 선택하십시오. 사람의 선택과 위임된 에이전트의 선택 모두 유효하지만, 계획에는 어떤 일이 발생했는지와 그 이유가 기록되어야 합니다.