Skip to content

Commit

Permalink
Big header loading refactor
Browse files Browse the repository at this point in the history
  • Loading branch information
nx10 committed Dec 11, 2024
1 parent 16ba37d commit 395c4d6
Show file tree
Hide file tree
Showing 11 changed files with 1,001 additions and 1,980 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ Based on [NIFTI-Reader-JS](https://github.com/rii-mango/NIFTI-Reader-JS).
- 🗜️ **Automatic Decompression**: Handles gzipped NIFTI files transparently
- 🎯 **Selective Loading**: Read only the header, or stop after specific slices
- 💻 **Browser-Ready**: Works directly in modern browsers
- 📦 **Lightweight**: Zero dependencies, focused implementation
- 📦 **Lightweight**: ~17kB minified, no dependencies

### Live demo

Expand Down
97 changes: 55 additions & 42 deletions demo/index.html
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>nifti-stream demos</title>
<link href="https://cdnjs.cloudflare.com/ajax/libs/tailwindcss/2.2.19/tailwind.min.css" rel="stylesheet">
<script src="./nifti-stream.js"></script>
<script src="../dist/nifti-stream.js"></script>
</head>

<body class="bg-gray-50">
<nav class="bg-white shadow-lg">
<div class="max-w-6xl mx-auto px-4">
Expand All @@ -15,15 +17,17 @@
<span class="font-semibold text-gray-700 text-lg">nifti-stream</span>
</div>
<div class="flex space-x-4">
<a href="https://github.com/childmindresearch/nifti-stream"
class="inline-flex items-center px-4 py-2 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50">
<a href="https://github.com/childmindresearch/nifti-stream"
class="inline-flex items-center px-4 py-2 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50">
<svg class="h-5 w-5 mr-2" fill="currentColor" viewBox="0 0 24 24">
<path fill-rule="evenodd" d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z" clip-rule="evenodd"/>
<path fill-rule="evenodd"
d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z"
clip-rule="evenodd" />
</svg>
GitHub
</a>
<a href="https://childmindresearch.github.io/nifti-stream"
class="inline-flex items-center px-4 py-2 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50">
<a href="https://childmindresearch.github.io/nifti-stream"
class="inline-flex items-center px-4 py-2 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50">
Documentation
</a>
</div>
Expand All @@ -35,11 +39,13 @@
<div class="prose max-w-none mb-8">
<h1 class="text-3xl font-bold text-gray-800 mb-4">nifti-stream Demo</h1>
<p class="text-gray-600 mb-4">
This demo uses a T1w scan to demonstrate the streaming capabilities. While we're only showing structural data here,
the library handles any n-dimensional NIFTI data you throw at it - your fMRI timeseries, DWI volumes, or
multi-echo sequences will work just fine. Just specify your preferred streaming dimension and you're set.
</p>
</div>
This demo uses a T1w scan to demonstrate the streaming capabilities. While we're only showing structural
data here,
the library handles any n-dimensional NIFTI data you throw at it - your fMRI timeseries, DWI volumes, or
multi-echo sequences will work just fine. Just specify your preferred streaming dimension and you're
set.
</p>
</div>
</div>

<div class="grid gap-8">
Expand All @@ -51,8 +57,10 @@ <h2 class="text-2xl font-semibold text-gray-800 mb-4">1. Quick Header Inspection
Stream just the header metadata without loading the image data. Perfect for quick inspection
or validation of NIFTI files before processing.
</p>
<pre id="headerInfo" class="bg-gray-50 p-4 rounded overflow-auto h-48 font-mono text-sm">Click "Load Header" to view NIFTI header information</pre>
<button id="loadHeader" class="mt-4 bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded transition-colors">
<pre id="headerInfo"
class="bg-gray-50 p-4 rounded overflow-auto h-48 font-mono text-sm">Click "Load Header" to view NIFTI header information</pre>
<button id="loadHeader"
class="mt-4 bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded transition-colors">
Load Header
</button>
</div>
Expand All @@ -61,11 +69,12 @@ <h2 class="text-2xl font-semibold text-gray-800 mb-4">1. Quick Header Inspection
<div class="bg-white rounded-lg shadow-md p-6">
<h2 class="text-2xl font-semibold text-gray-800 mb-4">2. Selective Slice Loading</h2>
<p class="text-gray-600 mb-4">
Load just the middle slice from the volume. This demonstrates how you can selectively
Load just the middle slice from the volume. This demonstrates how you can selectively
access parts of the data without loading the entire file.
</p>
<canvas id="slicePreview" class="bg-gray-200 rounded"></canvas>
<button id="loadSlice" class="mt-4 bg-green-500 hover:bg-green-600 text-white px-4 py-2 rounded transition-colors">
<button id="loadSlice"
class="mt-4 bg-green-500 hover:bg-green-600 text-white px-4 py-2 rounded transition-colors">
Load Middle Slice
</button>
</div>
Expand All @@ -82,15 +91,18 @@ <h2 class="text-2xl font-semibold text-gray-800 mb-4">3. Progressive Loading</h2
</div>
<div class="mt-4 flex items-center gap-4">
<div class="flex-1 bg-gray-200 rounded-full h-2">
<div id="loadingProgress" class="bg-blue-500 h-2 rounded-full transition-all duration-200"></div>
<div id="loadingProgress" class="bg-blue-500 h-2 rounded-full transition-all duration-200">
</div>
</div>
<span id="sliceCounter" class="text-sm text-gray-600">0/0 slices</span>
</div>
<div class="mt-4 space-x-2">
<button id="startLoading" class="bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded transition-colors">
<button id="startLoading"
class="bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded transition-colors">
Start Loading
</button>
<button id="stopLoading" class="bg-red-500 hover:bg-red-600 text-white px-4 py-2 rounded transition-colors">
<button id="stopLoading"
class="bg-red-500 hover:bg-red-600 text-white px-4 py-2 rounded transition-colors">
Stop
</button>
</div>
Expand All @@ -101,47 +113,47 @@ <h2 class="text-2xl font-semibold text-gray-800 mb-4">3. Progressive Loading</h2
<script>
const { NiftiStream } = niftiStream;
const DEMO_URL = 'https://fcp-indi.s3.amazonaws.com/data/Projects/HBN/MRI/Site-CBIC/sub-NDARAA396TWZ/anat/sub-NDARAA396TWZ_acq-HCP_T1w.nii.gz';

function normalizeData(data) {
const array = new Int16Array(data);
let min = Infinity;
let max = -Infinity;

for (let i = 0; i < array.length; i++) {
min = Math.min(min, array[i]);
max = Math.max(max, array[i]);
}

const range = max - min;
const rgba = new Uint8ClampedArray(array.length * 4);

for (let i = 0; i < array.length; i++) {
const value = ((array[i] - min) / range) * 255;
rgba[i * 4] = value;
rgba[i * 4 + 1] = value;
rgba[i * 4 + 2] = value;
rgba[i * 4 + 3] = 255;
}

return rgba;
}

document.getElementById('loadHeader').onclick = async () => {
const headerInfo = document.getElementById('headerInfo');
headerInfo.textContent = 'Loading...';

try {
const response = await fetch(DEMO_URL);
const reader = new NiftiStream(response.body, {
onHeader: (header) => {
headerInfo.textContent = JSON.stringify(header, null, 2);
headerInfo.textContent = header.formatHeader();
return true;
},
onError: (error) => {
headerInfo.textContent = `Error: ${error.message}`;
}
});

await reader.start();
} catch (e) {
headerInfo.textContent = `Error: ${e.message}`;
Expand All @@ -153,7 +165,7 @@ <h2 class="text-2xl font-semibold text-gray-800 mb-4">3. Progressive Loading</h2
const ctx = canvas.getContext('2d');
let middleSlice = 0;
let dims = [];

try {
const response = await fetch(DEMO_URL);
const reader = new NiftiStream(response.body, {
Expand All @@ -173,7 +185,7 @@ <h2 class="text-2xl font-semibold text-gray-800 mb-4">3. Progressive Loading</h2
}
}
});

await reader.start();
} catch (e) {
ctx.fillStyle = 'black';
Expand All @@ -192,7 +204,7 @@ <h2 class="text-2xl font-semibold text-gray-800 mb-4">3. Progressive Loading</h2
let dims = [];
const SLICES_PER_ROW = 24;
const SCALE_FACTOR = 0.1;

try {
const response = await fetch(DEMO_URL);
currentReader = new NiftiStream(response.body, {
Expand All @@ -202,48 +214,48 @@ <h2 class="text-2xl font-semibold text-gray-800 mb-4">3. Progressive Loading</h2
dims = header.dims;
const sliceWidth = Math.floor(dims[1] * SCALE_FACTOR);
const sliceHeight = Math.floor(dims[2] * SCALE_FACTOR);

canvas.width = sliceWidth * SLICES_PER_ROW;
canvas.height = sliceHeight * Math.ceil(totalSlices / SLICES_PER_ROW);

counter.textContent = `0/${totalSlices}`;
ctx.fillStyle = 'black';
ctx.fillRect(0, 0, canvas.width, canvas.height);
},
onSlice: (data, indices) => {
const sliceIndex = indices[3];
const normalized = normalizeData(data);

const tempCanvas = document.createElement('canvas');
const tempCtx = tempCanvas.getContext('2d');
tempCanvas.width = dims[1];
tempCanvas.height = dims[2];

const imageData = new ImageData(normalized, dims[1], dims[2]);
tempCtx.putImageData(imageData, 0, 0);

const row = Math.floor(sliceIndex / SLICES_PER_ROW);
const col = sliceIndex % SLICES_PER_ROW;
const x = col * Math.floor(dims[1] * SCALE_FACTOR);
const y = row * Math.floor(dims[2] * SCALE_FACTOR);
ctx.drawImage(tempCanvas, 0, 0, dims[1], dims[2],
x, y,
Math.floor(dims[1] * SCALE_FACTOR),
Math.floor(dims[2] * SCALE_FACTOR));

ctx.drawImage(tempCanvas, 0, 0, dims[1], dims[2],
x, y,
Math.floor(dims[1] * SCALE_FACTOR),
Math.floor(dims[2] * SCALE_FACTOR));

progress.style.width = `${((sliceIndex + 1) / totalSlices) * 100}%`;
counter.textContent = `${sliceIndex + 1}/${totalSlices}`;
}
});

await currentReader.start();
} catch (e) {
ctx.fillStyle = 'black';
ctx.fillText(`Error: ${e.message}`, 10, 20);
}
};

document.getElementById('stopLoading').onclick = async () => {
if (currentReader) {
await currentReader.stop();
Expand All @@ -252,4 +264,5 @@ <h2 class="text-2xl font-semibold text-gray-800 mb-4">3. Progressive Loading</h2
};
</script>
</body>

</html>
52 changes: 19 additions & 33 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,12 @@
*/

import { NiftiIOError } from './error';
import { detectNiftiVersion, NiftiHeader, NiftiVersion } from './nifti';
import { NiftiExtension } from './nifti-extension';
import { Nifti1Header } from './nifti1';
import { Nifti2Header } from './nifti2';
import { NiftiHeader, NiftiVersion } from './nifti';
import { NiftiExtension } from './niftiExtension';
import { NIFTI1_HEADER_SIZE, NIFTI2_HEADER_SIZE } from './niftiConstants';
import { uncompressStream } from './stream';

export { NiftiVersion, NiftiHeader, NiftiExtension, NiftiIOError, Nifti1Header, Nifti2Header };
export { NiftiVersion, NiftiHeader, NiftiExtension, NiftiIOError };

/**
* Configuration options for streaming NIFTI data
Expand Down Expand Up @@ -106,7 +105,6 @@ export class NiftiStream {

private buffer: Uint8Array = new Uint8Array(0);
private bufferOffset = 0;
private totalBytesRead = 0; // Add tracking of total bytes read

private async readBytes(length: number): Promise<Uint8Array> {
if (!this.reader) {
Expand Down Expand Up @@ -156,33 +154,23 @@ export class NiftiStream {
}

private async readHeader(): Promise<NiftiHeader> {
// First read enough for NIFTI1 header (348 bytes)
const initialHeaderBytes = await this.readBytes(348);
const version = detectNiftiVersion(initialHeaderBytes.buffer);

switch (version) {
case NiftiVersion.NIFTI1:
case NiftiVersion.NIFTI1_PAIR: {
const header = Nifti1Header.fromBytes(initialHeaderBytes.buffer);
return header;
}
// First read enough for NIFTI1 header
const initialHeaderBytes = await this.readBytes(NIFTI1_HEADER_SIZE);
const peek = NiftiHeader.peekVersion(new DataView(initialHeaderBytes.buffer));

case NiftiVersion.NIFTI2:
case NiftiVersion.NIFTI2_PAIR: {
// Need to read additional bytes for NIFTI2 header
const remainingBytes = await this.readBytes(540 - 348);
// Combine
const fullHeaderBytes = new Uint8Array(540);
fullHeaderBytes.set(initialHeaderBytes);
fullHeaderBytes.set(remainingBytes, 348);

const header = Nifti2Header.fromBytes(fullHeaderBytes.buffer);
return header;
}
if (!peek) {
throw new NiftiIOError('Not a valid NIFTI file');
}

case NiftiVersion.NONE:
throw new NiftiIOError('Not a valid NIFTI file');
if (!peek.nifti1) {
// Need to read additional bytes for NIFTI2 header
const remainingBytes = await this.readBytes(NIFTI2_HEADER_SIZE - NIFTI1_HEADER_SIZE);
const fullHeaderBytes = new Uint8Array(NIFTI2_HEADER_SIZE);
fullHeaderBytes.set(initialHeaderBytes);
fullHeaderBytes.set(remainingBytes, NIFTI1_HEADER_SIZE);
return NiftiHeader.parseNifti2(new DataView(fullHeaderBytes.buffer), peek.littleEndian);
}
return NiftiHeader.parseNifti1(new DataView(initialHeaderBytes.buffer), peek.littleEndian);
}

private async readExtension(): Promise<NiftiExtension | null> {
Expand Down Expand Up @@ -330,9 +318,7 @@ export class NiftiStream {
}
} catch (error) {
if (this.options.onError) {
this.options.onError(
error
);
this.options.onError(error);
}
} finally {
this.reader.releaseLock();
Expand Down
Loading

0 comments on commit 395c4d6

Please sign in to comment.