// SPDX-License-Identifier: Apache-2.0 // X-ray window/level for the surface layer, with percentile auto-contrast. // window = high - low, level = (low + high) / 2, both on the 0..1 scale of the CT atlas. // Auto-contrast takes the given percentiles of the atlas histogram, ignoring the empty (black) pixels. const $ = (id) => document.getElementById(id); const st = { low: 0, high: 1, auto: null }; let U = null; // the shader uniform (THREE.Vector2) from app.js function setLowHigh(low, high, src = 'api') { low = Math.max(0, Math.min(1, +low)); high = Math.max(0, Math.min(1, +high)); if (!(high > low)) high = Math.min(1, low + 0.001); st.low = low; st.high = high; if (U) U.value.set(low, high); sync(src); return state(); } const setWindow = (width, level) => { st.auto = null; const w = Math.max(0.001, Math.min(1, +width)), l = +level; return setLowHigh(l - w / 2, l + w / 2, 'api'); }; function state() { return { low: +st.low.toFixed(4), high: +st.high.toFixed(4), window: +(st.high - st.low).toFixed(4), level: +((st.high + st.low) / 2).toFixed(4), auto: st.auto }; } // histogram of the CT atlas (256 bins), drawn into a small canvas once let HIST = null; function histogram() { if (HIST) return HIST; const tex = window.unrollView?.gfx?.ctTexture?.(); const img = tex?.image; if (!img) return null; const W = 512, H = Math.max(1, Math.round(W * (img.height / img.width))); const c = document.createElement('canvas'); c.width = W; c.height = H; const x = c.getContext('2d', { willReadFrequently: true }); x.drawImage(img, 0, 0, W, H); const d = x.getImageData(0, 0, W, H).data, h = new Uint32Array(256); for (let i = 0; i < d.length; i += 4) if (d[i] > 3) h[d[i]]++; // 0..3 is the empty atlas background return (HIST = h); } function percentiles(h, pl, ph) { let n = 0; for (const v of h) n += v; if (!n) return [0, 1]; const at = (p) => { const k = n * p / 100; let s = 0; for (let i = 0; i < 256; i++) { s += h[i]; if (s >= k) return i / 255; } return 1; }; return [at(pl), at(ph)]; } function auto(pLow, pHigh) { pLow = +(pLow ?? setting('ctAutoLow', 1)); pHigh = +(pHigh ?? setting('ctAutoHigh', 99)); const h = histogram(); if (!h) return null; const [lo, hi] = percentiles(h, pLow, pHigh); st.auto = { pLow, pHigh }; setLowHigh(lo, hi, 'auto'); st.auto = { pLow, pHigh }; return state(); } function reset() { st.auto = null; return setLowHigh(0, 1, 'reset'); } function setting(k, d) { try { return window.unrollView.settings.get(k); } catch { return d; } } // ---------------------------------------------------------------- panel group function sync() { const w = $('ctwlWin'), l = $('ctwlLev'); if (w) { w.value = (st.high - st.low).toFixed(3); $('ctwlWinOut').textContent = Math.round((st.high - st.low) * 100) + '%'; } if (l) { l.value = ((st.high + st.low) / 2).toFixed(3); $('ctwlLevOut').textContent = Math.round((st.high + st.low) * 50) + '%'; } $('ctwlAuto')?.setAttribute('aria-pressed', !!st.auto); } function buildGroup(P) { if ($('uiCT')) return; const g = P.group('X-ray contrast', 'ui-ct'); g.id = 'uiCT'; g.insertAdjacentHTML('beforeend', `