kalibreeri.html 10.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220
  1. <!doctype html>
  2. <html lang="et">
  3. <head>
  4. <meta charset="utf-8">
  5. <meta name="viewport" content="width=device-width,initial-scale=1">
  6. <title>Värvikalibratsioon</title>
  7. <style>
  8. :root{--bg:#12151a;--card:#1b2028;--fg:#e8ecf1;--muted:#93a0b4;--acc:#4da3ff;--ok:#3ddc97;--warn:#ffb454;--err:#ff6b6b}
  9. *{box-sizing:border-box}
  10. body{margin:0;font:15px/1.45 system-ui,-apple-system,Segoe UI,Roboto,sans-serif;background:var(--bg);color:var(--fg)}
  11. header{display:flex;justify-content:space-between;align-items:center;padding:12px 16px;border-bottom:1px solid #2a3140}
  12. header a{color:var(--acc);text-decoration:none}
  13. main{max-width:720px;margin:0 auto;padding:18px}
  14. .card{background:var(--card);border:1px solid #2a3140;border-radius:12px;padding:16px;margin:12px 0}
  15. h1{font-size:20px;margin:0}
  16. h2{font-size:16px;margin:0 0 8px}
  17. p{margin:6px 0}
  18. .muted{color:var(--muted)}
  19. .row{display:flex;gap:10px;flex-wrap:wrap;margin-top:12px}
  20. button{font:inherit;padding:10px 16px;border-radius:9px;border:1px solid #33405a;background:#243044;color:var(--fg);cursor:pointer}
  21. button.primary{background:var(--acc);border-color:var(--acc);color:#08111f;font-weight:600}
  22. button:disabled{opacity:.45;cursor:not-allowed}
  23. .badge{display:inline-block;padding:2px 8px;border-radius:99px;background:#243044;color:var(--muted);font-size:12px}
  24. .badge.ok{background:#12362a;color:var(--ok)}
  25. .badge.err{background:#3a1717;color:var(--err)}
  26. .bar{height:8px;background:#243044;border-radius:99px;overflow:hidden;margin-top:10px}
  27. .bar>i{display:block;height:100%;width:0;background:var(--acc);transition:width .25s}
  28. #patch{position:fixed;inset:0;display:none;z-index:10}
  29. #patch .bar2{position:absolute;left:0;right:0;bottom:0;display:flex;gap:10px;align-items:center;justify-content:center;padding:14px;background:rgba(0,0,0,.55);backdrop-filter:blur(4px)}
  30. #patch .bar2 button{background:rgba(255,255,255,.92);color:#111;border:0;font-weight:600}
  31. table{width:100%;border-collapse:collapse;font-size:13px}
  32. td,th{padding:4px 6px;border-bottom:1px solid #2a3140;text-align:left}
  33. code{background:#0e1218;padding:1px 5px;border-radius:5px}
  34. </style>
  35. </head>
  36. <body>
  37. <header><h1>🎨 Värvikalibratsioon</h1><a href="/">← Nutikodu</a></header>
  38. <main>
  39. <div id="msg" class="card muted">Laen…</div>
  40. <section id="intro" class="card" hidden>
  41. <h2>Kuidas käib</h2>
  42. <ol>
  43. <li><b>Tume:</b> kata andur täiesti (must riie/karp; LED on plaadilt eemaldatud) → “Mõõda tume”. Nullväärtused on OK.</li>
  44. <li><b>Valge:</b> näita ekraanil täisvalget (see leht teeb ise) samal kaugusel ja nurga all, kus hakkadki mõõtma → “Mõõda valge”.</li>
  45. <li><b>24 plaastrit:</b> iga plaaster kuvatakse täisekraanil; vajuta “Mõõda” ja oota ~10–15 s.</li>
  46. <li><b>Sobita:</b> arvutan 3×8 maatriksi ja ΔE00 vead.</li>
  47. </ol>
  48. <p class="muted">Tähtis: püsi kogu seeria jooksul samas valgustuses, kauguses ja nurgas. Ekraani heledus maksimumi, auto-heledus ja sinise valguse filter välja.</p>
  49. <div class="row"><button class="primary" id="start">Alusta</button></div>
  50. </section>
  51. <section id="stage" class="card" hidden>
  52. <h2 id="stage-title">—</h2>
  53. <p id="stage-desc" class="muted"></p>
  54. <div id="stage-status"></div>
  55. <div class="bar"><i id="prog"></i></div>
  56. <div class="row" id="stage-actions"></div>
  57. </section>
  58. <section id="result" class="card" hidden>
  59. <h2>Tulemus</h2>
  60. <div id="result-body"></div>
  61. </section>
  62. </main>
  63. <div id="patch"><div class="bar2"><button id="measure">Mõõda see plaat</button><span id="patch-label" style="color:#fff;font:600 14px system-ui"></span></div></div>
  64. <script>
  65. const PATCHES = [
  66. ["tumedanahk","735244"],["heledanahk","C29682"],["taevasinine","627A9D"],
  67. ["lehestik","576C43"],["sinine lill","8580B1"],["rohekassinine","67BDAA"],
  68. ["oranž","D67E2C"],["sinakaslilla","505BA6"],["mõõdukas punane","C15A63"],
  69. ["lilla","5E3C6C"],["kollakasroheline","9DBC40"],["oranžkollane","E0A32E"],
  70. ["sinine","383D96"],["roheline","469449"],["punane","AF363C"],
  71. ["kollane","E7C71F"],["magenta","BB5695"],["tsüaan","0885A1"],
  72. ["valge","F3F3F2"],["hall 8","C8C8C8"],["hall 6.5","A0A0A0"],
  73. ["hall 5","7A7A79"],["hall 3.5","555555"],["must","343434"]
  74. ];
  75. const $ = (id) => document.getElementById(id);
  76. let st = {dark:null, white:null, idx:0, samples:3};
  77. async function api(path, opts={}) {
  78. const r = await fetch(path, {credentials:"same-origin", headers:{"Content-Type":"application/json"}, ...opts});
  79. let data = null;
  80. try { data = await r.json(); } catch(e) {}
  81. if (!r.ok) throw new Error((data && (data.detail||data.error)) || ("HTTP "+r.status));
  82. return data;
  83. }
  84. function say(text, kind) {
  85. const m = $("msg");
  86. m.hidden = false;
  87. m.className = "card " + (kind || "muted");
  88. m.textContent = text;
  89. }
  90. async function capture() {
  91. return await api("/api/varv/capture", {method:"POST", body: JSON.stringify({n: st.samples, timeout: 45})});
  92. }
  93. function progress(p) { $("prog").style.width = Math.round(p*100) + "%"; }
  94. async function init() {
  95. try {
  96. const me = await api("/api/me");
  97. if (!me.authed) { say("Vaja sisse logida. Mine /login ja tule tagasi.", "err"); return; }
  98. } catch(e) { say("Server ei vasta: " + e.message, "err"); return; }
  99. $("msg").hidden = true;
  100. $("intro").hidden = false;
  101. $("start").onclick = beginDark;
  102. }
  103. async function beginDark() {
  104. $("intro").hidden = true;
  105. $("stage").hidden = false;
  106. $("stage-title").textContent = "1/3 · Tume referents";
  107. $("stage-desc").textContent = "Kata andur täiesti (must riie või karp). Plaadi roheline toite-LED on eemaldatud — täielik pimedus ja nullväärtused on ootuspärased.";
  108. $("stage-status").textContent = "";
  109. $("stage-actions").innerHTML = '<button class="primary" id="go">Mõõda tume</button>';
  110. $("go").onclick = async () => {
  111. $("go").disabled = true;
  112. try {
  113. const r = await capture();
  114. $("stage-status").innerHTML = '<span class="badge ' + (r.stable?"ok":"err") + '">clear ' + r.clear + ' · CV ' + (r.cv*100).toFixed(1) + '%</span>';
  115. const res = await api("/api/varv/calib", {method:"POST", body: JSON.stringify({kind:"dark", last:r.n})});
  116. st.dark = res.reference;
  117. say("Tume salvestatud.", "ok");
  118. beginWhite();
  119. } catch(e) { $("go").disabled = false; say("Viga: " + e.message, "err"); }
  120. };
  121. }
  122. async function beginWhite() {
  123. $("stage-title").textContent = "2/3 · Valge referents";
  124. $("stage-desc").textContent = "Näita ekraanil valget pinda (nt ava see leht täisekraanil ja suuna andur sellele) samas valgustuses ja geomeetrias.";
  125. $("stage-status").textContent = "";
  126. $("stage-actions").innerHTML = '<button class="primary" id="go">Mõõda valge</button>';
  127. $("go").onclick = async () => {
  128. $("go").disabled = true;
  129. try {
  130. const r = await capture();
  131. $("stage-status").innerHTML = '<span class="badge ' + (r.stable?"ok":"err") + '">clear ' + r.clear + ' · CV ' + (r.cv*100).toFixed(1) + '%</span>';
  132. const res = await api("/api/varv/calib", {method:"POST", body: JSON.stringify({kind:"white", last:r.n})});
  133. st.white = res.reference;
  134. say("Valge salvestatud.", "ok");
  135. beginPatches();
  136. } catch(e) { $("go").disabled = false; say("Viga: " + e.message, "err"); }
  137. };
  138. }
  139. function beginPatches() {
  140. st.idx = 0;
  141. $("stage-title").textContent = "3/3 · Värvikaart";
  142. showPatch();
  143. }
  144. function showPatch() {
  145. const [name, hex] = PATCHES[st.idx];
  146. progress(st.idx / PATCHES.length);
  147. $("stage").hidden = true;
  148. $("patch").style.display = "block";
  149. $("patch").style.background = "#" + hex;
  150. const r = parseInt(hex.slice(0,2),16), g = parseInt(hex.slice(2,4),16), b = parseInt(hex.slice(4,6),16);
  151. const lum = (0.299*r + 0.587*g + 0.114*b);
  152. $("patch-label").textContent = (st.idx+1) + "/24 · " + name;
  153. $("patch-label").style.color = lum > 140 ? "#111" : "#fff";
  154. $("measure").onclick = measurePatch;
  155. }
  156. async function measurePatch() {
  157. $("measure").disabled = true;
  158. $("patch-label").textContent = "Mõõdan… (~10–15 s)";
  159. try {
  160. const r = await capture();
  161. if (r.saturated) throw new Error("Küllastunud — vähenda ekraani heledust või suurenda kaugust.");
  162. if (!r.stable) throw new Error("Mõõtmine pole stabiilne (CV " + (r.cv*100).toFixed(1) + "%) — oota ja proovi uuesti.");
  163. const [name, hex] = PATCHES[st.idx];
  164. await api("/api/varv/ref", {method:"POST", body: JSON.stringify({name:"wiz"+String(st.idx+1).padStart(2,"0"), hex, last:r.n})});
  165. st.idx++;
  166. if (st.idx >= PATCHES.length) { finish(); return; }
  167. showPatch();
  168. } catch(e) {
  169. $("patch-label").textContent = "Viga: " + e.message;
  170. } finally {
  171. $("measure").disabled = false;
  172. }
  173. }
  174. async function finish() {
  175. $("patch").style.display = "none";
  176. $("stage").hidden = false;
  177. $("stage-title").textContent = "Sobitan maatriksit…";
  178. $("stage-desc").textContent = "24 referentsi on salvestatud. Arvutan 3×8 maatriksi ja ΔE00.";
  179. $("stage-actions").innerHTML = "";
  180. progress(1);
  181. try {
  182. const r = await api("/api/varv/ccm/fit?prefix=wiz", {method:"POST"});
  183. $("result").hidden = false;
  184. const s = r.de00_loo, i = r.de00_in_sample;
  185. $("result-body").innerHTML =
  186. '<p><b>Referentse:</b> ' + r.n_refs + '<br>' +
  187. '<b>ΔE00 (in-sample):</b> keskmine ' + i.mean + ', mediaan ' + i.median + ', max ' + i.max + '<br>' +
  188. '<b>ΔE00 (LOO):</b> keskmine ' + s.mean + ', mediaan ' + s.median + ', max ' + s.max + '</p>' +
  189. '<p class="muted">LOO (leave-one-out) on aus hinnang uutele värvidele. Alla ~3 on hea, 3–6 rahuldav, üle 6 nõrk.</p>' +
  190. '<table><tr><th>Plaaster</th><th>Siht</th><th>Tulemus</th><th>ΔE00</th></tr>' +
  191. r.preview.map(p => '<tr><td>'+p.name+'</td><td><code>'+p.target+'</code></td><td><code>'+p.pred+'</code></td><td>'+p.de00+'</td></tr>').join('') +
  192. '</table>';
  193. $("stage-title").textContent = "Valmis";
  194. $("stage-desc").textContent = "Maatriks on salvestatud. Mine Värv-vahelehele ja lülita sisse kalibreeritud vaade.";
  195. } catch(e) {
  196. $("stage-title").textContent = "Sobitamine ebaõnnestus";
  197. $("stage-desc").textContent = e.message;
  198. }
  199. }
  200. init();
  201. </script>
  202. </body>
  203. </html>