-
Notifications
You must be signed in to change notification settings - Fork 27
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
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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; | ||
} | ||
|
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> |
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']); | ||||||||
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) { | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The current voxel values are extracted from cornerstone events There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There is orientation difference and handled it here: TF2.4_IVIM-MRI_CodeCollection/website/nifti-viewer/index.js Lines 92 to 94 in 738b6f1
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? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. 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'); | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) There was a problem hiding this comment. Choose a reason for hiding this commentThe 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); | ||||||||
}); | ||||||||
} |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.