calibrate.py 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157
  1. """Kalibreerimise abivahend AS7341 värviandurile.
  2. Loeb ClickHouse'ist (vaikimisi uus67.varvus) ja toetab:
  3. * capture-dark: salvesta tume referents (andur kaetud) -> calibration.json
  4. * capture-white: salvesta valge referents (valge pind, SAMA valgustus) -> calibration.json
  5. * show: näita viimaseid mõõtmisi (toores vs tume+valge kalibreeritud)
  6. Kalibreeritud reflekteeruvus: R = (S - D) / (W - D) iga kanali kohta.
  7. * Tume (D) eemaldab tumedavoolu ja plaadi toite-LED-i (roheline) panuse.
  8. * Valge (W) eemaldab valgustuse spektri ja kanalite tundlikkuse erinevuse.
  9. Protseduur:
  10. 1. Fikseeri valgustus ja geomeetria (nurk, kaugus) kogu seeria jooksul.
  11. 2. Kata andur (täiesti pime) -> capture-dark --last 5
  12. 3. Mõõda valge pind (nt valge paber/PTFE) -> capture-white --last 5
  13. 4. Mõõda proovid -> show --last 10
  14. Käivitamine (projekti .venv):
  15. CH_URL=http://100.87.1.24:8125 CH_USER=zigbee CH_PASS=... \
  16. .venv/bin/python varvusensor/collector/calibrate.py capture-dark --last 5
  17. """
  18. from __future__ import annotations
  19. import argparse
  20. import json
  21. import os
  22. import statistics
  23. import sys
  24. from pathlib import Path
  25. import requests
  26. import color
  27. from settings import CH_URL, CH_DB, CH_TABLE, CH_USER, CH_PASS
  28. CALIB_FILE = Path(
  29. os.environ.get("CALIB_FILE", Path(__file__).with_name("calibration.json"))
  30. )
  31. FIELDS = ("f1", "f2", "f3", "f4", "f5", "f6", "f7", "f8", "clear", "nir")
  32. BANDS = tuple(color.CHANNEL_WAVELENGTHS) # f1..f8
  33. def ch_query(sql: str) -> str:
  34. resp = requests.post(
  35. f"{CH_URL}/",
  36. params={"database": CH_DB, "query": sql},
  37. auth=(CH_USER, CH_PASS),
  38. timeout=15,
  39. )
  40. resp.raise_for_status()
  41. return resp.text
  42. def fetch_rows(last: int) -> list[dict]:
  43. cols = ", ".join(FIELDS + ("rgb_hex",))
  44. sql = (
  45. f"SELECT ts, {cols} FROM {CH_DB}.{CH_TABLE} "
  46. f"ORDER BY ts DESC LIMIT {int(last)} FORMAT JSONEachRow"
  47. )
  48. return [json.loads(line) for line in ch_query(sql).strip().splitlines() if line]
  49. def load_calib() -> dict:
  50. if not CALIB_FILE.exists():
  51. return {}
  52. try:
  53. return json.loads(CALIB_FILE.read_text())
  54. except Exception:
  55. return {}
  56. def save_calib(data: dict) -> None:
  57. CALIB_FILE.write_text(json.dumps(data, indent=2, ensure_ascii=False))
  58. def capture_reference(kind: str, last: int) -> int:
  59. rows = fetch_rows(last)
  60. if not rows:
  61. print("Andmeid pole — mõõda esmalt (kaetud andur / valge pind).")
  62. return 1
  63. ref: dict[str, float] = {}
  64. for field in FIELDS:
  65. values = [r[field] for r in rows if r.get(field) is not None]
  66. ref[field] = round(statistics.mean(values), 3) if values else 0.0
  67. data = load_calib()
  68. data[kind] = ref
  69. data[f"{kind}_rows"] = len(rows)
  70. save_calib(data)
  71. label = "Tume" if kind == "dark" else "Valge"
  72. print(f"{label} referents salvestatud ({len(rows)} reast) -> {CALIB_FILE}")
  73. print(json.dumps(ref, indent=2))
  74. if kind == "white" and "dark" not in data:
  75. print("NB! Tume referents puudub — soovitatav teha enne capture-dark (LED/tumedavool).")
  76. return 0
  77. def reflectance(
  78. channels: dict[str, float], dark: dict[str, float], white: dict[str, float]
  79. ) -> dict[str, float]:
  80. out: dict[str, float] = {}
  81. for band in BANDS:
  82. value = channels.get(band)
  83. d = dark.get(band, 0.0) or 0.0
  84. w = white.get(band, 0.0) or 0.0
  85. if value is not None and (w - d) > 0:
  86. out[band] = max(0.0, (value - d) / (w - d))
  87. return out
  88. def show(last: int) -> int:
  89. rows = fetch_rows(last)
  90. data = load_calib()
  91. dark = data.get("dark") or {}
  92. white = data.get("white")
  93. if white is None:
  94. print("(valge kalibratsioon puudub — näitan ainult toorest; tee capture-white)")
  95. elif not dark:
  96. print("(tume referents puudub — kasutan D=0; soovitatav capture-dark)")
  97. for row in rows:
  98. channels = {b: row[b] for b in BANDS if row.get(b) is not None}
  99. raw = color.analyze(channels)
  100. line = (
  101. f"{row['ts']} toores {raw['rgb_hex']} "
  102. f"(x={raw['x_chrom']:.3f} y={raw['y_chrom']:.3f})"
  103. )
  104. if white is not None:
  105. cal = color.analyze(reflectance(channels, dark, white))
  106. line += (
  107. f" | kalib. {cal['rgb_hex']} "
  108. f"(x={cal['x_chrom']:.3f} y={cal['y_chrom']:.3f})"
  109. )
  110. print(line)
  111. return 0
  112. def main() -> int:
  113. parser = argparse.ArgumentParser(description="AS7341 kalibreerimine")
  114. sub = parser.add_subparsers(dest="cmd", required=True)
  115. cd = sub.add_parser("capture-dark", help="salvesta tume referents (andur kaetud)")
  116. cd.add_argument("--last", type=int, default=5)
  117. cw = sub.add_parser("capture-white", help="salvesta valge referents")
  118. cw.add_argument("--last", type=int, default=5)
  119. sh = sub.add_parser("show", help="näita viimaseid mõõtmisi")
  120. sh.add_argument("--last", type=int, default=10)
  121. args = parser.parse_args()
  122. if args.cmd == "capture-dark":
  123. return capture_reference("dark", args.last)
  124. if args.cmd == "capture-white":
  125. return capture_reference("white", args.last)
  126. if args.cmd == "show":
  127. return show(args.last)
  128. return 1
  129. if __name__ == "__main__":
  130. sys.exit(main())