smk-playground/tests/test_color_features.py

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"]