Files
xiaoxia-saas/packages/domain/chroma_key_config.py

288 lines
9.0 KiB
Python
Executable File
Raw Permalink 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.
"""绿幕抠像配置领域模型 — 纯逻辑,无FFmpeg依赖.
抽离自 chroma_key_engine.py,包含:
- ChromaKeyConfig 数据类(解析/钳制/效果判断)
- 预设配置(绿幕/蓝幕/红幕等)
- 颜色归一化
- colorkey / chromakey 滤镜构建
- 便捷函数(apply_chroma_key_if_needed
"""
from __future__ import annotations
import logging
import re
from dataclasses import dataclass
from typing import Any
logger = logging.getLogger(__name__)
# ── 预设配置 ──────────────────────────────────────────────────────────────────
# 常见绿幕/蓝幕预设
CHROMA_KEY_PRESETS: dict[str, dict[str, Any]] = {
"green_screen": {
"key_color": "#00FF00",
"similarity": 0.3,
"blend": 0.1,
"spill_suppress": 0.5,
},
"blue_screen": {
"key_color": "#0000FF",
"similarity": 0.3,
"blend": 0.1,
"spill_suppress": 0.5,
},
"red_screen": {
"key_color": "#FF0000",
"similarity": 0.3,
"blend": 0.1,
"spill_suppress": 0.0,
},
"precise_green": {
"key_color": "#00FF00",
"similarity": 0.2,
"blend": 0.05,
"spill_suppress": 0.3,
},
"soft_green": {
"key_color": "#00FF00",
"similarity": 0.45,
"blend": 0.2,
"spill_suppress": 0.5,
},
}
VALID_PRESETS = set(CHROMA_KEY_PRESETS.keys())
# 参数范围
MIN_SIMILARITY = 0.01
MAX_SIMILARITY = 1.0
MIN_BLEND = 0.0
MAX_BLEND = 1.0
MIN_SPILL_SUPPRESS = 0.0
MAX_SPILL_SUPPRESS = 1.0
# 默认值
DEFAULT_KEY_COLOR = "#00FF00"
DEFAULT_SIMILARITY = 0.3
DEFAULT_BLEND = 0.1
DEFAULT_SPILL_SUPPRESS = 0.0
# ── 配置模型 ──────────────────────────────────────────────────────────────────
@dataclass
class ChromaKeyConfig:
"""绿幕抠像配置.
Attributes:
enabled: 是否启用抠像
key_color: 要抠除的颜色,支持 hex 格式(如 "#00FF00")或颜色名
similarity: 颜色相似度阈值 0.01~1.0,值越大抠除范围越大
blend: 边缘平滑/混合度 0.0~1.0,值越大边缘越柔和
spill_suppress: 溢色抑制 0.0~1.0,减少边缘的绿幕反光
"""
enabled: bool = False
key_color: str = DEFAULT_KEY_COLOR
similarity: float = DEFAULT_SIMILARITY
blend: float = DEFAULT_BLEND
spill_suppress: float = DEFAULT_SPILL_SUPPRESS
@classmethod
def from_dict(cls, data: dict[str, Any] | None) -> ChromaKeyConfig:
"""从字典解析配置,参数越界自动钳制."""
if not data or not data.get("enabled", False):
return cls(enabled=False)
key_color = str(data.get("key_color", DEFAULT_KEY_COLOR)).strip()
def _safe_float(val: Any, default: float) -> float:
try:
return float(val)
except (TypeError, ValueError):
return default
similarity = _safe_float(data.get("similarity", DEFAULT_SIMILARITY), DEFAULT_SIMILARITY)
blend = _safe_float(data.get("blend", DEFAULT_BLEND), DEFAULT_BLEND)
spill_suppress = _safe_float(data.get("spill_suppress", DEFAULT_SPILL_SUPPRESS), DEFAULT_SPILL_SUPPRESS)
# 钳制到合法范围
similarity = max(MIN_SIMILARITY, min(MAX_SIMILARITY, similarity))
blend = max(MIN_BLEND, min(MAX_BLEND, blend))
spill_suppress = max(MIN_SPILL_SUPPRESS, min(MAX_SPILL_SUPPRESS, spill_suppress))
return cls(
enabled=True,
key_color=key_color,
similarity=similarity,
blend=blend,
spill_suppress=spill_suppress,
)
@classmethod
def from_preset(cls, preset_name: str) -> ChromaKeyConfig | None:
"""从预设名称创建配置."""
preset = CHROMA_KEY_PRESETS.get(preset_name)
if not preset:
return None
return cls(
enabled=True,
key_color=preset["key_color"],
similarity=preset["similarity"],
blend=preset["blend"],
spill_suppress=preset["spill_suppress"],
)
def has_effect(self) -> bool:
"""判断是否有实际抠像效果."""
return self.enabled and self.similarity > 0
def validate(self) -> tuple[bool, str]:
"""校验配置是否有效."""
if not self.enabled:
return True, ""
if not self.key_color:
return False, "key_color 不能为空"
if not (MIN_SIMILARITY <= self.similarity <= MAX_SIMILARITY):
return False, f"similarity 必须在 {MIN_SIMILARITY}~{MAX_SIMILARITY} 之间"
if not (MIN_BLEND <= self.blend <= MAX_BLEND):
return False, f"blend 必须在 {MIN_BLEND}~{MAX_BLEND} 之间"
if not (MIN_SPILL_SUPPRESS <= self.spill_suppress <= MAX_SPILL_SUPPRESS):
return False, f"spill_suppress 必须在 {MIN_SPILL_SUPPRESS}~{MAX_SPILL_SUPPRESS} 之间"
return True, ""
# ── 颜色归一化 ────────────────────────────────────────────────────────────────
def normalize_color(color_str: str) -> str:
"""将颜色字符串转为 FFmpeg colorkey 接受的格式.
支持:
- "#RRGGBB" / "#RRGGBBAA" → 0xRRGGBB
- "0xRRGGBB" → 直接使用
- 颜色名(green/blue/red/black/white 等)→ 直接透传
"""
color = color_str.strip()
# hex 格式
hex_match = re.match(r"^#?([0-9a-fA-F]{6})([0-9a-fA-F]{2})?$", color)
if hex_match:
return f"0x{hex_match.group(1).upper()}"
# 已经是 0x 格式
if color.lower().startswith("0x"):
return color.upper()
# 颜色名直接透传(FFmpeg 支持常见颜色名)
return color
# ── 滤镜构建 ────────────────────────────────────────────────────────────────
def build_colorkey_filter(
config: ChromaKeyConfig,
input_label: str,
output_label: str,
) -> str:
"""构建 colorkey 滤镜字符串.
Args:
config: 抠像配置
input_label: 输入标签,如 "[0:v]" 或 "[v0]"
output_label: 输出标签,如 "[ck0]"
Returns:
FFmpeg 滤镜字符串
"""
if not config.has_effect():
return f"{input_label}copy{output_label}"
color = normalize_color(config.key_color)
similarity = config.similarity
blend = config.blend
# 基础 colorkey 滤镜
parts = [f"colorkey=color={color}:similarity={similarity}:blend={blend}"]
# 溢色抑制(通过 colorchannelmixer 降低绿色通道增益)
if config.spill_suppress > 0:
spill = config.spill_suppress
g_gain = max(0.3, 1.0 - spill * 0.7)
r_gain = 1.0 + spill * 0.15
b_gain = 1.0 + spill * 0.15
parts.append(f"colorchannelmixer=rr={r_gain}:gg={g_gain}:bb={b_gain}:aa=1")
return f"{input_label}{','.join(parts)}{output_label}"
def build_chromakey_filter(
config: ChromaKeyConfig,
input_label: str,
output_label: str,
) -> str:
"""使用 chromakey 滤镜(更高级的版本,支持更多参数).
注意:并非所有 FFmpeg 版本都支持 chromakey 滤镜,
优先使用 colorkey(兼容性更好)。
"""
if not config.has_effect():
return f"{input_label}copy{output_label}"
color = normalize_color(config.key_color)
similarity = config.similarity
blend = config.blend
return f"{input_label}chromakey=color={color}:similarity={similarity}:blend={blend}{output_label}"
# ── 工具函数 ────────────────────────────────────────────────────────────────
def apply_chroma_key_if_needed(
clip_config: dict[str, Any] | None,
input_label: str,
output_label: str,
) -> str | None:
"""便捷函数:根据 clip 配置判断是否需要应用绿幕抠像.
Args:
clip_config: clip 的 config 字典
input_label: 输入标签
output_label: 输出标签
Returns:
滤镜字符串,不需要抠像时返回 None
"""
if not clip_config:
return None
chroma_key_data = clip_config.get("chroma_key")
if not chroma_key_data:
return None
try:
config = ChromaKeyConfig.from_dict(chroma_key_data)
if not config.has_effect():
return None
return build_colorkey_filter(config, input_label, output_label)
except Exception as e:
logger.warning("[chroma-key] 应用抠像失败,跳过: %s", e)
return None
def get_preset_names() -> list[str]:
"""获取所有预设名称列表."""
return sorted(list(CHROMA_KEY_PRESETS.keys()))