diff --git a/website/dashboard/index.html b/website/dashboard/index.html
index 262e5a5..b03fb3e 100644
--- a/website/dashboard/index.html
+++ b/website/dashboard/index.html
@@ -4,6 +4,8 @@
IVIM MRI Algorithm Fitting Dashboard
+
+
diff --git a/website/favicon-32x32.png b/website/favicon-32x32.png
new file mode 100644
index 0000000..bc0bf14
Binary files /dev/null and b/website/favicon-32x32.png differ
diff --git a/website/index.html b/website/index.html
index e7d7ebd..24f2269 100644
--- a/website/index.html
+++ b/website/index.html
@@ -4,6 +4,7 @@
OSIPI Taskforce 2.4 IVIM MRI
+
@@ -20,6 +21,9 @@ Documentation
Data Visualization Dashboard
+
+ Nifti Viewer
+
diff --git a/website/nifti-viewer/index.css b/website/nifti-viewer/index.css
new file mode 100644
index 0000000..2374adf
--- /dev/null
+++ b/website/nifti-viewer/index.css
@@ -0,0 +1,39 @@
+.bar-title {
+ margin: 0;
+ font-size: 2rem;
+ font-weight: 600;
+}
+header {
+ padding: 1rem 2rem;
+ background-color: #072A6A;
+ color: #fff;
+}
+.divider {
+ margin: 0.5rem 0;
+ border: none;
+ border-top: 2px solid #62D58A;
+}
+.nifti-image-container {
+ position: relative;
+ width: 350px;
+ height: 350px;
+ border: 1px solid #6c757d;
+ color: white;
+ margin: 10px 10px 2rem;
+}
+
+.nifti-image-container > div[id^="nifti-image-"] {
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ flex: 1;
+}
+
+
+#image-and-data-info {
+ display: flex;
+ flex-wrap: wrap;
+}
+
diff --git a/website/nifti-viewer/index.html b/website/nifti-viewer/index.html
new file mode 100644
index 0000000..c5468f1
--- /dev/null
+++ b/website/nifti-viewer/index.html
@@ -0,0 +1,71 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Example of displaying a NIFTI image using Cornerstone
+
+ -
+ Upload a NIFTI file to view it using cornerstone.
+
+ -
+ Scroll to navigate between different slices.
+
+ -
+ Drag the mouse to get the synchronized view for the selected area.
+
+ -
+ A voxel intensity plot will be displayed when you drag or scroll through an image viewport.
+
+
+
+
+
+
+
+
Cursor Position
+
(x, y, z): (0, 0, 0)
+
+
+
+
+
+
+
+
+
+
+
diff --git a/website/nifti-viewer/index.js b/website/nifti-viewer/index.js
new file mode 100644
index 0000000..ec7b28c
--- /dev/null
+++ b/website/nifti-viewer/index.js
@@ -0,0 +1,231 @@
+cornerstoneNIFTIImageLoader.external.cornerstone = cornerstone;
+const ImageId = cornerstoneNIFTIImageLoader.nifti.ImageId;
+cornerstoneNIFTIImageLoader.nifti.streamingMode = true;
+const niftiReader = cornerstoneNIFTIImageLoader.external.niftiReader;
+
+let loaded = false;
+const synchronizer = new cornerstoneTools.Synchronizer("cornerstonenewimage", cornerstoneTools.updateImageSynchronizer);
+
+let voxel3dUnits = [0, 0, 0];
+let dims = [0, 0, 0, 0];
+let niftiImageBuffer = [];
+
+// Helper to calculate voxel position based on view
+function updateVoxelCoordinates(view, voxelCoords) {
+ if (view === 'axial') {
+ voxel3dUnits[0] = voxelCoords.x;
+ voxel3dUnits[1] = voxelCoords.y;
+ } else if (view === 'sagittal') {
+ voxel3dUnits[1] = voxelCoords.x;
+ voxel3dUnits[2] = voxelCoords.y;
+ } else if (view === 'coronal') {
+ voxel3dUnits[0] = voxelCoords.x;
+ voxel3dUnits[2] = voxelCoords.y;
+ }
+}
+
+// Event listener for mouse drag to update voxel coordinates
+function addMouseDragListener(element, view) {
+ element.addEventListener('cornerstonetoolsmousedrag', (event) => {
+ const voxelCoords = event.detail?.currentPoints?.image;
+ if (voxelCoords) {
+ updateVoxelCoordinates(view, voxelCoords);
+ handleVoxelClick(voxel3dUnits);
+ }
+ });
+}
+
+// Load and display NIfTI image on a specific element
+function loadAndViewImage(element, imageId, view) {
+ const imageIdObject = ImageId.fromURL(imageId);
+ element.dataset.imageId = imageIdObject.url;
+
+ cornerstone.loadAndCacheImage(imageIdObject.url).then(image => {
+ setupImageViewport(element, image);
+ setupImageTools(element, imageIdObject);
+
+ synchronizer.add(element);
+ addMouseDragListener(element, view);
+
+ element.addEventListener('cornerstonestackscroll', (event) => updateSliceIndex(view, event.detail.newImageIdIndex));
+ }).catch(err => {
+ console.error(`Error loading image for ${view} view:`, err);
+ });
+ element.addEventListener('click', function (event) {
+ //TODO: Update the clicked voxel information.
+ console.log(event)
+ });
+}
+
+// Setup viewport and display the image
+function setupImageViewport(element, image) {
+ const viewport = cornerstone.getDefaultViewportForImage(element, image);
+ cornerstone.displayImage(element, image, viewport);
+ cornerstone.resize(element, true);
+}
+
+// Enable tools and interactions for the displayed image
+function setupImageTools(element, imageIdObject) {
+ const numberOfSlices = cornerstone.metaData.get('multiFrameModule', imageIdObject.url).numberOfFrames;
+ const stack = {
+ currentImageIdIndex: imageIdObject.slice.index,
+ imageIds: Array.from({ length: numberOfSlices }, (_, i) => `nifti:${imageIdObject.filePath}#${imageIdObject.slice.dimension}-${i},t-0`)
+ };
+
+ cornerstoneTools.addStackStateManager(element, ['stack']);
+ cornerstoneTools.addToolState(element, 'stack', stack);
+ cornerstoneTools.mouseInput.enable(element);
+ cornerstoneTools.mouseWheelInput.enable(element);
+ cornerstoneTools.pan.activate(element, 2);
+ cornerstoneTools.stackScrollWheel.activate(element);
+ cornerstoneTools.orientationMarkers.enable(element);
+ cornerstoneTools.stackPrefetch.enable(element);
+ cornerstoneTools.referenceLines.tool.enable(element, synchronizer);
+ cornerstoneTools.crosshairs.enable(element, 1, synchronizer);
+}
+
+// Handle voxel click event
+function handleVoxelClick(currentVoxel) {
+ const [nx, ny, nz, nt] = dims;
+ let [voxelX, voxelY, voxelZ] = currentVoxel;
+
+ voxelX = Math.min(Math.max(Math.round(voxelX), 1), nx);
+ voxelY = Math.min(Math.max(ny - Math.round(voxelY), 1), ny);
+ voxelZ = Math.min(Math.max(nz - Math.round(voxelZ), 1), nz);
+
+ const voxelValues = getVoxelValuesAcrossTime(voxelX, voxelY, voxelZ, nx, ny, nz, nt);
+ updateVoxelCoordinatesDisplay(voxelX + 1, voxelY + 1, voxelZ + 1);
+ plotVoxelData(voxelValues);
+}
+
+// Extract voxel values across all time points
+function getVoxelValuesAcrossTime(x, y, z, nx, ny, nz, nt) {
+ const sliceSize = nx * ny;
+ const volumeSize = sliceSize * nz;
+ let voxelValues = [];
+
+ for (let t = 0; t < nt; t++) {
+ const voxelIndex = x + y * nx + z * nx * ny + t * volumeSize;
+ voxelValues.push(niftiImageBuffer[voxelIndex]);
+ }
+
+ return voxelValues;
+}
+
+// Update voxel coordinates display on the page
+function updateVoxelCoordinatesDisplay(x, y, z) {
+ document.getElementById('voxel-coordinates').innerText = `(x, y, z): (${x}, ${y}, ${z})`;
+}
+
+// Update the slice index based on the current view
+function updateSliceIndex(view, newIndex) {
+ if (view === 'axial') {
+ voxel3dUnits[2] = newIndex;
+ } else if (view === 'sagittal') {
+ voxel3dUnits[0] = newIndex;
+ } else if (view === 'coronal') {
+ voxel3dUnits[1] = newIndex;
+ }
+ handleVoxelClick(voxel3dUnits);
+}
+
+// Load NIfTI file and display the axial, sagittal, and coronal views
+function loadAllFileViews(file) {
+ const fileURL = URL.createObjectURL(file);
+ const imageId = `nifti:${fileURL}`;
+
+ cornerstoneNIFTIImageLoader.nifti.loadHeader(imageId).then((header) => {
+ dims = [...header.voxelLength, header.timeSlices];
+ loadAndViewImage(document.getElementById('nifti-image-z'), `${imageId}#z,t-0`, 'axial');
+ loadAndViewImage(document.getElementById('nifti-image-x'), `${imageId}#x,t-0`, 'sagittal');
+ loadAndViewImage(document.getElementById('nifti-image-y'), `${imageId}#y,t-0`, 'coronal');
+ });
+
+}
+
+
+// Plot voxel data using Plotly
+function plotVoxelData(values) {
+ const trace = {
+ y: values,
+ type: 'line',
+ };
+ const layout = {
+ title: 'Voxel Intensity Under the Cursor',
+ xaxis: { title: 'Time Point' },
+ yaxis: { title: 'Intensity' },
+ };
+ Plotly.newPlot('plot', [trace], layout);
+}
+
+// Initialize file upload and view
+document.getElementById('upload-and-view').addEventListener('click', () => {
+ const file = document.getElementById('nifti-file').files[0];
+ if (file) {
+ getNiftiArrayBuffer(file);
+ loadAllFileViews(file);
+ } else {
+ alert("Please select a NIFTI file to upload.");
+ }
+});
+
+// Enable cornerstone for the viewports
+cornerstone.enable(document.getElementById('nifti-image-z'));
+cornerstone.enable(document.getElementById('nifti-image-x'));
+cornerstone.enable(document.getElementById('nifti-image-y'));
+
+// Fetch NIfTI file data as ArrayBuffer
+async function getNiftiArrayBuffer(file) {
+ const data = await loadNiftiFile(file);
+ if (!data) return console.error('Failed to load NIfTI file');
+
+ try {
+ const header = niftiReader.readHeader(data);
+ niftiImageBuffer = createTypedArray(header, niftiReader.readImage(header, data));
+ } catch (error) {
+ console.error('Error processing file:', error);
+ }
+}
+
+// Create typed array based on NIfTI data type
+// Create a mapping between datatype codes and typed array constructors
+const typedArrayConstructorMap = {
+ [niftiReader.NIFTI1.TYPE_UINT8]: Uint8Array,
+ [niftiReader.NIFTI1.TYPE_UINT16]: Uint16Array,
+ [niftiReader.NIFTI1.TYPE_UINT32]: Uint32Array,
+ [niftiReader.NIFTI1.TYPE_INT8]: Int8Array,
+ [niftiReader.NIFTI1.TYPE_INT16]: Int16Array,
+ [niftiReader.NIFTI1.TYPE_INT32]: Int32Array,
+ [niftiReader.NIFTI1.TYPE_FLOAT32]: Float32Array,
+ [niftiReader.NIFTI1.TYPE_FLOAT64]: Float64Array,
+ [niftiReader.NIFTI1.TYPE_RGB]: Uint8Array,
+ [niftiReader.NIFTI1.TYPE_RGBA]: Uint8Array
+};
+
+// Create typed array based on NIfTI data type code
+function createTypedArray(header, imageBuffer) {
+ const TypedArrayConstructor = typedArrayConstructorMap[header.datatypeCode];
+
+ if (TypedArrayConstructor) {
+ return new TypedArrayConstructor(imageBuffer);
+ } else {
+ console.error('Unsupported datatype:', header.datatypeCode);
+ return null;
+ }
+}
+
+// Load the NIfTI file as an ArrayBuffer
+async function loadNiftiFile(file) {
+ return new Promise((resolve, reject) => {
+ const reader = new FileReader();
+ reader.onload = (event) => {
+ const arrayBuffer = event.target.result;
+ const data = niftiReader.isCompressed(arrayBuffer)
+ ? niftiReader.decompress(arrayBuffer)
+ : arrayBuffer;
+ resolve(data);
+ };
+ reader.onerror = (error) => reject(error);
+ reader.readAsArrayBuffer(file);
+ });
+}