Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
13 changes: 8 additions & 5 deletions packages/core/examples/volumeViewport3D/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,14 +81,15 @@ addButtonToToolbar({
viewport.render();
},
});

addDropdownToToolbar({
options: {
values: CONSTANTS.VIEWPORT_PRESETS.map((preset) => preset.name),
defaultValue: 'CT-Bone',
},
onSelectedValueChange: (presetName) => {
viewport.setProperties({ preset: presetName });
const renderingEngine = getRenderingEngine(renderingEngineId);
const viewport = renderingEngine.getViewport(viewportId);
viewport.setProperties({ preset: presetName as string });
viewport.render();
},
});
Expand All @@ -99,15 +100,17 @@ addDropdownToToolbar({
defaultValue: 1,
},
onSelectedValueChange: (sampleDistanceMultiplier) => {
viewport.setProperties({ sampleDistanceMultiplier });
const renderingEngine = getRenderingEngine(renderingEngineId);
const viewport = renderingEngine.getViewport(viewportId);
viewport.setProperties({
sampleDistanceMultiplier: Number(sampleDistanceMultiplier),
});
viewport.render();
},
});

// ============================= //

let viewport;

/**
* Runs the demo
*/
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/RenderingEngine/BaseVolumeViewport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1024,6 +1024,7 @@ abstract class BaseVolumeViewport extends Viewport {
colormap,
preset,
slabThickness,
sampleDistanceMultiplier,
});
}

Expand Down
9 changes: 5 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,9 @@ export class BaseStreamingImageVolume
const { transferSyntaxUID: transferSyntaxUID } =
metaData.get('transferSyntax', imageId) || {};

const imagePlaneModule = metaData.get('imagePlaneModule', imageId) || {};
const { rows, columns } = imagePlaneModule;
// Use the actual dimensions for this volume in order to support volumes not the same size as the raw data
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 +381,8 @@ export class BaseStreamingImageVolume

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

return {
Expand Down
4 changes: 4 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 { decimatedVolumeLoader } from './loaders/decimatedVolumeLoader';
// eslint-disable-next-line import/no-duplicates
import type * as Types from './types';
import type {
Expand All @@ -88,6 +89,8 @@ import {
addImageSlicesToViewports,
} from './RenderingEngine/helpers';

export * from './loaders/decimatedVolumeLoader';

// Add new types here so that they can be imported singly as required.
export type {
Types,
Expand Down Expand Up @@ -168,6 +171,7 @@ export {
geometryLoader,
cornerstoneMeshLoader,
ProgressiveRetrieveImages,
decimatedVolumeLoader,
cornerstoneStreamingImageVolumeLoader,
cornerstoneStreamingDynamicImageVolumeLoader,
StreamingDynamicImageVolume,
Expand Down
246 changes: 246 additions & 0 deletions packages/core/src/loaders/decimatedVolumeLoader.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,246 @@
import StreamingImageVolume from '../cache/classes/StreamingImageVolume';
import type { IRetrieveConfiguration } from '../types';
import { generateVolumePropsFromImageIds } from '../utilities/generateVolumePropsFromImageIds';
import decimate from '../utilities/decimate';
import VoxelManager from '../utilities/VoxelManager';
import {
applyDecimatedVolumeModifiers,
inPlaneDecimationModifier,
} from './decimatedVolumeModifiers';
import type {
DecimatedVolumeLoaderOptions,
DecimatedVolumeModifierContext,
points,
} from './decimatedVolumeModifiers';
interface IVolumeLoader {
promise: Promise<StreamingImageVolume>;
cancel: () => void;
decache: () => void;
}

/**
* Decimated volume loader that creates a StreamingImageVolume while delegating
* volume mutations to a plugin-style modifier chain. The loader still handles the
* responsibilities of preparing imageIds (including optional k-axis decimation),
* instantiating the streaming volume, and updating VTK/voxel-manager state, but it
* now relies on `decimatedVolumeModifiers` to encapsulate any transformations that
* are specific to projection parameters such as decimation.
*
* The loader currently wires a dedicated modifier set for:
* - **K-axis decimation handling**: Removes slices before metadata generation so
* that spacing reflects the decimated stack without manual scaling.
* - **In-plane decimation**: Implemented by `InPlaneDecimationModifier`, which
* updates dimensions, spacing, and DICOM metadata in a self-contained class.
*
* Additional modifiers can be introduced by implementing the shared
* `DecimatedVolumeModifier` interface. For example:
* ```
* const modifiers = [
* inPlaneDecimationModifier,
* orientationModifier, // rotates the direction matrix or redefines axis order
* partialRangeModifier, // limits the volume to a subset of frames / slices
* windowingModifier, // applies window/level or LUT adjustments at load time
* ];
* ```
* Modifiers are executed via `applyDecimatedVolumeModifiers`, so the loader still
* just selects the set to run and trusts the chain to mutate props appropriately.
*
* @param volumeId - The unique identifier for the volume
* @param options - Configuration options for volume loading
* @param options.imageIds - Array of DICOM imageIds to construct the volume from (required)
* @param options.progressiveRendering - Enable progressive rendering or provide custom
* retrieve configuration for streaming behavior
* @param options.ijkDecimation - Decimation factors for [I, J, K] axes where I/J affect
* in-plane resolution and K affects slice count. Defaults to [1, 1, 1] (no decimation).
* Example: [2, 2, 2] reduces dimensions by half in all axes.
*
* *
* @returns An object containing:
* - `promise`: Resolves to the created StreamingImageVolume instance
* - `cancel`: Function to cancel ongoing volume loading
* - `decache`: Function to destroy and remove the volume from cache
*
* @throws Error if imageIds are not provided or empty
*
* @example
* ```typescript
* const volumeLoader = decimatedVolumeLoader('volumeId', {
* imageIds: ['dicomweb:...', 'dicomweb:...'],
* ijkDecimation: [2, 2, 2], // Half resolution in all dimensions
* progressiveRendering: true
* });
*
* const volume = await volumeLoader.promise;
* // Later: volumeLoader.cancel() or volumeLoader.decache()
* ```
*/
export function decimatedVolumeLoader(
volumeId: string,
options: {
imageIds: string[];
progressiveRendering?: boolean | IRetrieveConfiguration;
ijkDecimation?: points.points3;
}
): IVolumeLoader {
if (!options || !options.imageIds || !options.imageIds.length) {
throw new Error(
'ImageIds must be provided to create a streaming image volume '
);
}

// Use provided decimation or default to [1, 1, 1]
// The hanging protocol service is responsible for providing decimation values
const [iDecimation = 1, jDecimation = iDecimation, kDecimation = 1] =
options.ijkDecimation ?? [];
const columnDecimation = Math.max(1, Math.floor(iDecimation));
const rowDecimation =
jDecimation > 1 ? Math.max(1, Math.floor(jDecimation)) : columnDecimation;
const kAxisDecimation = Math.max(1, Math.floor(kDecimation));
const hasInPlaneDecimation = columnDecimation > 1 || rowDecimation > 1;

const modifierOptions: DecimatedVolumeLoaderOptions = {
ijkDecimation: [
columnDecimation,
rowDecimation,
kAxisDecimation,
] as points.points3,
};
const modifiers = [inPlaneDecimationModifier];

function addDecimationToImageId(imageId: string, factor: number): string {
// Only add marker if decimation is applied
if (factor === 1) {
return imageId;
}
// URL fragments (after #) are NOT sent to the server in HTTP requests,
// so this keeps decimated imageIds unique for caching without affecting backend requests
return `${imageId}#decimation=${factor}`;
}

// Check if k-decimation has already been applied at the displaySet level
const expectedDecimatedCount = Math.floor(
options.imageIds.length / kAxisDecimation
);
const isAlreadyDecimated =
kAxisDecimation > 1 &&
options.imageIds.length <= expectedDecimatedCount + 1;

if (kAxisDecimation > 1 && !isAlreadyDecimated) {
const decimatedResult = decimate(options.imageIds, kAxisDecimation);

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

options.imageIds = decimatedImageIds as string[];
}

// Apply in-plane decimation parameter to imageIds
// row and column decimation are always the same in this implementation so we use column
if (columnDecimation > 1) {
options.imageIds = options.imageIds.map((imageId) =>
addDecimationToImageId(imageId, columnDecimation)
);
}

async function getStreamingImageVolume() {
const baseVolumeProps = generateVolumePropsFromImageIds(
options.imageIds,
volumeId
);

const modifierContext: DecimatedVolumeModifierContext = {
volumeId,
imageIds: options.imageIds,
options: modifierOptions,
};

const volumeProps = applyDecimatedVolumeModifiers(
baseVolumeProps,
modifiers,
modifierContext
);

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

const streamingImageVolume = new StreamingImageVolume(
{
volumeId,
metadata,
dimensions,
spacing,
origin,
direction,
imageIds,
dataType,
numberOfComponents,
},
{
imageIds,
loadStatus: {
loaded: false,
loading: false,
cancelled: false,
cachedFrames: [],
callbacks: [],
},
}
);

if (hasInPlaneDecimation) {
const vtkImageData = streamingImageVolume.imageData;
if (vtkImageData) {
vtkImageData.setDimensions(streamingImageVolume.dimensions);
vtkImageData.setSpacing(streamingImageVolume.spacing);
vtkImageData.modified();
}

const newVoxelManager = VoxelManager.createImageVolumeVoxelManager({
dimensions: streamingImageVolume.dimensions,
imageIds: streamingImageVolume.imageIds,
numberOfComponents: numberOfComponents,
});

streamingImageVolume.voxelManager = newVoxelManager;

if (vtkImageData) {
vtkImageData.set({
voxelManager: newVoxelManager,
});
}
}

return streamingImageVolume;
}

const streamingImageVolumePromise = getStreamingImageVolume();

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

export default decimatedVolumeLoader;
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import type { ImageVolumeProps } from '../../types';
import type {
DecimatedVolumeModifier,
DecimatedVolumeModifierContext,
} from './types';

/**
* Runs every modifier sequentially and returns the final props.
*/
export function applyDecimatedVolumeModifiers(
baseProps: ImageVolumeProps,
modifiers: DecimatedVolumeModifier[],
context: DecimatedVolumeModifierContext
): ImageVolumeProps {
return modifiers.reduce(
(currentProps, modifier) => modifier.apply(currentProps, context),
baseProps
);
}
Loading
Loading