From fb0b8db97767fc957b382e5e32d180c822e3a2a8 Mon Sep 17 00:00:00 2001 From: AhmedBasem Date: Tue, 8 Oct 2024 18:14:47 +0300 Subject: [PATCH 1/3] Developed nifti 4d viewer with voxel intensity plots. --- website/dashboard/index.html | 2 + website/favicon-32x32.png | Bin 0 -> 1936 bytes website/index.html | 4 + website/nifti-viewer/index.css | 39 ++++++ website/nifti-viewer/index.html | 71 ++++++++++ website/nifti-viewer/index.js | 232 ++++++++++++++++++++++++++++++++ 6 files changed, 348 insertions(+) create mode 100644 website/favicon-32x32.png create mode 100644 website/nifti-viewer/index.css create mode 100644 website/nifti-viewer/index.html create mode 100644 website/nifti-viewer/index.js diff --git a/website/dashboard/index.html b/website/dashboard/index.html index 262e5a5a..b03fb3e0 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 0000000000000000000000000000000000000000..bc0bf14917f1158d405c588a3d4941e98769fd91 GIT binary patch literal 1936 zcmV;B2XFX^P))LFVR--mIE|&0 z&ubGw6vw~Wg#N%-5?efY&^d&lr7dk!W4*;B#tNxuEQv}%w!72qVzRsJZhoBvDMCTe ze?U(?=v4#{Uc3oXsUS!_coh!{iXwXO$D7@zNt>VtAI#2YzVGduH?uPfgr1g_iX%mU zis9OYGii3|@)g$q7JY~!3^G}9toe(%1;P1qMReP@*9eII)wsyjBj9fqRk@=8f%C*) z)rz44QdUf>R#d_FiC1e?P8=K~zFE_Go%j)PtX!?=#9PE86)w}WX%RpuB6Q8? znBXO!X9e2}g$!|?cu4Eur4FvUmP@?oy?f1LOR&q872;LmuwvOlze7CXuf?-ONmn@& z#p;H}WJ4=+7H4z1w!&0yIJ(&I@a8u=Z0A`yF#`ero8?) zS!Dkj-iOzErfTV3{*F0D|Glq!wzFp$Q97|R!*_d@(b9MOJ8Bp!@MUguL*cBrT}Z|> zRNd80gXso4dpeVxI+;w4Pftwk&P+|ZAt>=A1*>lcFT`Mg+K&SW3I6o+dThXtj#Hbx zmB#a-4Bk9W;d?brihusehSz^#nBu|( zet0JBSupK|bX({BTKJ()3`{t)rq!N+h0bX@rj~Frm036oEE%^(B`*(Vq%sH`kJ-D3*groAj zr?0wiV#fSDUBH&83MMa|f^p zY?0Rs02~BN5^CVJPH&U&^#V<@$K54++%EwQzy*MpJ0{vBR3BgfUdaT&vjC+zSYTF0cgz$zW8eT{a0PF{9^dFI^96HfkGjuOt(l@9h4(z@q0E*Qn10MnJ>%N#m z%r}?gou13_t13uh){O>bp2yW#X4kwMMpf_yg2wRJRGxA_VA`apk}iH8u`J^Niq)0} z90$4p?)Y*?e;B9=1;d6Gpk90u?NkzN3E;=U%I3rhB-m$Yhfy>Rpjd4Za2nX9D5%FS z)c!W*&uw2J`M;L50tq$?+(O%yk2;-nIDtvRUuHQEopQoF^$G{+P1Fdo5 zcgZTt(1E=+1vm_1-ai32`$yR&-<+vK#Et-OJFtgW$WtNy46rUU0Np@qbWw^7jTJQu z|5@Om{?PD$nhfj)_|J0$C&jR#3DN&Ta3!or3dL=6h0}ukPt_h&HAX-^j{ODKV zKkUS{V-;};_!QU*u&-?@PlN_#k9#p)fGfZ+0M_=qEuaR_UMzlg;_7kYTJn(9W7tkqt-1pKI zUj`r`8i22Pyu^}O6!-%8Q1-Z2I-H~s WD|dr-xn4y80000 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 00000000..2374adfb --- /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 00000000..c5468f17 --- /dev/null +++ b/website/nifti-viewer/index.html @@ -0,0 +1,71 @@ + + + + + + + + + + + + +
+

NIFTI Image Viewer

+
+
+
+

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 00000000..3788af5a --- /dev/null +++ b/website/nifti-viewer/index.js @@ -0,0 +1,232 @@ +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) - 1; + voxelY = Math.min(Math.max(ny - Math.round(voxelY), 1), ny) - 1; + voxelZ = Math.min(Math.max(nz - Math.round(voxelZ), 1), nz) - 1; + + 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, + mode: 'markers', + type: 'bar', + }; + 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); + }); +} From 671a5258878de23c5aca9ab87c00205508faf213 Mon Sep 17 00:00:00 2001 From: AhmedBasem Date: Mon, 14 Oct 2024 20:10:14 +0300 Subject: [PATCH 2/3] change the bar plot to a line curve --- website/nifti-viewer/index.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/website/nifti-viewer/index.js b/website/nifti-viewer/index.js index 3788af5a..482889a2 100644 --- a/website/nifti-viewer/index.js +++ b/website/nifti-viewer/index.js @@ -148,8 +148,7 @@ function loadAllFileViews(file) { function plotVoxelData(values) { const trace = { y: values, - mode: 'markers', - type: 'bar', + type: 'line', }; const layout = { title: 'Voxel Intensity Under the Cursor', From 738b6f1aab7769f5d0135e266d4b2e64df4d0edc Mon Sep 17 00:00:00 2001 From: AhmedBasem Date: Mon, 14 Oct 2024 20:13:22 +0300 Subject: [PATCH 3/3] Fix selected voxel coordinates --- website/nifti-viewer/index.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/website/nifti-viewer/index.js b/website/nifti-viewer/index.js index 482889a2..ec7b28c9 100644 --- a/website/nifti-viewer/index.js +++ b/website/nifti-viewer/index.js @@ -89,9 +89,9 @@ function handleVoxelClick(currentVoxel) { const [nx, ny, nz, nt] = dims; let [voxelX, voxelY, voxelZ] = currentVoxel; - voxelX = Math.min(Math.max(Math.round(voxelX), 1), nx) - 1; - voxelY = Math.min(Math.max(ny - Math.round(voxelY), 1), ny) - 1; - voxelZ = Math.min(Math.max(nz - Math.round(voxelZ), 1), nz) - 1; + 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);