Skip to content

Commit fb0b8db

Browse files
committed
Developed nifti 4d viewer with voxel intensity plots.
1 parent 6c8a829 commit fb0b8db

File tree

6 files changed

+348
-0
lines changed

6 files changed

+348
-0
lines changed

website/dashboard/index.html

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
<meta charset="UTF-8">
55
<meta name="viewport" content="width=device-width, initial-scale=1.0">
66
<title>IVIM MRI Algorithm Fitting Dashboard</title>
7+
<link rel="icon" type="image/x-icon" href="../favicon-32x32.png">
8+
79
<!-- Load Plotly.js into the DOM -->
810
<script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.7.1/jszip.min.js"></script>
911
<script src="https://cdn.plot.ly/plotly-2.30.0.min.js"></script>

website/favicon-32x32.png

1.89 KB
Loading

website/index.html

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
<meta charset="UTF-8">
55
<meta name="viewport" content="width=device-width, initial-scale=1.0">
66
<title>OSIPI Taskforce 2.4 IVIM MRI</title>
7+
<link rel="icon" type="image/x-icon" href="favicon-32x32.png">
78
<link rel="stylesheet" href="index.css">
89
</head>
910
<body>
@@ -20,6 +21,9 @@ <h2>Documentation</h2>
2021
<a href="dashboard" class="card">
2122
<h2>Data Visualization Dashboard</h2>
2223
</a>
24+
<a href="nifti-viewer" class="card">
25+
<h2>Nifti Viewer</h2>
26+
</a>
2327
</div>
2428
</div>
2529
</main>

website/nifti-viewer/index.css

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
.bar-title {
2+
margin: 0;
3+
font-size: 2rem;
4+
font-weight: 600;
5+
}
6+
header {
7+
padding: 1rem 2rem;
8+
background-color: #072A6A;
9+
color: #fff;
10+
}
11+
.divider {
12+
margin: 0.5rem 0;
13+
border: none;
14+
border-top: 2px solid #62D58A;
15+
}
16+
.nifti-image-container {
17+
position: relative;
18+
width: 350px;
19+
height: 350px;
20+
border: 1px solid #6c757d;
21+
color: white;
22+
margin: 10px 10px 2rem;
23+
}
24+
25+
.nifti-image-container > div[id^="nifti-image-"] {
26+
position: absolute;
27+
top: 0;
28+
bottom: 0;
29+
left: 0;
30+
right: 0;
31+
flex: 1;
32+
}
33+
34+
35+
#image-and-data-info {
36+
display: flex;
37+
flex-wrap: wrap;
38+
}
39+

website/nifti-viewer/index.html

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
<!doctype html>
2+
<html lang="en">
3+
4+
<head>
5+
<meta name="charset" content="utf-8">
6+
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
7+
<link rel="icon" type="image/x-icon" href="../favicon-32x32.png">
8+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.2/dist/css/bootstrap.min.css"
9+
integrity="sha384-xOolHFLEh07PJGoPkLv1IbcEPTNtaed2xpHsD9ESMhqIYd0nLMwNLD69Npy4HI+N" crossorigin="anonymous">
10+
<link href="index.css" rel="stylesheet">
11+
</head>
12+
13+
<body>
14+
<header>
15+
<h1 class="bar-title">NIFTI Image Viewer </h1>
16+
<hr class="divider" />
17+
</header>
18+
<div class="container-fluid">
19+
<h2><small class="text-muted">Example of displaying a NIFTI image using Cornerstone</small></h2>
20+
<ul>
21+
<li class="lead">
22+
Upload a NIFTI file to view it using cornerstone.
23+
</li>
24+
<li class="lead">
25+
Scroll to navigate between different slices.
26+
</li>
27+
<li class="lead">
28+
Drag the mouse to get the synchronized view for the selected area.
29+
</li>
30+
<li class="lead">
31+
A voxel intensity plot will be displayed when you drag or scroll through an image viewport.
32+
</li>
33+
</ul>
34+
35+
<div class="row">
36+
<div class="col">
37+
<form id="form">
38+
<div class="form-group form-row">
39+
<label class="col-form-label" for="nifti-file">NIFTI File</label>
40+
<div class="col-sm-5">
41+
<input class="form-control" type="file" id="nifti-file">
42+
</div>
43+
<div class="col-sm-3">
44+
<button class="btn btn-primary" type="button" id="upload-and-view">Upload and View</button>
45+
</div>
46+
</div>
47+
</form>
48+
</div>
49+
</div>
50+
51+
<section id="image-and-data-info">
52+
<div class="nifti-image-container" id="nifti-image-z"></div>
53+
<div class="nifti-image-container" id="nifti-image-x"></div>
54+
<div class="nifti-image-container" id="nifti-image-y"></div>
55+
<div id="plot"></div>
56+
</section>
57+
<div id="voxel-info" class="mt-4">
58+
<h5>Cursor Position</h5>
59+
<p id="voxel-coordinates">(x, y, z): (0, 0, 0)</p>
60+
</div>
61+
</div>
62+
</body>
63+
<script src="https://cdn.jsdelivr.net/npm/cornerstone-core@2.6.1/dist/cornerstone.min.js"></script>
64+
<script src="https://cdn.jsdelivr.net/npm/cornerstone-math@0.1.6/dist/cornerstoneMath.min.js"></script>
65+
<script src="https://cdn.jsdelivr.net/npm/cornerstone-tools@2.1.0/dist/cornerstoneTools.min.js"></script>
66+
<script
67+
src="https://cdn.jsdelivr.net/npm/@cornerstonejs/nifti-image-loader@1.0.9/dist/cornerstoneNIFTIImageLoader.min.js"></script>
68+
<script src="https://cdn.plot.ly/plotly-2.30.0.min.js"></script>
69+
<script src="index.js"></script>
70+
71+
</html>

website/nifti-viewer/index.js

Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
1+
cornerstoneNIFTIImageLoader.external.cornerstone = cornerstone;
2+
const ImageId = cornerstoneNIFTIImageLoader.nifti.ImageId;
3+
cornerstoneNIFTIImageLoader.nifti.streamingMode = true;
4+
const niftiReader = cornerstoneNIFTIImageLoader.external.niftiReader;
5+
6+
let loaded = false;
7+
const synchronizer = new cornerstoneTools.Synchronizer("cornerstonenewimage", cornerstoneTools.updateImageSynchronizer);
8+
9+
let voxel3dUnits = [0, 0, 0];
10+
let dims = [0, 0, 0, 0];
11+
let niftiImageBuffer = [];
12+
13+
// Helper to calculate voxel position based on view
14+
function updateVoxelCoordinates(view, voxelCoords) {
15+
if (view === 'axial') {
16+
voxel3dUnits[0] = voxelCoords.x;
17+
voxel3dUnits[1] = voxelCoords.y;
18+
} else if (view === 'sagittal') {
19+
voxel3dUnits[1] = voxelCoords.x;
20+
voxel3dUnits[2] = voxelCoords.y;
21+
} else if (view === 'coronal') {
22+
voxel3dUnits[0] = voxelCoords.x;
23+
voxel3dUnits[2] = voxelCoords.y;
24+
}
25+
}
26+
27+
// Event listener for mouse drag to update voxel coordinates
28+
function addMouseDragListener(element, view) {
29+
element.addEventListener('cornerstonetoolsmousedrag', (event) => {
30+
const voxelCoords = event.detail?.currentPoints?.image;
31+
if (voxelCoords) {
32+
updateVoxelCoordinates(view, voxelCoords);
33+
handleVoxelClick(voxel3dUnits);
34+
}
35+
});
36+
}
37+
38+
// Load and display NIfTI image on a specific element
39+
function loadAndViewImage(element, imageId, view) {
40+
const imageIdObject = ImageId.fromURL(imageId);
41+
element.dataset.imageId = imageIdObject.url;
42+
43+
cornerstone.loadAndCacheImage(imageIdObject.url).then(image => {
44+
setupImageViewport(element, image);
45+
setupImageTools(element, imageIdObject);
46+
47+
synchronizer.add(element);
48+
addMouseDragListener(element, view);
49+
50+
element.addEventListener('cornerstonestackscroll', (event) => updateSliceIndex(view, event.detail.newImageIdIndex));
51+
}).catch(err => {
52+
console.error(`Error loading image for ${view} view:`, err);
53+
});
54+
element.addEventListener('click', function (event) {
55+
//TODO: Update the clicked voxel information.
56+
console.log(event)
57+
});
58+
}
59+
60+
// Setup viewport and display the image
61+
function setupImageViewport(element, image) {
62+
const viewport = cornerstone.getDefaultViewportForImage(element, image);
63+
cornerstone.displayImage(element, image, viewport);
64+
cornerstone.resize(element, true);
65+
}
66+
67+
// Enable tools and interactions for the displayed image
68+
function setupImageTools(element, imageIdObject) {
69+
const numberOfSlices = cornerstone.metaData.get('multiFrameModule', imageIdObject.url).numberOfFrames;
70+
const stack = {
71+
currentImageIdIndex: imageIdObject.slice.index,
72+
imageIds: Array.from({ length: numberOfSlices }, (_, i) => `nifti:${imageIdObject.filePath}#${imageIdObject.slice.dimension}-${i},t-0`)
73+
};
74+
75+
cornerstoneTools.addStackStateManager(element, ['stack']);
76+
cornerstoneTools.addToolState(element, 'stack', stack);
77+
cornerstoneTools.mouseInput.enable(element);
78+
cornerstoneTools.mouseWheelInput.enable(element);
79+
cornerstoneTools.pan.activate(element, 2);
80+
cornerstoneTools.stackScrollWheel.activate(element);
81+
cornerstoneTools.orientationMarkers.enable(element);
82+
cornerstoneTools.stackPrefetch.enable(element);
83+
cornerstoneTools.referenceLines.tool.enable(element, synchronizer);
84+
cornerstoneTools.crosshairs.enable(element, 1, synchronizer);
85+
}
86+
87+
// Handle voxel click event
88+
function handleVoxelClick(currentVoxel) {
89+
const [nx, ny, nz, nt] = dims;
90+
let [voxelX, voxelY, voxelZ] = currentVoxel;
91+
92+
voxelX = Math.min(Math.max(Math.round(voxelX), 1), nx) - 1;
93+
voxelY = Math.min(Math.max(ny - Math.round(voxelY), 1), ny) - 1;
94+
voxelZ = Math.min(Math.max(nz - Math.round(voxelZ), 1), nz) - 1;
95+
96+
const voxelValues = getVoxelValuesAcrossTime(voxelX, voxelY, voxelZ, nx, ny, nz, nt);
97+
updateVoxelCoordinatesDisplay(voxelX + 1, voxelY + 1, voxelZ + 1);
98+
plotVoxelData(voxelValues);
99+
}
100+
101+
// Extract voxel values across all time points
102+
function getVoxelValuesAcrossTime(x, y, z, nx, ny, nz, nt) {
103+
const sliceSize = nx * ny;
104+
const volumeSize = sliceSize * nz;
105+
let voxelValues = [];
106+
107+
for (let t = 0; t < nt; t++) {
108+
const voxelIndex = x + y * nx + z * nx * ny + t * volumeSize;
109+
voxelValues.push(niftiImageBuffer[voxelIndex]);
110+
}
111+
112+
return voxelValues;
113+
}
114+
115+
// Update voxel coordinates display on the page
116+
function updateVoxelCoordinatesDisplay(x, y, z) {
117+
document.getElementById('voxel-coordinates').innerText = `(x, y, z): (${x}, ${y}, ${z})`;
118+
}
119+
120+
// Update the slice index based on the current view
121+
function updateSliceIndex(view, newIndex) {
122+
if (view === 'axial') {
123+
voxel3dUnits[2] = newIndex;
124+
} else if (view === 'sagittal') {
125+
voxel3dUnits[0] = newIndex;
126+
} else if (view === 'coronal') {
127+
voxel3dUnits[1] = newIndex;
128+
}
129+
handleVoxelClick(voxel3dUnits);
130+
}
131+
132+
// Load NIfTI file and display the axial, sagittal, and coronal views
133+
function loadAllFileViews(file) {
134+
const fileURL = URL.createObjectURL(file);
135+
const imageId = `nifti:${fileURL}`;
136+
137+
cornerstoneNIFTIImageLoader.nifti.loadHeader(imageId).then((header) => {
138+
dims = [...header.voxelLength, header.timeSlices];
139+
loadAndViewImage(document.getElementById('nifti-image-z'), `${imageId}#z,t-0`, 'axial');
140+
loadAndViewImage(document.getElementById('nifti-image-x'), `${imageId}#x,t-0`, 'sagittal');
141+
loadAndViewImage(document.getElementById('nifti-image-y'), `${imageId}#y,t-0`, 'coronal');
142+
});
143+
144+
}
145+
146+
147+
// Plot voxel data using Plotly
148+
function plotVoxelData(values) {
149+
const trace = {
150+
y: values,
151+
mode: 'markers',
152+
type: 'bar',
153+
};
154+
const layout = {
155+
title: 'Voxel Intensity Under the Cursor',
156+
xaxis: { title: 'Time Point' },
157+
yaxis: { title: 'Intensity' },
158+
};
159+
Plotly.newPlot('plot', [trace], layout);
160+
}
161+
162+
// Initialize file upload and view
163+
document.getElementById('upload-and-view').addEventListener('click', () => {
164+
const file = document.getElementById('nifti-file').files[0];
165+
if (file) {
166+
getNiftiArrayBuffer(file);
167+
loadAllFileViews(file);
168+
} else {
169+
alert("Please select a NIFTI file to upload.");
170+
}
171+
});
172+
173+
// Enable cornerstone for the viewports
174+
cornerstone.enable(document.getElementById('nifti-image-z'));
175+
cornerstone.enable(document.getElementById('nifti-image-x'));
176+
cornerstone.enable(document.getElementById('nifti-image-y'));
177+
178+
// Fetch NIfTI file data as ArrayBuffer
179+
async function getNiftiArrayBuffer(file) {
180+
const data = await loadNiftiFile(file);
181+
if (!data) return console.error('Failed to load NIfTI file');
182+
183+
try {
184+
const header = niftiReader.readHeader(data);
185+
niftiImageBuffer = createTypedArray(header, niftiReader.readImage(header, data));
186+
} catch (error) {
187+
console.error('Error processing file:', error);
188+
}
189+
}
190+
191+
// Create typed array based on NIfTI data type
192+
// Create a mapping between datatype codes and typed array constructors
193+
const typedArrayConstructorMap = {
194+
[niftiReader.NIFTI1.TYPE_UINT8]: Uint8Array,
195+
[niftiReader.NIFTI1.TYPE_UINT16]: Uint16Array,
196+
[niftiReader.NIFTI1.TYPE_UINT32]: Uint32Array,
197+
[niftiReader.NIFTI1.TYPE_INT8]: Int8Array,
198+
[niftiReader.NIFTI1.TYPE_INT16]: Int16Array,
199+
[niftiReader.NIFTI1.TYPE_INT32]: Int32Array,
200+
[niftiReader.NIFTI1.TYPE_FLOAT32]: Float32Array,
201+
[niftiReader.NIFTI1.TYPE_FLOAT64]: Float64Array,
202+
[niftiReader.NIFTI1.TYPE_RGB]: Uint8Array,
203+
[niftiReader.NIFTI1.TYPE_RGBA]: Uint8Array
204+
};
205+
206+
// Create typed array based on NIfTI data type code
207+
function createTypedArray(header, imageBuffer) {
208+
const TypedArrayConstructor = typedArrayConstructorMap[header.datatypeCode];
209+
210+
if (TypedArrayConstructor) {
211+
return new TypedArrayConstructor(imageBuffer);
212+
} else {
213+
console.error('Unsupported datatype:', header.datatypeCode);
214+
return null;
215+
}
216+
}
217+
218+
// Load the NIfTI file as an ArrayBuffer
219+
async function loadNiftiFile(file) {
220+
return new Promise((resolve, reject) => {
221+
const reader = new FileReader();
222+
reader.onload = (event) => {
223+
const arrayBuffer = event.target.result;
224+
const data = niftiReader.isCompressed(arrayBuffer)
225+
? niftiReader.decompress(arrayBuffer)
226+
: arrayBuffer;
227+
resolve(data);
228+
};
229+
reader.onerror = (error) => reject(error);
230+
reader.readAsArrayBuffer(file);
231+
});
232+
}

0 commit comments

Comments
 (0)