test(wave118): 抽离chroma_key_config领域模型 + 45单测 #1013

Closed
xiaoxia wants to merge 1 commits from test/wave118-chroma-key-config into develop
3 changed files with 587 additions and 221 deletions
+19 -221
View File
@@ -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)
+287
View File
@@ -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()))
+281
View File
@@ -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