"""AS7341 spektri -> ligikaudne värvus (CIE 1931 XYZ -> sRGB). Kasutab Wyman, Sloan & Shirley (2013) analüütilist CIE 1931 2° CMF lähendust 8 kanali kesk lainepikkustel. NB! See EI ole kalibreeritud kolorimeetria — absoluutse värvuse jaoks on vaja valge referentsi ja fikseeritud valgustust (vt LOEMIND.md). """ from __future__ import annotations import math # ESPHome as7341 kanalite F1..F8 kesk lainepikkused (nm) CHANNEL_WAVELENGTHS: dict[str, int] = { "f1": 415, "f2": 445, "f3": 480, "f4": 515, "f5": 555, "f6": 590, "f7": 630, "f8": 680, } def _piecewise_gaussian(x: float, mu: float, sigma1: float, sigma2: float) -> float: sigma = sigma1 if x < mu else sigma2 return math.exp(-0.5 * ((x - mu) / sigma) ** 2) def cie_xyz_cmf(wavelength_nm: float) -> tuple[float, float, float]: """Wyman et al. (2013) lähendus CIE 1931 2° värvusfunktsioonidele.""" x_bar = ( 1.056 * _piecewise_gaussian(wavelength_nm, 599.8, 37.9, 31.0) + 0.362 * _piecewise_gaussian(wavelength_nm, 442.0, 16.0, 26.7) - 0.065 * _piecewise_gaussian(wavelength_nm, 501.1, 20.4, 26.2) ) y_bar = ( 0.821 * _piecewise_gaussian(wavelength_nm, 568.8, 46.9, 40.5) + 0.286 * _piecewise_gaussian(wavelength_nm, 530.9, 16.3, 31.1) ) z_bar = ( 1.217 * _piecewise_gaussian(wavelength_nm, 437.0, 11.8, 36.0) + 0.681 * _piecewise_gaussian(wavelength_nm, 459.0, 26.0, 13.8) ) return x_bar, y_bar, z_bar def channels_to_xyz(channels: dict[str, float]) -> tuple[float, float, float]: """Ligikaudne XYZ: iga kanali väärtus kaalutakse CMF-iga kesk lainepikkusel.""" x = y = z = 0.0 for name, wavelength in CHANNEL_WAVELENGTHS.items(): value = channels.get(name) if value is None: continue cx, cy, cz = cie_xyz_cmf(wavelength) x += value * cx y += value * cy z += value * cz return x, y, z def xyz_to_srgb(x: float, y: float, z: float) -> tuple[int, int, int]: """XYZ -> lineaarne sRGB (D65) -> gamma -> 8-bit. Heledus normaliseeritakse.""" r_lin = 3.2406 * x - 1.5372 * y - 0.4986 * z g_lin = -0.9689 * x + 1.8758 * y + 0.0415 * z b_lin = 0.0557 * x - 0.2040 * y + 1.0570 * z def gamma(c: float) -> float: c = max(0.0, c) return 12.92 * c if c <= 0.0031308 else 1.055 * c ** (1 / 2.4) - 0.055 r, g, b = gamma(r_lin), gamma(g_lin), gamma(b_lin) scale = max(r, g, b, 1e-9) return ( int(round(255 * r / scale)), int(round(255 * g / scale)), int(round(255 * b / scale)), ) def chromaticity_xy(x: float, y: float, z: float) -> tuple[float, float]: total = x + y + z if total <= 0: return 0.0, 0.0 return x / total, y / total def analyze(channels: dict[str, float]) -> dict: """Tagastab tuletatud värviväljad ClickHouse'i rea jaoks.""" x, y, z = channels_to_xyz(channels) cx, cy = chromaticity_xy(x, y, z) r, g, b = xyz_to_srgb(x, y, z) return { "x_chrom": round(cx, 5), "y_chrom": round(cy, 5), "r": r, "g": g, "b": b, "rgb_hex": f"#{r:02X}{g:02X}{b:02X}", }