Download lib/ctwl.js from SharpMeow/unroll: direct link, hf CLI and curl.
- Browser
- Download file 5.93 kB
-
https://huggingface.co/spaces/SharpMeow/unroll/resolve/main/lib/ctwl.js
- Command line
-
hf download hf://spaces/SharpMeow/unroll/lib/ctwl.js
-
curl -L -o ctwl.js https://huggingface.co/spaces/SharpMeow/unroll/resolve/main/lib/ctwl.js
5.93 kB
| // 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', ` | |
| <label class="ui-row" for="ctwlWin"><span>Window (contrast range) <output id="ctwlWinOut" class="dim"></output></span><input type="range" id="ctwlWin" min="0.02" max="1" step="0.01" data-action="ct.window" aria-label="X-ray window: the brightness range shown, narrower is more contrast"></label> | |
| <label class="ui-row" for="ctwlLev"><span>Level (center brightness) <output id="ctwlLevOut" class="dim"></output></span><input type="range" id="ctwlLev" min="0" max="1" step="0.01" data-action="ct.level" aria-label="X-ray level: the brightness at the middle of the window"></label> | |
| <div class="ui-grid"><button type="button" class="icon ui-btn" id="ctwlAuto" data-action="ct.auto" aria-pressed="false" title="Auto contrast: stretch the 1st to 99th percentile of the X-ray brightness (Settings sets the percentiles)"><span class="ui-lbl">Auto contrast</span></button> | |
| <button type="button" class="icon ui-btn" id="ctwlReset" data-action="ct.reset" title="Show the full X-ray brightness range"><span class="ui-lbl">Full range</span></button></div>`); | |
| P.body.append(g); | |
| const onWL = () => { st.auto = null; setWindow($('ctwlWin').value, $('ctwlLev').value); }; | |
| $('ctwlWin').addEventListener('input', onWL); $('ctwlLev').addEventListener('input', onWL); | |
| $('ctwlAuto').onclick = () => auto(); $('ctwlReset').onclick = () => reset(); | |
| P.addAction?.('ct.auto', { id: 'ctwlAuto', label: 'X-ray auto contrast (percentiles from Settings)', run: (a) => auto(...(Array.isArray(a) ? a : [])) }); | |
| P.addAction?.('ct.reset', { id: 'ctwlReset', label: 'X-ray full brightness range', run: () => reset() }); | |
| sync(); P.measure?.(); | |
| } | |
| function init() { | |
| const v = window.unrollView; | |
| if (!v?.gfx?.ctWindow) return setTimeout(init, 100); | |
| U = v.gfx.ctWindow; | |
| const api = { setWindow, setLowHigh, auto, reset, state, histogram: () => Array.from(histogram() || []), percentiles: (a, b) => percentiles(histogram(), a, b) }; | |
| v.ct = Object.assign(v.ct || {}, api); | |
| if (window.unrollPanel) buildGroup(window.unrollPanel); else addEventListener('unroll:panel', (e) => buildGroup(e.detail), { once: true }); | |
| // URL: ?ctwin=0.5&ctlevel=0.4 or ?ctauto=1 (or 2,98 for other percentiles); Settings: auto contrast on load | |
| const q = new URLSearchParams(location.search); | |
| const go = () => { | |
| if (q.has('ctwin') || q.has('ctlevel')) setWindow(q.get('ctwin') ?? 1, q.get('ctlevel') ?? 0.5); | |
| else if (q.has('ctauto')) { const [a, b] = q.get('ctauto').split(',').map(Number); auto(b ? a : undefined, b || undefined); } | |
| else if (setting('ctAuto', false)) auto(); | |
| }; | |
| const wait = () => (v.gfx.ctTexture()?.image ? go() : setTimeout(wait, 200)); wait(); | |
| addEventListener('unroll:settings', (e) => { if (e.detail.ctAuto && !st.auto) auto(); else if (st.auto) auto(); }); | |
| } | |
| init(); | |