Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 25 additions & 9 deletions examples/PixelExplorer.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
CATEGORY = "Visualization"
const CATEGORY = "Visualization"

if (typeof window._PixelExplorerLoaded === 'undefined') {
window._PixelExplorerLoaded = true;
Expand All @@ -12,9 +12,12 @@ script.onload = function() {
/// wait for nv and volumes before calling init
// calling init too early means niivue won't be detected yet
var poll = setInterval(function() {
if (window.nv && nv.volumes && nv.volumes.length > 0) {
try {
Boostlet.init();
clearInterval(poll);
run();
} catch(e) {
// framework not ready yet keep polling
}
}, 300);
};
Expand All @@ -23,8 +26,6 @@ document.head.appendChild(script);
const HALF = 4;

function run() {
Boostlet.init();

if (Boostlet.framework.name !== 'niivue') {
alert('Only niivue is supported right now :(');
return;
Expand All @@ -42,7 +43,8 @@ function setup(nv) {
const existingOnLocationChange = nv.onLocationChange;

// hook into location change — fires on every crosshair move / slice change
nv.onLocationChange = function(loc) {
// fire once immediately so the panel is populated on load without requiring a click
const triggerUpdate = function(loc) {
if (existingOnLocationChange) existingOnLocationChange(loc);

if (!nv.volumes || !nv.volumes.length) return;
Expand All @@ -64,6 +66,18 @@ function setup(nv) {
if (mc) drawMatrix(data, dims, mc);
}
};

nv.onLocationChange = triggerUpdate;

// make a loc object from current crosshair state so the panel fills immediately
const pos = nv.scene.crosshairPos;
const dims = nv.volumes[0].dims;
const initialVox = [
Math.round(pos[0] * dims[1]),
Math.round(pos[1] * dims[2]),
Math.round(pos[2] * dims[3])
];
triggerUpdate({ vox: initialVox });
}

// render voxel values as a labeled grayscale grid
Expand Down Expand Up @@ -121,14 +135,16 @@ function plot() {

let container = window.document.createElement('div');
container.id = 'PixelExplorerDiv';
container.style.cssText = 'position:fixed; top:10px; left:10px; z-index:1000; cursor:move;';
container.style.cssText = 'position:fixed; top:10px; left:10px; z-index:2147483647; cursor:move; border:2px solid #fff; box-shadow:0 0 0 1px #000; border-radius:3px; overflow:hidden; line-height:0;';
window.document.body.appendChild(container);

const size = Math.floor(400 / (HALF * 2 + 1)) * (HALF * 2 + 1);

const mc = document.createElement('canvas');
mc.id = 'PixelExplorerCanvas';
mc.width = 400;
mc.height = 400;
mc.style.cssText = 'display:block; width:400px; height:400px;';
mc.width = size;
mc.height = size;
mc.style.cssText = `display:block; width:${size}px; height:${size}px;`;
container.appendChild(mc);

// drag logic
Expand Down
75 changes: 52 additions & 23 deletions examples/roiStats.js
Original file line number Diff line number Diff line change
@@ -1,47 +1,49 @@
const CATEGORY = "Statistics";

if (typeof window._RoiStatsLoaded === 'undefined') {
window._RoiStatsLoaded = true;

(function() {

const CATEGORY = "Statistics";
const NUMPY_TS_URL = 'https://cdn.jsdelivr.net/npm/numpy-ts@1.3.0/dist/numpy-ts.browser.js';

// inject boostlet, but wait for nv and volumes before calling init
// calling init too early means niivue won't be detected yet
const script = document.createElement("script");
script.type = "text/javascript";
script.src = "https://boostlet.org/dist/boostlet.min.js";
// half width of the roi in voxels so the full neighborhood is (2*HALF+1)^2
const HALF = 4;

const script = document.createElement('script');
script.type = 'text/javascript';
script.src = 'https://boostlet.org/dist/boostlet.min.js';
script.onload = function() {
var poll = setInterval(function() {
if (window.nv && nv.volumes && nv.volumes.length > 0) {
try {
Boostlet.init();
clearInterval(poll);
run();
} catch(e) {
// framework not ready yet keep polling
}
}, 300);
};
document.head.appendChild(script);

// half-width of the roi in voxels, so the full neighborhood is (2*HALF+1)^2
const HALF = 4;

async function run() {
Boostlet.init();

function run() {
if (Boostlet.framework.name !== 'niivue') {
alert('Only niivue is supported right now :(');
return;
}

const nv = Boostlet.framework.instance;
setup(nv);
}

async function setup(nv) {
plot();

// load numpy-ts via stats wrapper
const stats = await Boostlet.stats();
// load numpy-ts directly no boostlet wrapper needed
const np = await import(NUMPY_TS_URL);

// save whatever handler was already on nv so we can chain it
const existingOnLocationChange = nv.onLocationChange;

nv.onLocationChange = async function(loc) {
const triggerUpdate = async function(loc) {
if (existingOnLocationChange) existingOnLocationChange(loc);
if (!nv.volumes || !nv.volumes.length) return;

Expand All @@ -50,18 +52,27 @@ async function setup(nv) {
const start = [v[0] - HALF, v[1] - HALF, v[2] - HALF];
const end = [v[0] + HALF, v[1] + HALF, v[2] + HALF];

// collapse the axis perpendicular to the current slice to a single voxel
// 2 = sagittal (x axis), 1 = coronal (y axis), default axial (z axis)
const fixedAxis = nv.opts.sliceType === 2 ? 0
: nv.opts.sliceType === 1 ? 1 : 2;
start[fixedAxis] = end[fixedAxis] = Math.round((start[fixedAxis] + end[fixedAxis]) / 2);

const { data } = Boostlet.get_subvolume(start, end);
if (!data || data.length === 0) return;

const result = stats.all(data, nv.volumes[0]);
const result = computeStats(np, data, nv.volumes[0]);
updatePanel(result);
};

nv.onLocationChange = triggerUpdate;

// fire immediately so panel populates on load
const pos = nv.scene.crosshairPos;
const dims = nv.volumes[0].dims;
triggerUpdate({ vox: [
Math.round(pos[0] * dims[1]),
Math.round(pos[1] * dims[2]),
Math.round(pos[2] * dims[3])
]});
}

// write computed stats into the panel dom elements
Expand All @@ -74,15 +85,15 @@ function updatePanel(s) {
document.getElementById('roi-p75').textContent = s.p75.toFixed(2);
}

// create and inject the stats panel, remove any existing one first
// create and inject the stats panel remove any existing one first
function plot() {
const existing = document.getElementById('RoiStatsDiv');
if (existing) existing.remove();

const div = document.createElement('div');
div.id = 'RoiStatsDiv';
div.style.cssText = `
position: fixed; top: 10px; right: 10px; z-index: 1000;
position: fixed; top: 10px; right: 10px; z-index: 2147483647;
background: rgba(0,0,0,0.85); color: #fff;
font-family: monospace; font-size: 13px;
padding: 12px 16px; border-radius: 6px;
Expand Down Expand Up @@ -123,4 +134,22 @@ function plot() {
});
}

// inline stat functions using numpy-ts directly
// apply nifti slope/intercept then compute all stats in one pass
function computeStats(np, data, volume) {
const slope = (volume && volume.hdr.scl_slope) || 1;
const inter = (volume && volume.hdr.scl_inter) || 0;
const a = np.array(Array.from(data), 'float32').multiply(slope).add(inter);
const scalar = v => typeof v === 'number' ? v : v.tolist ? v.tolist() : Number(v);
return {
mean : scalar(np.mean(a)),
std : scalar(np.std(a)),
min : scalar(np.min(a)),
max : scalar(np.max(a)),
p25 : scalar(np.percentile(a, 25)),
p75 : scalar(np.percentile(a, 75)),
};
}

})();
}
15 changes: 1 addition & 14 deletions src/boostlet.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
import {Util} from './util.js';
import {Framework} from './framework.js';
import {loadStats} from './stats.js';

export class Boostlet {

Expand Down Expand Up @@ -34,7 +32,7 @@ export class Boostlet {

if (this.framework) {

console.log('Found', this.framework, '!')
console.log('Found', this.framework.name, '!')

} else {

Expand Down Expand Up @@ -145,15 +143,4 @@ export class Boostlet {

}



// const stats = await Boostlet.stats();
// const result = stats.all(subvolumeData);
// gives you { mean, std, min, max, p25, p75 }
async stats() {

return loadStats();

}

}
79 changes: 0 additions & 79 deletions src/stats.js

This file was deleted.

Loading