| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127 |
- """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
- # D65 valgepunkt (CIE XYZ)
- D65_WHITE = (0.95047, 1.0, 1.08883)
- def _white_reference_xyz() -> tuple[float, float, float]:
- """XYZ, kui kõik kanalid = 1 (st ühtlane/valge spekter)."""
- x = y = z = 0.0
- for wavelength in CHANNEL_WAVELENGTHS.values():
- cx, cy, cz = cie_xyz_cmf(wavelength)
- x += cx
- y += cy
- z += cz
- return x, y, z
- _WHITE_XYZ = _white_reference_xyz()
- def analyze(channels: dict[str, float], white_balance: bool = True) -> dict:
- """Tagastab tuletatud värviväljad ClickHouse'i rea jaoks.
- white_balance=True korral skaleeritakse XYZ nii, et ühtlased kanalid
- (valge) vastavad D65 valgele — muidu annab mudel kerge roosa nihke.
- """
- x, y, z = channels_to_xyz(channels)
- if white_balance and _WHITE_XYZ[0] > 0 and _WHITE_XYZ[1] > 0 and _WHITE_XYZ[2] > 0:
- x *= D65_WHITE[0] / _WHITE_XYZ[0]
- y *= D65_WHITE[1] / _WHITE_XYZ[1]
- z *= D65_WHITE[2] / _WHITE_XYZ[2]
- 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}",
- }
|