diff --git a/packages/core/examples/registration/RegistrationConsole.ts b/packages/core/examples/registration/RegistrationConsole.ts
new file mode 100644
index 000000000..6b786ed1e
--- /dev/null
+++ b/packages/core/examples/registration/RegistrationConsole.ts
@@ -0,0 +1,106 @@
+import * as hdf5 from 'jsfive';
+import { getFormatedDateTime } from './utils';
+
+class RegistrationConsole {
+ private _consoleRoot: HTMLElement;
+ private _logRoot: HTMLDivElement;
+
+ constructor(container) {
+ const { consoleRoot, logRoot } =
+ RegistrationConsole.createLogWindow(container);
+
+ this._consoleRoot = consoleRoot;
+ this._logRoot = logRoot;
+ }
+
+ private static createLogWindow(container) {
+ const statusFieldset = document.createElement('fieldset');
+ const statusFieldsetLegent = document.createElement('legend');
+ const logRoot = document.createElement('div');
+
+ Object.assign(statusFieldset.style, {
+ fontSize: '12px',
+ height: '200px',
+ overflow: 'scroll',
+ });
+
+ statusFieldsetLegent.innerText = 'Processing logs';
+ logRoot.style.fontFamily = 'monospace';
+
+ statusFieldset.appendChild(statusFieldsetLegent);
+ statusFieldset.appendChild(logRoot);
+ container.appendChild(statusFieldset);
+
+ return {
+ consoleRoot: statusFieldset,
+ logRoot,
+ };
+ }
+
+ public log(text, preFormated = false) {
+ const { _consoleRoot: consoleRoot, _logRoot: logRoot } = this;
+ const node = document.createElement(preFormated ? 'pre' : 'p');
+
+ node.innerHTML = `${getFormatedDateTime()} ${text}`;
+ node.style.margin = '0';
+ node.style.fontSize = '10px';
+ logRoot.appendChild(node);
+
+ // Scroll to the end
+ consoleRoot.scrollBy(0, consoleRoot.scrollHeight - consoleRoot.scrollTop);
+ }
+
+ public clear() {
+ const { _logRoot: logRoot } = this;
+ while (logRoot.hasChildNodes()) {
+ logRoot.removeChild(logRoot.firstChild);
+ }
+ }
+
+ /**
+ * Log all image information
+ */
+ public logImageInfo(image) {
+ this.log(`image "${image.name}"`);
+ this.log(` origin: ${image.origin.join(', ')}`, true);
+ this.log(` spacing: ${image.spacing.join(', ')}`, true);
+ this.log(` direction: ${image.direction.join(', ')}`, true);
+ this.log(` size: ${image.size.join(', ')}`, true);
+ this.log(` imageType:`, true);
+ this.log(` dimension: ${image.imageType.dimension}`, true);
+ this.log(` components: ${image.imageType.components}`, true);
+ this.log(` componentType: ${image.imageType.componentType}`, true);
+ this.log(` pixelType: ${image.imageType.pixelType}`, true);
+ }
+
+ /**
+ * Log HDF5 transform and make it available for download
+ */
+ public logTransform(transform) {
+ let buffer = transform.data.buffer;
+
+ // Convert SharedArrayBuffer into ArrayBuffer
+ if (buffer instanceof SharedArrayBuffer) {
+ buffer = new Uint8ClampedArray(buffer).slice().buffer;
+ }
+
+ const fileName = transform.path;
+ const hdfFile = new hdf5.File(buffer, transform.path);
+ const transformBlob = new Blob([buffer], { type: 'application/x-hdf5' });
+ const url = URL.createObjectURL(transformBlob);
+
+ this.log(
+ `Download ${fileName}`
+ );
+
+ console.log('Transform (HDF5):', hdfFile);
+ }
+
+ destroy() {
+ this._consoleRoot.remove();
+ this._consoleRoot = null;
+ this._logRoot = null;
+ }
+}
+
+export { RegistrationConsole as default, RegistrationConsole };
diff --git a/packages/core/examples/registration/elastixParametersSettings.ts b/packages/core/examples/registration/elastixParametersSettings.ts
new file mode 100644
index 000000000..844d08943
--- /dev/null
+++ b/packages/core/examples/registration/elastixParametersSettings.ts
@@ -0,0 +1,196 @@
+const defaultNumberOfResolutions = 2;
+const defaultFinalGridSpacing = 8;
+
+const parametersSettings = {
+ NumberOfResolutions: {
+ inputType: 'number',
+ defaultValue: defaultNumberOfResolutions,
+ },
+ MaximumNumberOfIterations: {
+ inputType: 'number',
+ defaultValue: 256,
+ },
+ Registration: {
+ inputType: 'dropdown',
+ values: [
+ 'MultiResolutionRegistration',
+ 'MultiResolutionRegistrationWithFeatures',
+ 'MultiMetricMultiResolutionRegistration',
+ ],
+ },
+ Metric: {
+ inputType: 'dropdown',
+ values: [
+ 'AdvancedKappaStatistic',
+ 'AdvancedMattesMutualInformation',
+ 'AdvancedMeanSquares',
+ 'AdvancedNormalizedCorrelation',
+ 'CorrespondingPointsEuclideanDistanceMetric',
+ 'DisplacementMagnitudePenalty',
+ 'DistancePreservingRigidityPenalty',
+ 'GradientDifference',
+ 'KNNGraphAlphaMutualInformation',
+ 'MissingStructurePenalty',
+ 'NormalizedGradientCorrelation',
+ 'NormalizedMutualInformation',
+ 'PCAMetric',
+ 'PCAMetric2',
+ 'PatternIntensity',
+ 'PolydataDummyPenalty',
+ 'StatisticalShapePenalty',
+ 'SumOfPairwiseCorrelationCoefficientsMetric',
+ 'SumSquaredTissueVolumeDifference',
+ 'TransformBendingEnergyPenalty',
+ 'TransformRigidityPenalty',
+ 'VarianceOverLastDimensionMetric',
+ ],
+ },
+ Interpolator: {
+ inputType: 'dropdown',
+ values: [
+ 'BSplineInterpolator',
+ 'BSplineInterpolatorFloat',
+ 'LinearInterpolator',
+ 'NearestNeighborInterpolator',
+ 'RayCastInterpolator',
+ 'ReducedDimensionBSplineInterpolator',
+ ],
+ },
+ FixedImagePyramid: {
+ inputType: 'dropdown',
+ values: [
+ 'FixedGenericImagePyramid',
+ 'FixedRecursiveImagePyramid',
+ 'FixedSmoothingImagePyramid',
+ 'FixedShrinkingImagePyramid',
+ 'OpenCLFixedGenericImagePyramid',
+ ],
+ },
+ MovingImagePyramid: {
+ inputType: 'dropdown',
+ values: [
+ 'MovingGenericImagePyramid',
+ 'MovingRecursiveImagePyramid',
+ 'MovingShrinkingImagePyramid',
+ 'MovingSmoothingImagePyramid',
+ 'OpenCLMovingGenericImagePyramid',
+ ],
+ },
+ Optimizer: {
+ inputType: 'dropdown',
+ values: [
+ 'AdaGrad',
+ 'AdaptiveStochasticGradientDescent',
+ 'AdaptiveStochasticLBFGS',
+ 'AdaptiveStochasticVarianceReducedGradient',
+ 'CMAEvolutionStrategy',
+ 'ConjugateGradient',
+ 'ConjugateGradientFRPR',
+ 'FiniteDifferenceGradientDescent',
+ 'FullSearch',
+ 'Powell',
+ 'PreconditionedGradientDescent',
+ 'PreconditionedStochasticGradientDescent',
+ 'QuasiNewtonLBFGS',
+ 'RSGDEachParameterApart',
+ 'RegularStepGradientDescent',
+ 'Simplex',
+ 'SimultaneousPerturbation',
+ 'StandardGradientDescent',
+ ],
+ },
+ Resampler: {
+ inputType: 'dropdown',
+ values: ['DefaultResampler', 'OpenCLResampler'],
+ },
+ ResampleInterpolator: {
+ inputType: 'dropdown',
+ values: [
+ 'FinalBSplineInterpolator',
+ 'FinalBSplineInterpolatorFloat',
+ 'FinalLinearInterpolator',
+ 'FinalNearestNeighborInterpolator',
+ 'FinalReducedDimensionBSplineInterpolator',
+ 'FinalRayCastInterpolator',
+ ],
+ },
+ FinalBSplineInterpolationOrder: {
+ inputType: 'number',
+ },
+ ImageSampler: {
+ inputType: 'dropdown',
+ values: [
+ 'Random',
+ 'RandomCoordinate',
+ 'Full',
+ 'Grid',
+ 'MultiInputRandomCoordinate',
+ 'RandomSparseMask',
+ ],
+ },
+ NumberOfSpatialSamples: {
+ inputType: 'number',
+ defaultValue: 2048,
+ },
+ CheckNumberOfSamples: {
+ inputType: 'dropdown',
+ values: ['true', 'false'],
+ defaultValue: 'true',
+ },
+ MaximumNumberOfSamplingAttempts: {
+ inputType: 'number',
+ defaultValue: 8,
+ },
+ NewSamplesEveryIteration: {
+ inputType: 'dropdown',
+ values: ['true', 'false'],
+ defaultValue: 'true',
+ },
+ NumberOfSamplesForExactGradient: {
+ inputType: 'number',
+ defaultValue: 4096,
+ },
+ DefaultPixelValue: {
+ inputType: 'number',
+ defaultValue: 0,
+ },
+ AutomaticParameterEstimation: {
+ inputType: 'dropdown',
+ values: ['true', 'false'],
+ defaultValue: 'true',
+ },
+ AutomaticScalesEstimation: {
+ inputType: 'dropdown',
+ values: ['true', 'false'],
+ defaultValue: 'true',
+ },
+ AutomaticTransformInitialization: {
+ inputType: 'dropdown',
+ values: ['true', 'false'],
+ defaultValue: 'true',
+ },
+ Metric0Weight: {
+ inputType: 'number',
+ defaultValue: '1.0',
+ step: 0.1,
+ },
+ Metric1Weight: {
+ inputType: 'number',
+ defaultValue: '1.0',
+ step: 0.1,
+ },
+ FinalGridSpacing: {
+ inputType: 'number',
+ defaultValue: defaultFinalGridSpacing,
+ },
+ // ResultImageFormat: {
+ // inputType: 'dropdown',
+ // values: ['mhd', 'nii', 'nrrd', 'vti'],
+ // },
+};
+
+export {
+ defaultNumberOfResolutions,
+ defaultFinalGridSpacing,
+ parametersSettings,
+};
diff --git a/packages/core/examples/registration/index.ts b/packages/core/examples/registration/index.ts
new file mode 100644
index 000000000..a686ab43f
--- /dev/null
+++ b/packages/core/examples/registration/index.ts
@@ -0,0 +1,585 @@
+import {
+ defaultParameterMap,
+ elastix,
+ DefaultParameterMapOptions,
+ DefaultParameterMapResult,
+ ElastixOptions,
+} from '@itk-wasm/elastix';
+import {
+ Image,
+ ImageType,
+ IntTypes,
+ FloatTypes,
+ PixelTypes,
+ Metadata,
+} from 'itk-wasm';
+import {
+ RenderingEngine,
+ Types,
+ Enums,
+ cache,
+ volumeLoader,
+ getRenderingEngine,
+} from '@cornerstonejs/core';
+import {
+ initDemo,
+ setCtTransferFunctionForVolumeActor,
+ setTitleAndDescription,
+ addButtonToToolbar,
+ addNumberInputToToolbar,
+} from '../../../../utils/demo/helpers';
+import * as cornerstoneTools from '@cornerstonejs/tools';
+import addDropDownToToolbar from '../../../../utils/demo/helpers/addDropdownToToolbar';
+import {
+ defaultNumberOfResolutions,
+ defaultFinalGridSpacing,
+ parametersSettings,
+} from './elastixParametersSettings';
+import { getImageIds, stringify } from './utils';
+import RegistrationConsole from './RegistrationConsole';
+
+// This is for debugging purposes
+console.warn(
+ 'Click on index.ts to open source code for this example --------->'
+);
+
+const dataTypesMap = {
+ Int8: IntTypes.Int8,
+ UInt8: IntTypes.UInt8,
+ Int16: IntTypes.Int16,
+ UInt16: IntTypes.UInt16,
+ Int32: IntTypes.Int32,
+ UInt32: IntTypes.UInt32,
+ Int64: IntTypes.Int64,
+ UInt64: IntTypes.UInt64,
+ Float32: FloatTypes.Float32,
+ Float64: FloatTypes.Float64,
+};
+
+const {
+ WindowLevelTool,
+ StackScrollMouseWheelTool,
+ ToolGroupManager,
+ Enums: csToolsEnums,
+} = cornerstoneTools;
+
+const { ViewportType } = Enums;
+const { MouseBindings } = csToolsEnums;
+const renderingEngineId = 'myRenderingEngine';
+const volumeLoaderScheme = 'cornerstoneStreamingImageVolume'; // Loader id which defines which volume loader to use
+const toolGroupIds = new Set();
+let webWorker = null;
+
+const volumesInfo = [
+ {
+ volumeId: `${volumeLoaderScheme}:CT_VOLUME_ID_1`,
+ wadoRsRoot: 'https://d33do7qe4w26qo.cloudfront.net/dicomweb',
+ StudyInstanceUID: '1.3.6.1.4.1.25403.345050719074.3824.20170125095438.5',
+ SeriesInstanceUID: '1.3.6.1.4.1.25403.345050719074.3824.20170125095449.8',
+ },
+ {
+ volumeId: `${volumeLoaderScheme}:CT_VOLUME_ID_2`,
+ wadoRsRoot: 'https://d33do7qe4w26qo.cloudfront.net/dicomweb',
+ StudyInstanceUID: '1.3.6.1.4.1.25403.345050719074.3824.20170125095258.1',
+ SeriesInstanceUID: '1.3.6.1.4.1.25403.345050719074.3824.20170125095305.12',
+ },
+];
+
+const viewportsInfo = [
+ {
+ toolGroupId: 'VOLUME_TOOLGROUP_ID',
+ volumeInfo: volumesInfo[0],
+ viewportInput: {
+ viewportId: 'CT_VOLUME_FIXED',
+ type: ViewportType.ORTHOGRAPHIC,
+ element: null,
+ defaultOptions: {
+ orientation: Enums.OrientationAxis.CORONAL,
+ background: [0.2, 0, 0.2],
+ },
+ },
+ },
+ {
+ toolGroupId: 'VOLUME_TOOLGROUP_ID',
+ volumeInfo: volumesInfo[1],
+ viewportInput: {
+ viewportId: 'CT_VOLUME_MOVING',
+ type: ViewportType.ORTHOGRAPHIC,
+ element: null,
+ defaultOptions: {
+ orientation: Enums.OrientationAxis.CORONAL,
+ background: [0.2, 0, 0.2],
+ },
+ },
+ },
+];
+
+const transformNames = [
+ 'translation',
+ 'rigid',
+ 'affine',
+ 'bspline',
+ 'spline',
+ // 'groupwise', // 2D+time or 3D+time
+];
+
+let activeTransformName = transformNames[1];
+let currentParameterMap = {};
+
+const defaultParameterMaps = {};
+
+// ==[ Set up page ]============================================================
+
+setTitleAndDescription(
+ 'Registration',
+ 'Spatially align two volumes from different frames of reference using the itk-wasm/elastix package. Please note that in this demo, we only explore the different parameters that are available. Visually, you will not see any rendering of the registered moving image on top of the fixed image yet. '
+);
+
+const content = document.getElementById('content');
+const viewportGrid = document.createElement('div');
+
+Object.assign(viewportGrid.style, {
+ display: 'grid',
+ gridTemplateColumns: `1fr 1fr`,
+ width: '100%',
+ height: '400px',
+ paddingTop: '5px',
+ gap: '5px',
+});
+
+content.appendChild(viewportGrid);
+
+const regConsole = new RegistrationConsole(content);
+
+// ==[ Toolbar ]================================================================
+const toolbar = document.getElementById('demo-toolbar');
+const toolbarTransformSection = document.createElement('div');
+const toolbarParamsSection = document.createElement('div');
+
+[toolbarTransformSection, toolbarParamsSection].forEach((toolbarSection) => {
+ toolbarSection.style.margin = '0px 0px 10px';
+});
+
+// Parameters container
+const toolbarParamsContainer = document.createElement('fieldset');
+const toolbarParamsContainerLegend = document.createElement('legend');
+
+toolbarParamsContainer.style.display = 'grid';
+toolbarParamsContainer.style.gridTemplateColumns = 'repeat(2, max-content 1fr)';
+
+toolbarParamsContainerLegend.innerText = 'Parameters';
+
+toolbarParamsContainer.appendChild(toolbarParamsContainerLegend);
+toolbarParamsSection.appendChild(toolbarParamsContainer);
+
+toolbar.append(toolbarTransformSection, toolbarParamsSection);
+
+addDropDownToToolbar({
+ labelText: 'Transform ',
+ container: toolbarTransformSection,
+ options: {
+ values: transformNames,
+ defaultValue: activeTransformName,
+ },
+ onSelectedValueChange: (value) => {
+ activeTransformName = value.toString();
+ loadParameterMap(activeTransformName);
+ },
+});
+
+Object.keys(parametersSettings).forEach((parameterName) => {
+ const parameterSettings = parametersSettings[parameterName];
+ const { inputType, defaultValue } = parameterSettings;
+ const id = parameterName;
+ const onChange = (newValue: string | number) => {
+ newValue = newValue.toString();
+
+ // Some parameters need to update a global variable
+ parameterSettings.onChange?.(newValue);
+
+ if (newValue === '') {
+ delete currentParameterMap[parameterName];
+ } else {
+ currentParameterMap[parameterName] = [newValue];
+ }
+ };
+
+ const label = document.createElement('label');
+ label.htmlFor = id;
+ label.innerText = parameterName;
+ label.style.margin = '0px 5px';
+ toolbarParamsContainer.append(label);
+
+ if (inputType === 'number') {
+ const { step = 1 } = parameterSettings;
+
+ addNumberInputToToolbar({
+ id,
+ value: defaultValue ?? 0,
+ step,
+ container: toolbarParamsContainer,
+ onChange,
+ });
+ } else if (inputType === 'dropdown') {
+ const { values } = parameterSettings;
+ const dropdownValues = ['', ...values];
+
+ addDropDownToToolbar({
+ id,
+ options: {
+ values: dropdownValues,
+ defaultValue: defaultValue ?? dropdownValues[0],
+ },
+ container: toolbarParamsContainer,
+ onSelectedValueChange: onChange,
+ });
+ }
+});
+
+addButtonToToolbar({
+ id: 'btnRegister',
+ title: 'Register volumes',
+ onClick: async () => {
+ regConsole.clear();
+
+ // Fake call just to get a new webWorker because we need to make sure
+ // it will be destroyed even if an error occur during registration
+ // Is there a better way to get a WebWorker?
+ const { webWorker } = await defaultParameterMap(undefined, 'rigid', {
+ numberOfResolutions: 4,
+ });
+
+ // Use the same parameter map updated by the user
+ const parameterMap = currentParameterMap;
+
+ regConsole.log(`Parameters map:\n${stringify(parameterMap, 4)}`, true);
+
+ const [fixedViewportInfo, movingViewportInfo] = viewportsInfo;
+ const { viewportId: fixedViewportId } = fixedViewportInfo.viewportInput;
+ const { viewportId: movingViewportId } = movingViewportInfo.viewportInput;
+ const fixedImage = getImageFromViewport(fixedViewportId, 'fixed');
+ const movingImage = getImageFromViewport(movingViewportId, 'moving');
+
+ regConsole.logImageInfo(fixedImage);
+ regConsole.logImageInfo(movingImage);
+
+ const elastixOptions: ElastixOptions = {
+ fixed: fixedImage,
+ moving: movingImage,
+ initialTransform: undefined,
+ initialTransformParameterObject: undefined,
+ };
+
+ regConsole.log(`Registration in progress (${activeTransformName})...`);
+
+ console.log('Registration:');
+ console.log(' parameterMap:', parameterMap);
+ console.log(' options:', elastixOptions);
+
+ try {
+ const startTime = performance.now();
+ const elastixResult = await elastix(
+ webWorker,
+ [parameterMap],
+ 'transform.h5',
+ elastixOptions
+ );
+
+ const totalTime = performance.now() - startTime;
+ const { result, transform, transformParameterObject } = elastixResult;
+
+ console.log('Elastix result');
+ console.log(' result:', result);
+ console.log(' transform:', transform);
+ console.log(' transformParameterObject:', transformParameterObject);
+
+ regConsole.log(
+ `transformParameterObject:\n${stringify(transformParameterObject, 4)}`,
+ true
+ );
+
+ regConsole.log('Resulting image:');
+ regConsole.logImageInfo(result);
+ regConsole.logTransform(transform);
+ regConsole.log(`Total time: ${(totalTime / 1000).toFixed(3)} seconds`);
+ regConsole.log('Registration complete');
+ } catch (error: any) {
+ window.error = error;
+ let message = 'unknown error';
+
+ if (typeof error === 'string') {
+ message = error.toUpperCase();
+ } else if (error.message) {
+ message = error.message;
+ }
+
+ regConsole.log(`An error ocurred during : ${message}`);
+ console.log('Error: ', error);
+ } finally {
+ webWorker.terminate();
+ }
+ },
+});
+
+// =============================================================================
+
+async function getElastixParameterMap(
+ transformName: string
+): Promise {
+ const parameterMapOptions: DefaultParameterMapOptions = {
+ numberOfResolutions: defaultNumberOfResolutions,
+ finalGridSpacing: defaultFinalGridSpacing,
+ };
+
+ const parameterMap: DefaultParameterMapResult = await defaultParameterMap(
+ webWorker,
+ transformName,
+ parameterMapOptions
+ );
+
+ webWorker = parameterMap.webWorker;
+
+ return parameterMap;
+}
+
+async function loadAndCacheAllParameterMaps() {
+ for (let i = 0, len = transformNames.length; i < len; i++) {
+ const transformName = transformNames[i];
+ const { parameterMap } = await getElastixParameterMap(transformName);
+
+ parameterMap['AutomaticTransformInitialization'] = ['true'];
+ defaultParameterMaps[transformName] = parameterMap;
+ console.log(`Default parameter map (${transformName}):`, parameterMap);
+ }
+}
+
+/**
+ * Update the screen with all parameter values for a given transformation
+ * @param transformName - Transformation name
+ */
+function loadParameterMap(transformName: string) {
+ const parameterMap = defaultParameterMaps[transformName];
+
+ // Update the current parameter map that shall be updated on every input change
+ currentParameterMap = parameterMap;
+
+ // For each parameters that has settings which means an input field on the screen
+ Object.keys(parametersSettings).forEach((parameterName) => {
+ const parameterSettings = parametersSettings[parameterName];
+ const parameterValues = parameterMap[parameterName];
+ const input = document.getElementById(parameterName) as HTMLInputElement;
+
+ // Disable the input field if there is the parameter is not
+ // in the default parameter map
+ input.disabled = !parameterValues;
+
+ if (!parameterValues) {
+ return;
+ }
+
+ // Get the first value because the values are always stored as an array
+ const parameterValue = parameterValues[0];
+
+ // Update the input field
+ input.value = parameterValue;
+
+ // Some parameters needs to update a global variable
+ parameterSettings.onLoad?.(parameterValue);
+ });
+}
+
+/**
+ * Get the ITK Image from a given viewport
+ * @param viewportId - Viewport Id
+ * @param imageName - Any random name that shall be set in the image
+ * @returns An ITK Image that can be used as fixed or moving image
+ */
+function getImageFromViewport(viewportId, imageName?: string): Image {
+ const renderingEngine = getRenderingEngine(renderingEngineId);
+ const viewport = (
+ renderingEngine.getViewport(viewportId)
+ );
+
+ const { actor: volumeActor } = viewport.getDefaultActor();
+ const imageData = volumeActor.getMapper().getInputData();
+ const pointData = imageData.getPointData();
+ const scalars = pointData.getScalars();
+ const dimensions = imageData.getDimensions();
+ const origin = imageData.getOrigin();
+ const spacing = imageData.getSpacing();
+ const directionArray = imageData.getDirection();
+ const direction = new Float64Array(directionArray);
+ const numComponents = pointData.getNumberOfComponents();
+ const dataType = scalars
+ .getDataType()
+ .replace(/^Ui/, 'UI')
+ .replace(/Array$/, '');
+ const metadata: Metadata = undefined;
+ const scalarData = scalars.getData();
+ const imageType: ImageType = new ImageType(
+ dimensions.length,
+ dataTypesMap[dataType],
+ PixelTypes.Scalar,
+ numComponents
+ );
+
+ const image = new Image(imageType);
+
+ image.name = imageName;
+ image.origin = origin;
+ image.spacing = spacing;
+ image.direction = direction;
+ image.size = dimensions;
+ image.metadata = metadata;
+ image.data = scalarData;
+
+ // image.data = new scalarData.constructor(scalarData.length);
+ // image.data.set(scalarData, 0);
+
+ return image;
+}
+
+async function initializeVolumeViewport(
+ viewport: Types.IVolumeViewport,
+ volumeId: string,
+ imageIds: string[]
+) {
+ let volume = cache.getVolume(volumeId) as any;
+
+ if (!volume) {
+ volume = await volumeLoader.createAndCacheVolume(volumeId, {
+ imageIds,
+ });
+
+ // Set the volume to load
+ volume.load();
+ }
+
+ // Set the volume on the viewport
+ await viewport.setVolumes([
+ { volumeId, callback: setCtTransferFunctionForVolumeActor },
+ ]);
+
+ return volume;
+}
+
+async function initializeViewport(
+ renderingEngine,
+ toolGroup,
+ viewportInfo,
+ imageIds,
+ volumeId
+) {
+ const { viewportInput } = viewportInfo;
+ const element = document.createElement('div');
+
+ // Disable right click context menu so we can have right click tools
+ element.oncontextmenu = (e) => e.preventDefault();
+
+ element.id = viewportInput.viewportId;
+ element.style.overflow = 'hidden';
+
+ viewportInput.element = element;
+ viewportGrid.appendChild(element);
+
+ const { viewportId } = viewportInput;
+ const { id: renderingEngineId } = renderingEngine;
+
+ renderingEngine.enableElement(viewportInput);
+
+ // Set the tool group on the viewport
+ toolGroup.addViewport(viewportId, renderingEngineId);
+
+ // Get the stack viewport that was created
+ const viewport = renderingEngine.getViewport(viewportId);
+
+ if (viewportInput.type === ViewportType.STACK) {
+ // Set the stack on the viewport
+ (viewport).setStack(imageIds);
+ } else if (viewportInput.type === ViewportType.ORTHOGRAPHIC) {
+ await initializeVolumeViewport(
+ viewport as Types.IVolumeViewport,
+ volumeId,
+ imageIds
+ );
+ } else {
+ throw new Error('Invalid viewport type');
+ }
+
+ regConsole.log(
+ `Viewport ${viewportId} initialized (${imageIds.length} slices)`
+ );
+}
+
+function initializeToolGroup(toolGroupId) {
+ let toolGroup = ToolGroupManager.getToolGroup(toolGroupId);
+
+ if (toolGroup) {
+ return toolGroup;
+ }
+
+ // Define a tool group, which defines how mouse events map to tool commands for
+ // Any viewport using the group
+ toolGroup = ToolGroupManager.createToolGroup(toolGroupId);
+
+ // Add the tools to the tool group
+ toolGroup.addTool(WindowLevelTool.toolName);
+ toolGroup.addTool(StackScrollMouseWheelTool.toolName);
+
+ toolGroup.setToolActive(WindowLevelTool.toolName, {
+ bindings: [
+ {
+ mouseButton: MouseBindings.Secondary, // Right Click
+ },
+ ],
+ });
+
+ // As the Stack Scroll mouse wheel is a tool using the `mouseWheelCallback`
+ // hook instead of mouse buttons, it does not need to assign any mouse button.
+ toolGroup.setToolActive(StackScrollMouseWheelTool.toolName);
+
+ return toolGroup;
+}
+
+/**
+ * Runs the demo
+ */
+async function run() {
+ // Init Cornerstone and related libraries
+ await initDemo();
+
+ // Add tools to Cornerstone3D
+ cornerstoneTools.addTool(WindowLevelTool);
+ cornerstoneTools.addTool(StackScrollMouseWheelTool);
+
+ // Instantiate a rendering engine
+ const renderingEngine = new RenderingEngine(renderingEngineId);
+
+ for (let i = 0; i < viewportsInfo.length; i++) {
+ const viewportInfo = viewportsInfo[i];
+ const { volumeInfo, toolGroupId } = viewportInfo;
+ const { wadoRsRoot, StudyInstanceUID, SeriesInstanceUID, volumeId } =
+ volumeInfo;
+ const toolGroup = initializeToolGroup(toolGroupId);
+ const imageIds = await getImageIds(
+ wadoRsRoot,
+ StudyInstanceUID,
+ SeriesInstanceUID
+ );
+
+ toolGroupIds.add(toolGroupId);
+
+ await initializeViewport(
+ renderingEngine,
+ toolGroup,
+ viewportInfo,
+ imageIds,
+ volumeId
+ );
+ }
+
+ await loadAndCacheAllParameterMaps();
+ loadParameterMap(activeTransformName);
+}
+
+run();
diff --git a/packages/core/examples/registration/utils.ts b/packages/core/examples/registration/utils.ts
new file mode 100644
index 000000000..1105d9317
--- /dev/null
+++ b/packages/core/examples/registration/utils.ts
@@ -0,0 +1,69 @@
+import { createImageIdsAndCacheMetaData } from '../../../../utils/demo/helpers';
+
+const imageIdsCache = new Map();
+
+/**
+ * Get the current date/time ("YYYY-MM-DD hh:mm:ss.SSS")
+ */
+export function getFormatedDateTime() {
+ const now = new Date();
+ const day = `0${now.getDate()}`.slice(-2);
+ const month = `0${now.getMonth() + 1}`.slice(-2);
+ const year = now.getFullYear();
+ const hours = `0${now.getHours()}`.slice(-2);
+ const minutes = `0${now.getMinutes()}`.slice(-2);
+ const seconds = `0${now.getSeconds()}`.slice(-2);
+ const ms = `00${now.getMilliseconds()}`.slice(-3);
+
+ return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}.${ms}`;
+}
+
+/**
+ * Converts a JavaScript object to a JSON string ignoring circular references
+ * @param obj - The object to convert to a JSON string
+ * @param space - Parameter passed to JSON.stringify() that's used to insert
+ * white space (including indentation, line break characters, etc.) into the
+ * output JSON string for readability purposes
+ * @returns A JSON string representing the given object, or undefined.
+ */
+export function stringify(obj, space = 0) {
+ const cache = new Set();
+ const str = JSON.stringify(
+ obj,
+ (key, value) => {
+ if (typeof value === 'object' && value !== null) {
+ if (cache.has(value)) {
+ // Circular reference found, discard key
+ return;
+ }
+ // Store value in our collection
+ cache.add(value);
+ }
+ return value;
+ },
+ space
+ );
+
+ return str;
+}
+
+export async function getImageIds(
+ wadoRsRoot: string,
+ StudyInstanceUID: string,
+ SeriesInstanceUID: string
+) {
+ const imageIdsKey = `${StudyInstanceUID}:${SeriesInstanceUID}`;
+ let imageIds = imageIdsCache.get(imageIdsKey);
+
+ if (!imageIds) {
+ imageIds = await createImageIdsAndCacheMetaData({
+ wadoRsRoot,
+ StudyInstanceUID,
+ SeriesInstanceUID,
+ });
+
+ imageIdsCache.set(imageIdsKey, imageIds);
+ }
+
+ return imageIds;
+}
diff --git a/packages/core/package.json b/packages/core/package.json
index 00032ab5b..7d0f23745 100644
--- a/packages/core/package.json
+++ b/packages/core/package.json
@@ -35,6 +35,10 @@
"gl-matrix": "^3.4.3",
"lodash.clonedeep": "4.5.0"
},
+ "devDependencies": {
+ "@itk-wasm/elastix": "0.2.2",
+ "jsfive": "0.3.14"
+ },
"contributors": [
{
"name": "Cornerstone.js Contributors",
diff --git a/utils/ExampleRunner/example-info.json b/utils/ExampleRunner/example-info.json
index 5180950b5..75a6fde37 100644
--- a/utils/ExampleRunner/example-info.json
+++ b/utils/ExampleRunner/example-info.json
@@ -126,6 +126,10 @@
"volumeSlabScroll": {
"name": "Volume Slab Scroll",
"description": "Demonstrates how to use the slab scroll tool to scroll through a volume"
+ },
+ "registration": {
+ "name": "Registration",
+ "description": "Demonstrates how to register two volumes using ITK Elastix"
}
},
"tools-basic": {
diff --git a/utils/demo/helpers/addNumberInputToToolbar.ts b/utils/demo/helpers/addNumberInputToToolbar.ts
new file mode 100644
index 000000000..82c5a24fe
--- /dev/null
+++ b/utils/demo/helpers/addNumberInputToToolbar.ts
@@ -0,0 +1,43 @@
+export default function addButtonToToolbar({
+ id,
+ value,
+ min,
+ max,
+ step = 1,
+ style,
+ container,
+ onChange,
+}: {
+ id?: string;
+ value: number;
+ min?: number;
+ max?: number;
+ step?: number;
+ style?: Record;
+ container?: HTMLElement;
+ onChange?: (value: number) => void;
+}) {
+ const input = document.createElement('input');
+
+ input.id = id;
+ input.type = 'number';
+ input.value = value.toString();
+ input.min = min?.toString();
+ input.max = max?.toString();
+ input.step = step.toString();
+
+ if (style) {
+ Object.assign(input.style, style);
+ }
+
+ input.onchange = (evt) => {
+ const input = evt.target;
+
+ if (input) {
+ onChange?.(input.valueAsNumber);
+ }
+ };
+
+ container = container ?? document.getElementById('demo-toolbar');
+ container.append(input);
+}
diff --git a/utils/demo/helpers/addRadioGroupToToolbar.ts b/utils/demo/helpers/addRadioGroupToToolbar.ts
new file mode 100644
index 000000000..83a18e531
--- /dev/null
+++ b/utils/demo/helpers/addRadioGroupToToolbar.ts
@@ -0,0 +1,52 @@
+export default function addRadioGroupToToolbar({
+ name,
+ options,
+ container,
+ onChange,
+}: {
+ name: string;
+ options: {
+ values: string[];
+ defaultValue: string;
+ };
+ container?: HTMLElement;
+ onChange: (value: string) => void;
+}) {
+ container = container ?? document.getElementById('demo-toolbar');
+
+ // Only a single element with all buttons have to be appended to the container
+ // element otherwise it breaks the grid layout adding one element per grid cell
+ const radioGroupElement = document.createElement('div');
+
+ const { values, defaultValue } = options;
+
+ values.forEach((value) => {
+ const radioItemElement = document.createElement('span');
+ const input = document.createElement('input');
+ const label = document.createElement('label') as HTMLLabelElement;
+ const id = `${name}_${value}`;
+
+ input.type = 'radio';
+ input.id = id;
+ input.name = name;
+ input.value = value;
+ input.checked = value === defaultValue;
+
+ label.htmlFor = id;
+ label.innerText = value;
+
+ radioItemElement.appendChild(input);
+ radioItemElement.appendChild(label);
+ radioGroupElement.appendChild(radioItemElement);
+ });
+
+ container.appendChild(radioGroupElement);
+
+ radioGroupElement.addEventListener('change', (evt) => {
+ const radioButton = evt.target;
+
+ if (onChange) {
+ onChange(radioButton.value);
+ }
+ });
+}
diff --git a/utils/demo/helpers/index.js b/utils/demo/helpers/index.js
index ed7cf7c26..fe5e4b2ad 100644
--- a/utils/demo/helpers/index.js
+++ b/utils/demo/helpers/index.js
@@ -9,9 +9,11 @@ import setPetColorMapTransferFunctionForVolumeActor from './setPetColorMapTransf
import setTitleAndDescription from './setTitleAndDescription';
import addButtonToToolbar from './addButtonToToolbar';
import addCheckboxToToolbar from './addCheckboxToToolbar';
+import addNumberInputToToolbar from './addNumberInputToToolbar';
import addToggleButtonToToolbar from './addToggleButtonToToolbar';
import addDropdownToToolbar from './addDropdownToToolbar';
import addSliderToToolbar from './addSliderToToolbar';
+import addRadioGroupToToolbar from './addRadioGroupToToolbar';
import camera from './camera';
export {
@@ -21,8 +23,10 @@ export {
setTitleAndDescription,
addButtonToToolbar,
addCheckboxToToolbar,
+ addNumberInputToToolbar,
addDropdownToToolbar,
addSliderToToolbar,
+ addRadioGroupToToolbar,
addToggleButtonToToolbar,
setPetColorMapTransferFunctionForVolumeActor,
setPetTransferFunctionForVolumeActor,
diff --git a/yarn.lock b/yarn.lock
index f74fe6367..26a76e330 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1233,6 +1233,13 @@
dependencies:
regenerator-runtime "^0.13.11"
+"@babel/runtime@^7.15.4":
+ version "7.23.2"
+ resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.23.2.tgz#062b0ac103261d68a966c4c7baf2ae3e62ec3885"
+ integrity sha512-mM8eg4yl5D6i3lu2QKPuPH4FArvJ8KhTofbE7jwMUv9KX5mBvwPAqnV3MlyBNqdp9RyRKP6Yck8TrfYrPvX3bg==
+ dependencies:
+ regenerator-runtime "^0.14.0"
+
"@babel/template@^7.12.7", "@babel/template@^7.18.10", "@babel/template@^7.20.7", "@babel/template@^7.3.3":
version "7.20.7"
resolved "https://registry.npmjs.org/@babel/template/-/template-7.20.7.tgz#a15090c2839a83b02aa996c0b4994005841fd5a8"
@@ -2501,6 +2508,13 @@
resolved "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz#e45e384e4b8ec16bce2fd903af78450f6bf7ec98"
integrity sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==
+"@itk-wasm/elastix@0.2.2":
+ version "0.2.2"
+ resolved "https://registry.yarnpkg.com/@itk-wasm/elastix/-/elastix-0.2.2.tgz#b12552a017c885297eecd31e95f73d6a507baa3a"
+ integrity sha512-ogpFMIHIC2DvzNhR9XVF1pzac6hTug4yK1lPDMnVcW8QkI2koVRjfiWkJhi/5hlQwQcXgiaWBPaw0Ad/d2uq6g==
+ dependencies:
+ itk-wasm "^1.0.0-b.146"
+
"@jest/console@^29.5.0":
version "29.5.0"
resolved "https://registry.npmjs.org/@jest/console/-/console-29.5.0.tgz#593a6c5c0d3f75689835f1b3b4688c4f8544cb57"
@@ -4325,6 +4339,11 @@
dependencies:
defer-to-connect "^2.0.1"
+"@thewtex/zstddec@^0.1.2":
+ version "0.1.2"
+ resolved "https://registry.yarnpkg.com/@thewtex/zstddec/-/zstddec-0.1.2.tgz#33abeb1b9c6c1f7dca04aa1761c0471e3f493c85"
+ integrity sha512-Bv50pouFqlmIZDcAA2Nrpk9tjJpAPlqHHeD5h0noK+oNXMimrZ/hMbJK2N09Svr6TI/S6nT63dzkWoim4ZzTuw==
+
"@tokenizer/token@^0.3.0":
version "0.3.0"
resolved "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz#fe98a93fe789247e998c75e74e9c7c63217aa276"
@@ -5857,6 +5876,15 @@ axios@^1.0.0:
form-data "^4.0.0"
proxy-from-env "^1.1.0"
+axios@^1.4.0:
+ version "1.5.1"
+ resolved "https://registry.yarnpkg.com/axios/-/axios-1.5.1.tgz#11fbaa11fc35f431193a9564109c88c1f27b585f"
+ integrity sha512-Q28iYCWzNHjAm+yEAot5QaAMxhMghWLFVf7rRdwhUI+c2jix2DUXjAHXVi+s1ibs3mjPO/cCgbA++3BjD0vP/A==
+ dependencies:
+ follow-redirects "^1.15.0"
+ form-data "^4.0.0"
+ proxy-from-env "^1.1.0"
+
axobject-query@^3.1.1:
version "3.1.1"
resolved "https://registry.npmjs.org/axobject-query/-/axobject-query-3.1.1.tgz#3b6e5c6d4e43ca7ba51c5babf99d22a9c68485e1"
@@ -10915,7 +10943,7 @@ glob@^7.0.0, glob@^7.0.3, glob@^7.0.5, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, gl
once "^1.3.0"
path-is-absolute "^1.0.0"
-glob@^8.0.1, glob@^8.0.3:
+glob@^8.0.1, glob@^8.0.3, glob@^8.1.0:
version "8.1.0"
resolved "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz#d388f656593ef708ee3e34640fdfb99a9fd1c33e"
integrity sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==
@@ -12731,6 +12759,23 @@ isurl@^1.0.0-alpha5:
has-to-string-tag-x "^1.2.0"
is-object "^1.0.1"
+itk-wasm@^1.0.0-b.146:
+ version "1.0.0-b.149"
+ resolved "https://registry.yarnpkg.com/itk-wasm/-/itk-wasm-1.0.0-b.149.tgz#7fcedad26c7a9116c0dce004417ce4aec9a4ad5d"
+ integrity sha512-G1wvURAGMz/0XjHK1TKb4dZLwMV4+zsUZcuEkokH2fVtxWotj93OL3mrJvs7fhQ0Jenusf4COFN4yZ9Xt37SEA==
+ dependencies:
+ "@babel/runtime" "^7.15.4"
+ "@thewtex/zstddec" "^0.1.2"
+ "@types/emscripten" "^1.39.6"
+ axios "^1.4.0"
+ commander "^9.4.0"
+ fs-extra "^10.0.0"
+ glob "^8.1.0"
+ markdown-table "^3.0.3"
+ mime-types "^2.1.35"
+ wasm-feature-detect "^1.5.1"
+ webworker-promise "^0.4.2"
+
jackspeak@^2.0.3:
version "2.1.1"
resolved "https://registry.npmjs.org/jackspeak/-/jackspeak-2.1.1.tgz#2a42db4cfbb7e55433c28b6f75d8b796af9669cd"
@@ -13272,6 +13317,13 @@ jsesc@~0.5.0:
resolved "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d"
integrity sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==
+jsfive@0.3.14:
+ version "0.3.14"
+ resolved "https://registry.yarnpkg.com/jsfive/-/jsfive-0.3.14.tgz#5738bd6d96f97ec13d9f042880e695ae55476262"
+ integrity sha512-CptUQRZw1JHgQKhuZX6ozL/5PndJWetPz3K/Raeh5U/ise8FO84BIcBwbUBq2WsCl/zrnu3phnwazL+IuvaQ5A==
+ dependencies:
+ pako "^2.0.4"
+
json-buffer@3.0.0:
version "3.0.0"
resolved "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.0.tgz#5b1f397afc75d677bde8bcfc0e47e1f9a3d9a898"
@@ -14415,6 +14467,11 @@ markdown-it@^12.3.2:
mdurl "^1.0.1"
uc.micro "^1.0.5"
+markdown-table@^3.0.3:
+ version "3.0.3"
+ resolved "https://registry.yarnpkg.com/markdown-table/-/markdown-table-3.0.3.tgz#e6331d30e493127e031dd385488b5bd326e4a6bd"
+ integrity sha512-Z1NL3Tb1M9wH4XESsCDEksWoKTdlUafKc4pt0GRwjUyXaCFZ+dc3g2erqB6zm3szA2IUSi7VnPI+o/9jnxh9hw==
+
marked@^4.0.10, marked@^4.0.16, marked@^4.2.12, marked@^4.2.4, marked@^4.3.0:
version "4.3.0"
resolved "https://registry.npmjs.org/marked/-/marked-4.3.0.tgz#796362821b019f734054582038b116481b456cf3"
@@ -14663,7 +14720,7 @@ mime-types@2.1.18:
dependencies:
mime-db "~1.33.0"
-mime-types@^2.1.12, mime-types@^2.1.27, mime-types@^2.1.31, mime-types@~2.1.17, mime-types@~2.1.19, mime-types@~2.1.24, mime-types@~2.1.34:
+mime-types@^2.1.12, mime-types@^2.1.27, mime-types@^2.1.31, mime-types@^2.1.35, mime-types@~2.1.17, mime-types@~2.1.19, mime-types@~2.1.24, mime-types@~2.1.34:
version "2.1.35"
resolved "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a"
integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==
@@ -18492,6 +18549,11 @@ regenerator-runtime@^0.13.11, regenerator-runtime@^0.13.4:
resolved "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz#f6dca3e7ceec20590d07ada785636a90cdca17f9"
integrity sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==
+regenerator-runtime@^0.14.0:
+ version "0.14.0"
+ resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz#5e19d68eb12d486f797e15a3c6a918f7cec5eb45"
+ integrity sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==
+
regenerator-transform@^0.15.1:
version "0.15.1"
resolved "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.15.1.tgz#f6c4e99fc1b4591f780db2586328e4d9a9d8dc56"
@@ -21574,6 +21636,11 @@ walker@^1.0.8:
dependencies:
makeerror "1.0.12"
+wasm-feature-detect@^1.5.1:
+ version "1.5.1"
+ resolved "https://registry.yarnpkg.com/wasm-feature-detect/-/wasm-feature-detect-1.5.1.tgz#0db57a7d7f8c26b743dde85386215ae2b135e78a"
+ integrity sha512-GHr23qmuehNXHY4902/hJ6EV5sUANIJC3R/yMfQ7hWDg3nfhlcJfnIL96R2ohpIwa62araN6aN4bLzzzq5GXkg==
+
watchpack@^2.4.0:
version "2.4.0"
resolved "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz#fa33032374962c78113f93c7f2fb4c54c9862a5d"
@@ -21793,6 +21860,11 @@ webworker-promise@0.5.0:
resolved "https://registry.npmjs.org/webworker-promise/-/webworker-promise-0.5.0.tgz#eb1aa89f26ca6a49765f332668644d74c2c8e320"
integrity sha512-14iR79jHAV7ozwvbfif+3wCaApT3I1g8Lo0rJZrwAu6wxZGx/08Y8KXz6as6ZLNUEEufeiEBBYrqyDBClXOsEw==
+webworker-promise@^0.4.2:
+ version "0.4.4"
+ resolved "https://registry.yarnpkg.com/webworker-promise/-/webworker-promise-0.4.4.tgz#722b0ccade10ccb4e810325e5ebff00eb0e1b1be"
+ integrity sha512-NfdSlaWqd+0iSrQudB0N0MELfJ9TVTlynhXMpi06piuZhyc9Yy7Hz6BFu2HUkvIb9lCS0pFW42ptd/JnXVnptg==
+
well-known-symbols@^2.0.0:
version "2.0.0"
resolved "https://registry.npmjs.org/well-known-symbols/-/well-known-symbols-2.0.0.tgz#e9c7c07dbd132b7b84212c8174391ec1f9871ba5"