Popiai-skill仓库
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

191 lines
6.1 KiB

#!/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):轻微平滑,不过度模糊
- Despill:g = 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())