color.py 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127
  1. """AS7341 spektri -> ligikaudne värvus (CIE 1931 XYZ -> sRGB).
  2. Kasutab Wyman, Sloan & Shirley (2013) analüütilist CIE 1931 2° CMF
  3. lähendust 8 kanali kesk lainepikkustel.
  4. NB! See EI ole kalibreeritud kolorimeetria — absoluutse värvuse jaoks on
  5. vaja valge referentsi ja fikseeritud valgustust (vt LOEMIND.md).
  6. """
  7. from __future__ import annotations
  8. import math
  9. # ESPHome as7341 kanalite F1..F8 kesk lainepikkused (nm)
  10. CHANNEL_WAVELENGTHS: dict[str, int] = {
  11. "f1": 415,
  12. "f2": 445,
  13. "f3": 480,
  14. "f4": 515,
  15. "f5": 555,
  16. "f6": 590,
  17. "f7": 630,
  18. "f8": 680,
  19. }
  20. def _piecewise_gaussian(x: float, mu: float, sigma1: float, sigma2: float) -> float:
  21. sigma = sigma1 if x < mu else sigma2
  22. return math.exp(-0.5 * ((x - mu) / sigma) ** 2)
  23. def cie_xyz_cmf(wavelength_nm: float) -> tuple[float, float, float]:
  24. """Wyman et al. (2013) lähendus CIE 1931 2° värvusfunktsioonidele."""
  25. x_bar = (
  26. 1.056 * _piecewise_gaussian(wavelength_nm, 599.8, 37.9, 31.0)
  27. + 0.362 * _piecewise_gaussian(wavelength_nm, 442.0, 16.0, 26.7)
  28. - 0.065 * _piecewise_gaussian(wavelength_nm, 501.1, 20.4, 26.2)
  29. )
  30. y_bar = (
  31. 0.821 * _piecewise_gaussian(wavelength_nm, 568.8, 46.9, 40.5)
  32. + 0.286 * _piecewise_gaussian(wavelength_nm, 530.9, 16.3, 31.1)
  33. )
  34. z_bar = (
  35. 1.217 * _piecewise_gaussian(wavelength_nm, 437.0, 11.8, 36.0)
  36. + 0.681 * _piecewise_gaussian(wavelength_nm, 459.0, 26.0, 13.8)
  37. )
  38. return x_bar, y_bar, z_bar
  39. def channels_to_xyz(channels: dict[str, float]) -> tuple[float, float, float]:
  40. """Ligikaudne XYZ: iga kanali väärtus kaalutakse CMF-iga kesk lainepikkusel."""
  41. x = y = z = 0.0
  42. for name, wavelength in CHANNEL_WAVELENGTHS.items():
  43. value = channels.get(name)
  44. if value is None:
  45. continue
  46. cx, cy, cz = cie_xyz_cmf(wavelength)
  47. x += value * cx
  48. y += value * cy
  49. z += value * cz
  50. return x, y, z
  51. def xyz_to_srgb(x: float, y: float, z: float) -> tuple[int, int, int]:
  52. """XYZ -> lineaarne sRGB (D65) -> gamma -> 8-bit. Heledus normaliseeritakse."""
  53. r_lin = 3.2406 * x - 1.5372 * y - 0.4986 * z
  54. g_lin = -0.9689 * x + 1.8758 * y + 0.0415 * z
  55. b_lin = 0.0557 * x - 0.2040 * y + 1.0570 * z
  56. def gamma(c: float) -> float:
  57. c = max(0.0, c)
  58. return 12.92 * c if c <= 0.0031308 else 1.055 * c ** (1 / 2.4) - 0.055
  59. r, g, b = gamma(r_lin), gamma(g_lin), gamma(b_lin)
  60. scale = max(r, g, b, 1e-9)
  61. return (
  62. int(round(255 * r / scale)),
  63. int(round(255 * g / scale)),
  64. int(round(255 * b / scale)),
  65. )
  66. def chromaticity_xy(x: float, y: float, z: float) -> tuple[float, float]:
  67. total = x + y + z
  68. if total <= 0:
  69. return 0.0, 0.0
  70. return x / total, y / total
  71. # D65 valgepunkt (CIE XYZ)
  72. D65_WHITE = (0.95047, 1.0, 1.08883)
  73. def _white_reference_xyz() -> tuple[float, float, float]:
  74. """XYZ, kui kõik kanalid = 1 (st ühtlane/valge spekter)."""
  75. x = y = z = 0.0
  76. for wavelength in CHANNEL_WAVELENGTHS.values():
  77. cx, cy, cz = cie_xyz_cmf(wavelength)
  78. x += cx
  79. y += cy
  80. z += cz
  81. return x, y, z
  82. _WHITE_XYZ = _white_reference_xyz()
  83. def analyze(channels: dict[str, float], white_balance: bool = True) -> dict:
  84. """Tagastab tuletatud värviväljad ClickHouse'i rea jaoks.
  85. white_balance=True korral skaleeritakse XYZ nii, et ühtlased kanalid
  86. (valge) vastavad D65 valgele — muidu annab mudel kerge roosa nihke.
  87. """
  88. x, y, z = channels_to_xyz(channels)
  89. if white_balance and _WHITE_XYZ[0] > 0 and _WHITE_XYZ[1] > 0 and _WHITE_XYZ[2] > 0:
  90. x *= D65_WHITE[0] / _WHITE_XYZ[0]
  91. y *= D65_WHITE[1] / _WHITE_XYZ[1]
  92. z *= D65_WHITE[2] / _WHITE_XYZ[2]
  93. cx, cy = chromaticity_xy(x, y, z)
  94. r, g, b = xyz_to_srgb(x, y, z)
  95. return {
  96. "x_chrom": round(cx, 5),
  97. "y_chrom": round(cy, 5),
  98. "r": r,
  99. "g": g,
  100. "b": b,
  101. "rgb_hex": f"#{r:02X}{g:02X}{b:02X}",
  102. }