86 lines
3.4 KiB
Python
86 lines
3.4 KiB
Python
import math
|
|
|
|
import pytest
|
|
|
|
from smk_playground.features.color import hex_to_oklab, oklab_to_oklch
|
|
from smk_playground.features.extract import (
|
|
analytical_palette,
|
|
extract_artwork,
|
|
palette_features,
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("hex_value", "expected"),
|
|
[
|
|
("#ff0000", (0.62795536, 0.22486306, 0.12584630)),
|
|
("#00ff00", (0.86643961, -0.23388757, 0.17949848)),
|
|
("#0000ff", (0.45201372, -0.03245698, -0.31152815)),
|
|
],
|
|
)
|
|
def test_known_srgb_oklab_reference_colors(hex_value: str, expected: tuple[float, ...]) -> None:
|
|
color = hex_to_oklab(hex_value)
|
|
assert (color.oklab_l, color.oklab_a, color.oklab_b) == pytest.approx(expected, abs=2e-7)
|
|
|
|
|
|
def test_achromatic_oklch_hue_is_null() -> None:
|
|
assert oklab_to_oklch(0.5, 0.0, 0.0) == (0.5, 0.0, None)
|
|
assert hex_to_oklab("#ffffff").oklch_h_deg is None
|
|
|
|
|
|
def test_uniform_weights_and_duplicate_source_colors() -> None:
|
|
palette, flags = analytical_palette(["#FF0000", "#ff0000", "#0000ff"])
|
|
assert len(palette) == 2
|
|
assert math.fsum(row["weight"] for row in palette) == pytest.approx(1.0)
|
|
assert [row["source_rank"] for row in palette] == [1, 3]
|
|
assert flags == ["PALETTE_DUPLICATE_SOURCE_COLOR"]
|
|
|
|
|
|
def test_one_color_palette_boundaries() -> None:
|
|
palette, _ = analytical_palette(["#ff0000"])
|
|
features, _ = palette_features(palette)
|
|
assert features["palette_entropy_01"] == 0.0
|
|
assert features["effective_palette_size"] == pytest.approx(1.0)
|
|
assert features["palette_spread_oklab"] == pytest.approx(0.0)
|
|
assert features["hue_concentration_01"] == pytest.approx(1.0)
|
|
|
|
|
|
def test_two_color_centroid_entropy_spread_and_hue() -> None:
|
|
palette, _ = analytical_palette(["#ff0000", "#0000ff"])
|
|
features, _ = palette_features(palette)
|
|
red = hex_to_oklab("#ff0000")
|
|
blue = hex_to_oklab("#0000ff")
|
|
assert features["centroid_l"] == pytest.approx((red.oklab_l + blue.oklab_l) / 2)
|
|
assert features["centroid_a"] == pytest.approx((red.oklab_a + blue.oklab_a) / 2)
|
|
assert features["palette_entropy_01"] == pytest.approx(1.0)
|
|
assert features["effective_palette_size"] == pytest.approx(2.0)
|
|
assert 0 < features["palette_spread_oklab"] < 1
|
|
assert 0 <= features["hue_concentration_01"] <= 1
|
|
|
|
|
|
def test_geometry_logs_and_null_propagation_are_deterministic() -> None:
|
|
canonical = {
|
|
"smk_entity_id": "entity", "object_number": "A1", "colors": ["#ffffff"],
|
|
"net_width_cm": 10.0, "net_height_cm": 20.0,
|
|
"image_width": 100, "image_height": 200, "quality_flags": [],
|
|
}
|
|
first, _ = extract_artwork(canonical)
|
|
second, _ = extract_artwork(dict(canonical))
|
|
assert first == second
|
|
assert first["net_area_cm2"] == 200.0
|
|
assert first["log_net_area_cm2"] == pytest.approx(math.log(200.0))
|
|
canonical["net_width_cm"] = None
|
|
missing, _ = extract_artwork(canonical)
|
|
assert missing["net_area_cm2"] is None
|
|
assert "PHYSICAL_GEOMETRY_MISSING" in missing["feature_quality_flags"]
|
|
|
|
|
|
def test_conservative_aspect_mismatch_flag() -> None:
|
|
canonical = {
|
|
"smk_entity_id": "entity", "object_number": "A1", "colors": ["#ffffff"],
|
|
"net_width_cm": 10.0, "net_height_cm": 10.0,
|
|
"image_width": 100, "image_height": 300, "quality_flags": [],
|
|
}
|
|
feature, _ = extract_artwork(canonical)
|
|
assert feature["aspect_mismatch_log"] == pytest.approx(math.log(3.0))
|
|
assert "PHYSICAL_IMAGE_ASPECT_MISMATCH" in feature["feature_quality_flags"]
|