Files
Popiai-skill/social-media/hilo-promo/scripts/green_screen_keying.py
T

192 lines
6.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""
绿幕抠像脚本 - 三步法流水线
从 Kling Avatar 生成的绿幕视频输出透明 MOV
使用方式:
python green_screen_keying.py --input talking_head_green.mp4 --output pip_clean.mov
三步法:
1. ffmpeg chromakey 提取 RGBA 帧
2. Python PIL 后处理(硬阈值、平滑、Despill)
3. 组装透明 MOV
"""
import argparse
import os
import shutil
import subprocess
import sys
from pathlib import Path
import numpy as np
from PIL import Image, ImageFilter
# ─────────────────────────── 配置 ───────────────────────────
ALPHA_THRESHOLD = 120 # 硬阈值:消除半透明像素(保留更多发丝)
GAUSSIAN_BLUR_RADIUS = 1 # 轻微平滑(不要用 2,过度模糊)
DESPILL_OFFSET = 5 # 绿色抑制强度(+5 比 +10 更激进)
CHROMAKEY_SIMILARITY = 0.20 # ffmpeg chromakey similarity
CHROMAKEY_BLEND = 0.05 # ffmpeg chromakey blend(保守参数,避免人物透明)
TARGET_FPS = 24
# ─────────────────────────── 辅助函数 ───────────────────────────
def sample_green_color(frame_path: str) -> str:
"""用 PIL 采样实际背景绿色均值(不要写死颜色值)"""
img = Image.open(frame_path).convert("RGBA")
arr = np.array(img)
# 采样顶部区域(通常是纯绿幕背景)
top = arr[:100, :, :]
# 找出绿色像素(绿色分量大于红和蓝)
mask = (
(top[:, :, 1] > 100)
& (top[:, :, 1] > top[:, :, 0])
& (top[:, :, 1] > top[:, :, 2])
)
if mask.sum() == 0:
print("⚠️ 自动采样失败,使用默认绿色 #00FF00")
return "00FF00"
avg = top[mask].mean(axis=0).astype(int)
hex_color = f"{avg[0]:02X}{avg[1]:02X}{avg[2]:02X}"
print(f"📊 采样绿色均值: #{hex_color} (R={avg[0]}, G={avg[1]}, B={avg[2]})")
return hex_color
def process_frame(input_path: str, output_path: str) -> None:
"""
单帧处理:硬阈值 + 轻微平滑 + Despill
关键参数说明:
- Alpha 硬阈值 > 120 → 255:消除半透明像素,保留更多发丝
- 禁止 MinFilter:会严重损伤辫子/卷发等细节发丝
- GaussianBlur(1):轻微平滑,不过度模糊
- Despillg = min(g, avg(r,b) + 5):激进绿色抑制
"""
img = Image.open(input_path).convert("RGBA")
arr = np.array(img, dtype=np.float32)
r, g, b, a = arr[:, :, 0], arr[:, :, 1], arr[:, :, 2], arr[:, :, 3]
# 硬阈值:消除半透明像素
a_clean = np.where(a > ALPHA_THRESHOLD, 255.0, 0.0)
alpha_img = Image.fromarray(a_clean.astype(np.uint8), "L")
# 轻微平滑(避免锯齿,但不腐蚀)
alpha_smooth = alpha_img.filter(ImageFilter.GaussianBlur(GAUSSIAN_BLUR_RADIUS))
# 激进 Despill:钳制绿通道
avg_rb = (r + b) / 2.0
g_new = np.minimum(g, avg_rb + DESPILL_OFFSET)
# 组装 RGBA
result = np.stack(
[r, g_new, b, np.array(alpha_smooth, dtype=np.float32)], axis=-1
).astype(np.uint8)
Image.fromarray(result, "RGBA").save(output_path)
def run(args: argparse.Namespace) -> None:
input_path = Path(args.input)
output_path = Path(args.output)
temp_dir = Path("_temp_keying")
frames_dir = temp_dir / "frames"
clean_dir = temp_dir / "clean_frames"
# 清理临时目录
for d in [frames_dir, clean_dir]:
if d.exists():
shutil.rmtree(d)
d.mkdir(parents=True, exist_ok=True)
# ── Step 1: 提取首帧用于采样绿色 ──
print("\n📸 Step 1: 提取首帧...")
subprocess.run(
[
"ffmpeg",
"-i",
str(input_path),
"-vframes",
"1",
"-q:v",
"2",
str(temp_dir / "first_frame.png"),
],
check=True,
capture_output=True,
)
# 采样绿色
green_hex = sample_green_color(str(temp_dir / "first_frame.png"))
# ── Step 2: ffmpeg chromakey 提取 RGBA 帧 ──
print(f"\n🎬 Step 2: ffmpeg chromakey 提取帧(green=#{green_hex}...")
subprocess.run(
[
"ffmpeg",
"-i",
str(input_path),
"-vf",
f"chromakey=0x{green_hex}:{CHROMAKEY_SIMILARITY}:{CHROMAKEY_BLEND},"
f"format=rgba,fps={TARGET_FPS}",
"-q:v",
"2",
str(frames_dir / "frame_%04d.png"),
],
check=True,
capture_output=True,
)
frame_files = sorted(frames_dir.glob("frame_*.png"))
total = len(frame_files)
print(f" 共提取 {total} 帧")
# ── Step 3: Python PIL 后处理 + 组装 ──
print(
f"\n🧹 Step 3: Python 后处理(阈值={ALPHA_THRESHOLD}, "
f"Blur={GAUSSIAN_BLUR_RADIUS}, Despill=+{DESPILL_OFFSET}..."
)
for i, frame_file in enumerate(frame_files, 1):
if i % 50 == 0 or i == 1 or i == total:
print(f" 处理帧 {i}/{total}...")
process_frame(str(frame_file), str(clean_dir / frame_file.name))
# ── Step 4: 组装透明 MOV ──
print("\n🎞️ Step 4: 组装透明 MOV...")
subprocess.run(
[
"ffmpeg",
"-framerate",
str(TARGET_FPS),
"-i",
str(clean_dir / "frame_%04d.png"),
"-c:v",
"png",
"-pix_fmt",
"rgba",
str(output_path),
],
check=True,
capture_output=True,
)
# 清理临时文件
shutil.rmtree(temp_dir)
print(f"\n✅ 完成!输出:{output_path}")
duration = total / TARGET_FPS
print(f" 时长: {duration:.2f}s | 帧数: {total} | 帧率: {TARGET_FPS}fps")
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="绿幕抠像三步法流水线")
parser.add_argument("--input", "-i", required=True, help="输入绿幕视频路径")
parser.add_argument("--output", "-o", required=True, help="输出透明 MOV 路径")
run(parser.parse_args())