test(wave118): 抽离chroma_key_config领域模型 + 45单测 #1013
@@ -2,129 +2,31 @@
|
||||
|
||||
支持将指定颜色(默认绿色)变为透明,可用于虚拟背景、画中画背景替换等场景。
|
||||
|
||||
使用方式:
|
||||
config = ChromaKeyConfig(key_color="#00FF00", similarity=0.3, blend=0.1)
|
||||
engine = ChromaKeyEngine(config)
|
||||
filter_str = engine.build_filter(input_label, output_label)
|
||||
# 结果: [in]colorkey=color=0x00FF00:similarity=0.3:blend=0.1[out]
|
||||
|
||||
降级策略:
|
||||
- 参数越界自动钳制
|
||||
- 素材格式不支持时跳过(调用方捕获异常)
|
||||
注:核心领域模型已抽离到 packages/domain/chroma_key_config.py,
|
||||
本模块保留薄包装层,确保向后兼容。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import re
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional
|
||||
|
||||
from packages.domain.chroma_key_config import ( # noqa: F401 — 向后兼容
|
||||
CHROMA_KEY_PRESETS,
|
||||
ChromaKeyConfig,
|
||||
apply_chroma_key_if_needed,
|
||||
build_chromakey_filter as _build_chromakey_filter_base,
|
||||
build_colorkey_filter as _build_colorkey_filter_base,
|
||||
normalize_color as _normalize_color_base,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# ── 配置模型 ──────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@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 = "#00FF00"
|
||||
similarity: float = 0.3
|
||||
blend: float = 0.1
|
||||
spill_suppress: float = 0.0
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: dict | None) -> "ChromaKeyConfig":
|
||||
"""从字典解析配置,参数越界自动钳制。"""
|
||||
if not data or not data.get("enabled", False):
|
||||
return cls(enabled=False)
|
||||
|
||||
key_color = str(data.get("key_color", "#00FF00")).strip()
|
||||
|
||||
def _safe_float(val, default):
|
||||
try:
|
||||
return float(val)
|
||||
except (TypeError, ValueError):
|
||||
return default
|
||||
|
||||
similarity = _safe_float(data.get("similarity", 0.3), 0.3)
|
||||
blend = _safe_float(data.get("blend", 0.1), 0.1)
|
||||
spill_suppress = _safe_float(data.get("spill_suppress", 0.0), 0.0)
|
||||
|
||||
# 钳制到合法范围
|
||||
similarity = max(0.01, min(1.0, similarity))
|
||||
blend = max(0.0, min(1.0, blend))
|
||||
spill_suppress = max(0.0, min(1.0, spill_suppress))
|
||||
|
||||
return cls(
|
||||
enabled=True,
|
||||
key_color=key_color,
|
||||
similarity=similarity,
|
||||
blend=blend,
|
||||
spill_suppress=spill_suppress,
|
||||
)
|
||||
|
||||
def has_effect(self) -> bool:
|
||||
"""判断是否有实际抠像效果。"""
|
||||
return self.enabled and self.similarity > 0
|
||||
|
||||
|
||||
# ── 预设配置 ──────────────────────────────────────────────────────────────────
|
||||
|
||||
# 常见绿幕/蓝幕预设
|
||||
CHROMA_KEY_PRESETS = {
|
||||
"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,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
# ── 引擎实现 ──────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
class ChromaKeyEngine:
|
||||
"""绿幕抠像引擎。
|
||||
"""绿幕抠像引擎.
|
||||
|
||||
基于 FFmpeg colorkey 滤镜实现,将指定颜色变为透明。
|
||||
适用于绿幕/蓝幕视频的背景去除,配合画中画或 overlay 实现虚拟背景。
|
||||
薄包装层,实际逻辑委托给 packages.domain.chroma_key_config。
|
||||
"""
|
||||
|
||||
def __init__(self, config: ChromaKeyConfig):
|
||||
@@ -132,117 +34,13 @@ class ChromaKeyEngine:
|
||||
|
||||
@staticmethod
|
||||
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
|
||||
"""将颜色字符串转为 FFmpeg colorkey 接受的格式."""
|
||||
return _normalize_color_base(color_str)
|
||||
|
||||
def build_filter(self, input_label: str, output_label: str) -> str:
|
||||
"""构建 colorkey 滤镜字符串。
|
||||
|
||||
Args:
|
||||
input_label: 输入标签,如 "[0:v]" 或 "[v0]"
|
||||
output_label: 输出标签,如 "[ck0]"
|
||||
|
||||
Returns:
|
||||
FFmpeg 滤镜字符串,如 "[v0]colorkey=color=0x00FF00:similarity=0.3:blend=0.1[ck0]"
|
||||
|
||||
Raises:
|
||||
ValueError: 配置无效时抛出(调用方应捕获并降级)
|
||||
"""
|
||||
if not self.config.has_effect():
|
||||
# 无效果,直接直通
|
||||
return f"{input_label}copy{output_label}"
|
||||
|
||||
color = self._normalize_color(self.config.key_color)
|
||||
similarity = self.config.similarity
|
||||
blend = self.config.blend
|
||||
|
||||
# 基础 colorkey 滤镜
|
||||
parts = [f"colorkey=color={color}:similarity={similarity}:blend={blend}"]
|
||||
|
||||
# 溢色抑制(通过 colorchannelmixer 降低绿色通道增益)
|
||||
if self.config.spill_suppress > 0:
|
||||
# 降低绿通道增益,减少绿幕反光溢出
|
||||
spill = self.config.spill_suppress
|
||||
# 绿通道增益 = 1 - spill_factor
|
||||
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=" f"rr={r_gain}:" f"gg={g_gain}:" f"bb={b_gain}:" f"aa=1")
|
||||
|
||||
filter_str = f"{input_label}{','.join(parts)}{output_label}"
|
||||
return filter_str
|
||||
"""构建 colorkey 滤镜字符串."""
|
||||
return _build_colorkey_filter_base(self.config, input_label, output_label)
|
||||
|
||||
def build_filter_chromakey(self, input_label: str, output_label: str) -> str:
|
||||
"""使用 chromakey 滤镜(更高级的版本,支持更多参数)。
|
||||
|
||||
注意:并非所有 FFmpeg 版本都支持 chromakey 滤镜,
|
||||
优先使用 colorkey(兼容性更好)。
|
||||
|
||||
Args:
|
||||
input_label: 输入标签
|
||||
output_label: 输出标签
|
||||
|
||||
Returns:
|
||||
FFmpeg 滤镜字符串
|
||||
"""
|
||||
if not self.config.has_effect():
|
||||
return f"{input_label}copy{output_label}"
|
||||
|
||||
color = self._normalize_color(self.config.key_color)
|
||||
similarity = self.config.similarity
|
||||
blend = self.config.blend
|
||||
|
||||
return f"{input_label}" f"chromakey=color={color}:similarity={similarity}:blend={blend}" f"{output_label}"
|
||||
|
||||
|
||||
def apply_chroma_key_if_needed(
|
||||
clip_config: dict | None,
|
||||
input_label: str,
|
||||
output_label: str,
|
||||
) -> Optional[str]:
|
||||
"""便捷函数:根据 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
|
||||
|
||||
engine = ChromaKeyEngine(config)
|
||||
return engine.build_filter(input_label, output_label)
|
||||
except Exception as e:
|
||||
logger.warning("[chroma-key] 应用抠像失败,跳过: %s", e)
|
||||
return None
|
||||
"""使用 chromakey 滤镜(更高级的版本,支持更多参数)."""
|
||||
return _build_chromakey_filter_base(self.config, input_label, output_label)
|
||||
|
||||
Executable
+287
@@ -0,0 +1,287 @@
|
||||
"""绿幕抠像配置领域模型 — 纯逻辑,无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()))
|
||||
Executable
+281
@@ -0,0 +1,281 @@
|
||||
"""chroma_key_config 领域模型单测."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from packages.domain.chroma_key_config import (
|
||||
CHROMA_KEY_PRESETS,
|
||||
ChromaKeyConfig,
|
||||
apply_chroma_key_if_needed,
|
||||
build_chromakey_filter,
|
||||
build_colorkey_filter,
|
||||
get_preset_names,
|
||||
normalize_color,
|
||||
)
|
||||
|
||||
# ── ChromaKeyConfig.from_dict 测试 ────────────────────────────────────────
|
||||
|
||||
|
||||
class TestChromaKeyConfigFromDict:
|
||||
def test_none_returns_disabled(self):
|
||||
cfg = ChromaKeyConfig.from_dict(None)
|
||||
assert cfg.enabled is False
|
||||
|
||||
def test_empty_dict_returns_disabled(self):
|
||||
cfg = ChromaKeyConfig.from_dict({})
|
||||
assert cfg.enabled is False
|
||||
|
||||
def test_disabled_returns_disabled(self):
|
||||
cfg = ChromaKeyConfig.from_dict({"enabled": False})
|
||||
assert cfg.enabled is False
|
||||
|
||||
def test_enabled_default_params(self):
|
||||
cfg = ChromaKeyConfig.from_dict({"enabled": True})
|
||||
assert cfg.enabled is True
|
||||
assert cfg.key_color == "#00FF00"
|
||||
assert cfg.similarity == 0.3
|
||||
assert cfg.blend == 0.1
|
||||
assert cfg.spill_suppress == 0.0
|
||||
|
||||
def test_custom_params(self):
|
||||
cfg = ChromaKeyConfig.from_dict(
|
||||
{
|
||||
"enabled": True,
|
||||
"key_color": "#0000FF",
|
||||
"similarity": 0.5,
|
||||
"blend": 0.2,
|
||||
"spill_suppress": 0.4,
|
||||
}
|
||||
)
|
||||
assert cfg.key_color == "#0000FF"
|
||||
assert cfg.similarity == 0.5
|
||||
assert cfg.blend == 0.2
|
||||
assert cfg.spill_suppress == 0.4
|
||||
|
||||
def test_similarity_clamped_low(self):
|
||||
cfg = ChromaKeyConfig.from_dict({"enabled": True, "similarity": 0.001})
|
||||
assert cfg.similarity == 0.01
|
||||
|
||||
def test_similarity_clamped_high(self):
|
||||
cfg = ChromaKeyConfig.from_dict({"enabled": True, "similarity": 2.0})
|
||||
assert cfg.similarity == 1.0
|
||||
|
||||
def test_blend_clamped_low(self):
|
||||
cfg = ChromaKeyConfig.from_dict({"enabled": True, "blend": -0.5})
|
||||
assert cfg.blend == 0.0
|
||||
|
||||
def test_blend_clamped_high(self):
|
||||
cfg = ChromaKeyConfig.from_dict({"enabled": True, "blend": 1.5})
|
||||
assert cfg.blend == 1.0
|
||||
|
||||
def test_spill_suppress_clamped(self):
|
||||
cfg = ChromaKeyConfig.from_dict({"enabled": True, "spill_suppress": 2.0})
|
||||
assert cfg.spill_suppress == 1.0
|
||||
|
||||
def test_invalid_similarity_type_uses_default(self):
|
||||
cfg = ChromaKeyConfig.from_dict({"enabled": True, "similarity": "high"})
|
||||
assert cfg.similarity == 0.3
|
||||
|
||||
def test_key_color_stripped(self):
|
||||
cfg = ChromaKeyConfig.from_dict({"enabled": True, "key_color": " #00FF00 "})
|
||||
assert cfg.key_color == "#00FF00"
|
||||
|
||||
|
||||
# ── from_preset 测试 ──────────────────────────────────────────────────────
|
||||
|
||||
|
||||
class TestFromPreset:
|
||||
def test_green_screen_preset(self):
|
||||
cfg = ChromaKeyConfig.from_preset("green_screen")
|
||||
assert cfg is not None
|
||||
assert cfg.enabled is True
|
||||
assert cfg.key_color == "#00FF00"
|
||||
assert cfg.similarity == 0.3
|
||||
|
||||
def test_blue_screen_preset(self):
|
||||
cfg = ChromaKeyConfig.from_preset("blue_screen")
|
||||
assert cfg is not None
|
||||
assert cfg.key_color == "#0000FF"
|
||||
|
||||
def test_invalid_preset_returns_none(self):
|
||||
assert ChromaKeyConfig.from_preset("nonexistent") is None
|
||||
|
||||
def test_all_presets_valid(self):
|
||||
for name in CHROMA_KEY_PRESETS:
|
||||
cfg = ChromaKeyConfig.from_preset(name)
|
||||
assert cfg is not None
|
||||
assert cfg.enabled is True
|
||||
|
||||
|
||||
# ── has_effect / validate 测试 ────────────────────────────────────────────
|
||||
|
||||
|
||||
class TestHasEffectAndValidate:
|
||||
def test_disabled_no_effect(self):
|
||||
cfg = ChromaKeyConfig(enabled=False)
|
||||
assert cfg.has_effect() is False
|
||||
|
||||
def test_enabled_has_effect(self):
|
||||
cfg = ChromaKeyConfig(enabled=True, similarity=0.3)
|
||||
assert cfg.has_effect() is True
|
||||
|
||||
def test_zero_similarity_no_effect(self):
|
||||
cfg = ChromaKeyConfig(enabled=True, similarity=0.0)
|
||||
# similarity 被钳制后为 0.01,所以应该有效果
|
||||
# 等等,from_dict 才会钳制,直接构造不会
|
||||
assert cfg.has_effect() is False
|
||||
|
||||
def test_validate_disabled_valid(self):
|
||||
cfg = ChromaKeyConfig(enabled=False)
|
||||
ok, msg = cfg.validate()
|
||||
assert ok is True
|
||||
assert msg == ""
|
||||
|
||||
def test_validate_enabled_valid(self):
|
||||
cfg = ChromaKeyConfig(enabled=True, key_color="#00FF00")
|
||||
ok, msg = cfg.validate()
|
||||
assert ok is True
|
||||
|
||||
def test_validate_empty_color_invalid(self):
|
||||
cfg = ChromaKeyConfig(enabled=True, key_color="")
|
||||
ok, msg = cfg.validate()
|
||||
assert ok is False
|
||||
assert "key_color" in msg
|
||||
|
||||
def test_validate_similarity_out_of_range(self):
|
||||
cfg = ChromaKeyConfig(enabled=True, similarity=2.0)
|
||||
ok, msg = cfg.validate()
|
||||
assert ok is False
|
||||
assert "similarity" in msg
|
||||
|
||||
|
||||
# ── normalize_color 测试 ──────────────────────────────────────────────────
|
||||
|
||||
|
||||
class TestNormalizeColor:
|
||||
def test_hex_with_hash(self):
|
||||
assert normalize_color("#00FF00") == "0x00FF00"
|
||||
|
||||
def test_hex_lowercase(self):
|
||||
assert normalize_color("#00ff00") == "0x00FF00"
|
||||
|
||||
def test_hex_without_hash(self):
|
||||
assert normalize_color("00FF00") == "0x00FF00"
|
||||
|
||||
def test_hex_with_alpha(self):
|
||||
assert normalize_color("#00FF00FF") == "0x00FF00"
|
||||
|
||||
def test_already_0x_format(self):
|
||||
assert normalize_color("0x00FF00") == "0X00FF00"
|
||||
|
||||
def test_0x_lowercase(self):
|
||||
assert normalize_color("0x00ff00") == "0X00FF00"
|
||||
|
||||
def test_color_name_passthrough(self):
|
||||
assert normalize_color("green") == "green"
|
||||
assert normalize_color("blue") == "blue"
|
||||
|
||||
def test_whitespace_stripped(self):
|
||||
assert normalize_color(" #FF0000 ") == "0xFF0000"
|
||||
|
||||
|
||||
# ── build_colorkey_filter 测试 ────────────────────────────────────────────
|
||||
|
||||
|
||||
class TestBuildColorkeyFilter:
|
||||
def test_disabled_returns_copy(self):
|
||||
cfg = ChromaKeyConfig(enabled=False)
|
||||
result = build_colorkey_filter(cfg, "[in]", "[out]")
|
||||
assert "copy" in result
|
||||
assert "[in]" in result
|
||||
assert "[out]" in result
|
||||
|
||||
def test_basic_colorkey(self):
|
||||
cfg = ChromaKeyConfig(enabled=True, key_color="#00FF00", similarity=0.3, blend=0.1)
|
||||
result = build_colorkey_filter(cfg, "[v]", "[ck]")
|
||||
assert "colorkey=" in result
|
||||
assert "color=0x00FF00" in result
|
||||
assert "similarity=0.3" in result
|
||||
assert "blend=0.1" in result
|
||||
assert "[v]" in result
|
||||
assert "[ck]" in result
|
||||
|
||||
def test_with_spill_suppress(self):
|
||||
cfg = ChromaKeyConfig(enabled=True, key_color="#00FF00", spill_suppress=0.5)
|
||||
result = build_colorkey_filter(cfg, "[in]", "[out]")
|
||||
assert "colorchannelmixer=" in result
|
||||
assert "rr=" in result
|
||||
assert "gg=" in result
|
||||
assert "bb=" in result
|
||||
|
||||
def test_no_spill_suppress_no_colorchannelmixer(self):
|
||||
cfg = ChromaKeyConfig(enabled=True, spill_suppress=0.0)
|
||||
result = build_colorkey_filter(cfg, "[in]", "[out]")
|
||||
assert "colorchannelmixer" not in result
|
||||
|
||||
|
||||
# ── build_chromakey_filter 测试 ───────────────────────────────────────────
|
||||
|
||||
|
||||
class TestBuildChromakeyFilter:
|
||||
def test_disabled_returns_copy(self):
|
||||
cfg = ChromaKeyConfig(enabled=False)
|
||||
result = build_chromakey_filter(cfg, "[in]", "[out]")
|
||||
assert "copy" in result
|
||||
|
||||
def test_basic_chromakey(self):
|
||||
cfg = ChromaKeyConfig(enabled=True, key_color="#00FF00", similarity=0.3, blend=0.1)
|
||||
result = build_chromakey_filter(cfg, "[v]", "[ck]")
|
||||
assert "chromakey=" in result
|
||||
assert "color=0x00FF00" in result
|
||||
assert "similarity=0.3" in result
|
||||
assert "blend=0.1" in result
|
||||
|
||||
def test_contains_input_and_output_labels(self):
|
||||
cfg = ChromaKeyConfig(enabled=True)
|
||||
result = build_chromakey_filter(cfg, "[in_v]", "[out_v]")
|
||||
assert "[in_v]" in result
|
||||
assert "[out_v]" in result
|
||||
|
||||
|
||||
# ── apply_chroma_key_if_needed 测试 ───────────────────────────────────────
|
||||
|
||||
|
||||
class TestApplyChromaKeyIfNeeded:
|
||||
def test_none_config_returns_none(self):
|
||||
assert apply_chroma_key_if_needed(None, "[in]", "[out]") is None
|
||||
|
||||
def test_no_chroma_key_returns_none(self):
|
||||
assert apply_chroma_key_if_needed({}, "[in]", "[out]") is None
|
||||
|
||||
def test_disabled_chroma_key_returns_none(self):
|
||||
config = {"chroma_key": {"enabled": False}}
|
||||
assert apply_chroma_key_if_needed(config, "[in]", "[out]") is None
|
||||
|
||||
def test_enabled_chroma_key_returns_filter(self):
|
||||
config = {"chroma_key": {"enabled": True, "key_color": "#00FF00"}}
|
||||
result = apply_chroma_key_if_needed(config, "[in]", "[out]")
|
||||
assert result is not None
|
||||
assert "colorkey" in result
|
||||
|
||||
def test_invalid_config_handles_exception(self):
|
||||
# 传入无效配置触发异常,应该返回 None 而不是抛出
|
||||
config = {"chroma_key": "invalid_string"}
|
||||
result = apply_chroma_key_if_needed(config, "[in]", "[out]")
|
||||
assert result is None
|
||||
|
||||
|
||||
# ── 预设工具函数测试 ───────────────────────────────────────────────────────
|
||||
|
||||
|
||||
class TestPresetUtils:
|
||||
def test_get_preset_names_returns_sorted_list(self):
|
||||
names = get_preset_names()
|
||||
assert isinstance(names, list)
|
||||
assert len(names) == len(CHROMA_KEY_PRESETS)
|
||||
assert names == sorted(names)
|
||||
|
||||
def test_all_preset_names_in_presets_dict(self):
|
||||
for name in get_preset_names():
|
||||
assert name in CHROMA_KEY_PRESETS
|
||||
Reference in New Issue
Block a user