|
|
@@ -32,6 +32,7 @@ from pathlib import Path
|
|
|
from zoneinfo import ZoneInfo
|
|
|
|
|
|
import color
|
|
|
+import numpy as np
|
|
|
import paho.mqtt.client as mqtt
|
|
|
import requests
|
|
|
from fastapi import Body, Depends, FastAPI, Form, HTTPException, Query, Request
|
|
|
@@ -61,6 +62,8 @@ VARV_STATE_TOPIC = os.environ.get("VARV_STATE_TOPIC", "varvus/switch/sensor/stat
|
|
|
VARV_BANDS = ("f1", "f2", "f3", "f4", "f5", "f6", "f7", "f8")
|
|
|
VARV_CALIB_FIELDS = VARV_BANDS + ("clear", "nir")
|
|
|
VARV_CALIB_TABLE = os.environ.get("VARV_CALIB_TABLE", "varvus_calib")
|
|
|
+VARV_REF_TABLE = os.environ.get("VARV_REF_TABLE", "varvus_ccm_ref")
|
|
|
+VARV_CCM_TABLE = os.environ.get("VARV_CCM_TABLE", "varvus_ccm")
|
|
|
|
|
|
WEB_ACCESS_TOKEN = os.environ.get("WEB_ACCESS_TOKEN", "")
|
|
|
WEB_PASSWORD = os.environ.get("WEB_PASSWORD", "")
|
|
|
@@ -578,6 +581,130 @@ def varv_reflectance(channels: dict, dark: dict, white: dict) -> dict:
|
|
|
return out
|
|
|
|
|
|
|
|
|
+# --- Värvikaardi (CCM) kalibratsioon: 3x8 maatriks kanalitest -> lineaarne sRGB ---
|
|
|
+
|
|
|
+
|
|
|
+def varv_load_ccm() -> list[list[float]] | None:
|
|
|
+ try:
|
|
|
+ rows = ch_query(
|
|
|
+ f"SELECT matrix FROM {CH_DB}.{VARV_CCM_TABLE} FINAL "
|
|
|
+ f"ORDER BY updated_at DESC LIMIT 1"
|
|
|
+ )
|
|
|
+ except HTTPException:
|
|
|
+ return None
|
|
|
+ if not rows:
|
|
|
+ return None
|
|
|
+ try:
|
|
|
+ m = json.loads(rows[0]["matrix"])
|
|
|
+ if len(m) == 3 and all(len(row) == len(VARV_BANDS) for row in m):
|
|
|
+ return [[float(x) for x in row] for row in m]
|
|
|
+ except Exception:
|
|
|
+ return None
|
|
|
+ return None
|
|
|
+
|
|
|
+
|
|
|
+def _srgb_to_linear(v: float) -> float:
|
|
|
+ c = v / 255.0
|
|
|
+ return c / 12.92 if c <= 0.04045 else ((c + 0.055) / 1.055) ** 2.4
|
|
|
+
|
|
|
+
|
|
|
+def _linear_to_srgb8(c: float) -> int:
|
|
|
+ c = max(0.0, min(1.0, c))
|
|
|
+ v = 12.92 * c if c <= 0.0031308 else 1.055 * (c ** (1 / 2.4)) - 0.055
|
|
|
+ return int(round(255 * max(0.0, min(1.0, v))))
|
|
|
+
|
|
|
+
|
|
|
+def varv_apply_ccm(matrix: list[list[float]], channels: dict) -> str:
|
|
|
+ vec = np.array([float(channels.get(b) or 0.0) for b in VARV_BANDS], dtype=float)
|
|
|
+ lin = np.array(matrix, dtype=float) @ vec
|
|
|
+ r = _linear_to_srgb8(float(lin[0]))
|
|
|
+ g = _linear_to_srgb8(float(lin[1]))
|
|
|
+ b = _linear_to_srgb8(float(lin[2]))
|
|
|
+ return f"#{r:02X}{g:02X}{b:02X}"
|
|
|
+
|
|
|
+
|
|
|
+@app.post("/api/varv/ref", dependencies=[Depends(require_auth)])
|
|
|
+async def varv_ref_set(payload: dict = Body(...)) -> dict:
|
|
|
+ """Salvesta referentsvärv (ekraanil kuvatud) CCM-i jaoks."""
|
|
|
+ name = str(payload.get("name", "")).strip().lower()
|
|
|
+ hexv = str(payload.get("hex", "")).strip().lstrip("#")
|
|
|
+ if len(hexv) != 6:
|
|
|
+ raise HTTPException(status_code=422, detail="hex peab olema 6-kohaline (nt FF0000)")
|
|
|
+ try:
|
|
|
+ tr, tg, tb = int(hexv[0:2], 16), int(hexv[2:4], 16), int(hexv[4:6], 16)
|
|
|
+ except ValueError:
|
|
|
+ raise HTTPException(status_code=422, detail="vigane hex")
|
|
|
+ last = max(1, min(int(payload.get("last", 5)), 30))
|
|
|
+ rows = ch_query(
|
|
|
+ f"SELECT {', '.join(VARV_CALIB_FIELDS)} FROM {CH_DB}.varvus "
|
|
|
+ f"ORDER BY ts DESC LIMIT {last}"
|
|
|
+ )
|
|
|
+ if not rows:
|
|
|
+ raise HTTPException(status_code=409, detail="Mõõtmisi pole")
|
|
|
+ ref: dict[str, float] = {}
|
|
|
+ for f in VARV_CALIB_FIELDS:
|
|
|
+ vals = [float(r[f]) for r in rows if r.get(f) is not None]
|
|
|
+ ref[f] = round(sum(vals) / len(vals), 3) if vals else 0.0
|
|
|
+ row = {"name": name, "tr": tr, "tg": tg, "tb": tb, **ref}
|
|
|
+ resp = requests.post(
|
|
|
+ f"{CH_URL}/",
|
|
|
+ params={"database": CH_DB, "query": f"INSERT INTO {CH_DB}.{VARV_REF_TABLE} FORMAT JSONEachRow"},
|
|
|
+ data=json.dumps(row) + "\n",
|
|
|
+ auth=(CH_USER, CH_PASS),
|
|
|
+ timeout=10,
|
|
|
+ )
|
|
|
+ if resp.status_code != 200:
|
|
|
+ raise HTTPException(status_code=502, detail=f"ClickHouse: {resp.text[:200]}")
|
|
|
+ return {"name": name, "target": hexv.upper(), "reference": ref, "rows": len(rows)}
|
|
|
+
|
|
|
+
|
|
|
+@app.get("/api/varv/ref")
|
|
|
+async def varv_ref_get() -> dict:
|
|
|
+ rows = ch_query(
|
|
|
+ f"SELECT name, tr, tg, tb, {', '.join(VARV_CALIB_FIELDS)} "
|
|
|
+ f"FROM {CH_DB}.{VARV_REF_TABLE} FINAL ORDER BY name"
|
|
|
+ )
|
|
|
+ return {"refs": rows}
|
|
|
+
|
|
|
+
|
|
|
+@app.post("/api/varv/ccm/fit", dependencies=[Depends(require_auth)])
|
|
|
+async def varv_ccm_fit() -> dict:
|
|
|
+ """Sobita 3x8 maatriks (kanalid -> lineaarne sRGB) referentsidest."""
|
|
|
+ rows = ch_query(
|
|
|
+ f"SELECT name, tr, tg, tb, {', '.join(VARV_BANDS)} "
|
|
|
+ f"FROM {CH_DB}.{VARV_REF_TABLE} FINAL ORDER BY name"
|
|
|
+ )
|
|
|
+ if len(rows) < 3:
|
|
|
+ raise HTTPException(status_code=422, detail="Vaja vähemalt 3 referentsvärvi (soovitav 6-8).")
|
|
|
+ C = np.array([[float(r[b] or 0.0) for b in VARV_BANDS] for r in rows], dtype=float)
|
|
|
+ T = np.array(
|
|
|
+ [[_srgb_to_linear(float(r["tr"])), _srgb_to_linear(float(r["tg"])), _srgb_to_linear(float(r["tb"]))]
|
|
|
+ for r in rows],
|
|
|
+ dtype=float,
|
|
|
+ )
|
|
|
+ M, *_ = np.linalg.lstsq(C, T, rcond=None) # (8,3)
|
|
|
+ matrix = M.T.tolist() # (3,8)
|
|
|
+ resp = requests.post(
|
|
|
+ f"{CH_URL}/",
|
|
|
+ params={
|
|
|
+ "database": CH_DB,
|
|
|
+ "query": f"INSERT INTO {CH_DB}.{VARV_CCM_TABLE} (id, matrix, n_refs) FORMAT JSONEachRow",
|
|
|
+ },
|
|
|
+ data=json.dumps({"id": 1, "matrix": json.dumps(matrix), "n_refs": len(rows)}) + "\n",
|
|
|
+ auth=(CH_USER, CH_PASS),
|
|
|
+ timeout=10,
|
|
|
+ )
|
|
|
+ if resp.status_code != 200:
|
|
|
+ raise HTTPException(status_code=502, detail=f"ClickHouse: {resp.text[:200]}")
|
|
|
+ return {"n_refs": len(rows), "matrix": matrix}
|
|
|
+
|
|
|
+
|
|
|
+@app.get("/api/varv/ccm")
|
|
|
+async def varv_ccm_get() -> dict:
|
|
|
+ m = varv_load_ccm()
|
|
|
+ return {"matrix": m, "ready": m is not None}
|
|
|
+
|
|
|
+
|
|
|
@app.get("/api/varv/latest")
|
|
|
async def varv_latest(limit: int = Query(60, ge=1, le=300)) -> dict:
|
|
|
"""Värvisensori viimased mõõtmised (avalik; kalibreeritud väljad kui referentsid olemas)."""
|
|
|
@@ -591,6 +718,7 @@ async def varv_latest(limit: int = Query(60, ge=1, le=300)) -> dict:
|
|
|
calib = varv_load_calib()
|
|
|
dark, white = calib.get("dark"), calib.get("white")
|
|
|
ready = bool(dark and white)
|
|
|
+ ccm = varv_load_ccm()
|
|
|
out = []
|
|
|
for r in rows:
|
|
|
bands = {b: r.get(b) for b in VARV_BANDS}
|
|
|
@@ -614,8 +742,10 @@ async def varv_latest(limit: int = Query(60, ge=1, le=300)) -> dict:
|
|
|
item["rgb_hex_cal"] = cc["rgb_hex"]
|
|
|
item["x_chrom_cal"] = cc["x_chrom"]
|
|
|
item["y_chrom_cal"] = cc["y_chrom"]
|
|
|
+ if ccm:
|
|
|
+ item["rgb_hex_ccm"] = varv_apply_ccm(ccm, bands)
|
|
|
out.append(item)
|
|
|
- return {"rows": out, "calibrated": ready}
|
|
|
+ return {"rows": out, "calibrated": ready, "ccm": ccm is not None}
|
|
|
|
|
|
|
|
|
@app.get("/api/varv/calib")
|