Skip to content

Commit 279f17b

Browse files
authored
Merge pull request #1 from niivue/cdrake/dicom-manifest-demo
DICOM manifest demo
2 parents 1a63b77 + 9cbda68 commit 279f17b

File tree

4 files changed

+259
-131
lines changed

4 files changed

+259
-131
lines changed

index.html

Lines changed: 30 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,47 +1,60 @@
11
<!doctype html>
22
<html lang="en">
3-
43
<head>
54
<meta charset="UTF-8" />
65
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
76
<link rel="stylesheet" href="./niivue.css" />
8-
<title>NiiVue + dcm2niix WASM</title>
7+
<title>NiiVue DICOM Viewer</title>
98
</head>
10-
119
<body>
1210
<header id="header">
1311
<div id="dropTarget" class="drop-target">
14-
<p>Drop a folder of DICOM files here</p>
15-
<p>Or use the file input button below</p>
12+
<p><strong>Option 1:</strong> Drop a folder of DICOM files here</p>
13+
<p>Or use the folder selection below</p>
1614
</div>
17-
<!-- ordered list for instructions -->
15+
1816
<ol id="instructions">
1917
<li>
20-
<!-- directory input -->
21-
<label for="fileInput" id="fileInputLabel" class="file-input-label">
22-
Choose a folder of DICOM files
18+
<!-- DICOM folder input using dcm2niix -->
19+
<label for="fileInput" class="file-input-label">
20+
Choose a folder of DICOM files (uses dcm2niix)
2321
</label>
2422
<input type="file" id="fileInput" webkitdirectory multiple />
2523
</li>
24+
2625
<li>
27-
<!-- file select for converted files returned from dcm2niix -->
28-
<label for="fileSelect" id="fileSelectLabel">View a converted file</label>
26+
<!-- NIfTI file selection if multiple output files -->
27+
<label for="fileSelect" id="fileSelectLabel">
28+
View a converted file (from dcm2niix)
29+
</label>
2930
<select id="fileSelect" class="hidden"></select>
3031
</li>
32+
3133
<li>
32-
<!-- optional: save nii file in the niivue canvas from conversion -->
33-
<label for="saveButton" id="saveButtonLabel">Optional: save the nifti file you are viewing</label>
34+
<!-- Save converted file -->
35+
<label for="saveButton" id="saveButtonLabel">
36+
Save the currently viewed NIfTI file
37+
</label>
3438
<button id="saveButton" class="hidden">Save selected file</button>
3539
</li>
40+
41+
<li>
42+
<!-- Load demo using dicom-loader -->
43+
<label for="loadManifestBtn">
44+
Or load a demo DICOM series (uses dicom-loader)
45+
</label>
46+
<button id="loadManifestBtn">Load demo from manifest</button>
47+
</li>
3648
</ol>
3749

38-
<div id="loadingCircle" class="loading-circle hidden"></div>
50+
<div id="loadingCircle" class="loading-circle hidden">Loading...</div>
3951
</header>
52+
4053
<main id="canvas-container">
41-
<canvas id="gl"></canvas>
54+
<canvas id="gl" width="800" height="600"></canvas>
4255
</main>
56+
4357
<footer id="intensity">&nbsp;</footer>
4458
<script type="module" src="/main.js"></script>
4559
</body>
46-
47-
</html>
60+
</html>

main.js

Lines changed: 122 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,130 @@
11
import { Niivue, NVImage, DRAG_MODE, SLICE_TYPE, MULTIPLANAR_TYPE, SHOW_RENDER } from '@niivue/niivue'
22
import { Dcm2niix } from '@niivue/dcm2niix'
3+
import { dicomLoader } from '@niivue/dicom-loader'
34
import './niivue.css'
45

5-
// page-wide niivue instance
6-
const nv = new Niivue({
7-
dragAndDropEnabled: false, // disable drag and drop since we want to use input from the file input element
8-
})
6+
/**
7+
* Load DICOM files from a manifest URL using the Dcm2niix WebAssembly backend.
8+
*
9+
* This function fetches a text-based manifest file containing relative paths to DICOM images,
10+
* downloads and wraps them as `File` objects with proper `webkitRelativePath` attributes,
11+
* runs the DICOM-to-NIfTI conversion via `@niivue/dcm2niix`, and updates the UI with the result.
12+
*
13+
* This is a manual reference implementation that mirrors the functionality of `@niivue/dicom-loader`.
14+
*
15+
* @param manifestUrl - A full URL pointing to a manifest text file with relative DICOM paths
16+
* @returns A promise that resolves to an array of converted NIfTI `File` objects
17+
*/
18+
async function loadDicomsUsingDcm2niixFromManifest(manifestUrl) {
19+
console.log('Starting manual DICOM load from manifest via dcm2niix...')
20+
try {
21+
hideSaveButton()
22+
showLoadingCircle()
23+
24+
const baseUrl = new URL(manifestUrl)
25+
const response = await fetch(manifestUrl)
26+
const text = await response.text()
27+
const urls = text.trim().split('\n')
28+
29+
const dicomFiles = await Promise.all(
30+
urls.map(async (relativePath) => {
31+
const url = new URL(relativePath, baseUrl)
32+
const res = await fetch(url)
33+
if (!res.ok) throw new Error(`Failed to fetch DICOM: ${url}`)
34+
const arrayBuffer = await res.arrayBuffer()
35+
const filename = url.pathname.split('/').pop()
36+
const fullPath = `series/${filename}`
37+
const file = new File([arrayBuffer], filename)
38+
Object.defineProperty(file, 'webkitRelativePath', {
39+
value: fullPath,
40+
writable: false
41+
})
42+
return file
43+
})
44+
)
45+
46+
const dcm2niix = new Dcm2niix()
47+
await dcm2niix.init()
48+
let resultFileList = await dcm2niix.input(dicomFiles).run()
49+
resultFileList = resultFileList.filter(f => f.name.endsWith('.nii') || f.name.endsWith('.nii.gz'))
50+
51+
updateSelectItems(resultFileList)
52+
hideLoadingCircle()
53+
showFileSelect()
54+
55+
fileSelect.value = 0
56+
fileSelect.dispatchEvent(new Event('change'))
57+
showText('Loaded via manual dcm2niix')
58+
59+
return resultFileList
60+
} catch (err) {
61+
console.error('Error in loadDicomsUsingDcm2niixFromManifest:', err)
62+
hideLoadingCircle()
63+
hideFileSelect()
64+
showText('Error loading DICOMs manually')
65+
return []
66+
}
67+
}
68+
69+
/**
70+
* Load DICOMs from a manifest using @niivue/dicom-loader
71+
* and display the time taken to load and decode them.
72+
*
73+
* @param manifestURL - URL to a text manifest with relative DICOM file paths
74+
*/
75+
async function loadDicomsWithNiivueLoader(manifestURL) {
76+
console.log('Loading DICOM manifest via dicom-loader...')
77+
showLoadingCircle()
78+
const startTime = performance.now()
79+
80+
nv.useDicomLoader({ loader: dicomLoader })
81+
82+
await nv.loadDicoms([
83+
{
84+
url: manifestURL,
85+
isManifest: true
86+
}
87+
])
88+
89+
const vol = nv.volumes[nv.volumes.length - 1]
90+
const name = vol?.name || 'Unnamed volume'
91+
const endTime = performance.now()
92+
const elapsed = ((endTime - startTime) / 1000).toFixed(2)
93+
hideLoadingCircle()
94+
showText(`Loaded ${name} in ${elapsed} seconds`)
95+
showSaveButton()
96+
}
97+
98+
// NiiVue instance
99+
const nv = new Niivue({ dragAndDropEnabled: false })
100+
9101
// reference to the results list from dcm2niix for use later
10102
let resultFileList = []
11103
let conversionTime = 0
12104
let downloadFile = null
13105

14106

15-
const handleSaveButtonClick = () => {
16-
let url = URL.createObjectURL(downloadFile);
17-
const downloadLink = document.createElement('a');
18-
downloadLink.href = url;
19-
downloadLink.download = downloadFile.name;
20-
downloadLink.click()
107+
const handleSaveButtonClick = async () => {
108+
109+
if (nv.volumes.length === 0) {
110+
if(downloadFile) {
111+
let url = URL.createObjectURL(downloadFile);
112+
const downloadLink = document.createElement('a');
113+
downloadLink.href = url;
114+
downloadLink.download = downloadFile.name;
115+
downloadLink.click()
116+
117+
}
118+
else {
119+
console.log('no volumes found')
120+
}
121+
return
122+
}
123+
const vol = nv.volumes[0]
124+
const name = vol.name || 'volume'
125+
const ext = vol.niiFile?.name?.endsWith('.nii.gz') ? '.nii.gz' : '.nii'
126+
console.log('saving ', name)
127+
await nv.saveImage({filename: `${name}${ext}`})
21128
}
22129

23130
const showSaveButton = () => {
@@ -240,6 +347,11 @@ async function main() {
240347
// when user clicks save
241348
saveButton.onclick = handleSaveButtonClick
242349

350+
// when a user clicks load manifest
351+
document.getElementById('loadManifestBtn').onclick = () => {
352+
loadDicomsWithNiivueLoader('https://niivue.github.io/niivue-demo-images/dicom/niivue-manifest.txt')
353+
}
354+
243355
// crosshair location change event
244356
nv.onLocationChange = handleLocationChange
245357
// get canvas element

0 commit comments

Comments
 (0)