Skip to content
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
82 commits
Select commit Hold shift + click to select a range
efac35c
Initialize dynamic loading example
mbellehumeur Aug 18, 2025
ab63317
start
mbellehumeur Aug 19, 2025
4b8e7dd
feat(volumeDynamicLoading): enhance dynamic loading with configurable…
mbellehumeur Aug 25, 2025
9089c4f
feat(loaders): add decimateVolumeLoader to loaders and exports
mbellehumeur Sep 4, 2025
ec1195f
feat(loaders): integrate decimateVolumeLoader with volumeLoader and e…
mbellehumeur Sep 15, 2025
b1d35e2
feat(loaders): enhance decimateVolumeLoader with in-plane decimation …
mbellehumeur Sep 16, 2025
68f414a
feat(loaders): enhance decimateVolumeLoader to support image post-pro…
mbellehumeur Sep 18, 2025
6bb8570
Added comment for possible DICOM value change
mbellehumeur Sep 18, 2025
fe6f98c
feat(loaders): adjust decimation calculations
mbellehumeur Sep 18, 2025
4c2585e
feat(examples): Rename Volume Decimated Loading example
mbellehumeur Sep 19, 2025
d29112a
fix(cache): update image dimensions handling in BaseStreamingImageVol…
mbellehumeur Sep 23, 2025
d7c7aac
fix(volumeDecimatedLoading): update decimation values and streamline …
mbellehumeur Sep 23, 2025
190f00a
fix(decimateImagePixels): improve decimation logic and handle trivial…
mbellehumeur Sep 23, 2025
ccf1036
revert unnecessary changes
mbellehumeur Sep 23, 2025
bfc3233
fix(volumeDecimatedLoading): enforce consistent GPU rendering setting…
mbellehumeur Sep 24, 2025
7e28d55
fix(decimateImagePixels): PR comment - correct decimation calculation…
mbellehumeur Sep 26, 2025
b83ccb6
docs(decimateImagePixels): add detailed function documentation for im…
mbellehumeur Sep 26, 2025
cb4c661
refactor(decimateVolumeLoader): replace individual decimation paramet…
mbellehumeur Sep 26, 2025
cf24463
Rename to enhanced volume loading
mbellehumeur Sep 26, 2025
ae3e64f
Rename the volume loader.
mbellehumeur Sep 26, 2025
4b22eaa
Rename example
mbellehumeur Sep 26, 2025
38341b8
clean-up decimatePixels comments
mbellehumeur Sep 26, 2025
3869f72
refactor(enhancedVolumeLoader): update ijkDecimation defaults and adj…
mbellehumeur Sep 29, 2025
c42cbdb
revert
mbellehumeur Sep 29, 2025
db43879
Add enhanced volume loading with timing display
mbellehumeur Oct 2, 2025
f220885
Add LengthTool to volumeEnhancedLoading example; update ijkDecimation…
mbellehumeur Oct 2, 2025
8191f1b
Refactor logging and error handling in volume rendering components; r…
mbellehumeur Oct 2, 2025
c419ccd
stack caching off
mbellehumeur Oct 4, 2025
31aad68
Implement image decimation checks in BaseStreamingImageVolume; remove…
mbellehumeur Oct 13, 2025
d6c676a
Put sampleDistanceMultiplier back
mbellehumeur Oct 14, 2025
4d76bce
Refactor volumeEnhancedLoading example: update dropdown labels, add c…
mbellehumeur Oct 14, 2025
5ad581e
Enhance VolumeViewport3D: add sampleDistanceMultiplier support and im…
mbellehumeur Oct 14, 2025
1d65e49
Refactor image loading and decimation handling: remove localStorage c…
mbellehumeur Oct 15, 2025
b2365c6
Implement decimation parameter handling in image loaders: add functio…
mbellehumeur Oct 17, 2025
6723e4e
Remove unnecessary console logs in Cache and ImageVolume classes for …
mbellehumeur Oct 17, 2025
53ebb3a
Refactor imageLoader and applyPreset: streamline cached image handlin…
mbellehumeur Oct 17, 2025
f7f6ff9
Refactor BaseStreamingImageVolume and improve comments: remove redund…
mbellehumeur Oct 17, 2025
989738b
Improve error handling and type definitions: add debug logging for im…
mbellehumeur Oct 17, 2025
cecd441
Add debug logging for requested decimation in enhancedVolumeLoader: s…
mbellehumeur Oct 18, 2025
743299c
remove localStorage
mbellehumeur Oct 18, 2025
c7e412b
Refactor StreamingImageVolume and enhancedVolumeLoader: remove image …
mbellehumeur Oct 29, 2025
f660483
Fix first rendering of volume rendering tool after applying decimatio…
mbellehumeur Oct 29, 2025
2925de9
Enhance enhancedVolumeLoader documentation: clarify decimation capabi…
mbellehumeur Oct 29, 2025
2872560
Delete packages/core/src/utilities/decimateImagePixels.ts
mbellehumeur Nov 3, 2025
e02aeb1
Update enhancedVolumeLoader documentation: clarify decimation handlin…
mbellehumeur Nov 3, 2025
099a8e6
Merge branch 'feat/dynamic-loading' of https://github.com/mbellehumeu…
mbellehumeur Nov 3, 2025
81a80f9
Merge branch 'main' into feat/dynamic-loading
mbellehumeur Nov 3, 2025
b7432d2
Refactor image loading: Remove unused stripDecimationParam function a…
mbellehumeur Nov 3, 2025
72ceb6e
Refactor BaseVolumeViewport and VolumeViewport3D: Clean up whitespace…
mbellehumeur Nov 3, 2025
38722f0
Enhance BaseStreamingImageVolume: Restore warning for cancelled image…
mbellehumeur Nov 3, 2025
c3ee413
Refactor addOrUpdateSurfaceToElement: Change actor type casting from …
mbellehumeur Nov 3, 2025
234993e
Refactor addVolumesAsIndependentComponents: Update actor type casting…
mbellehumeur Nov 3, 2025
1008ea9
Refactor BaseVolumeViewport: Remove unused imports for setConfigurati…
mbellehumeur Nov 3, 2025
aa6ac57
Remove decimation from generateVolume and move to enhancedVolumeLoade…
mbellehumeur Nov 26, 2025
8eb9d6c
update enhancedVolumeLaoder description
mbellehumeur Nov 26, 2025
a98d20d
Update comment as per review
mbellehumeur Nov 26, 2025
ef9e9f2
Move volume cropping tool changes to another PR
mbellehumeur Nov 26, 2025
3d6adec
Export enhancedVolumeLoader from core index for improved accessibilit…
mbellehumeur Nov 26, 2025
25b0879
pull origin main
mbellehumeur Nov 26, 2025
6df4557
Update import path for inPlaneDecimationModifier to include file exte…
mbellehumeur Nov 26, 2025
e3649b0
Refactor import path for inPlaneDecimationModifier and remove unused …
mbellehumeur Nov 27, 2025
d383c03
Merge branch 'main' of https://github.com/mbellehumeur/cornerstone3D …
mbellehumeur Nov 28, 2025
dbe277c
refactor: update ijkDecimation type to use points.points3 for enhance…
mbellehumeur Dec 2, 2025
16ae96d
fix: export points namespace for enhanced volume modifiers
mbellehumeur Dec 2, 2025
9dd1766
fix: add points export to enhanced volume modifiers
mbellehumeur Dec 2, 2025
dce4c12
fix: correct type assertion for defaultActor in addVolumesAsIndepende…
mbellehumeur Dec 10, 2025
29a21f1
fix: update type assertion for actor in addOrUpdateSurfaceToElement f…
mbellehumeur Dec 10, 2025
ee53f95
refactor: remove unused variables from volumeViewport3D example
mbellehumeur Dec 10, 2025
cdb49f5
refactor: enhance viewport property handling in volumeViewport3D example
mbellehumeur Dec 10, 2025
f47735b
docs: clarify decimation parameter usage in enhancedVolumeLoader
mbellehumeur Dec 10, 2025
739b41b
refactor: remove unused parameter from applyPreset function
mbellehumeur Dec 10, 2025
6129eb0
refactor: simplify actor type assertion in addVolumesAsIndependentCom…
mbellehumeur Dec 10, 2025
1514cf6
refactor: update volume dimensions calculation to use imageIds length
mbellehumeur Dec 10, 2025
fdecd4b
refactor: add comment to clarify spacing calculation in generateVolum…
mbellehumeur Dec 10, 2025
b6ebe9a
refactor: replace enhancedVolumeLoader with decimatedVolumeLoader and…
mbellehumeur Dec 17, 2025
b16e431
Merge remote-tracking branch 'origin/main' into feat/dynamic-loading
wayfarer3130 Dec 18, 2025
8175c2f
Correct array access syntax for dimensions
mbellehumeur Dec 18, 2025
467c6b1
Document inPlaneDecimationModifier functionality
mbellehumeur Dec 18, 2025
4fd0e12
Enhance documentation for low resolution image loader
mbellehumeur Dec 18, 2025
f81bf44
Fix syntax for accessing dimensions in BaseStreamingImageVolume
mbellehumeur Dec 18, 2025
5a23b80
Refactor decimation parameter handling in addDecimationToImageId func…
mbellehumeur Dec 19, 2025
47ce69c
Merge remote-tracking branch 'origin/main' into feat/dynamic-loading
wayfarer3130 Dec 19, 2025
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
10 changes: 6 additions & 4 deletions packages/core/src/cache/classes/BaseStreamingImageVolume.ts
Original file line number Diff line number Diff line change
Expand Up @@ -320,8 +320,10 @@ export default class BaseStreamingImageVolume
const { transferSyntaxUID: transferSyntaxUID } =
metaData.get('transferSyntax', imageId) || {};

const imagePlaneModule = metaData.get('imagePlaneModule', imageId) || {};
const { rows, columns } = imagePlaneModule;
// Note: per-image rows/columns may be full-res after in-plane decimation.
// Always use the current volume's dimensions for allocation.
const targetRows = this.dimensions?.[1];
const targetCols = this.dimensions?.[0];
const imageIdIndex = this.getImageIdIndex(imageId);

const modalityLutModule = metaData.get('modalityLutModule', imageId) || {};
Expand Down Expand Up @@ -380,8 +382,8 @@ export default class BaseStreamingImageVolume

const targetBuffer = {
type: this.dataType,
rows,
columns,
rows: targetRows,
columns: targetCols,
};

return {
Expand Down
20 changes: 20 additions & 0 deletions packages/core/src/cache/classes/StreamingImageVolume.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ import BaseStreamingImageVolume from './BaseStreamingImageVolume';
* It implements load method to load the imageIds and insert them into the volume.
*/
export default class StreamingImageVolume extends BaseStreamingImageVolume {
private imagePostProcess?: (
image: PixelDataTypedArray
) => PixelDataTypedArray;
constructor(
imageVolumeProperties: ImageVolumeProps,
streamingProperties: IStreamingVolumeProperties
Expand All @@ -21,6 +24,23 @@ export default class StreamingImageVolume extends BaseStreamingImageVolume {
}
super(imageVolumeProperties, streamingProperties);
}
public setImagePostProcess(
fn: (image: PixelDataTypedArray) => PixelDataTypedArray
) {
this.imagePostProcess = fn;
}

// Override successCallback to apply post-process if set
public override successCallback(imageId: string, image: PixelDataTypedArray) {
if (this.imagePostProcess) {
try {
image = this.imagePostProcess(image) || image;
} catch (e) {
console.warn('imagePostProcess failed, using original image', e);
}
}
super.successCallback(imageId, image);
}

/**
* Return the scalar data (buffer)
Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ import * as volumeLoader from './loaders/volumeLoader';
import * as imageLoader from './loaders/imageLoader';
import * as geometryLoader from './loaders/geometryLoader';
import ProgressiveRetrieveImages from './loaders/ProgressiveRetrieveImages';
import decimateVolumeLoader from './loaders/decimateVolumeLoader';
// eslint-disable-next-line import/no-duplicates
import type * as Types from './types';
import type {
Expand Down Expand Up @@ -168,6 +169,7 @@ export {
geometryLoader,
cornerstoneMeshLoader,
ProgressiveRetrieveImages,
decimateVolumeLoader,
cornerstoneStreamingImageVolumeLoader,
cornerstoneStreamingDynamicImageVolumeLoader,
StreamingDynamicImageVolume,
Expand Down
196 changes: 196 additions & 0 deletions packages/core/src/loaders/decimateVolumeLoader.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
import cache from '../cache/cache';
import StreamingImageVolume from '../cache/classes/StreamingImageVolume';
import { RequestType } from '../enums';
import imageLoadPoolManager from '../requestPool/imageLoadPoolManager';
import type { IRetrieveConfiguration } from '../types';
import { generateVolumePropsFromImageIds } from '../utilities/generateVolumePropsFromImageIds';
import { loadImage } from './imageLoader';
import decimate from '../utilities/decimate';
import decimateImagePixels from '../utilities/decimateImagePixels';

interface IVolumeLoader {
promise: Promise<StreamingImageVolume>;
cancel: () => void;
decache: () => void;
}

/**
* It handles loading of a image by streaming in its imageIds. It will be the
* volume loader if the schema for the volumeID is `decimateImageVolume`.
* This function returns a promise that resolves to the StreamingImageVolume instance.
*
*
* @param volumeId - The ID of the volume
* @param options - options for loading, imageIds
* @returns a promise that resolves to a StreamingImageVolume
*/
export function decimateVolumeLoader(
volumeId: string,
options: {
imageIds: string[];
progressiveRendering?: boolean | IRetrieveConfiguration;
kDecimation?: number;
iDecimation?: number;
}
): IVolumeLoader {
if (!options || !options.imageIds || !options.imageIds.length) {
throw new Error(
'ImageIds must be provided to create a streaming image volume'
);
}

const inPlaneDecimation =
options.iDecimation && options.iDecimation > 1 ? options.iDecimation : 1;
const kAxisDecimation =
options.kDecimation && options.kDecimation > 1 ? options.kDecimation : 1;

const originalImageIds = options.imageIds.slice();
const decimatedResult = decimate(originalImageIds, kAxisDecimation);

const decimatedImageIds =
Array.isArray(decimatedResult) &&
decimatedResult.length &&
typeof decimatedResult[0] === 'number'
? decimatedResult.map((idx) => originalImageIds[idx])
: decimatedResult;

options.imageIds = decimatedImageIds as string[];

async function getStreamingImageVolume() {
/**
* Check if we are using the `wadouri:` scheme, and if so, preload first,
* middle, and last image metadata as these are the images the current
* streaming image loader may explicitly request metadata from. The last image
* metadata would only be specifically requested if the imageId array order is
* reversed in the `sortImageIdsAndGetSpacing.ts` file.
*/
if (options.imageIds[0].split(':')[0] === 'wadouri') {
const [middleImageIndex, lastImageIndex] = [
Math.floor(options.imageIds.length / 2),
options.imageIds.length - 1,
];
const indexesToPrefetch = [0, middleImageIndex, lastImageIndex];
await Promise.all(
indexesToPrefetch.map((index) => {
// check if image is cached
if (cache.isLoaded(options.imageIds[index])) {
return Promise.resolve(true);
}
return new Promise((resolve, reject) => {
const imageId = options.imageIds[index];
imageLoadPoolManager.addRequest(
async () => {
loadImage(imageId)
.then(() => {
console.log(`Prefetched imageId: ${imageId}`);
resolve(true);
})
.catch((err) => {
reject(err);
});
},
RequestType.Prefetch,
{ volumeId },
1 // priority
);
});
})
).catch(console.error);
}

const volumeProps = generateVolumePropsFromImageIds(
options.imageIds,
volumeId
);

let {
dimensions,
spacing,
origin,
direction,
metadata,
imageIds,
dataType,
numberOfComponents,
} = volumeProps;

// Start from current props and apply decimations independently
let newDimensions = [...dimensions] as typeof dimensions;
let newSpacing = [...spacing] as typeof spacing;

// Apply in‑plane decimation (columns = x = index 0, rows = y = index 1)
if (inPlaneDecimation > 1) {
newDimensions[0] = Math.ceil(newDimensions[0] / inPlaneDecimation);
newDimensions[1] = Math.ceil(newDimensions[1] / inPlaneDecimation);
newSpacing[0] = newSpacing[0] * inPlaneDecimation; // column spacing (x)
newSpacing[1] = newSpacing[1] * inPlaneDecimation; // row spacing (y)

// DICOM: Rows = Y, Columns = X
metadata.Rows = newDimensions[1];
metadata.Columns = newDimensions[0];
// DICOM PixelSpacing = [row, column] = [y, x]
metadata.PixelSpacing = [newSpacing[1], newSpacing[0]];
}

// Do NOT scale Z spacing here. We decimated imageIds before
// generating volume props, so sortImageIdsAndGetSpacing already
// computed the effective z-spacing between the kept frames.

// Commit any updates
dimensions = newDimensions;
spacing = newSpacing;
const streamingImageVolume = new StreamingImageVolume(
// ImageVolume properties
{
volumeId,
metadata,
dimensions,
spacing,
origin,
direction,
imageIds,
dataType,
numberOfComponents,
},
// Streaming properties
{
imageIds,
loadStatus: {
loaded: false,
loading: false,
cancelled: false,
cachedFrames: [],
callbacks: [],
},
}
);
streamingImageVolume.setImagePostProcess(
(image) =>
decimateImagePixels(
image as unknown as import('../types').IImage,
inPlaneDecimation
) as unknown as import('../types').PixelDataTypedArray
);
console.debug(streamingImageVolume);
return streamingImageVolume;
}

const streamingImageVolumePromise = getStreamingImageVolume();

return {
promise: streamingImageVolumePromise,
decache: () => {
streamingImageVolumePromise.then((streamingImageVolume) => {
streamingImageVolume.destroy();
streamingImageVolume = null;
});
},
cancel: () => {
streamingImageVolumePromise.then((streamingImageVolume) => {
streamingImageVolume.cancelLoading();
});
},
};
}

export default decimateVolumeLoader;
2 changes: 2 additions & 0 deletions packages/core/src/loaders/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { cornerstoneStreamingImageVolumeLoader } from './cornerstoneStreamingImageVolumeLoader';
import { cornerstoneStreamingDynamicImageVolumeLoader } from './cornerstoneStreamingDynamicImageVolumeLoader';
import { cornerstoneMeshLoader } from './cornerstoneMeshLoader';
import { decimateVolumeLoader } from './decimateVolumeLoader';
import * as geometryLoader from './geometryLoader';
import * as imageLoader from './imageLoader';
import * as volumeLoader from './volumeLoader';
Expand All @@ -9,6 +10,7 @@ export {
cornerstoneStreamingImageVolumeLoader,
cornerstoneStreamingDynamicImageVolumeLoader,
cornerstoneMeshLoader,
decimateVolumeLoader,
geometryLoader,
imageLoader,
volumeLoader,
Expand Down
3 changes: 3 additions & 0 deletions packages/core/src/loaders/volumeLoader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,13 @@ import {
import { generateVolumePropsFromImageIds } from '../utilities/generateVolumePropsFromImageIds';
import type { StreamingDynamicImageVolume } from '../cache';
import { cornerstoneStreamingImageVolumeLoader } from './cornerstoneStreamingImageVolumeLoader';
import { decimateVolumeLoader } from './decimateVolumeLoader';

interface VolumeLoaderOptions {
imageIds: string[];
progressiveRendering?: boolean;
kDecimation?: number; // Optional parameter for decimation factor
iDecimation?: number; // Optional parameter for interleave decimation
}

interface DerivedVolumeOptions {
Expand Down
70 changes: 70 additions & 0 deletions packages/core/src/utilities/decimateImagePixels.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import type { IImage, PixelDataTypedArray } from '../types';
export default function decimateImagePixels(image: IImage, factor: number) {
// Trivial case: no decimation requested
if (!factor || factor <= 1) {
return image;
}

const rows = image.rows ?? image.height;
const cols = image.columns ?? image.width;
const newRows = Math.ceil(rows / factor);
const newCols = Math.ceil(cols / factor);

const pixelData = image.getPixelData();
const numComponents = image.numberOfComponents || 1;
const OutArrayCtor = pixelData.constructor as unknown as new (
length: number
) => PixelDataTypedArray;
const out = new OutArrayCtor(newRows * newCols * numComponents);

let outIndex = 0;
for (let r = 0; r < newRows; r++) {
const inR = r * factor;
if (inR >= rows) break;
for (let c = 0; c < newCols; c++) {
const inC = c * factor;
if (inC >= cols) break;
const src = (inR * cols + inC) * numComponents;
for (let k = 0; k < numComponents; k++) {
out[outIndex++] = pixelData[src + k];
}
}
}

// Spacing: prefer explicit row/column spacing; preserve Z
const s = (image as unknown as { spacing?: [number, number, number] })
.spacing;
const newSpacing: [number, number, number] = [
(image.columnPixelSpacing ?? s?.[0] ?? 1) * factor,
(image.rowPixelSpacing ?? s?.[1] ?? 1) * factor,
s?.[2] ?? image.sliceThickness ?? 1,
];
const colSpacing = newSpacing[0];
const rowSpacing = newSpacing[1];

// Carry over imageFrame when present
let imageFrame = image.imageFrame
? {
...image.imageFrame,
rows: newRows,
columns: newCols,
pixelData: out,
pixelDataLength: out.length,
}
: undefined;
// Note: some loaders attach a non-standard imageInfo; skip updating to avoid type casting.

return {
...image,
rows: newRows,
columns: newCols,
width: newCols,
height: newRows,
rowPixelSpacing: rowSpacing,
columnPixelSpacing: colSpacing,
spacing: newSpacing,
sizeInBytes: out.byteLength,
getPixelData: () => imageFrame?.pixelData ?? out,
imageFrame,
};
}
Loading
Loading