From 2a59cfce4d16d21812b48628d9dff7aaae8feaf3 Mon Sep 17 00:00:00 2001 From: Leonardo Campos Date: Thu, 26 Oct 2023 14:05:37 -0300 Subject: [PATCH 1/6] feat(registration): example (poc) --- packages/core/examples/registration/index.ts | 852 ++++++++++++++++++ packages/core/package.json | 1 + utils/ExampleRunner/example-info.json | 4 + utils/demo/helpers/addNumberInputToToolbar.ts | 43 + utils/demo/helpers/addRadioGroupToToolbar.ts | 52 ++ utils/demo/helpers/index.js | 4 + yarn.lock | 69 +- 7 files changed, 1023 insertions(+), 2 deletions(-) create mode 100644 packages/core/examples/registration/index.ts create mode 100644 utils/demo/helpers/addNumberInputToToolbar.ts create mode 100644 utils/demo/helpers/addRadioGroupToToolbar.ts diff --git a/packages/core/examples/registration/index.ts b/packages/core/examples/registration/index.ts new file mode 100644 index 000000000..a56660194 --- /dev/null +++ b/packages/core/examples/registration/index.ts @@ -0,0 +1,852 @@ +import { + defaultParameterMap, + elastix, + DefaultParameterMapOptions, + DefaultParameterMapResult, + ElastixOptions, +} from '@itk-wasm/elastix'; +import { Image, ImageType, FloatTypes, PixelTypes, Metadata } from 'itk-wasm'; +import { + RenderingEngine, + Types, + Enums, + cache, + volumeLoader, + getRenderingEngine, +} from '@cornerstonejs/core'; +import { + initDemo, + createImageIdsAndCacheMetaData, + setCtTransferFunctionForVolumeActor, + setTitleAndDescription, + addButtonToToolbar, + addNumberInputToToolbar, +} from '../../../../utils/demo/helpers'; +import * as cornerstoneTools from '@cornerstonejs/tools'; +import addDropDownToToolbar from '../../../../utils/demo/helpers/addDropdownToToolbar'; + +// This is for debugging purposes +console.warn( + 'Click on index.ts to open source code for this example --------->' +); + +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(); +const imageIdsCache = new Map(); +let webWorker = null; + +const volumesInfo = [ + { + volumeId: `${volumeLoaderScheme}:CT_VOLUME_ID_1`, + + // Neptune + 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', + + // Juno + // wadoRsRoot: 'http://localhost/dicom-web', + // StudyInstanceUID: '1.3.6.1.4.1.25403.345050719074.3824.20170125112931.11', + // SeriesInstanceUID: '1.3.6.1.4.1.25403.345050719074.3824.20170125113028.6', + + // Registration Patient + // wadoRsRoot: 'http://localhost/dicom-web', + // StudyInstanceUID: '1.2.276.0.7230010.3.1.2.8323329.48268.1698701222.813076', + // SeriesInstanceUID: '1.2.826.0.1.3680043.8.498.84963501100167841758743213842999883579', + }, + { + volumeId: `${volumeLoaderScheme}:CT_VOLUME_ID_2`, + + // Neptune + 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', + + // Juno + // wadoRsRoot: 'http://localhost/dicom-web', + // StudyInstanceUID: '1.3.6.1.4.1.25403.345050719074.3824.20170125113417.1', + // SeriesInstanceUID: '1.3.6.1.4.1.25403.345050719074.3824.20170125113420.1', + + // Registration Patient + // wadoRsRoot: 'http://localhost/dicom-web', + // StudyInstanceUID: '1.2.826.0.1.3680043.8.498.12964356232224616523807475287136128640', + // SeriesInstanceUID: '1.2.826.0.1.3680043.8.498.75254374739656968107738714604787374813', + }, +]; + +const viewportsInfo = [ + { + toolGroupId: 'VOLUME_TOOLGROUP_ID', + volumeInfo: volumesInfo[0], + viewportInput: { + viewportId: 'CT_VOLUME_AXIAL_FIXED', + type: ViewportType.ORTHOGRAPHIC, + element: null, + defaultOptions: { + orientation: Enums.OrientationAxis.AXIAL, + background: [0.2, 0, 0.2], + }, + }, + }, + { + toolGroupId: 'VOLUME_TOOLGROUP_ID', + volumeInfo: volumesInfo[1], + viewportInput: { + viewportId: 'CT_VOLUME_AXIAL_MOVING', + type: ViewportType.ORTHOGRAPHIC, + element: null, + defaultOptions: { + orientation: Enums.OrientationAxis.AXIAL, + background: [0.2, 0, 0.2], + }, + }, + }, +]; + +const defaultNumberOfResolutions = 2; +const defaultFinalGridSpacing = 8; +const transformNames = [ + 'translation', + 'rigid', + 'affine', + 'bspline', + 'spline', + // 'groupwise', // 2D+time or 3D+time +]; + +let activeTransformName = transformNames[1]; +let currentParameterMap = {}; + +const defaultParameterMaps = {}; +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'], + }, +}; + +// ==[ Set up page ]============================================================ + +setTitleAndDescription( + 'Registration', + 'Spatially align two volumes from different frames of reference' +); + +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 statusFieldset = document.createElement('fieldset'); +statusFieldset.style.fontSize = '12px'; +statusFieldset.style.height = '200px'; +statusFieldset.style.overflow = 'scroll'; +content.appendChild(statusFieldset); + +const statusFieldsetLegent = document.createElement('legend'); +statusFieldsetLegent.innerText = 'Processing logs'; +statusFieldset.appendChild(statusFieldsetLegent); + +const statusNode = document.createElement('div'); +statusNode.style.fontFamily = 'monospace'; +statusFieldset.appendChild(statusNode); + +const logStatus = (text, preFormated = false) => { + const node = document.createElement(preFormated ? 'pre' : 'p'); + + node.innerText = `${getFormatedDateTime()} ${text}`; + node.style.margin = '0'; + node.style.fontSize = '10px'; + statusNode.appendChild(node); + + // Scroll to the end + statusFieldset.scrollBy( + 0, + statusFieldset.scrollHeight - statusFieldset.scrollTop + ); +}; + +const clearStatus = () => { + while (statusNode.hasChildNodes()) { + statusNode.removeChild(statusNode.firstChild); + } +}; + +// ==[ 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 () => { + clearStatus(); + + // Use the same parameter map updated by the user + const parameterMap = currentParameterMap; + + logStatus(`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'); + + logImageInfo(fixedImage); + logImageInfo(movingImage); + + const elastixOptions: ElastixOptions = { + fixed: fixedImage, + moving: movingImage, + initialTransform: undefined, + initialTransformParameterObject: undefined, + }; + + logStatus('Registration in progress (rigid)...'); + + console.log('Registration:', parameterMap); + console.log(' parameterMap:', parameterMap); + console.log(' options:', elastixOptions); + + 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); + + logStatus( + `transformParameterObject:\n${stringify(transformParameterObject, 4)}`, + true + ); + + logStatus(`Total time: ${(totalTime / 1000).toFixed(3)} seconds`); + logStatus('Registration complete'); + }, +}); + +// ============================================================================= + +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); + + 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 current date/time ("YYYY-MM-DD hh:mm:ss.SSS") + */ +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. + */ +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; +} + +/** + * Log all image information + */ +function logImageInfo(image) { + logStatus(`image "${image.name}"`); + logStatus(` origin: ${image.origin.join(', ')}`, true); + logStatus(` spacing: ${image.spacing.join(', ')}`, true); + logStatus(` direction: ${image.direction.join(', ')}`, true); + logStatus(` size: ${image.size.join(', ')}`, true); + logStatus(` imageType:`, true); + logStatus(` dimension: ${image.imageType.dimension}`, true); + logStatus(` components: ${image.imageType.components}`, true); + logStatus(` componentType: ${image.imageType.componentType}`, true); + logStatus(` pixelType: ${image.imageType.pixelType}`, true); +} + +/** + * 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('Array', ''); + const metadata: Metadata = undefined; + const scalarData = scalars.getData(); + const imageType: ImageType = new ImageType( + dimensions.length, + FloatTypes[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 = 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'); + } +} + +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; +} + +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; +} + +/** + * 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/package.json b/packages/core/package.json index 00032ab5b..1802e41bb 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -31,6 +31,7 @@ }, "dependencies": { "@kitware/vtk.js": "27.3.1", + "@itk-wasm/elastix": "0.2.2", "detect-gpu": "^5.0.22", "gl-matrix": "^3.4.3", "lodash.clonedeep": "4.5.0" 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..c51c02ced 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" @@ -14415,6 +14460,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 +14713,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 +18542,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 +21629,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 +21853,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" From 140eb6a70789de5a587d25285a89ab849af98842 Mon Sep 17 00:00:00 2001 From: Leonardo Campos Date: Wed, 1 Nov 2023 16:12:40 -0300 Subject: [PATCH 2/6] feat(registration) removed ResultImageFormat --- packages/core/examples/registration/index.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/core/examples/registration/index.ts b/packages/core/examples/registration/index.ts index a56660194..4ac0ebff0 100644 --- a/packages/core/examples/registration/index.ts +++ b/packages/core/examples/registration/index.ts @@ -310,10 +310,10 @@ const parametersSettings = { inputType: 'number', defaultValue: defaultFinalGridSpacing, }, - ResultImageFormat: { - inputType: 'dropdown', - values: ['mhd', 'nii', 'nrrd', 'vti'], - }, + // ResultImageFormat: { + // inputType: 'dropdown', + // values: ['mhd', 'nii', 'nrrd', 'vti'], + // }, }; // ==[ Set up page ]============================================================ From ab5a01767d0f8d70e6cd079b6f2e3fe201300003 Mon Sep 17 00:00:00 2001 From: Leonardo Campos Date: Thu, 2 Nov 2023 15:44:19 -0300 Subject: [PATCH 3/6] code review + minor improvements --- .../registration/elastixParametersSettings.ts | 196 +++++++++ packages/core/examples/registration/index.ts | 376 ++++++------------ packages/core/examples/registration/utils.ts | 15 + packages/core/package.json | 5 +- 4 files changed, 340 insertions(+), 252 deletions(-) create mode 100644 packages/core/examples/registration/elastixParametersSettings.ts create mode 100644 packages/core/examples/registration/utils.ts 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 index 4ac0ebff0..627fa72c0 100644 --- a/packages/core/examples/registration/index.ts +++ b/packages/core/examples/registration/index.ts @@ -5,7 +5,15 @@ import { DefaultParameterMapResult, ElastixOptions, } from '@itk-wasm/elastix'; -import { Image, ImageType, FloatTypes, PixelTypes, Metadata } from 'itk-wasm'; +import { + Image, + ImageType, + IntTypes, + FloatTypes, + PixelTypes, + Metadata, +} from 'itk-wasm'; +import * as hdf5 from 'jsfive'; import { RenderingEngine, Types, @@ -24,12 +32,31 @@ import { } from '../../../../utils/demo/helpers'; import * as cornerstoneTools from '@cornerstonejs/tools'; import addDropDownToToolbar from '../../../../utils/demo/helpers/addDropdownToToolbar'; +import { + defaultNumberOfResolutions, + defaultFinalGridSpacing, + parametersSettings, +} from './elastixParametersSettings'; +import { getFormatedDateTime } from './utils'; // 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, @@ -50,14 +77,14 @@ const volumesInfo = [ volumeId: `${volumeLoaderScheme}:CT_VOLUME_ID_1`, // Neptune - 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', + // 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', // Juno - // wadoRsRoot: 'http://localhost/dicom-web', - // StudyInstanceUID: '1.3.6.1.4.1.25403.345050719074.3824.20170125112931.11', - // SeriesInstanceUID: '1.3.6.1.4.1.25403.345050719074.3824.20170125113028.6', + wadoRsRoot: 'http://localhost/dicom-web', + StudyInstanceUID: '1.3.6.1.4.1.25403.345050719074.3824.20170125112931.11', + SeriesInstanceUID: '1.3.6.1.4.1.25403.345050719074.3824.20170125113028.6', // Registration Patient // wadoRsRoot: 'http://localhost/dicom-web', @@ -68,14 +95,14 @@ const volumesInfo = [ volumeId: `${volumeLoaderScheme}:CT_VOLUME_ID_2`, // Neptune - 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', + // 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', // Juno - // wadoRsRoot: 'http://localhost/dicom-web', - // StudyInstanceUID: '1.3.6.1.4.1.25403.345050719074.3824.20170125113417.1', - // SeriesInstanceUID: '1.3.6.1.4.1.25403.345050719074.3824.20170125113420.1', + wadoRsRoot: 'http://localhost/dicom-web', + StudyInstanceUID: '1.3.6.1.4.1.25403.345050719074.3824.20170125113417.1', + SeriesInstanceUID: '1.3.6.1.4.1.25403.345050719074.3824.20170125113420.1', // Registration Patient // wadoRsRoot: 'http://localhost/dicom-web', @@ -89,11 +116,11 @@ const viewportsInfo = [ toolGroupId: 'VOLUME_TOOLGROUP_ID', volumeInfo: volumesInfo[0], viewportInput: { - viewportId: 'CT_VOLUME_AXIAL_FIXED', + viewportId: 'CT_VOLUME_FIXED', type: ViewportType.ORTHOGRAPHIC, element: null, defaultOptions: { - orientation: Enums.OrientationAxis.AXIAL, + orientation: Enums.OrientationAxis.CORONAL, background: [0.2, 0, 0.2], }, }, @@ -102,19 +129,17 @@ const viewportsInfo = [ toolGroupId: 'VOLUME_TOOLGROUP_ID', volumeInfo: volumesInfo[1], viewportInput: { - viewportId: 'CT_VOLUME_AXIAL_MOVING', + viewportId: 'CT_VOLUME_MOVING', type: ViewportType.ORTHOGRAPHIC, element: null, defaultOptions: { - orientation: Enums.OrientationAxis.AXIAL, + orientation: Enums.OrientationAxis.CORONAL, background: [0.2, 0, 0.2], }, }, }, ]; -const defaultNumberOfResolutions = 2; -const defaultFinalGridSpacing = 8; const transformNames = [ 'translation', 'rigid', @@ -128,193 +153,6 @@ let activeTransformName = transformNames[1]; let currentParameterMap = {}; const defaultParameterMaps = {}; -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'], - // }, -}; // ==[ Set up page ]============================================================ @@ -354,7 +192,7 @@ statusFieldset.appendChild(statusNode); const logStatus = (text, preFormated = false) => { const node = document.createElement(preFormated ? 'pre' : 'p'); - node.innerText = `${getFormatedDateTime()} ${text}`; + node.innerHTML = `${getFormatedDateTime()} ${text}`; node.style.margin = '0'; node.style.fontSize = '10px'; statusNode.appendChild(node); @@ -463,6 +301,13 @@ addButtonToToolbar({ onClick: async () => { clearStatus(); + // 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; @@ -484,35 +329,56 @@ addButtonToToolbar({ initialTransformParameterObject: undefined, }; - logStatus('Registration in progress (rigid)...'); + logStatus(`Registration in progress (${activeTransformName})...`); - console.log('Registration:', parameterMap); + console.log('Registration:'); console.log(' parameterMap:', parameterMap); console.log(' options:', elastixOptions); - 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); - - logStatus( - `transformParameterObject:\n${stringify(transformParameterObject, 4)}`, - true - ); + 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); + + logStatus( + `transformParameterObject:\n${stringify(transformParameterObject, 4)}`, + true + ); + + logStatus('Resulting image:'); + logImageInfo(result); + + logTransform(transform); + + logStatus(`Total time: ${(totalTime / 1000).toFixed(3)} seconds`); + logStatus('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; + } - logStatus(`Total time: ${(totalTime / 1000).toFixed(3)} seconds`); - logStatus('Registration complete'); + logStatus(`An error ocurred during : ${message}`); + console.log('Error: ', error); + } finally { + webWorker.terminate(); + } }, }); @@ -582,22 +448,6 @@ function loadParameterMap(transformName: string) { }); } -/** - * Get the current date/time ("YYYY-MM-DD hh:mm:ss.SSS") - */ -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 @@ -643,6 +493,23 @@ function logImageInfo(image) { logStatus(` pixelType: ${image.imageType.pixelType}`, true); } +function 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); + + logStatus(`Download ${fileName}`); + console.log('Transform (HDF5):', hdfFile); +} + /** * Get the ITK Image from a given viewport * @param viewportId - Viewport Id @@ -654,6 +521,7 @@ function getImageFromViewport(viewportId, imageName?: string): Image { const viewport = ( renderingEngine.getViewport(viewportId) ); + const { actor: volumeActor } = viewport.getDefaultActor(); const imageData = volumeActor.getMapper().getInputData(); const pointData = imageData.getPointData(); @@ -664,12 +532,15 @@ function getImageFromViewport(viewportId, imageName?: string): Image { const directionArray = imageData.getDirection(); const direction = new Float64Array(directionArray); const numComponents = pointData.getNumberOfComponents(); - const dataType = scalars.getDataType().replace('Array', ''); + const dataType = scalars + .getDataType() + .replace(/^Ui/, 'UI') + .replace(/Array$/, ''); const metadata: Metadata = undefined; const scalarData = scalars.getData(); const imageType: ImageType = new ImageType( dimensions.length, - FloatTypes[dataType], + dataTypesMap[dataType], PixelTypes.Scalar, numComponents ); @@ -682,9 +553,10 @@ function getImageFromViewport(viewportId, imageName?: string): Image { image.direction = direction; image.size = dimensions; image.metadata = metadata; - image.data = new scalarData.constructor(scalarData.length); + image.data = scalarData; - image.data.set(scalarData, 0); + // image.data = new scalarData.constructor(scalarData.length); + // image.data.set(scalarData, 0); return image; } @@ -755,6 +627,8 @@ async function initializeViewport( } else { throw new Error('Invalid viewport type'); } + + logStatus(`Viewport ${viewportId} initialized (${imageIds.length} slices)`); } function initializeToolGroup(toolGroupId) { diff --git a/packages/core/examples/registration/utils.ts b/packages/core/examples/registration/utils.ts new file mode 100644 index 000000000..e809044e7 --- /dev/null +++ b/packages/core/examples/registration/utils.ts @@ -0,0 +1,15 @@ +/** + * 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}`; +} diff --git a/packages/core/package.json b/packages/core/package.json index 1802e41bb..7d0f23745 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -31,11 +31,14 @@ }, "dependencies": { "@kitware/vtk.js": "27.3.1", - "@itk-wasm/elastix": "0.2.2", "detect-gpu": "^5.0.22", "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", From 91c4af01073db645aa7a55a1e55e966cf88bac0a Mon Sep 17 00:00:00 2001 From: Leonardo Campos Date: Thu, 2 Nov 2023 16:35:42 -0300 Subject: [PATCH 4/6] removed unused dataset UIDs --- packages/core/examples/registration/index.ts | 36 ++++---------------- 1 file changed, 6 insertions(+), 30 deletions(-) diff --git a/packages/core/examples/registration/index.ts b/packages/core/examples/registration/index.ts index 627fa72c0..ac5272f90 100644 --- a/packages/core/examples/registration/index.ts +++ b/packages/core/examples/registration/index.ts @@ -75,39 +75,15 @@ let webWorker = null; const volumesInfo = [ { volumeId: `${volumeLoaderScheme}:CT_VOLUME_ID_1`, - - // Neptune - // 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', - - // Juno - wadoRsRoot: 'http://localhost/dicom-web', - StudyInstanceUID: '1.3.6.1.4.1.25403.345050719074.3824.20170125112931.11', - SeriesInstanceUID: '1.3.6.1.4.1.25403.345050719074.3824.20170125113028.6', - - // Registration Patient - // wadoRsRoot: 'http://localhost/dicom-web', - // StudyInstanceUID: '1.2.276.0.7230010.3.1.2.8323329.48268.1698701222.813076', - // SeriesInstanceUID: '1.2.826.0.1.3680043.8.498.84963501100167841758743213842999883579', + 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`, - - // Neptune - // 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', - - // Juno - wadoRsRoot: 'http://localhost/dicom-web', - StudyInstanceUID: '1.3.6.1.4.1.25403.345050719074.3824.20170125113417.1', - SeriesInstanceUID: '1.3.6.1.4.1.25403.345050719074.3824.20170125113420.1', - - // Registration Patient - // wadoRsRoot: 'http://localhost/dicom-web', - // StudyInstanceUID: '1.2.826.0.1.3680043.8.498.12964356232224616523807475287136128640', - // SeriesInstanceUID: '1.2.826.0.1.3680043.8.498.75254374739656968107738714604787374813', + 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', }, ]; From 1dd91941586cf90e824a91e3df66f661d2dd1bba Mon Sep 17 00:00:00 2001 From: Leonardo Campos Date: Thu, 2 Nov 2023 17:06:25 -0300 Subject: [PATCH 5/6] reduced the example page size --- .../registration/RegistrationConsole.ts | 106 ++++++++++++ packages/core/examples/registration/index.ts | 154 ++---------------- packages/core/examples/registration/utils.ts | 54 ++++++ 3 files changed, 178 insertions(+), 136 deletions(-) create mode 100644 packages/core/examples/registration/RegistrationConsole.ts 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/index.ts b/packages/core/examples/registration/index.ts index ac5272f90..1fe042326 100644 --- a/packages/core/examples/registration/index.ts +++ b/packages/core/examples/registration/index.ts @@ -13,7 +13,6 @@ import { PixelTypes, Metadata, } from 'itk-wasm'; -import * as hdf5 from 'jsfive'; import { RenderingEngine, Types, @@ -24,7 +23,6 @@ import { } from '@cornerstonejs/core'; import { initDemo, - createImageIdsAndCacheMetaData, setCtTransferFunctionForVolumeActor, setTitleAndDescription, addButtonToToolbar, @@ -37,7 +35,8 @@ import { defaultFinalGridSpacing, parametersSettings, } from './elastixParametersSettings'; -import { getFormatedDateTime } from './utils'; +import { getImageIds, stringify } from './utils'; +import RegistrationConsole from './RegistrationConsole'; // This is for debugging purposes console.warn( @@ -69,7 +68,6 @@ const { MouseBindings } = csToolsEnums; const renderingEngineId = 'myRenderingEngine'; const volumeLoaderScheme = 'cornerstoneStreamingImageVolume'; // Loader id which defines which volume loader to use const toolGroupIds = new Set(); -const imageIdsCache = new Map(); let webWorker = null; const volumesInfo = [ @@ -151,40 +149,7 @@ Object.assign(viewportGrid.style, { content.appendChild(viewportGrid); -const statusFieldset = document.createElement('fieldset'); -statusFieldset.style.fontSize = '12px'; -statusFieldset.style.height = '200px'; -statusFieldset.style.overflow = 'scroll'; -content.appendChild(statusFieldset); - -const statusFieldsetLegent = document.createElement('legend'); -statusFieldsetLegent.innerText = 'Processing logs'; -statusFieldset.appendChild(statusFieldsetLegent); - -const statusNode = document.createElement('div'); -statusNode.style.fontFamily = 'monospace'; -statusFieldset.appendChild(statusNode); - -const logStatus = (text, preFormated = false) => { - const node = document.createElement(preFormated ? 'pre' : 'p'); - - node.innerHTML = `${getFormatedDateTime()} ${text}`; - node.style.margin = '0'; - node.style.fontSize = '10px'; - statusNode.appendChild(node); - - // Scroll to the end - statusFieldset.scrollBy( - 0, - statusFieldset.scrollHeight - statusFieldset.scrollTop - ); -}; - -const clearStatus = () => { - while (statusNode.hasChildNodes()) { - statusNode.removeChild(statusNode.firstChild); - } -}; +const regConsole = new RegistrationConsole(content); // ==[ Toolbar ]================================================================ const toolbar = document.getElementById('demo-toolbar'); @@ -275,7 +240,7 @@ addButtonToToolbar({ id: 'btnRegister', title: 'Register volumes', onClick: async () => { - clearStatus(); + 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 @@ -287,7 +252,7 @@ addButtonToToolbar({ // Use the same parameter map updated by the user const parameterMap = currentParameterMap; - logStatus(`Parameters map:\n${stringify(parameterMap, 4)}`, true); + regConsole.log(`Parameters map:\n${stringify(parameterMap, 4)}`, true); const [fixedViewportInfo, movingViewportInfo] = viewportsInfo; const { viewportId: fixedViewportId } = fixedViewportInfo.viewportInput; @@ -295,8 +260,8 @@ addButtonToToolbar({ const fixedImage = getImageFromViewport(fixedViewportId, 'fixed'); const movingImage = getImageFromViewport(movingViewportId, 'moving'); - logImageInfo(fixedImage); - logImageInfo(movingImage); + regConsole.logImageInfo(fixedImage); + regConsole.logImageInfo(movingImage); const elastixOptions: ElastixOptions = { fixed: fixedImage, @@ -305,7 +270,7 @@ addButtonToToolbar({ initialTransformParameterObject: undefined, }; - logStatus(`Registration in progress (${activeTransformName})...`); + regConsole.log(`Registration in progress (${activeTransformName})...`); console.log('Registration:'); console.log(' parameterMap:', parameterMap); @@ -328,18 +293,16 @@ addButtonToToolbar({ console.log(' transform:', transform); console.log(' transformParameterObject:', transformParameterObject); - logStatus( + regConsole.log( `transformParameterObject:\n${stringify(transformParameterObject, 4)}`, true ); - logStatus('Resulting image:'); - logImageInfo(result); - - logTransform(transform); - - logStatus(`Total time: ${(totalTime / 1000).toFixed(3)} seconds`); - logStatus('Registration complete'); + 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'; @@ -350,7 +313,7 @@ addButtonToToolbar({ message = error.message; } - logStatus(`An error ocurred during : ${message}`); + regConsole.log(`An error ocurred during : ${message}`); console.log('Error: ', error); } finally { webWorker.terminate(); @@ -424,68 +387,6 @@ function loadParameterMap(transformName: string) { }); } -/** - * 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. - */ -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; -} - -/** - * Log all image information - */ -function logImageInfo(image) { - logStatus(`image "${image.name}"`); - logStatus(` origin: ${image.origin.join(', ')}`, true); - logStatus(` spacing: ${image.spacing.join(', ')}`, true); - logStatus(` direction: ${image.direction.join(', ')}`, true); - logStatus(` size: ${image.size.join(', ')}`, true); - logStatus(` imageType:`, true); - logStatus(` dimension: ${image.imageType.dimension}`, true); - logStatus(` components: ${image.imageType.components}`, true); - logStatus(` componentType: ${image.imageType.componentType}`, true); - logStatus(` pixelType: ${image.imageType.pixelType}`, true); -} - -function 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); - - logStatus(`Download ${fileName}`); - console.log('Transform (HDF5):', hdfFile); -} - /** * Get the ITK Image from a given viewport * @param viewportId - Viewport Id @@ -604,7 +505,9 @@ async function initializeViewport( throw new Error('Invalid viewport type'); } - logStatus(`Viewport ${viewportId} initialized (${imageIds.length} slices)`); + regConsole.log( + `Viewport ${viewportId} initialized (${imageIds.length} slices)` + ); } function initializeToolGroup(toolGroupId) { @@ -637,27 +540,6 @@ function initializeToolGroup(toolGroupId) { return toolGroup; } -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; -} - /** * Runs the demo */ diff --git a/packages/core/examples/registration/utils.ts b/packages/core/examples/registration/utils.ts index e809044e7..1105d9317 100644 --- a/packages/core/examples/registration/utils.ts +++ b/packages/core/examples/registration/utils.ts @@ -1,3 +1,7 @@ +import { createImageIdsAndCacheMetaData } from '../../../../utils/demo/helpers'; + +const imageIdsCache = new Map(); + /** * Get the current date/time ("YYYY-MM-DD hh:mm:ss.SSS") */ @@ -13,3 +17,53 @@ export function getFormatedDateTime() { 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; +} From 1cf2b2c9cdd71ae33d4eb12770c0428ad2a837f3 Mon Sep 17 00:00:00 2001 From: Alireza Date: Thu, 2 Nov 2023 16:48:56 -0400 Subject: [PATCH 6/6] add docs and fix initial translation estimation --- packages/core/examples/registration/index.ts | 3 ++- yarn.lock | 7 +++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/core/examples/registration/index.ts b/packages/core/examples/registration/index.ts index ac5272f90..606d2041c 100644 --- a/packages/core/examples/registration/index.ts +++ b/packages/core/examples/registration/index.ts @@ -134,7 +134,7 @@ const defaultParameterMaps = {}; setTitleAndDescription( 'Registration', - 'Spatially align two volumes from different frames of reference' + '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'); @@ -384,6 +384,7 @@ async function loadAndCacheAllParameterMaps() { const transformName = transformNames[i]; const { parameterMap } = await getElastixParameterMap(transformName); + parameterMap['AutomaticTransformInitialization'] = ['true']; defaultParameterMaps[transformName] = parameterMap; console.log(`Default parameter map (${transformName}):`, parameterMap); } diff --git a/yarn.lock b/yarn.lock index c51c02ced..26a76e330 100644 --- a/yarn.lock +++ b/yarn.lock @@ -13317,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"