-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathindex.html
More file actions
224 lines (183 loc) · 8.06 KB
/
Copy pathindex.html
File metadata and controls
224 lines (183 loc) · 8.06 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>NiiVue Explorer</title>
<link rel="stylesheet" href="style.css">
<script src="https://cdn.jsdelivr.net/npm/@niivue/niivue@0.66.0/dist/niivue.umd.min.js"></script>
</head>
<body>
<div id="left">
<canvas id="viewer"></canvas>
<input type="file" id="volumeInput" title="Load a volume">
</div>
<div id="right">
<span id="info">loading</span>
<canvas id="matrix-canvas"></canvas>
</div>
<script>
// sets up everything, attaches to viewer etc
const nv = new niivue.Niivue({
crosshairColor: [0, 1, 1, 0.1],
crosshairWidth: 0.2,
show3Dcrosshair: true,
textHeight: 0
});
window.nv = nv;
const viewer = document.getElementById('viewer');
const mc = document.getElementById('matrix-canvas');
const ctx = mc.getContext('2d');
const infoBox = document.getElementById('info');
// 9x9 hover neighborhood
const HALF = 4;
let boxActive = false;
nv.attachToCanvas(viewer);
// handles window resize
function resize() {
viewer.width = viewer.parentElement.clientWidth;
viewer.height = viewer.parentElement.clientHeight;
nv.resizeListener();
}
window.addEventListener('resize', resize);
resize();
// enables native drag selection
nv.opts.dragMode = 5;
// gets fractional coordinates from mouse event, returns an [x,y,z] . !!! Not in RAS space
function fracAt(e) {
const rect = viewer.getBoundingClientRect();
const frac = nv.canvasPos2frac([e.clientX - rect.left, e.clientY - rect.top]);
return frac && frac[0] >= 0 ? frac : null;
}
// samples region and draws it : only used in hover
function sampleRegion(voxA, voxB) {
if (!nv.volumes.length || !voxA || !voxB) return; // bails early if no volume
// collapses the slice plane axis to a single voxel
// axial(0) Z=2, coronal(1) Y=1, sagittal(2) X=0 so that you don't get a 3D chunk of data
const fixedAxis = nv.opts.sliceType === nv.sliceTypeSagittal ? 0
: nv.opts.sliceType === nv.sliceTypeCoronal ? 1 : 2;
const s = [...voxA]; // Makes a copy, instead of a reference.
const end = [...voxB];
s[fixedAxis] = end[fixedAxis] = Math.round((voxA[fixedAxis] + voxB[fixedAxis]) / 2); // Flattens axis, finds midpoint
const [data, dims] = nv.volumes[0].getVolumeData(s, end);
if (data && dims) drawMatrix(data, dims);
}
// creates the blue overlay if it does not exist
async function ensureOverlayLoaded() {
if (nv.volumes.length > 1) return; // ASks if there is more than 1 volume loaded, checks if overlay already exuists
const overlay = await nv.volumes[0].clone();
overlay.zeroImage();
overlay.colormap = 'blue';
overlay.opacity = 0.3;
overlay.cal_min = 0;
overlay.cal_max = 1;
nv.addVolume(overlay);
}
// gets a neighborhood around the crosshair
// loc.vox is already in RAS coordinates which is what getVolumeData needs
// also getVolumeData clamps OOB coordinates itself so you dont need to deal with that
nv.onLocationChange = function(loc) {
if (boxActive || !nv.volumes.length) return;
const v = loc.vox; // Cursor position in RAS coordinates.
sampleRegion(
[v[0] - HALF, v[1] - HALF, v[2] - HALF],
[v[0] + HALF, v[1] + HALF, v[2] + HALF]
);
};
// clear box and resume hover on left click
viewer.addEventListener('mousedown', function(e) {
if (e.button === 0 && boxActive) {
boxActive = false;
if (nv.volumes.length > 1) {
nv.volumes[1].zeroImage();
nv.updateGLVolume();
}
const frac = fracAt(e);
if (frac) {
const v = nv.frac2vox(frac);
sampleRegion(
[v[0] - HALF, v[1] - HALF, v[2] - HALF],
[v[0] + HALF, v[1] + HALF, v[2] + HALF]
);
}
}
});
// handles box select mode the drag coordinates already define a single slice
nv.onDragRelease = async function(info) {
if (info.voxStart[0] < 0) return; // Makes sure the box drawn is actually valid.
boxActive = true;
await ensureOverlayLoaded();
nv.volumes[1].zeroImage(); // Zeroes out the overlay, clear.
// collapses the slice plane axis to a single voxel
// axial(0) Z=2, coronal(1) Y=1, sagittal(2) X=0
const fixedAxis = nv.opts.sliceType === nv.sliceTypeSagittal ? 0
: nv.opts.sliceType === nv.sliceTypeCoronal ? 1 : 2;
const start = [...info.voxStart]; // Defines the two corners of the box, so start is where you began drawing the box, end is where you stopped clickign.
const end = [...info.voxEnd]; // Copies these two into new arrays.
// Averages the 2 fixed axis parts. With this midpoints we can collapse the 3D shape into 2D slice.
start[fixedAxis] = end[fixedAxis] = Math.round((info.voxStart[fixedAxis] + info.voxEnd[fixedAxis]) / 2);
const [data, dims] = nv.volumes[0].getVolumeData(start, end);
const boxVoxels = new Float32Array(data.length).fill(1);
nv.volumes[1].setVolumeData(start, end, boxVoxels); // Fills the overlay volume with 1's from start point to end point.
nv.updateGLVolume();
if (data && dims) drawMatrix(data, dims);
};
// loads initial volume
(async () => {
try {
await nv.loadVolumes([{ url: './ultrasound.nrrd', name: 'ultrasound.nrrd' }]);
nv.setSliceType(nv.sliceTypeAxial);
infoBox.textContent = 'Hover to inspect, right click drag to select';
} catch {
infoBox.textContent = 'failed to load';
}
})();
// load a volume from file picker
document.getElementById('volumeInput').addEventListener('change', async (e) => {
const file = e.target.files[0];
if (!file) return;
await nv.loadVolumes([{ url: URL.createObjectURL(file), name: file.name }]);
nv.setSliceType(nv.sliceTypeAxial);
if (nv.volumes.length > 1) {
nv.removeVolume(nv.volumes[1]);
boxActive = false;
}
});
// render voxel values as a labeled grayscale grid
function drawMatrix(data, dims) {
const { cal_min, cal_max } = nv.volumes[0];
const range = cal_max - cal_min || 1; // ramnge between display values.
// getVolumeData returns dims as X Y Z and the collapsed axis has size 1
// the first non 1 dim is cols second is rows
const [cols, rows] = dims.filter(d => d > 1);
if (!cols || !rows) return;
const cell = Math.floor(Math.min(mc.clientWidth / cols, mc.clientHeight / rows)); // picks between max cell width and max cell heigh. picks the smallest
if (cell < 1) return;
mc.width = cols * cell; // make sure the canvas is the correct size to the amount of stuff.
mc.height = rows * cell;
// Figures out whether to show a label or not. needs to be changed IMO
const showLabels = cell >= 8;
if (showLabels) {
ctx.font = `${Math.max(Math.floor(cell * 0.28), 6)}px monospace`;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
}
// Loops through every voxel in the region, draws a grey square for each one based on how bright it is. If the cells are
// big enough, it prints out raw number on top.
for (let r = 0; r < rows; r++) {
for (let c = 0; c < cols; c++) {
const val = data[c + (rows - 1 - r) * cols];
const norm = (val - cal_min) / range; // Gives a range from 0 - 1, gets normalized to 0-255 in the line below.
const g = Math.round(norm * 255);
ctx.fillStyle = `rgb(${g},${g},${g})`;
ctx.fillRect(c * cell, r * cell, cell, cell);
if (showLabels) {
ctx.fillStyle = norm > 0.55 ? 'rgba(0,0,0,0.8)' : 'rgba(255,255,255,0.8)';
ctx.fillText(val.toFixed(0), c * cell + cell / 2, r * cell + cell / 2);
}
}
}
infoBox.textContent = `${cols}x${rows} vox [${cal_min.toFixed(0)}, ${cal_max.toFixed(0)}]`;
}
</script>
</body>
</html>