Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Developed NIFTI 4d viewer with voxel intensity plots. #84

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
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
2 changes: 2 additions & 0 deletions website/dashboard/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>IVIM MRI Algorithm Fitting Dashboard</title>
<link rel="icon" type="image/x-icon" href="../favicon-32x32.png">

<!-- Load Plotly.js into the DOM -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.7.1/jszip.min.js"></script>
<script src="https://cdn.plot.ly/plotly-2.30.0.min.js"></script>
Expand Down
Binary file added website/favicon-32x32.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions website/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>OSIPI Taskforce 2.4 IVIM MRI</title>
<link rel="icon" type="image/x-icon" href="favicon-32x32.png">
<link rel="stylesheet" href="index.css">
</head>
<body>
Expand All @@ -20,6 +21,9 @@ <h2>Documentation</h2>
<a href="dashboard" class="card">
<h2>Data Visualization Dashboard</h2>
</a>
<a href="nifti-viewer" class="card">
<h2>Nifti Viewer</h2>
</a>
</div>
</div>
</main>
Expand Down
39 changes: 39 additions & 0 deletions website/nifti-viewer/index.css
Original file line number Diff line number Diff line change
@@ -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;
}

71 changes: 71 additions & 0 deletions website/nifti-viewer/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
<!doctype html>
<html lang="en">

<head>
<meta name="charset" content="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<link rel="icon" type="image/x-icon" href="../favicon-32x32.png">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css"
integrity="sha384-xOolHFLEh07PJGoPkLv1IbcEPTNtaed2xpHsD9ESMhqIYd0nLMwNLD69Npy4HI+N" crossorigin="anonymous">
<link href="index.css" rel="stylesheet">
</head>

<body>
<header>
<h1 class="bar-title">NIFTI Image Viewer </h1>
<hr class="divider" />
</header>
<div class="container-fluid">
<h2><small class="text-muted">Example of displaying a NIFTI image using Cornerstone</small></h2>
<ul>
<li class="lead">
Upload a NIFTI file to view it using cornerstone.
</li>
<li class="lead">
Scroll to navigate between different slices.
</li>
<li class="lead">
Drag the mouse to get the synchronized view for the selected area.
</li>
<li class="lead">
A voxel intensity plot will be displayed when you drag or scroll through an image viewport.
</li>
</ul>

<div class="row">
<div class="col">
<form id="form">
<div class="form-group form-row">
<label class="col-form-label" for="nifti-file">NIFTI File</label>
<div class="col-sm-5">
<input class="form-control" type="file" id="nifti-file">
</div>
<div class="col-sm-3">
<button class="btn btn-primary" type="button" id="upload-and-view">Upload and View</button>
</div>
</div>
</form>
</div>
</div>

<section id="image-and-data-info">
<div class="nifti-image-container" id="nifti-image-z"></div>
<div class="nifti-image-container" id="nifti-image-x"></div>
<div class="nifti-image-container" id="nifti-image-y"></div>
<div id="plot"></div>
</section>
<div id="voxel-info" class="mt-4">
<h5>Cursor Position</h5>
<p id="voxel-coordinates">(x, y, z): (0, 0, 0)</p>
</div>
</div>
</body>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/cornerstone.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/cornerstoneMath.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/cornerstoneTools.min.js"></script>
<script
src="https://cdn.jsdelivr.net/npm/@cornerstonejs/[email protected]/dist/cornerstoneNIFTIImageLoader.min.js"></script>
<script src="https://cdn.plot.ly/plotly-2.30.0.min.js"></script>
<script src="index.js"></script>

</html>
231 changes: 231 additions & 0 deletions website/nifti-viewer/index.js
Original file line number Diff line number Diff line change
@@ -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']);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we also want a rescale tool, sometimes I can barely see anything in those images.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You mean a tool for zooming in/out, right?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, brightness/contrast is what they seem to call it here. https://www.cornerstonejs.org/live-examples/videocolor

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

got it, will work on that.

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) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't seem to be correct, select bright parts of images but the value is low. I select dark parts of the image but the value is high. Most likely x, y, and z aren't what you think they are. But also is there no cornerstone based way to do this?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The current voxel values are extracted from cornerstone events cornerstonestackscroll and cornerstonetoolsmousedrag. But I found that I was reducing 1 from the extracted values that's what was causing the issue. I fixed that and it is more precise now.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's still not working. See how I have a very bright part selected but the values are quite low. Other areas show up much higher on the plot than this.
I'm guessing it's either image orientation differences or still some indexing problems. I don't see how you're handling the orientation differences, so that's one place to start. What I mean by that is that indexing into the raw data doesn't necessarily equate to the displayed x, y, z. There's a possible axis flip and permutation based on the raw data so it's always displayed in the correct orientation.
image

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is orientation difference and handled it here:

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);

I used the complement of y and z due to the axis flip. and the indexing seems accurate according to the images I tested. Can you share the image you're using here so that I can reproduce the issue?

Copy link
Contributor

@etpeterson etpeterson Oct 18, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can't share the images because of patient privacy, but you can't count on that transformation always being the same. I opened the same image in yours and a full viewer and I see the images and notations are flipped left/right. Here's a screenshot showing that and some header info.
image

But what you want is to rely on cornerstone to do that stuff, not reinvent it yourself. https://docs.cornerstonejs.org/concepts/pixel-coordinate-system.html

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');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see anything in this view.
image

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a bug in synchronizing the sagittal view initially that I'm still trying to figure out.
For now you can fix this by scrolling through the view that is not synched, it will work normally after that.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually I'm noticing it's both the sagittal and coronal views that don't act correctly.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please share with me the image so I can try debugging. (I can't find this issue with the images I'm using)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same problem sharing images, sorry. Now I'm not seeing the coronal view misbehave. Definitely the sagittal one though. Sounds like you see that too though.

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);
});
}
Loading