Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5cbac9a15d |
Executable
+470
@@ -0,0 +1,470 @@
|
||||
"""ChromaKeyConfig 绿幕抠像配置单测.
|
||||
|
||||
纯逻辑模块,覆盖:数据类、from_dict解析、from_preset预设、has_effect、
|
||||
validate校验、normalize_color颜色归一化、colorkey滤镜构建、
|
||||
chromakey滤镜构建、apply_chroma_key_if_needed便捷函数、get_preset_names。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from packages.domain.chroma_key_config import (
|
||||
CHROMA_KEY_PRESETS,
|
||||
ChromaKeyConfig,
|
||||
MAX_BLEND,
|
||||
MAX_SIMILARITY,
|
||||
MAX_SPILL_SUPPRESS,
|
||||
MIN_BLEND,
|
||||
MIN_SIMILARITY,
|
||||
MIN_SPILL_SUPPRESS,
|
||||
VALID_PRESETS,
|
||||
apply_chroma_key_if_needed,
|
||||
build_chromakey_filter,
|
||||
build_colorkey_filter,
|
||||
get_preset_names,
|
||||
normalize_color,
|
||||
)
|
||||
|
||||
|
||||
class TestChromaKeyConfigDefaults:
|
||||
def test_default_disabled(self):
|
||||
config = ChromaKeyConfig()
|
||||
assert config.enabled is False
|
||||
assert config.key_color == "#00FF00"
|
||||
assert config.similarity == 0.3
|
||||
assert config.blend == 0.1
|
||||
assert config.spill_suppress == 0.0
|
||||
|
||||
def test_default_has_no_effect(self):
|
||||
config = ChromaKeyConfig()
|
||||
assert config.has_effect() is False
|
||||
|
||||
def test_default_validate_passes(self):
|
||||
config = ChromaKeyConfig()
|
||||
ok, msg = config.validate()
|
||||
assert ok is True
|
||||
assert msg == ""
|
||||
|
||||
|
||||
class TestFromDict:
|
||||
def test_none_returns_disabled(self):
|
||||
config = ChromaKeyConfig.from_dict(None)
|
||||
assert config.enabled is False
|
||||
|
||||
def test_empty_dict_returns_disabled(self):
|
||||
config = ChromaKeyConfig.from_dict({})
|
||||
assert config.enabled is False
|
||||
|
||||
def test_enabled_false_returns_disabled(self):
|
||||
config = ChromaKeyConfig.from_dict({"enabled": False})
|
||||
assert config.enabled is False
|
||||
|
||||
def test_basic_enabled(self):
|
||||
config = ChromaKeyConfig.from_dict({"enabled": True, "key_color": "#FF0000"})
|
||||
assert config.enabled is True
|
||||
assert config.key_color == "#FF0000"
|
||||
|
||||
def test_default_values_when_enabled(self):
|
||||
config = ChromaKeyConfig.from_dict({"enabled": True})
|
||||
assert config.enabled is True
|
||||
assert config.key_color == "#00FF00"
|
||||
assert config.similarity == 0.3
|
||||
assert config.blend == 0.1
|
||||
assert config.spill_suppress == 0.0
|
||||
|
||||
def test_custom_values(self):
|
||||
config = ChromaKeyConfig.from_dict(
|
||||
{
|
||||
"enabled": True,
|
||||
"key_color": "#0000FF",
|
||||
"similarity": 0.5,
|
||||
"blend": 0.2,
|
||||
"spill_suppress": 0.3,
|
||||
}
|
||||
)
|
||||
assert config.key_color == "#0000FF"
|
||||
assert config.similarity == 0.5
|
||||
assert config.blend == 0.2
|
||||
assert config.spill_suppress == 0.3
|
||||
|
||||
def test_similarity_clamped_below_min(self):
|
||||
config = ChromaKeyConfig.from_dict({"enabled": True, "similarity": 0.0})
|
||||
assert config.similarity == MIN_SIMILARITY
|
||||
|
||||
def test_similarity_clamped_above_max(self):
|
||||
config = ChromaKeyConfig.from_dict({"enabled": True, "similarity": 2.0})
|
||||
assert config.similarity == MAX_SIMILARITY
|
||||
|
||||
def test_blend_clamped_below_min(self):
|
||||
config = ChromaKeyConfig.from_dict({"enabled": True, "blend": -1.0})
|
||||
assert config.blend == MIN_BLEND
|
||||
|
||||
def test_blend_clamped_above_max(self):
|
||||
config = ChromaKeyConfig.from_dict({"enabled": True, "blend": 2.0})
|
||||
assert config.blend == MAX_BLEND
|
||||
|
||||
def test_spill_suppress_clamped_below_min(self):
|
||||
config = ChromaKeyConfig.from_dict({"enabled": True, "spill_suppress": -0.5})
|
||||
assert config.spill_suppress == MIN_SPILL_SUPPRESS
|
||||
|
||||
def test_spill_suppress_clamped_above_max(self):
|
||||
config = ChromaKeyConfig.from_dict({"enabled": True, "spill_suppress": 2.0})
|
||||
assert config.spill_suppress == MAX_SPILL_SUPPRESS
|
||||
|
||||
def test_invalid_float_values_use_default(self):
|
||||
config = ChromaKeyConfig.from_dict(
|
||||
{
|
||||
"enabled": True,
|
||||
"similarity": "not_a_number",
|
||||
"blend": None,
|
||||
}
|
||||
)
|
||||
assert config.similarity == 0.3
|
||||
assert config.blend == 0.1
|
||||
|
||||
def test_key_color_stripped(self):
|
||||
config = ChromaKeyConfig.from_dict({"enabled": True, "key_color": " #FF0000 "})
|
||||
assert config.key_color == "#FF0000"
|
||||
|
||||
|
||||
class TestFromPreset:
|
||||
def test_green_screen_preset(self):
|
||||
config = ChromaKeyConfig.from_preset("green_screen")
|
||||
assert config is not None
|
||||
assert config.enabled is True
|
||||
assert config.key_color == "#00FF00"
|
||||
assert config.similarity == 0.3
|
||||
assert config.blend == 0.1
|
||||
assert config.spill_suppress == 0.5
|
||||
|
||||
def test_blue_screen_preset(self):
|
||||
config = ChromaKeyConfig.from_preset("blue_screen")
|
||||
assert config is not None
|
||||
assert config.key_color == "#0000FF"
|
||||
|
||||
def test_red_screen_preset(self):
|
||||
config = ChromaKeyConfig.from_preset("red_screen")
|
||||
assert config is not None
|
||||
assert config.key_color == "#FF0000"
|
||||
assert config.spill_suppress == 0.0
|
||||
|
||||
def test_invalid_preset_returns_none(self):
|
||||
config = ChromaKeyConfig.from_preset("nonexistent")
|
||||
assert config is None
|
||||
|
||||
def test_all_presets_are_valid(self):
|
||||
for name in CHROMA_KEY_PRESETS:
|
||||
config = ChromaKeyConfig.from_preset(name)
|
||||
assert config is not None
|
||||
assert config.enabled is True
|
||||
assert config.has_effect() is True
|
||||
ok, _ = config.validate()
|
||||
assert ok is True
|
||||
|
||||
|
||||
class TestHasEffect:
|
||||
def test_disabled_no_effect(self):
|
||||
config = ChromaKeyConfig(enabled=False)
|
||||
assert config.has_effect() is False
|
||||
|
||||
def test_enabled_but_zero_similarity(self):
|
||||
config = ChromaKeyConfig(enabled=True, similarity=0.0)
|
||||
assert config.has_effect() is False
|
||||
|
||||
def test_enabled_with_similarity_has_effect(self):
|
||||
config = ChromaKeyConfig(enabled=True, similarity=0.01)
|
||||
assert config.has_effect() is True
|
||||
|
||||
def test_full_config_has_effect(self):
|
||||
config = ChromaKeyConfig(
|
||||
enabled=True,
|
||||
key_color="#00FF00",
|
||||
similarity=0.3,
|
||||
blend=0.1,
|
||||
spill_suppress=0.5,
|
||||
)
|
||||
assert config.has_effect() is True
|
||||
|
||||
|
||||
class TestValidate:
|
||||
def test_disabled_valid(self):
|
||||
config = ChromaKeyConfig(enabled=False)
|
||||
ok, msg = config.validate()
|
||||
assert ok is True
|
||||
assert msg == ""
|
||||
|
||||
def test_valid_config(self):
|
||||
config = ChromaKeyConfig(
|
||||
enabled=True,
|
||||
key_color="#00FF00",
|
||||
similarity=0.3,
|
||||
blend=0.1,
|
||||
spill_suppress=0.5,
|
||||
)
|
||||
ok, msg = config.validate()
|
||||
assert ok is True
|
||||
assert msg == ""
|
||||
|
||||
def test_empty_key_color_invalid(self):
|
||||
config = ChromaKeyConfig(enabled=True, key_color="")
|
||||
ok, msg = config.validate()
|
||||
assert ok is False
|
||||
assert "key_color" in msg
|
||||
|
||||
def test_similarity_below_min_invalid(self):
|
||||
config = ChromaKeyConfig(enabled=True, similarity=0.001)
|
||||
ok, msg = config.validate()
|
||||
assert ok is False
|
||||
assert "similarity" in msg
|
||||
|
||||
def test_similarity_above_max_invalid(self):
|
||||
config = ChromaKeyConfig(enabled=True, similarity=1.5)
|
||||
ok, msg = config.validate()
|
||||
assert ok is False
|
||||
assert "similarity" in msg
|
||||
|
||||
def test_blend_below_min_invalid(self):
|
||||
config = ChromaKeyConfig(enabled=True, blend=-0.1)
|
||||
ok, msg = config.validate()
|
||||
assert ok is False
|
||||
assert "blend" in msg
|
||||
|
||||
def test_blend_above_max_invalid(self):
|
||||
config = ChromaKeyConfig(enabled=True, blend=1.5)
|
||||
ok, msg = config.validate()
|
||||
assert ok is False
|
||||
assert "blend" in msg
|
||||
|
||||
def test_spill_suppress_below_min_invalid(self):
|
||||
config = ChromaKeyConfig(enabled=True, spill_suppress=-0.1)
|
||||
ok, msg = config.validate()
|
||||
assert ok is False
|
||||
assert "spill_suppress" in msg
|
||||
|
||||
def test_spill_suppress_above_max_invalid(self):
|
||||
config = ChromaKeyConfig(enabled=True, spill_suppress=1.5)
|
||||
ok, msg = config.validate()
|
||||
assert ok is False
|
||||
assert "spill_suppress" in msg
|
||||
|
||||
def test_boundary_values_valid(self):
|
||||
config = ChromaKeyConfig(
|
||||
enabled=True,
|
||||
key_color="#000",
|
||||
similarity=MIN_SIMILARITY,
|
||||
blend=MIN_BLEND,
|
||||
spill_suppress=MIN_SPILL_SUPPRESS,
|
||||
)
|
||||
ok, _ = config.validate()
|
||||
assert ok is True
|
||||
|
||||
config2 = ChromaKeyConfig(
|
||||
enabled=True,
|
||||
key_color="#fff",
|
||||
similarity=MAX_SIMILARITY,
|
||||
blend=MAX_BLEND,
|
||||
spill_suppress=MAX_SPILL_SUPPRESS,
|
||||
)
|
||||
ok2, _ = config2.validate()
|
||||
assert ok2 is True
|
||||
|
||||
|
||||
class TestNormalizeColor:
|
||||
def test_hex_with_hash(self):
|
||||
assert normalize_color("#FF0000") == "0xFF0000"
|
||||
|
||||
def test_hex_without_hash(self):
|
||||
assert normalize_color("FF0000") == "0xFF0000"
|
||||
|
||||
def test_hex_lowercase(self):
|
||||
assert normalize_color("#ff0000") == "0xFF0000"
|
||||
|
||||
def test_hex_mixed_case(self):
|
||||
assert normalize_color("#aBcDeF") == "0xABCDEF"
|
||||
|
||||
def test_hex_with_alpha(self):
|
||||
assert normalize_color("#FF000080") == "0xFF0000"
|
||||
|
||||
def test_0x_format_passthrough(self):
|
||||
assert normalize_color("0xFF0000") == "0XFF0000"
|
||||
|
||||
def test_0x_lowercase(self):
|
||||
assert normalize_color("0xff0000") == "0XFF0000"
|
||||
|
||||
def test_color_name_passthrough(self):
|
||||
assert normalize_color("green") == "green"
|
||||
|
||||
def test_color_name_blue(self):
|
||||
assert normalize_color("blue") == "blue"
|
||||
|
||||
def test_whitespace_stripped(self):
|
||||
assert normalize_color(" #FF0000 ") == "0xFF0000"
|
||||
|
||||
def test_black_color(self):
|
||||
assert normalize_color("#000000") == "0x000000"
|
||||
|
||||
def test_white_color(self):
|
||||
assert normalize_color("#FFFFFF") == "0xFFFFFF"
|
||||
|
||||
|
||||
class TestBuildColorkeyFilter:
|
||||
def test_disabled_returns_copy(self):
|
||||
config = ChromaKeyConfig(enabled=False)
|
||||
result = build_colorkey_filter(config, "[0:v]", "[out]")
|
||||
assert result == "[0:v]copy[out]"
|
||||
|
||||
def test_zero_similarity_returns_copy(self):
|
||||
config = ChromaKeyConfig(enabled=True, similarity=0.0)
|
||||
result = build_colorkey_filter(config, "[v0]", "[ck]")
|
||||
assert result == "[v0]copy[ck]"
|
||||
|
||||
def test_basic_colorkey(self):
|
||||
config = ChromaKeyConfig(enabled=True, key_color="#00FF00", similarity=0.3, blend=0.1)
|
||||
result = build_colorkey_filter(config, "[0:v]", "[ck]")
|
||||
assert "colorkey=color=0x00FF00:similarity=0.3:blend=0.1" in result
|
||||
assert result.startswith("[0:v]")
|
||||
assert result.endswith("[ck]")
|
||||
|
||||
def test_with_spill_suppress(self):
|
||||
config = ChromaKeyConfig(
|
||||
enabled=True,
|
||||
key_color="#00FF00",
|
||||
similarity=0.3,
|
||||
blend=0.1,
|
||||
spill_suppress=0.5,
|
||||
)
|
||||
result = build_colorkey_filter(config, "[0:v]", "[ck]")
|
||||
assert "colorkey=" in result
|
||||
assert "colorchannelmixer=" in result
|
||||
# Spill suppress reduces green gain
|
||||
assert "gg=" in result
|
||||
assert "rr=" in result
|
||||
assert "bb=" in result
|
||||
|
||||
def test_no_spill_suppress_no_colorchannelmixer(self):
|
||||
config = ChromaKeyConfig(
|
||||
enabled=True,
|
||||
key_color="#00FF00",
|
||||
similarity=0.3,
|
||||
blend=0.1,
|
||||
spill_suppress=0.0,
|
||||
)
|
||||
result = build_colorkey_filter(config, "[0:v]", "[ck]")
|
||||
assert "colorchannelmixer" not in result
|
||||
|
||||
def test_different_labels(self):
|
||||
config = ChromaKeyConfig(enabled=True, key_color="#FF0000", similarity=0.5)
|
||||
result = build_colorkey_filter(config, "[v_in]", "[v_out]")
|
||||
assert result.startswith("[v_in]")
|
||||
assert result.endswith("[v_out]")
|
||||
|
||||
def test_spill_suppress_gain_values(self):
|
||||
config = ChromaKeyConfig(
|
||||
enabled=True,
|
||||
key_color="#00FF00",
|
||||
similarity=0.3,
|
||||
blend=0.1,
|
||||
spill_suppress=1.0,
|
||||
)
|
||||
result = build_colorkey_filter(config, "[0:v]", "[ck]")
|
||||
# At spill=1.0: g_gain = max(0.3, 1.0 - 1.0*0.7) = max(0.3, 0.3) = 0.3
|
||||
assert "gg=0.3" in result
|
||||
|
||||
|
||||
class TestBuildChromakeyFilter:
|
||||
def test_disabled_returns_copy(self):
|
||||
config = ChromaKeyConfig(enabled=False)
|
||||
result = build_chromakey_filter(config, "[0:v]", "[out]")
|
||||
assert result == "[0:v]copy[out]"
|
||||
|
||||
def test_basic_chromakey(self):
|
||||
config = ChromaKeyConfig(enabled=True, key_color="#0000FF", similarity=0.4, blend=0.2)
|
||||
result = build_chromakey_filter(config, "[0:v]", "[ck]")
|
||||
assert "chromakey=color=0x0000FF:similarity=0.4:blend=0.2" in result
|
||||
assert result.startswith("[0:v]")
|
||||
assert result.endswith("[ck]")
|
||||
|
||||
def test_spill_suppress_not_included(self):
|
||||
"""chromakey滤镜不包含spill_suppress(只有colorkey有)."""
|
||||
config = ChromaKeyConfig(
|
||||
enabled=True,
|
||||
key_color="#00FF00",
|
||||
similarity=0.3,
|
||||
blend=0.1,
|
||||
spill_suppress=0.5,
|
||||
)
|
||||
result = build_chromakey_filter(config, "[0:v]", "[ck]")
|
||||
assert "colorchannelmixer" not in result
|
||||
assert "chromakey=" in result
|
||||
|
||||
|
||||
class TestApplyChromaKeyIfNeeded:
|
||||
def test_none_clip_config_returns_none(self):
|
||||
result = apply_chroma_key_if_needed(None, "[0:v]", "[ck]")
|
||||
assert result is None
|
||||
|
||||
def test_no_chroma_key_returns_none(self):
|
||||
result = apply_chroma_key_if_needed({"other": "data"}, "[0:v]", "[ck]")
|
||||
assert result is None
|
||||
|
||||
def test_chroma_key_disabled_returns_none(self):
|
||||
config = {"chroma_key": {"enabled": False}}
|
||||
result = apply_chroma_key_if_needed(config, "[0:v]", "[ck]")
|
||||
assert result is None
|
||||
|
||||
def test_chroma_key_enabled_returns_filter(self):
|
||||
config = {
|
||||
"chroma_key": {
|
||||
"enabled": True,
|
||||
"key_color": "#00FF00",
|
||||
"similarity": 0.3,
|
||||
"blend": 0.1,
|
||||
}
|
||||
}
|
||||
result = apply_chroma_key_if_needed(config, "[0:v]", "[ck]")
|
||||
assert result is not None
|
||||
assert "colorkey=" in result
|
||||
assert result.startswith("[0:v]")
|
||||
assert result.endswith("[ck]")
|
||||
|
||||
def test_invalid_config_returns_none(self):
|
||||
"""配置异常时应该返回None而不是抛异常."""
|
||||
config = {"chroma_key": "invalid_data"}
|
||||
result = apply_chroma_key_if_needed(config, "[0:v]", "[ck]")
|
||||
assert result is None
|
||||
|
||||
|
||||
class TestGetPresetNames:
|
||||
def test_returns_sorted_list(self):
|
||||
names = get_preset_names()
|
||||
assert isinstance(names, list)
|
||||
assert names == sorted(names)
|
||||
|
||||
def test_contains_known_presets(self):
|
||||
names = get_preset_names()
|
||||
assert "green_screen" in names
|
||||
assert "blue_screen" in names
|
||||
assert "red_screen" in names
|
||||
|
||||
def test_count_matches_presets_dict(self):
|
||||
names = get_preset_names()
|
||||
assert len(names) == len(CHROMA_KEY_PRESETS)
|
||||
|
||||
|
||||
class TestPresetsAndConstants:
|
||||
def test_valid_presets_equals_preset_keys(self):
|
||||
assert VALID_PRESETS == set(CHROMA_KEY_PRESETS.keys())
|
||||
|
||||
def test_each_preset_has_required_keys(self):
|
||||
for name, preset in CHROMA_KEY_PRESETS.items():
|
||||
assert "key_color" in preset, f"{name} missing key_color"
|
||||
assert "similarity" in preset, f"{name} missing similarity"
|
||||
assert "blend" in preset, f"{name} missing blend"
|
||||
assert "spill_suppress" in preset, f"{name} missing spill_suppress"
|
||||
|
||||
def test_min_less_than_max(self):
|
||||
assert MIN_SIMILARITY < MAX_SIMILARITY
|
||||
assert MIN_BLEND <= MAX_BLEND
|
||||
assert MIN_SPILL_SUPPRESS <= MAX_SPILL_SUPPRESS
|
||||
|
||||
def test_min_similarity_positive(self):
|
||||
assert MIN_SIMILARITY > 0
|
||||
Reference in New Issue
Block a user