From 0c0104382dd87c2102f1c92a29eda4cc9e70d671 Mon Sep 17 00:00:00 2001 From: Alireza Date: Tue, 16 Apr 2024 14:35:55 -0400 Subject: [PATCH 01/16] Refactor OrientationMarkerTool to improve viewport resizing and event handling --- packages/tools/src/enums/Events.ts | 6 + .../src/store/ToolGroupManager/ToolGroup.ts | 16 +++ .../tools/src/tools/OrientationMarkerTool.ts | 116 ++++++++++-------- 3 files changed, 89 insertions(+), 49 deletions(-) diff --git a/packages/tools/src/enums/Events.ts b/packages/tools/src/enums/Events.ts index 146f2a91f2..7c6ae14568 100644 --- a/packages/tools/src/enums/Events.ts +++ b/packages/tools/src/enums/Events.ts @@ -18,6 +18,12 @@ enum Events { */ TOOL_ACTIVATED = 'CORNERSTONE_TOOLS_TOOL_ACTIVATED', + // fired when a viewport is added to the toolGroup + TOOLGROUP_VIEWPORT_ADDED = 'CORNERSTONE_TOOLS_TOOLGROUP_VIEWPORT_ADDED', + + // fired when a viewport is removed from the toolGroup + TOOLGROUP_VIEWPORT_REMOVED = 'CORNERSTONE_TOOLS_TOOLGROUP_VIEWPORT_REMOVED', + /** * Triggers on the eventTarget when a mode of a tool is changed (active, passive, enabled and disabled). * diff --git a/packages/tools/src/store/ToolGroupManager/ToolGroup.ts b/packages/tools/src/store/ToolGroupManager/ToolGroup.ts index 24df09da15..c17cff4321 100644 --- a/packages/tools/src/store/ToolGroupManager/ToolGroup.ts +++ b/packages/tools/src/store/ToolGroupManager/ToolGroup.ts @@ -239,6 +239,14 @@ export default class ToolGroup implements IToolGroup { if (runtimeSettings.get('useCursors')) { this.setViewportsCursorByToolName(toolName); } + + const eventDetail = { + toolGroupId: this.id, + viewportId, + renderingEngineId: renderingEngineUIDToUse, + }; + + triggerEvent(eventTarget, Events.TOOLGROUP_VIEWPORT_ADDED, eventDetail); } /** @@ -273,6 +281,14 @@ export default class ToolGroup implements IToolGroup { this.viewportsInfo.splice(indices[i], 1); } } + + const eventDetail = { + toolGroupId: this.id, + viewportId, + renderingEngineId, + }; + + triggerEvent(eventTarget, Events.TOOLGROUP_VIEWPORT_REMOVED, eventDetail); } public setActiveStrategy(toolName: string, strategyName: string) { diff --git a/packages/tools/src/tools/OrientationMarkerTool.ts b/packages/tools/src/tools/OrientationMarkerTool.ts index 970394ba37..705916354c 100644 --- a/packages/tools/src/tools/OrientationMarkerTool.ts +++ b/packages/tools/src/tools/OrientationMarkerTool.ts @@ -9,11 +9,13 @@ import vtkPolyData from '@kitware/vtk.js/Common/DataModel/PolyData'; import { BaseTool } from './base'; import { Enums, + eventTarget, getEnabledElementByIds, getRenderingEngines, } from '@cornerstonejs/core'; import { filterViewportsWithToolEnabled } from '../utilities/viewportFilters'; import { getToolGroup } from '../store/ToolGroupManager'; +import { Events } from '../enums'; const OverlayMarkerType = { ANNOTATED_CUBE: 1, @@ -36,8 +38,6 @@ class OrientationMarkerTool extends BaseTool { static OVERLAY_MARKER_TYPES = OverlayMarkerType; - configuration_invalidated = true; - constructor( toolProps = {}, defaultToolProps = { @@ -88,18 +88,16 @@ class OrientationMarkerTool extends BaseTool { ) { super(toolProps, defaultToolProps); this.orientationMarkers = {}; - this.configuration_invalidated = true; } onSetToolEnabled = (): void => { this.initViewports(); - this.configuration_invalidated = true; this._subscribeToViewportEvents(); }; onSetToolActive = (): void => { this.initViewports(); - this.configuration_invalidated = true; + this._subscribeToViewportEvents(); }; @@ -108,11 +106,6 @@ class OrientationMarkerTool extends BaseTool { this._unsubscribeToViewportNewVolumeSet(); }; - reset = () => { - this.configuration_invalidated = true; - this.initViewports(); - }; - _getViewportsInfo = () => { const viewports = getToolGroup(this.toolGroupId).viewportsInfo; @@ -130,50 +123,77 @@ class OrientationMarkerTool extends BaseTool { }; _unsubscribeToViewportNewVolumeSet() { - const viewportsInfo = this._getViewportsInfo(); - viewportsInfo.forEach(({ viewportId, renderingEngineId }) => { - const { viewport } = getEnabledElementByIds( - viewportId, - renderingEngineId - ); - const { element } = viewport; - - element.removeEventListener( - Enums.Events.VOLUME_VIEWPORT_NEW_VOLUME, - this.reset - ); - - // // // unsubscribe to element resize events - const resizeObserver = this._resizeObservers.get(viewportId); - resizeObserver.unobserve(element); + const unsubscribe = () => { + const viewportsInfo = this._getViewportsInfo(); + viewportsInfo.forEach(({ viewportId, renderingEngineId }) => { + const { viewport } = getEnabledElementByIds( + viewportId, + renderingEngineId + ); + const { element } = viewport; + + element.removeEventListener( + Enums.Events.VOLUME_VIEWPORT_NEW_VOLUME, + this.initViewports + ); + + const resizeObserver = this._resizeObservers.get(viewportId); + resizeObserver.unobserve(element); + }); + }; + + eventTarget.removeEventListener(Events.TOOLGROUP_VIEWPORT_ADDED, (evt) => { + if (evt.detail.toolGroupId !== this.toolGroupId) { + return; + } + unsubscribe(); + this.initViewports(); }); } _subscribeToViewportEvents() { - const viewportsInfo = this._getViewportsInfo(); - - viewportsInfo.forEach(({ viewportId, renderingEngineId }) => { - const { viewport } = getEnabledElementByIds( - viewportId, - renderingEngineId - ); - const { element } = viewport; - - element.addEventListener( - Enums.Events.VOLUME_VIEWPORT_NEW_VOLUME, - this.reset - ); - - const resizeObserver = new ResizeObserver(() => { - // Todo: i wish there was a better way to do this - setTimeout(() => { - this.reset(); - }, 100); + const subscribeToElementResize = () => { + const viewportsInfo = this._getViewportsInfo(); + viewportsInfo.forEach(({ viewportId, renderingEngineId }) => { + const { viewport } = getEnabledElementByIds( + viewportId, + renderingEngineId + ); + const { element } = viewport; + this.initViewports(); + + element.addEventListener( + Enums.Events.VOLUME_VIEWPORT_NEW_VOLUME, + this.initViewports + ); + + const resizeObserver = new ResizeObserver(() => { + // Todo: i wish there was a better way to do this + setTimeout(() => { + const { viewport } = getEnabledElementByIds( + viewportId, + renderingEngineId + ); + this.resize(viewportId); + viewport.render(); + }, 100); + }); + + resizeObserver.observe(element); + + this._resizeObservers.set(viewportId, resizeObserver); }); + }; - resizeObserver.observe(element); + subscribeToElementResize(); - this._resizeObservers.set(viewportId, resizeObserver); + eventTarget.addEventListener(Events.TOOLGROUP_VIEWPORT_ADDED, (evt) => { + if (evt.detail.toolGroupId !== this.toolGroupId) { + return; + } + + subscribeToElementResize(); + this.initViewports(); }); } @@ -276,8 +296,6 @@ class OrientationMarkerTool extends BaseTool { viewport.addWidget(this.getToolName(), orientationWidget); renderWindow.render(); viewport.getRenderingEngine().render(); - - this.configuration_invalidated = false; } private async createCustomActor() { From b99bfd9f1a27557c1a52f136454664259f137e9c Mon Sep 17 00:00:00 2001 From: Alireza Date: Tue, 16 Apr 2024 17:10:35 -0400 Subject: [PATCH 02/16] Refactor getCalibratedLengthUnits and getCalibratedScale functions in BidirectionalTool.ts --- .../src/tools/annotation/BidirectionalTool.ts | 43 ++++++++++++------- 1 file changed, 28 insertions(+), 15 deletions(-) diff --git a/packages/tools/src/tools/annotation/BidirectionalTool.ts b/packages/tools/src/tools/annotation/BidirectionalTool.ts index b7964a7aa9..04297d35f4 100644 --- a/packages/tools/src/tools/annotation/BidirectionalTool.ts +++ b/packages/tools/src/tools/annotation/BidirectionalTool.ts @@ -2,10 +2,7 @@ import { vec2, vec3 } from 'gl-matrix'; import { getEnabledElement, utilities as csUtils } from '@cornerstonejs/core'; import type { Types } from '@cornerstonejs/core'; -import { - getCalibratedLengthUnits, - getCalibratedScale, -} from '../../utilities/getCalibratedUnits'; +import { getCalibratedLengthUnitsAndScale } from '../../utilities/getCalibratedUnits'; import { roundNumber } from '../../utilities'; import { AnnotationTool } from '../base'; import throttle from '../../utilities/throttle'; @@ -40,7 +37,6 @@ import { TextBoxHandle, PublicToolProps, ToolProps, - InteractionTypes, SVGDrawingHelper, } from '../../types'; import { BidirectionalAnnotation } from '../../types/ToolSpecificAnnotationTypes'; @@ -1272,17 +1268,32 @@ class BidirectionalTool extends AnnotationTool { } const { imageData, dimensions } = image; - const scale = getCalibratedScale(image); - const dist1 = this._calculateLength(worldPos1, worldPos2) / scale; - const dist2 = this._calculateLength(worldPos3, worldPos4) / scale; - const length = dist1 > dist2 ? dist1 : dist2; - const width = dist1 > dist2 ? dist2 : dist1; - const index1 = transformWorldToIndex(imageData, worldPos1); const index2 = transformWorldToIndex(imageData, worldPos2); const index3 = transformWorldToIndex(imageData, worldPos3); const index4 = transformWorldToIndex(imageData, worldPos4); + const handles1 = [index1, index2]; + const handles2 = [index3, index4]; + + const { scale: scale1, units: units1 } = getCalibratedLengthUnitsAndScale( + image, + handles1 + ); + + const { scale: scale2, units: units2 } = getCalibratedLengthUnitsAndScale( + image, + handles2 + ); + + const dist1 = this._calculateLength(worldPos1, worldPos2) / scale1; + const dist2 = this._calculateLength(worldPos3, worldPos4) / scale2; + const length = dist1 > dist2 ? dist1 : dist2; + const width = dist1 > dist2 ? dist2 : dist1; + + const lengthUnit = dist1 > dist2 ? units1 : units2; + const widthUnit = dist1 > dist2 ? units2 : units1; + this._isInsideVolume(index1, index2, index3, index4, dimensions) ? (this.isHandleOutsideImage = false) : (this.isHandleOutsideImage = true); @@ -1290,7 +1301,9 @@ class BidirectionalTool extends AnnotationTool { cachedStats[targetId] = { length, width, - unit: getCalibratedLengthUnits(null, image), + unit: units1, + lengthUnit, + widthUnit, }; } @@ -1321,7 +1334,7 @@ class BidirectionalTool extends AnnotationTool { function defaultGetTextLines(data, targetId): string[] { const { cachedStats, label } = data; - const { length, width, unit } = cachedStats[targetId]; + const { length, width, unit, lengthUnit, widthUnit } = cachedStats[targetId]; const textLines = []; if (label) { @@ -1334,8 +1347,8 @@ function defaultGetTextLines(data, targetId): string[] { // spaceBetweenSlices & pixelSpacing & // magnitude in each direction? Otherwise, this is "px"? textLines.push( - `L: ${roundNumber(length)} ${unit}`, - `W: ${roundNumber(width)} ${unit}` + `L: ${roundNumber(length)} ${lengthUnit || unit}`, + `W: ${roundNumber(width)} ${widthUnit || unit}` ); return textLines; From 187e9744ae4c7ec373265eeebc3e378f5774a39b Mon Sep 17 00:00:00 2001 From: Alireza Date: Tue, 16 Apr 2024 23:33:42 -0400 Subject: [PATCH 03/16] Fix image loading bug in setDefaultVolumeVOI.ts --- .../core/src/RenderingEngine/helpers/setDefaultVolumeVOI.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/core/src/RenderingEngine/helpers/setDefaultVolumeVOI.ts b/packages/core/src/RenderingEngine/helpers/setDefaultVolumeVOI.ts index 5f11c531c7..73095e7a12 100644 --- a/packages/core/src/RenderingEngine/helpers/setDefaultVolumeVOI.ts +++ b/packages/core/src/RenderingEngine/helpers/setDefaultVolumeVOI.ts @@ -186,7 +186,10 @@ async function getVOIFromMinMax( let image = cache.getImage(imageId); if (!imageVolume.referencedImageIds?.length) { - image = await loadAndCacheImage(imageId, options); + // we should ignore the cache here, + // since we want to load the image from with the most + // recent prescale settings + image = await loadAndCacheImage(imageId, { ...options, ignoreCache: true }); } const imageScalarData = image From 91f3d1ba45731e22e1880844a44ee428fe8c3b12 Mon Sep 17 00:00:00 2001 From: Alireza Date: Tue, 16 Apr 2024 23:33:52 -0400 Subject: [PATCH 04/16] Refactor OrientationMarkerTool to bind event handlers in initViewports method --- packages/tools/src/tools/OrientationMarkerTool.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/tools/src/tools/OrientationMarkerTool.ts b/packages/tools/src/tools/OrientationMarkerTool.ts index 705916354c..b1ed241a6e 100644 --- a/packages/tools/src/tools/OrientationMarkerTool.ts +++ b/packages/tools/src/tools/OrientationMarkerTool.ts @@ -134,7 +134,7 @@ class OrientationMarkerTool extends BaseTool { element.removeEventListener( Enums.Events.VOLUME_VIEWPORT_NEW_VOLUME, - this.initViewports + this.initViewports.bind(this) ); const resizeObserver = this._resizeObservers.get(viewportId); @@ -164,7 +164,7 @@ class OrientationMarkerTool extends BaseTool { element.addEventListener( Enums.Events.VOLUME_VIEWPORT_NEW_VOLUME, - this.initViewports + this.initViewports.bind(this) ); const resizeObserver = new ResizeObserver(() => { From c8bd4469f5b0c6eaad0b763228b32cfd61fe45d7 Mon Sep 17 00:00:00 2001 From: Alireza Date: Wed, 17 Apr 2024 11:16:12 -0400 Subject: [PATCH 05/16] Refactor RectangleROITool to use getCalibratedLengthUnitsAndScale in RectangleROITool.ts --- .../src/tools/annotation/CircleROITool.ts | 49 +++++----- .../src/tools/annotation/EllipticalROITool.ts | 67 ++++++-------- .../src/tools/annotation/RectangleROITool.ts | 46 +++++----- .../tools/src/utilities/getCalibratedUnits.ts | 92 ++++--------------- 4 files changed, 93 insertions(+), 161 deletions(-) diff --git a/packages/tools/src/tools/annotation/CircleROITool.ts b/packages/tools/src/tools/annotation/CircleROITool.ts index efba98d01e..f157fdc966 100644 --- a/packages/tools/src/tools/annotation/CircleROITool.ts +++ b/packages/tools/src/tools/annotation/CircleROITool.ts @@ -7,13 +7,8 @@ import { } from '@cornerstonejs/core'; import type { Types } from '@cornerstonejs/core'; -import { - getCalibratedLengthUnits, - getCalibratedAreaUnits, - getCalibratedScale, - getCalibratedAspect, -} from '../../utilities/getCalibratedUnits'; -import { roundNumber } from '../../utilities'; +import { getCalibratedAspect } from '../../utilities/getCalibratedUnits'; +import { getCalibratedLengthUnitsAndScale, roundNumber } from '../../utilities'; import throttle from '../../utilities/throttle'; import { addAnnotation, @@ -891,30 +886,30 @@ class CircleROITool extends AnnotationTool { const { dimensions, imageData, metadata, hasPixelSpacing } = image; - const worldPos1Index = transformWorldToIndex(imageData, worldPos1); + const pos1Index = transformWorldToIndex(imageData, worldPos1); - worldPos1Index[0] = Math.floor(worldPos1Index[0]); - worldPos1Index[1] = Math.floor(worldPos1Index[1]); - worldPos1Index[2] = Math.floor(worldPos1Index[2]); + pos1Index[0] = Math.floor(pos1Index[0]); + pos1Index[1] = Math.floor(pos1Index[1]); + pos1Index[2] = Math.floor(pos1Index[2]); - const worldPos2Index = transformWorldToIndex(imageData, worldPos2); + const pos2Index = transformWorldToIndex(imageData, worldPos2); - worldPos2Index[0] = Math.floor(worldPos2Index[0]); - worldPos2Index[1] = Math.floor(worldPos2Index[1]); - worldPos2Index[2] = Math.floor(worldPos2Index[2]); + pos2Index[0] = Math.floor(pos2Index[0]); + pos2Index[1] = Math.floor(pos2Index[1]); + pos2Index[2] = Math.floor(pos2Index[2]); // Check if one of the indexes are inside the volume, this then gives us // Some area to do stats over. - if (this._isInsideVolume(worldPos1Index, worldPos2Index, dimensions)) { - const iMin = Math.min(worldPos1Index[0], worldPos2Index[0]); - const iMax = Math.max(worldPos1Index[0], worldPos2Index[0]); + if (this._isInsideVolume(pos1Index, pos2Index, dimensions)) { + const iMin = Math.min(pos1Index[0], pos2Index[0]); + const iMax = Math.max(pos1Index[0], pos2Index[0]); - const jMin = Math.min(worldPos1Index[1], worldPos2Index[1]); - const jMax = Math.max(worldPos1Index[1], worldPos2Index[1]); + const jMin = Math.min(pos1Index[1], pos2Index[1]); + const jMax = Math.max(pos1Index[1], pos2Index[1]); - const kMin = Math.min(worldPos1Index[2], worldPos2Index[2]); - const kMax = Math.max(worldPos1Index[2], worldPos2Index[2]); + const kMin = Math.min(pos1Index[2], pos2Index[2]); + const kMax = Math.max(pos1Index[2], pos2Index[2]); const boundsIJK = [ [iMin, iMax], @@ -942,7 +937,11 @@ class CircleROITool extends AnnotationTool { worldPos2 ); const isEmptyArea = worldWidth === 0 && worldHeight === 0; - const scale = getCalibratedScale(image); + const handles = [pos1Index, pos2Index]; + const { scale, units, areaUnits } = getCalibratedLengthUnitsAndScale( + image, + handles + ); const aspect = getCalibratedAspect(image); const area = Math.abs( Math.PI * @@ -986,9 +985,9 @@ class CircleROITool extends AnnotationTool { statsArray: stats.array, pointsInShape: pointsInShape, isEmptyArea, - areaUnit: getCalibratedAreaUnits(null, image), + areaUnit: areaUnits, radius: worldWidth / 2 / scale, - radiusUnit: getCalibratedLengthUnits(null, image), + radiusUnit: units, perimeter: (2 * Math.PI * (worldWidth / 2)) / scale, modalityUnit, }; diff --git a/packages/tools/src/tools/annotation/EllipticalROITool.ts b/packages/tools/src/tools/annotation/EllipticalROITool.ts index 449e188075..a4d37f95cd 100644 --- a/packages/tools/src/tools/annotation/EllipticalROITool.ts +++ b/packages/tools/src/tools/annotation/EllipticalROITool.ts @@ -7,10 +7,7 @@ import { } from '@cornerstonejs/core'; import type { Types } from '@cornerstonejs/core'; -import { - getCalibratedAreaUnits, - getCalibratedScale, -} from '../../utilities/getCalibratedUnits'; +import { getCalibratedLengthUnitsAndScale } from '../../utilities/getCalibratedUnits'; import { roundNumber } from '../../utilities'; import throttle from '../../utilities/throttle'; import { @@ -56,10 +53,7 @@ import { EllipticalROIAnnotation } from '../../types/ToolSpecificAnnotationTypes import triggerAnnotationRenderForViewportIds from '../../utilities/triggerAnnotationRenderForViewportIds'; import { pointInShapeCallback } from '../../utilities/'; import { StyleSpecifier } from '../../types/AnnotationStyle'; -import { - ModalityUnitOptions, - getModalityUnit, -} from '../../utilities/getModalityUnit'; +import { getModalityUnit } from '../../utilities/getModalityUnit'; import { isViewportPreScaled } from '../../utilities/viewport/isViewportPreScaled'; import { BasicStatsCalculator } from '../../utilities/math/basic'; @@ -791,12 +785,7 @@ class EllipticalROITool extends AnnotationTool { areaUnit: null, }; - this._calculateCachedStats( - annotation, - viewport, - renderingEngine, - enabledElement - ); + this._calculateCachedStats(annotation, viewport, renderingEngine); } else if (annotation.invalidated) { this._throttledCalculateCachedStats( annotation, @@ -972,12 +961,7 @@ class EllipticalROITool extends AnnotationTool { return renderStatus; }; - _calculateCachedStats = ( - annotation, - viewport, - renderingEngine, - enabledElement - ) => { + _calculateCachedStats = (annotation, viewport, renderingEngine) => { const data = annotation.data; const { element } = viewport; @@ -1010,34 +994,34 @@ class EllipticalROITool extends AnnotationTool { continue; } - const { dimensions, imageData, metadata, hasPixelSpacing } = image; + const { dimensions, imageData, metadata } = image; - const worldPos1Index = transformWorldToIndex(imageData, worldPos1); + const pos1Index = transformWorldToIndex(imageData, worldPos1); - worldPos1Index[0] = Math.floor(worldPos1Index[0]); - worldPos1Index[1] = Math.floor(worldPos1Index[1]); - worldPos1Index[2] = Math.floor(worldPos1Index[2]); + pos1Index[0] = Math.floor(pos1Index[0]); + pos1Index[1] = Math.floor(pos1Index[1]); + pos1Index[2] = Math.floor(pos1Index[2]); - const worldPos2Index = transformWorldToIndex(imageData, worldPos2); + const post2Index = transformWorldToIndex(imageData, worldPos2); - worldPos2Index[0] = Math.floor(worldPos2Index[0]); - worldPos2Index[1] = Math.floor(worldPos2Index[1]); - worldPos2Index[2] = Math.floor(worldPos2Index[2]); + post2Index[0] = Math.floor(post2Index[0]); + post2Index[1] = Math.floor(post2Index[1]); + post2Index[2] = Math.floor(post2Index[2]); // Check if one of the indexes are inside the volume, this then gives us // Some area to do stats over. - if (this._isInsideVolume(worldPos1Index, worldPos2Index, dimensions)) { + if (this._isInsideVolume(pos1Index, post2Index, dimensions)) { this.isHandleOutsideImage = false; - const iMin = Math.min(worldPos1Index[0], worldPos2Index[0]); - const iMax = Math.max(worldPos1Index[0], worldPos2Index[0]); + const iMin = Math.min(pos1Index[0], post2Index[0]); + const iMax = Math.max(pos1Index[0], post2Index[0]); - const jMin = Math.min(worldPos1Index[1], worldPos2Index[1]); - const jMax = Math.max(worldPos1Index[1], worldPos2Index[1]); + const jMin = Math.min(pos1Index[1], post2Index[1]); + const jMax = Math.max(pos1Index[1], post2Index[1]); - const kMin = Math.min(worldPos1Index[2], worldPos2Index[2]); - const kMax = Math.max(worldPos1Index[2], worldPos2Index[2]); + const kMin = Math.min(pos1Index[2], post2Index[2]); + const kMax = Math.max(pos1Index[2], post2Index[2]); const boundsIJK = [ [iMin, iMax], @@ -1065,7 +1049,13 @@ class EllipticalROITool extends AnnotationTool { worldPos2 ); const isEmptyArea = worldWidth === 0 && worldHeight === 0; - const scale = getCalibratedScale(image); + + const handles = [pos1Index, post2Index]; + const { scale, areaUnits } = getCalibratedLengthUnitsAndScale( + image, + handles + ); + const area = Math.abs(Math.PI * (worldWidth / 2) * (worldHeight / 2)) / scale / @@ -1095,7 +1085,6 @@ class EllipticalROITool extends AnnotationTool { ); const stats = this.configuration.statsCalculator.getStatistics(); - cachedStats[targetId] = { Modality: metadata.Modality, area, @@ -1105,7 +1094,7 @@ class EllipticalROITool extends AnnotationTool { statsArray: stats.array, pointsInShape, isEmptyArea, - areaUnit: getCalibratedAreaUnits(null, image), + areaUnit: areaUnits, modalityUnit, }; } else { diff --git a/packages/tools/src/tools/annotation/RectangleROITool.ts b/packages/tools/src/tools/annotation/RectangleROITool.ts index a74874039c..7c2c8e62c1 100644 --- a/packages/tools/src/tools/annotation/RectangleROITool.ts +++ b/packages/tools/src/tools/annotation/RectangleROITool.ts @@ -7,10 +7,7 @@ import { } from '@cornerstonejs/core'; import type { Types } from '@cornerstonejs/core'; -import { - getCalibratedAreaUnits, - getCalibratedScale, -} from '../../utilities/getCalibratedUnits'; +import { getCalibratedLengthUnitsAndScale } from '../../utilities/getCalibratedUnits'; import { roundNumber } from '../../utilities'; import throttle from '../../utilities/throttle'; import { @@ -878,37 +875,35 @@ class RectangleROITool extends AnnotationTool { } const { dimensions, imageData, metadata } = image; - const scalarData = - 'getScalarData' in image ? image.getScalarData() : image.scalarData; - const worldPos1Index = transformWorldToIndex(imageData, worldPos1); + const pos1Index = transformWorldToIndex(imageData, worldPos1); - worldPos1Index[0] = Math.floor(worldPos1Index[0]); - worldPos1Index[1] = Math.floor(worldPos1Index[1]); - worldPos1Index[2] = Math.floor(worldPos1Index[2]); + pos1Index[0] = Math.floor(pos1Index[0]); + pos1Index[1] = Math.floor(pos1Index[1]); + pos1Index[2] = Math.floor(pos1Index[2]); - const worldPos2Index = transformWorldToIndex(imageData, worldPos2); + const pos2Index = transformWorldToIndex(imageData, worldPos2); - worldPos2Index[0] = Math.floor(worldPos2Index[0]); - worldPos2Index[1] = Math.floor(worldPos2Index[1]); - worldPos2Index[2] = Math.floor(worldPos2Index[2]); + pos2Index[0] = Math.floor(pos2Index[0]); + pos2Index[1] = Math.floor(pos2Index[1]); + pos2Index[2] = Math.floor(pos2Index[2]); // Check if one of the indexes are inside the volume, this then gives us // Some area to do stats over. - if (this._isInsideVolume(worldPos1Index, worldPos2Index, dimensions)) { + if (this._isInsideVolume(pos1Index, pos2Index, dimensions)) { this.isHandleOutsideImage = false; // Calculate index bounds to iterate over - const iMin = Math.min(worldPos1Index[0], worldPos2Index[0]); - const iMax = Math.max(worldPos1Index[0], worldPos2Index[0]); + const iMin = Math.min(pos1Index[0], pos2Index[0]); + const iMax = Math.max(pos1Index[0], pos2Index[0]); - const jMin = Math.min(worldPos1Index[1], worldPos2Index[1]); - const jMax = Math.max(worldPos1Index[1], worldPos2Index[1]); + const jMin = Math.min(pos1Index[1], pos2Index[1]); + const jMax = Math.max(pos1Index[1], pos2Index[1]); - const kMin = Math.min(worldPos1Index[2], worldPos2Index[2]); - const kMax = Math.max(worldPos1Index[2], worldPos2Index[2]); + const kMin = Math.min(pos1Index[2], pos2Index[2]); + const kMax = Math.max(pos1Index[2], pos2Index[2]); const boundsIJK = [ [iMin, iMax], @@ -922,7 +917,12 @@ class RectangleROITool extends AnnotationTool { worldPos1, worldPos2 ); - const scale = getCalibratedScale(image); + + const handles = [pos1Index, pos2Index]; + const { scale, areaUnits } = getCalibratedLengthUnitsAndScale( + image, + handles + ); const area = Math.abs(worldWidth * worldHeight) / (scale * scale); @@ -959,7 +959,7 @@ class RectangleROITool extends AnnotationTool { max: stats.max?.value, statsArray: stats.array, pointsInShape: pointsInShape, - areaUnit: getCalibratedAreaUnits(null, image), + areaUnit: areaUnits, modalityUnit, }; } else { diff --git a/packages/tools/src/utilities/getCalibratedUnits.ts b/packages/tools/src/utilities/getCalibratedUnits.ts index 0ca5842a44..305a6109e1 100644 --- a/packages/tools/src/utilities/getCalibratedUnits.ts +++ b/packages/tools/src/utilities/getCalibratedUnits.ts @@ -21,72 +21,7 @@ const UNIT_MAPPING = { }; const EPS = 1e-3; - -/** - * Extracts the length units and the type of calibration for those units - * into the response. The length units will typically be either mm or px - * while the calibration type can be any of a number of different calibration types. - * - * Volumetric images have no calibration type, so are just the raw mm. - * - * TODO: Handle region calibration - * - * @param handles - used to detect if the spacing information is different - * between various points (eg angled ERMF or US Region). - * Currently unused, but needed for correct US Region handling - * @param image - to extract the calibration from - * image.calibration - calibration value to extract units form - * @returns String containing the units and type of calibration - */ -const getCalibratedLengthUnits = (handles, image): string => { - const { calibration, hasPixelSpacing } = image; - // Anachronistic - moving to using calibration consistently, but not completed yet - const units = hasPixelSpacing ? 'mm' : PIXEL_UNITS; - if ( - !calibration || - (!calibration.type && !calibration.sequenceOfUltrasoundRegions) - ) { - return units; - } - if (calibration.type === CalibrationTypes.UNCALIBRATED) { - return PIXEL_UNITS; - } - if (calibration.sequenceOfUltrasoundRegions) { - return 'US Region'; - } - return `${units} ${calibration.type}`; -}; - const SQUARE = '\xb2'; -/** - * Extracts the area units, including the squared sign plus calibration type. - */ -const getCalibratedAreaUnits = (handles, image): string => { - const { calibration, hasPixelSpacing } = image; - const units = (hasPixelSpacing ? 'mm' : PIXEL_UNITS) + SQUARE; - if (!calibration || !calibration.type) { - return units; - } - if (calibration.sequenceOfUltrasoundRegions) { - return 'US Region'; - } - return `${units} ${calibration.type}`; -}; - -/** - * Gets the scale divisor for converting from internal spacing to - * image spacing for calibrated images. - */ -const getCalibratedScale = (image, handles = []) => { - if (image.calibration?.sequenceOfUltrasoundRegions) { - // image.spacing / image.us.space - } else if (image.calibration?.scale) { - return image.calibration.scale; - } else { - return 1; - } -}; - /** * Extracts the calibrated length units, area units, and the scale * for converting from internal spacing to image spacing. @@ -96,10 +31,9 @@ const getCalibratedScale = (image, handles = []) => { * @returns Object containing the units, area units, and scale */ const getCalibratedLengthUnitsAndScale = (image, handles) => { - const [imageIndex1, imageIndex2] = handles; const { calibration, hasPixelSpacing } = image; let units = hasPixelSpacing ? 'mm' : PIXEL_UNITS; - const areaUnits = units + SQUARE; + let areaUnits = units + SQUARE; let scale = 1; let calibrationType = ''; @@ -115,6 +49,15 @@ const getCalibratedLengthUnitsAndScale = (image, handles) => { } if (calibration.sequenceOfUltrasoundRegions) { + let imageIndex1, imageIndex2; + if (Array.isArray(handles) && handles.length === 2) { + [imageIndex1, imageIndex2] = handles; + } else if (typeof handles === 'function') { + const points = handles(); + imageIndex1 = points[0]; + imageIndex2 = points[1]; + } + let regions = calibration.sequenceOfUltrasoundRegions.filter( (region) => imageIndex1[0] >= region.regionLocationMinX0 && @@ -165,17 +108,23 @@ const getCalibratedLengthUnitsAndScale = (image, handles) => { ); if (isSamePhysicalDelta) { - scale = 1 / (physicalDeltaX * physicalDeltaY * 100); + // 1 to 1 aspect ratio, we use just one of them + scale = 1 / (physicalDeltaX * 10); calibrationType = 'US Region'; units = 'mm'; + areaUnits = 'mm' + SQUARE; } else { + // here we are showing at the aspect ratio of the physical delta + // if they are not the same, then we should show px, but the correct solution + // is to grab each point separately and scale them individually + // Todo: implement this return { units: PIXEL_UNITS, areaUnits: PIXEL_UNITS + SQUARE, scale }; } } else if (calibration.scale) { scale = calibration.scale; } - // everything except REGION/Uncalibratted + // everything except REGION/Uncalibrated const types = [ CalibrationTypes.ERMF, CalibrationTypes.USER, @@ -272,13 +221,8 @@ const getCalibratedProbeUnitsAndValue = (image, handles) => { */ const getCalibratedAspect = (image) => image.calibration?.aspect || 1; -export default getCalibratedLengthUnits; - export { - getCalibratedAreaUnits, - getCalibratedLengthUnits, getCalibratedLengthUnitsAndScale, - getCalibratedScale, getCalibratedAspect, getCalibratedProbeUnitsAndValue, }; From 95c74dbe1c43e76fd5a93b07887c47c6a9f4a773 Mon Sep 17 00:00:00 2001 From: Alireza Date: Wed, 17 Apr 2024 11:16:26 -0400 Subject: [PATCH 06/16] Refactor PlanarFreehandROITool to use getCalibratedLengthUnitsAndScale in PlanarFreehandROITool.ts --- .../tools/annotation/PlanarFreehandROITool.ts | 67 ++++++++++++++++--- 1 file changed, 56 insertions(+), 11 deletions(-) diff --git a/packages/tools/src/tools/annotation/PlanarFreehandROITool.ts b/packages/tools/src/tools/annotation/PlanarFreehandROITool.ts index 4fbe322299..4ed78a42da 100644 --- a/packages/tools/src/tools/annotation/PlanarFreehandROITool.ts +++ b/packages/tools/src/tools/annotation/PlanarFreehandROITool.ts @@ -7,11 +7,8 @@ import { import type { Types } from '@cornerstonejs/core'; import { vec3 } from 'gl-matrix'; -import { - getCalibratedAreaUnits, - getCalibratedScale, -} from '../../utilities/getCalibratedUnits'; -import { roundNumber } from '../../utilities'; +import { getCalibratedLengthUnitsAndScale } from '../../utilities/getCalibratedUnits'; +import { math, roundNumber } from '../../utilities'; import { polyline } from '../../utilities/math'; import { filterAnnotationsForDisplay } from '../../utilities/planar'; import throttle from '../../utilities/throttle'; @@ -727,11 +724,6 @@ class PlanarFreehandROITool extends ContourSegmentationBaseTool { const deltaInX = vec3.distance(originalWorldPoint, deltaXPoint); const deltaInY = vec3.distance(originalWorldPoint, deltaYPoint); - const scale = getCalibratedScale(image); - let area = polyline.getArea(canvasCoordinates) / scale / scale; - // Convert from canvas_pixels ^2 to mm^2 - area *= deltaInX * deltaInY; - const worldPosIndex = csUtils.transformWorldToIndex(imageData, points[0]); worldPosIndex[0] = Math.floor(worldPosIndex[0]); worldPosIndex[1] = Math.floor(worldPosIndex[1]); @@ -764,6 +756,59 @@ class PlanarFreehandROITool extends ContourSegmentationBaseTool { kMax = Math.max(kMax, worldPosIndex[2]); } + const worldPosIndex2 = csUtils.transformWorldToIndex( + imageData, + points[1] + ); + worldPosIndex2[0] = Math.floor(worldPosIndex2[0]); + worldPosIndex2[1] = Math.floor(worldPosIndex2[1]); + worldPosIndex2[2] = Math.floor(worldPosIndex2[2]); + + const { scale, areaUnits } = getCalibratedLengthUnitsAndScale( + image, + () => { + const polyline = data.contour.polyline; + const numPoints = polyline.length; + const projectedPolyline = new Array(numPoints); + + for (let i = 0; i < numPoints; i++) { + projectedPolyline[i] = viewport.worldToCanvas(polyline[i]); + } + + const { + maxX: canvasMaxX, + maxY: canvasMaxY, + minX: canvasMinX, + minY: canvasMinY, + } = math.polyline.getAABB(projectedPolyline); + + const topLeftBBWorld = viewport.canvasToWorld([ + canvasMinX, + canvasMinY, + ]); + + const topLeftBBIndex = csUtils.transformWorldToIndex( + imageData, + topLeftBBWorld + ); + + const bottomRightBBWorld = viewport.canvasToWorld([ + canvasMaxX, + canvasMaxY, + ]); + + const bottomRightBBIndex = csUtils.transformWorldToIndex( + imageData, + bottomRightBBWorld + ); + + return [topLeftBBIndex, bottomRightBBIndex]; + } + ); + let area = polyline.getArea(canvasCoordinates) / scale / scale; + // Convert from canvas_pixels ^2 to mm^2 + area *= deltaInX * deltaInY; + // Expand bounding box const iDelta = 0.01 * (iMax - iMin); const jDelta = 0.01 * (jMax - jMin); @@ -852,7 +897,7 @@ class PlanarFreehandROITool extends ContourSegmentationBaseTool { stdDev: stats.stdDev?.value, statsArray: stats.array, pointsInShape: pointsInShape, - areaUnit: getCalibratedAreaUnits(null, image), + areaUnit: areaUnits, modalityUnit, }; } From 7368a9510f07c575bfe56747903565c676e81a2d Mon Sep 17 00:00:00 2001 From: Alireza Date: Wed, 17 Apr 2024 11:23:25 -0400 Subject: [PATCH 07/16] Refactor SplineROITool to use getCalibratedLengthUnitsAndScale in SplineROITool.ts --- .../src/tools/annotation/SplineROITool.ts | 40 +++++++++++++++++-- 1 file changed, 36 insertions(+), 4 deletions(-) diff --git a/packages/tools/src/tools/annotation/SplineROITool.ts b/packages/tools/src/tools/annotation/SplineROITool.ts index c9c7d7983f..57dfbb33f4 100644 --- a/packages/tools/src/tools/annotation/SplineROITool.ts +++ b/packages/tools/src/tools/annotation/SplineROITool.ts @@ -37,8 +37,7 @@ import { throttle, roundNumber, triggerAnnotationRenderForViewportIds, - getCalibratedScale, - getCalibratedAreaUnits, + getCalibratedLengthUnitsAndScale, } from '../../utilities'; import getMouseModifierKey from '../../eventDispatchers/shared/getMouseModifier'; import { getViewportIdsWithToolToRender } from '../../utilities/viewportFilters'; @@ -1163,7 +1162,40 @@ class SplineROITool extends ContourSegmentationBaseTool { const deltaInX = vec3.distance(originalWorldPoint, deltaXPoint); const deltaInY = vec3.distance(originalWorldPoint, deltaYPoint); - const scale = getCalibratedScale(image); + const { imageData } = image; + const { scale, areaUnits } = getCalibratedLengthUnitsAndScale( + image, + () => { + const { + maxX: canvasMaxX, + maxY: canvasMaxY, + minX: canvasMinX, + minY: canvasMinY, + } = math.polyline.getAABB(canvasCoordinates); + + const topLeftBBWorld = viewport.canvasToWorld([ + canvasMinX, + canvasMinY, + ]); + + const topLeftBBIndex = utilities.transformWorldToIndex( + imageData, + topLeftBBWorld + ); + + const bottomRightBBWorld = viewport.canvasToWorld([ + canvasMaxX, + canvasMaxY, + ]); + + const bottomRightBBIndex = utilities.transformWorldToIndex( + imageData, + bottomRightBBWorld + ); + + return [topLeftBBIndex, bottomRightBBIndex]; + } + ); let area = math.polyline.getArea(canvasCoordinates) / scale / scale; // Convert from canvas_pixels ^2 to mm^2 @@ -1172,7 +1204,7 @@ class SplineROITool extends ContourSegmentationBaseTool { cachedStats[targetId] = { Modality: metadata.Modality, area, - areaUnit: getCalibratedAreaUnits(null, image), + areaUnit: areaUnits, }; } From 5b6625f2a92e36eaa5980627c828faf396a158bb Mon Sep 17 00:00:00 2001 From: Alireza Date: Wed, 17 Apr 2024 11:28:12 -0400 Subject: [PATCH 08/16] api --- common/reviews/api/tools.api.md | 34 ++++++++++++++++++++++----------- 1 file changed, 23 insertions(+), 11 deletions(-) diff --git a/common/reviews/api/tools.api.md b/common/reviews/api/tools.api.md index b57b68e7ed..0109125d8b 100644 --- a/common/reviews/api/tools.api.md +++ b/common/reviews/api/tools.api.md @@ -2091,7 +2091,7 @@ export class EllipticalROITool extends AnnotationTool { // (undocumented) addNewAnnotation: (evt: EventTypes_2.InteractionEventType) => EllipticalROIAnnotation; // (undocumented) - _calculateCachedStats: (annotation: any, viewport: any, renderingEngine: any, enabledElement: any) => any; + _calculateCachedStats: (annotation: any, viewport: any, renderingEngine: any) => any; // (undocumented) cancel: (element: HTMLDivElement) => any; // (undocumented) @@ -2241,6 +2241,10 @@ enum Events { // (undocumented) TOOL_MODE_CHANGED = "CORNERSTONE_TOOLS_TOOL_MODE_CHANGED", // (undocumented) + TOOLGROUP_VIEWPORT_ADDED = "CORNERSTONE_TOOLS_TOOLGROUP_VIEWPORT_ADDED", + // (undocumented) + TOOLGROUP_VIEWPORT_REMOVED = "CORNERSTONE_TOOLS_TOOLGROUP_VIEWPORT_REMOVED", + // (undocumented) TOUCH_DRAG = "CORNERSTONE_TOOLS_TOUCH_DRAG", // (undocumented) TOUCH_END = "CORNERSTONE_TOOLS_TOUCH_END", @@ -2519,13 +2523,25 @@ function getBrushThresholdForToolGroup(toolGroupId: string): any; function getBrushToolInstances(toolGroupId: string, toolName?: string): any[]; // @public (undocumented) -const getCalibratedAreaUnits: (handles: any, image: any) => string; +const getCalibratedAspect: (image: any) => any; // @public (undocumented) -const getCalibratedLengthUnits: (handles: any, image: any) => string; +const getCalibratedLengthUnitsAndScale: (image: any, handles: any) => { + units: string; + areaUnits: string; + scale: number; +}; // @public (undocumented) -const getCalibratedScale: (image: any, handles?: any[]) => any; +const getCalibratedProbeUnitsAndValue: (image: any, handles: any) => { + units: string[]; + values: any[]; + calibrationType?: undefined; +} | { + units: string[]; + values: any[]; + calibrationType: string; +}; // @public (undocumented) function getCanvasEllipseCorners(ellipseCanvasPoints: CanvasCoordinates): Array; @@ -3683,8 +3699,6 @@ export class OrientationMarkerTool extends BaseTool { // (undocumented) static AXIS: number; // (undocumented) - configuration_invalidated: boolean; - // (undocumented) createAnnotatedCubeActor(): Promise; // (undocumented) static CUBE: number; @@ -3707,8 +3721,6 @@ export class OrientationMarkerTool extends BaseTool { // (undocumented) polyDataURL: any; // (undocumented) - reset: () => void; - // (undocumented) resize: (viewportId: any) => void; // (undocumented) _resizeObservers: Map; @@ -5983,9 +5995,9 @@ declare namespace utilities { touch, triggerEvent, calibrateImageSpacing, - getCalibratedLengthUnits, - getCalibratedAreaUnits, - getCalibratedScale, + getCalibratedLengthUnitsAndScale, + getCalibratedProbeUnitsAndValue, + getCalibratedAspect, segmentation_2 as segmentation, contours, triggerAnnotationRenderForViewportIds, From 0694422e85a154226091f1a2dec5a83a162f3144 Mon Sep 17 00:00:00 2001 From: Alireza Date: Wed, 17 Apr 2024 11:43:49 -0400 Subject: [PATCH 09/16] Refactor getCalibratedUnits functions to use more descriptive names in index.ts --- packages/tools/src/utilities/index.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/tools/src/utilities/index.ts b/packages/tools/src/utilities/index.ts index 6f1721046e..8cb92bd706 100644 --- a/packages/tools/src/utilities/index.ts +++ b/packages/tools/src/utilities/index.ts @@ -12,9 +12,9 @@ import isObject from './isObject'; import clip from './clip'; import calibrateImageSpacing from './calibrateImageSpacing'; import { - getCalibratedLengthUnits, - getCalibratedAreaUnits, - getCalibratedScale, + getCalibratedLengthUnitsAndScale, + getCalibratedProbeUnitsAndValue, + getCalibratedAspect, } from './getCalibratedUnits'; import triggerAnnotationRenderForViewportIds from './triggerAnnotationRenderForViewportIds'; import triggerAnnotationRenderForToolGroupIds from './triggerAnnotationRenderForToolGroupIds'; @@ -67,9 +67,9 @@ export { touch, triggerEvent, calibrateImageSpacing, - getCalibratedLengthUnits, - getCalibratedAreaUnits, - getCalibratedScale, + getCalibratedLengthUnitsAndScale, + getCalibratedProbeUnitsAndValue, + getCalibratedAspect, segmentation, contours, triggerAnnotationRenderForViewportIds, From d6acd9bc5e547b4fb6853e7ccfacb0c3498f76ee Mon Sep 17 00:00:00 2001 From: Alireza Date: Wed, 17 Apr 2024 16:13:35 -0400 Subject: [PATCH 10/16] fix loading on ios --- .../core/src/RenderingEngine/StackViewport.ts | 8 ++- .../helpers/setDefaultVolumeVOI.ts | 3 + packages/core/src/init.ts | 67 ++++++++++++++----- .../core/src/types/Cornerstone3DConfig.ts | 5 ++ .../core/src/utilities/loadImageToCanvas.ts | 10 ++- .../src/imageLoader/createImage.ts | 10 +++ .../src/shared/decodeImageFrame.ts | 31 +++++++-- .../src/types/DICOMLoaderImageOptions.ts | 1 + .../src/BaseStreamingImageVolume.ts | 17 ++++- .../stackPrefetch/stackContextPrefetch.ts | 10 ++- .../utilities/stackPrefetch/stackPrefetch.ts | 10 ++- 11 files changed, 143 insertions(+), 29 deletions(-) diff --git a/packages/core/src/RenderingEngine/StackViewport.ts b/packages/core/src/RenderingEngine/StackViewport.ts index f850b33558..9b6af6b467 100644 --- a/packages/core/src/RenderingEngine/StackViewport.ts +++ b/packages/core/src/RenderingEngine/StackViewport.ts @@ -76,7 +76,11 @@ import pixelToCanvas from './helpers/cpuFallback/rendering/pixelToCanvas'; import resize from './helpers/cpuFallback/rendering/resize'; import cache from '../cache'; -import { getConfiguration, getShouldUseCPURendering } from '../init'; +import { + canRenderFloatTextures, + getConfiguration, + getShouldUseCPURendering, +} from '../init'; import { createProgressive } from '../loaders/ProgressiveRetrieveImages'; import { ImagePixelModule, @@ -2064,6 +2068,8 @@ class StackViewport extends Viewport implements IStackViewport, IImagesLoader { }, useRGBA: false, transferSyntaxUID, + useNativeDataType: this.useNativeDataType, + allowFloatRendering: canRenderFloatTextures(), priority: 5, requestType: RequestType.Interaction, additionalDetails, diff --git a/packages/core/src/RenderingEngine/helpers/setDefaultVolumeVOI.ts b/packages/core/src/RenderingEngine/helpers/setDefaultVolumeVOI.ts index 73095e7a12..41e046c1dc 100644 --- a/packages/core/src/RenderingEngine/helpers/setDefaultVolumeVOI.ts +++ b/packages/core/src/RenderingEngine/helpers/setDefaultVolumeVOI.ts @@ -9,6 +9,7 @@ import * as metaData from '../../metaData'; import { getMinMax, windowLevel } from '../../utilities'; import { RequestType } from '../../enums'; import cache from '../../cache'; +import { canRenderFloatTextures } from '../../init'; const PRIORITY = 0; const REQUEST_TYPE = RequestType.Prefetch; @@ -166,6 +167,8 @@ async function getVOIFromMinMax( }, priority: PRIORITY, requestType: REQUEST_TYPE, + useNativeDataType, + allowFloatRendering: canRenderFloatTextures(), preScale: { enabled: true, scalingParameters: scalingParametersToUse, diff --git a/packages/core/src/init.ts b/packages/core/src/init.ts index 2bf2229b80..9bce0867ef 100644 --- a/packages/core/src/init.ts +++ b/packages/core/src/init.ts @@ -13,11 +13,12 @@ import CentralizedWebWorkerManager from './webWorkerManager/webWorkerManager'; const defaultConfig: Cornerstone3DConfig = { gpuTier: undefined, detectGPUConfig: {}, + isMobile: false, // is mobile device rendering: { useCPURendering: false, // GPU rendering options preferSizeOverAccuracy: false, - useNorm16Texture: false, // _hasNorm16TextureSupport(), + useNorm16Texture: false, strictZSpacingForVolumeViewport: true, }, // cache @@ -27,11 +28,12 @@ const defaultConfig: Cornerstone3DConfig = { let config: Cornerstone3DConfig = { gpuTier: undefined, detectGPUConfig: {}, + isMobile: false, // is mobile device rendering: { useCPURendering: false, // GPU rendering options preferSizeOverAccuracy: false, - useNorm16Texture: false, // _hasNorm16TextureSupport(), + useNorm16Texture: false, strictZSpacingForVolumeViewport: true, }, // cache @@ -77,23 +79,32 @@ function hasSharedArrayBuffer() { } } -// Todo: commenting this out until proper support for int16 textures -// are added to browsers, current implementation is buggy -// function _hasNorm16TextureSupport() { -// const gl = _getGLContext(); +function _hasNorm16TextureSupport() { + const gl = _getGLContext(); + + if (gl) { + const ext = (gl as WebGL2RenderingContext).getExtension( + 'EXT_texture_norm16' + ); + + if (ext) { + return true; + } + } -// if (gl) { -// const ext = (gl as WebGL2RenderingContext).getExtension( -// 'EXT_texture_norm16' -// ); + return false; +} -// if (ext) { -// return true; -// } -// } +function isMobile() { + const ua = navigator.userAgent; + return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test( + ua + ); +} -// return false; -// } +function isMobileIOS() { + return /iPhone|iPod/.test(navigator.platform); +} /** * Initialize the cornerstone-core. If the browser has a webgl context and @@ -112,6 +123,20 @@ async function init(configuration = config): Promise { // merge configs config = deepMerge(defaultConfig, configuration); + config.isMobile = isMobile(); + + if (config.isMobile) { + // iOS devices don't have support for OES_texture_float_linear + // and thus we should use native data type if we are on iOS + if (isMobileIOS()) { + config.rendering.useNorm16Texture = _hasNorm16TextureSupport(); + + if (!config.rendering.useNorm16Texture) { + config.rendering.preferSizeOverAccuracy = true; + } + } + } + // gpuTier const hasWebGLContext = _hasActiveWebGLContext(); if (!hasWebGLContext) { @@ -165,6 +190,15 @@ function setPreferSizeOverAccuracy(status: boolean): void { _updateRenderingPipelinesForAllViewports(); } +/** + * Only IPhone IOS cannot render float textures right now due to the lack of support for OES_texture_float_linear. + * So we should not use float textures on IOS devices. + */ +function canRenderFloatTextures(): boolean { + const isMobile = config.isMobile; + return !isMobileIOS() && !isMobile; +} + /** * Resets the cornerstone-core init state if it has been manually * initialized to force use the cpu rendering (e.g., for tests) @@ -286,4 +320,5 @@ export { getConfiguration, setConfiguration, getWebWorkerManager, + canRenderFloatTextures, }; diff --git a/packages/core/src/types/Cornerstone3DConfig.ts b/packages/core/src/types/Cornerstone3DConfig.ts index b43c0988e0..a38671e98c 100644 --- a/packages/core/src/types/Cornerstone3DConfig.ts +++ b/packages/core/src/types/Cornerstone3DConfig.ts @@ -8,6 +8,11 @@ type Cornerstone3DConfig = { * https://github.com/pmndrs/detect-gpu/blob/master/src/index.ts#L82 */ gpuTier?: TierResult; + + /** + * Whether the device is mobile or not. + */ + isMobile: boolean; /** * When the `gpuTier` is not provided, the `detectGPUConfig` is passed as * an argument to the `getGPUTier` method. diff --git a/packages/core/src/utilities/loadImageToCanvas.ts b/packages/core/src/utilities/loadImageToCanvas.ts index 360fd41ff5..755f972fa3 100644 --- a/packages/core/src/utilities/loadImageToCanvas.ts +++ b/packages/core/src/utilities/loadImageToCanvas.ts @@ -6,7 +6,7 @@ import { RequestType } from '../enums'; import imageLoadPoolManager from '../requestPool/imageLoadPoolManager'; import renderToCanvasGPU from './renderToCanvasGPU'; import renderToCanvasCPU from './renderToCanvasCPU'; -import { getConfiguration } from '../init'; +import { canRenderFloatTextures, getConfiguration } from '../init'; export interface LoadImageOptions { canvas: HTMLCanvasElement; @@ -113,17 +113,21 @@ export default function loadImageToCanvas( ); } - const { useNorm16Texture } = getConfiguration().rendering; + const { useNorm16Texture, preferSizeOverAccuracy } = + getConfiguration().rendering; + const useNativeDataType = useNorm16Texture || preferSizeOverAccuracy; // IMPORTANT: Request type should be passed if not the 'interaction' // highest priority will be used for the request type in the imageRetrievalPool const options = { targetBuffer: { - type: useNorm16Texture ? undefined : 'Float32Array', + type: useNativeDataType ? undefined : 'Float32Array', }, preScale: { enabled: true, }, + useNativeDataType, + allowFloatRendering: canRenderFloatTextures(), useRGBA: !!useCPURendering, requestType, }; diff --git a/packages/dicomImageLoader/src/imageLoader/createImage.ts b/packages/dicomImageLoader/src/imageLoader/createImage.ts index 2b8bd3fba4..ba36bf606b 100644 --- a/packages/dicomImageLoader/src/imageLoader/createImage.ts +++ b/packages/dicomImageLoader/src/imageLoader/createImage.ts @@ -144,6 +144,16 @@ function createImage( options.targetBuffer.arrayBuffer instanceof SharedArrayBuffer; const { decodeConfig } = getOptions(); + + // check if the options to use the 16 bit data type is set + // on the image load options, and prefer that over the global + // options of the dicom loader + decodeConfig.use16BitDataType = + (options && options.targetBuffer?.type === 'Uint16Array') || + options.targetBuffer?.type === 'Int16Array' + ? true + : options.useNativeDataType || decodeConfig.use16BitDataType; + const decodePromise = decodeImageFrame( imageFrame, transferSyntax, diff --git a/packages/dicomImageLoader/src/shared/decodeImageFrame.ts b/packages/dicomImageLoader/src/shared/decodeImageFrame.ts index 9e23db9cf5..88dc4238fd 100644 --- a/packages/dicomImageLoader/src/shared/decodeImageFrame.ts +++ b/packages/dicomImageLoader/src/shared/decodeImageFrame.ts @@ -219,6 +219,20 @@ function postProcessDecodedPixels( isColorImage(imageFrame.photometricInterpretation) && options.targetBuffer?.offset === undefined; + const canRenderFloat = + typeof options.allowFloatRendering !== 'undefined' + ? options.allowFloatRendering + : true; + + const willScale = options.preScale?.enabled; + + const areThereAnyNonIntegerScalingParameter = + willScale && + Object.values(options.preScale.scalingParameters).some( + (v) => typeof v === 'number' && !Number.isInteger(v) + ); + const disableScale = !canRenderFloat && areThereAnyNonIntegerScalingParameter; + if (type && !invalidColorType) { pixelDataArray = _handleTargetBuffer( options, @@ -226,7 +240,7 @@ function postProcessDecodedPixels( typedArrayConstructors, pixelDataArray ); - } else if (options.preScale.enabled) { + } else if (options.preScale.enabled && !disableScale) { pixelDataArray = _handlePreScaleSetup( options, minBeforeScale, @@ -244,11 +258,11 @@ function postProcessDecodedPixels( let minAfterScale = minBeforeScale; let maxAfterScale = maxBeforeScale; - if (options.preScale.enabled) { + if (options.preScale.enabled && !disableScale) { const scalingParameters = options.preScale.scalingParameters; _validateScalingParameters(scalingParameters); - const { rescaleSlope, rescaleIntercept, suvbw } = scalingParameters; + const { rescaleSlope, rescaleIntercept } = scalingParameters; const isSlopeAndInterceptNumbers = typeof rescaleSlope === 'number' && typeof rescaleIntercept === 'number'; @@ -269,6 +283,13 @@ function postProcessDecodedPixels( maxAfterScale = maxAfterScale * suvbw; } } + } else if (disableScale) { + imageFrame.preScale = { + scaled: false, + }; + + minAfterScale = minBeforeScale; + maxAfterScale = maxBeforeScale; } // assign the array buffer to the pixelData only if it is not a SharedArrayBuffer @@ -311,7 +332,9 @@ function _handleTargetBuffer( const TypedArrayConstructor = typedArrayConstructors[type]; if (!TypedArrayConstructor) { - throw new Error(`target array ${type} is not supported`); + throw new Error( + `target array ${type} is not supported, you need to set use16BitDataType to true if you want to use Uint16Array or Int16Array.` + ); } if (rows && rows != imageFrame.rows) { diff --git a/packages/dicomImageLoader/src/types/DICOMLoaderImageOptions.ts b/packages/dicomImageLoader/src/types/DICOMLoaderImageOptions.ts index 60ced8d3c2..21fcd7f7df 100644 --- a/packages/dicomImageLoader/src/types/DICOMLoaderImageOptions.ts +++ b/packages/dicomImageLoader/src/types/DICOMLoaderImageOptions.ts @@ -3,6 +3,7 @@ import { LoadRequestFunction } from './LoadRequestFunction'; export interface DICOMLoaderImageOptions { useRGBA?: boolean; + useNativeDataType?: boolean; skipCreateImage?: boolean; preScale?: { enabled: boolean; diff --git a/packages/streaming-image-volume-loader/src/BaseStreamingImageVolume.ts b/packages/streaming-image-volume-loader/src/BaseStreamingImageVolume.ts index 38366232c0..3c7f8ad73e 100644 --- a/packages/streaming-image-volume-loader/src/BaseStreamingImageVolume.ts +++ b/packages/streaming-image-volume-loader/src/BaseStreamingImageVolume.ts @@ -10,6 +10,7 @@ import { utilities as csUtils, utilities, ProgressiveRetrieveImages, + canRenderFloatTextures, } from '@cornerstonejs/core'; import type { Types, @@ -397,6 +398,12 @@ export default class BaseStreamingImageVolume typeof scalingParameters.rescaleSlope === 'number' && typeof scalingParameters.rescaleIntercept === 'number'; + const areThereAnyNonIntegerScalingParameter = Object.values( + scalingParameters + ).some((value) => typeof value === 'number' && !Number.isInteger(value)); + + const allowFloatRendering = canRenderFloatTextures(); + /** * So this is has limitation right now, but we need to somehow indicate * whether the volume has been scaled with the scaling parameters or not. @@ -410,6 +417,13 @@ export default class BaseStreamingImageVolume * not, which we store it in the this.scaling.PT.suvbw. */ this.isPreScaled = isSlopeAndInterceptNumbers; + + // in case where the hardware/os does not support float rendering but the + // requested scaling params are not integers, we need to disable pre-scaling + if (!allowFloatRendering && areThereAnyNonIntegerScalingParameter) { + this.isPreScaled = false; + } + const frameIndex = this.imageIdIndexToFrameIndex(imageIdIndex); return { @@ -429,8 +443,9 @@ export default class BaseStreamingImageVolume columns, }, skipCreateImage: true, + allowFloatRendering, preScale: { - enabled: true, + enabled: this.isPreScaled, // we need to pass in the scalingParameters here, since the streaming // volume loader doesn't go through the createImage phase in the loader, // and therefore doesn't have the scalingParameters diff --git a/packages/tools/src/utilities/stackPrefetch/stackContextPrefetch.ts b/packages/tools/src/utilities/stackPrefetch/stackContextPrefetch.ts index cc224fa76a..5aa37595da 100644 --- a/packages/tools/src/utilities/stackPrefetch/stackContextPrefetch.ts +++ b/packages/tools/src/utilities/stackPrefetch/stackContextPrefetch.ts @@ -5,6 +5,7 @@ import { imageLoadPoolManager, cache, getConfiguration as getCoreConfiguration, + canRenderFloatTextures, } from '@cornerstonejs/core'; import { addToolState, getToolState } from './state'; import { @@ -217,7 +218,10 @@ function prefetch(element) { .loadAndCacheImage(imageId, options) .then(() => doneCallback(imageId)); - const { useNorm16Texture } = getCoreConfiguration().rendering; + const { useNorm16Texture, preferSizeOverAccuracy } = + getCoreConfiguration().rendering; + + const useNativeDataType = useNorm16Texture || preferSizeOverAccuracy; indicesToRequestCopy.forEach((imageIdIndex) => { const imageId = stack.imageIds[imageIdIndex]; @@ -225,11 +229,13 @@ function prefetch(element) { // highest priority will be used for the request type in the imageRetrievalPool const options = { targetBuffer: { - type: useNorm16Texture ? undefined : 'Float32Array', + type: useNativeDataType ? undefined : 'Float32Array', }, preScale: { enabled: true, }, + useNativeDataType, + allowFloatRendering: canRenderFloatTextures(), requestType, }; diff --git a/packages/tools/src/utilities/stackPrefetch/stackPrefetch.ts b/packages/tools/src/utilities/stackPrefetch/stackPrefetch.ts index f87a3f9195..0a814353d9 100644 --- a/packages/tools/src/utilities/stackPrefetch/stackPrefetch.ts +++ b/packages/tools/src/utilities/stackPrefetch/stackPrefetch.ts @@ -5,6 +5,7 @@ import { imageLoadPoolManager, cache, getConfiguration as getCoreConfiguration, + canRenderFloatTextures, } from '@cornerstonejs/core'; import { addToolState, getToolState } from './state'; import { @@ -166,18 +167,23 @@ function prefetch(element) { const requestFn = (imageId, options) => imageLoader.loadAndCacheImage(imageId, options); - const { useNorm16Texture } = getCoreConfiguration().rendering; + const { useNorm16Texture, preferSizeOverAccuracy } = + getCoreConfiguration().rendering; + + const useNativeDataType = useNorm16Texture || preferSizeOverAccuracy; imageIdsToPrefetch.forEach((imageId) => { // IMPORTANT: Request type should be passed if not the 'interaction' // highest priority will be used for the request type in the imageRetrievalPool const options = { targetBuffer: { - type: useNorm16Texture ? undefined : 'Float32Array', + type: useNativeDataType ? undefined : 'Float32Array', }, preScale: { enabled: true, }, + allowFloatRendering: canRenderFloatTextures(), + useNativeDataType, requestType, }; From a6add52d2cf11e5e300775c6c75fa5b739b2982e Mon Sep 17 00:00:00 2001 From: Alireza Date: Wed, 17 Apr 2024 16:25:25 -0400 Subject: [PATCH 11/16] Add isMobile property to Cornerstone3DConfig --- common/reviews/api/core.api.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/common/reviews/api/core.api.md b/common/reviews/api/core.api.md index 87185347cb..7360ea38a9 100644 --- a/common/reviews/api/core.api.md +++ b/common/reviews/api/core.api.md @@ -377,6 +377,7 @@ function convertVolumeToStackViewport({ viewport, options, }: { // @public (undocumented) type Cornerstone3DConfig = { gpuTier?: TierResult; + isMobile: boolean; detectGPUConfig: GetGPUTier; rendering: { preferSizeOverAccuracy: boolean; @@ -3095,6 +3096,8 @@ export class StackViewport extends Viewport implements IStackViewport, IImagesLo }; useRGBA: boolean; transferSyntaxUID: any; + useNativeDataType: boolean; + allowFloatRendering: boolean; priority: number; requestType: RequestType; additionalDetails: { From 32aa1d5b4518b6b279f02aba9dbb6bd1ac1ef770 Mon Sep 17 00:00:00 2001 From: Alireza Date: Wed, 17 Apr 2024 17:12:32 -0400 Subject: [PATCH 12/16] Refactor code to allow float rendering in DICOMLoaderImageOptions, createImage, stackPrefetch, stackContextPrefetch, and loadImageToCanvas --- .../core/src/RenderingEngine/StackViewport.ts | 7 +------ .../helpers/setDefaultVolumeVOI.ts | 2 -- .../generateVolumePropsFromImageIds.ts | 14 ++++++++----- .../core/src/utilities/hasFloatRescale.ts | 13 ++++++++++++ packages/core/src/utilities/index.ts | 2 ++ .../core/src/utilities/loadImageToCanvas.ts | 3 +-- .../src/imageLoader/createImage.ts | 2 ++ .../src/shared/decodeImageFrame.ts | 20 ++++++++++--------- .../src/types/DICOMLoaderImageOptions.ts | 1 + .../src/BaseStreamingImageVolume.ts | 17 ++++++++-------- .../stackPrefetch/stackContextPrefetch.ts | 2 -- .../utilities/stackPrefetch/stackPrefetch.ts | 2 -- 12 files changed, 49 insertions(+), 36 deletions(-) create mode 100644 packages/core/src/utilities/hasFloatRescale.ts diff --git a/packages/core/src/RenderingEngine/StackViewport.ts b/packages/core/src/RenderingEngine/StackViewport.ts index 9b6af6b467..79860f04a9 100644 --- a/packages/core/src/RenderingEngine/StackViewport.ts +++ b/packages/core/src/RenderingEngine/StackViewport.ts @@ -76,11 +76,7 @@ import pixelToCanvas from './helpers/cpuFallback/rendering/pixelToCanvas'; import resize from './helpers/cpuFallback/rendering/resize'; import cache from '../cache'; -import { - canRenderFloatTextures, - getConfiguration, - getShouldUseCPURendering, -} from '../init'; +import { getConfiguration, getShouldUseCPURendering } from '../init'; import { createProgressive } from '../loaders/ProgressiveRetrieveImages'; import { ImagePixelModule, @@ -2069,7 +2065,6 @@ class StackViewport extends Viewport implements IStackViewport, IImagesLoader { useRGBA: false, transferSyntaxUID, useNativeDataType: this.useNativeDataType, - allowFloatRendering: canRenderFloatTextures(), priority: 5, requestType: RequestType.Interaction, additionalDetails, diff --git a/packages/core/src/RenderingEngine/helpers/setDefaultVolumeVOI.ts b/packages/core/src/RenderingEngine/helpers/setDefaultVolumeVOI.ts index 41e046c1dc..19255cfbf9 100644 --- a/packages/core/src/RenderingEngine/helpers/setDefaultVolumeVOI.ts +++ b/packages/core/src/RenderingEngine/helpers/setDefaultVolumeVOI.ts @@ -9,7 +9,6 @@ import * as metaData from '../../metaData'; import { getMinMax, windowLevel } from '../../utilities'; import { RequestType } from '../../enums'; import cache from '../../cache'; -import { canRenderFloatTextures } from '../../init'; const PRIORITY = 0; const REQUEST_TYPE = RequestType.Prefetch; @@ -168,7 +167,6 @@ async function getVOIFromMinMax( priority: PRIORITY, requestType: REQUEST_TYPE, useNativeDataType, - allowFloatRendering: canRenderFloatTextures(), preScale: { enabled: true, scalingParameters: scalingParametersToUse, diff --git a/packages/core/src/utilities/generateVolumePropsFromImageIds.ts b/packages/core/src/utilities/generateVolumePropsFromImageIds.ts index 4d35f9c797..ed9681e8e7 100644 --- a/packages/core/src/utilities/generateVolumePropsFromImageIds.ts +++ b/packages/core/src/utilities/generateVolumePropsFromImageIds.ts @@ -1,5 +1,9 @@ import { vec3 } from 'gl-matrix'; -import { getConfiguration, getShouldUseSharedArrayBuffer } from '../init'; +import { + canRenderFloatTextures, + getConfiguration, + getShouldUseSharedArrayBuffer, +} from '../init'; import createFloat32SharedArray from './createFloat32SharedArray'; import createInt16SharedArray from './createInt16SharedArray'; import createUint16SharedArray from './createUInt16SharedArray'; @@ -7,6 +11,7 @@ import createUint8SharedArray from './createUint8SharedArray'; import getScalingParameters from './getScalingParameters'; import makeVolumeMetadata from './makeVolumeMetadata'; import sortImageIdsAndGetSpacing from './sortImageIdsAndGetSpacing'; +import { hasFloatRescale } from './hasFloatRescale'; import { ImageVolumeProps, Mat3, Point3 } from '../types'; import cache from '../cache'; import { Events } from '../enums'; @@ -36,9 +41,8 @@ function generateVolumePropsFromImageIds( // The prescale is ALWAYS used with modality LUT, so we can assume that // if the rescale slope is not an integer, we need to use Float32 - const hasFloatRescale = - scalingParameters.rescaleIntercept % 1 !== 0 || - scalingParameters.rescaleSlope % 1 !== 0; + const floatAfterScale = hasFloatRescale(scalingParameters); + const canRenderFloat = canRenderFloatTextures(); const { BitsAllocated, @@ -110,7 +114,7 @@ function generateVolumePropsFromImageIds( // Temporary fix for 16 bit images to use Float32 // until the new dicom image loader handler the conversion // correctly - if (!use16BitDataType || hasFloatRescale) { + if (!use16BitDataType || (canRenderFloat && floatAfterScale)) { sizeInBytes = length * 4; scalarData = useSharedArrayBuffer ? createFloat32SharedArray(length) diff --git a/packages/core/src/utilities/hasFloatRescale.ts b/packages/core/src/utilities/hasFloatRescale.ts new file mode 100644 index 0000000000..40bdfa75a8 --- /dev/null +++ b/packages/core/src/utilities/hasFloatRescale.ts @@ -0,0 +1,13 @@ +import { ScalingParameters } from '../types'; + +/** + * Checks if the scaling parameters contain a float rescale value. + * @param scalingParameters - The scaling parameters to check. + * @returns True if the scaling parameters contain a float rescale value, false otherwise. + */ +export const hasFloatRescale = (scalingParameters: ScalingParameters) => { + const hasFloatRescale = Object.values(scalingParameters).some( + (value) => typeof value === 'number' && !Number.isInteger(value) + ); + return hasFloatRescale; +}; diff --git a/packages/core/src/utilities/index.ts b/packages/core/src/utilities/index.ts index a7a5007c48..450d625207 100644 --- a/packages/core/src/utilities/index.ts +++ b/packages/core/src/utilities/index.ts @@ -71,6 +71,7 @@ import convertToGrayscale from './convertToGrayscale'; import getViewportImageIds from './getViewportImageIds'; import { getRandomSampleFromArray } from './getRandomSampleFromArray'; import { getVolumeId } from './getVolumeId'; +import { hasFloatRescale } from './hasFloatRescale'; // name spaces import * as planar from './planar'; @@ -162,4 +163,5 @@ export { getRandomSampleFromArray, getVolumeId, color, + hasFloatRescale, }; diff --git a/packages/core/src/utilities/loadImageToCanvas.ts b/packages/core/src/utilities/loadImageToCanvas.ts index 755f972fa3..da6e5270ba 100644 --- a/packages/core/src/utilities/loadImageToCanvas.ts +++ b/packages/core/src/utilities/loadImageToCanvas.ts @@ -6,7 +6,7 @@ import { RequestType } from '../enums'; import imageLoadPoolManager from '../requestPool/imageLoadPoolManager'; import renderToCanvasGPU from './renderToCanvasGPU'; import renderToCanvasCPU from './renderToCanvasCPU'; -import { canRenderFloatTextures, getConfiguration } from '../init'; +import { getConfiguration } from '../init'; export interface LoadImageOptions { canvas: HTMLCanvasElement; @@ -127,7 +127,6 @@ export default function loadImageToCanvas( enabled: true, }, useNativeDataType, - allowFloatRendering: canRenderFloatTextures(), useRGBA: !!useCPURendering, requestType, }; diff --git a/packages/dicomImageLoader/src/imageLoader/createImage.ts b/packages/dicomImageLoader/src/imageLoader/createImage.ts index ba36bf606b..1c4cecd162 100644 --- a/packages/dicomImageLoader/src/imageLoader/createImage.ts +++ b/packages/dicomImageLoader/src/imageLoader/createImage.ts @@ -121,6 +121,8 @@ function createImage( const imageFrame = getImageFrame(imageId); imageFrame.decodeLevel = options.decodeLevel; + options.allowFloatRendering = cornerstone.canRenderFloatTextures(); + // Get the scaling parameters from the metadata if (options.preScale.enabled) { const scalingParameters = getScalingParameters( diff --git a/packages/dicomImageLoader/src/shared/decodeImageFrame.ts b/packages/dicomImageLoader/src/shared/decodeImageFrame.ts index 88dc4238fd..515c83ba8b 100644 --- a/packages/dicomImageLoader/src/shared/decodeImageFrame.ts +++ b/packages/dicomImageLoader/src/shared/decodeImageFrame.ts @@ -212,28 +212,30 @@ function postProcessDecodedPixels( }; const type = options.targetBuffer?.type; - // Sometimes the type is specified before the DICOM header data has been - // read. This is fine except for color data, where the wrong type gets - // specified. Don't use the target buffer in that case. - const invalidColorType = - isColorImage(imageFrame.photometricInterpretation) && - options.targetBuffer?.offset === undefined; const canRenderFloat = typeof options.allowFloatRendering !== 'undefined' ? options.allowFloatRendering : true; + // Sometimes the type is specified before the DICOM header data has been + // read. This is fine except for color data, where the wrong type gets + // specified. Don't use the target buffer in that case. + const invalidType = + isColorImage(imageFrame.photometricInterpretation) && + options.targetBuffer?.offset === undefined; + const willScale = options.preScale?.enabled; - const areThereAnyNonIntegerScalingParameter = + const hasFloatRescale = willScale && Object.values(options.preScale.scalingParameters).some( (v) => typeof v === 'number' && !Number.isInteger(v) ); - const disableScale = !canRenderFloat && areThereAnyNonIntegerScalingParameter; + const disableScale = + !options.preScale.enabled || (!canRenderFloat && hasFloatRescale); - if (type && !invalidColorType) { + if (type && !invalidType) { pixelDataArray = _handleTargetBuffer( options, imageFrame, diff --git a/packages/dicomImageLoader/src/types/DICOMLoaderImageOptions.ts b/packages/dicomImageLoader/src/types/DICOMLoaderImageOptions.ts index 21fcd7f7df..fa2c3897a7 100644 --- a/packages/dicomImageLoader/src/types/DICOMLoaderImageOptions.ts +++ b/packages/dicomImageLoader/src/types/DICOMLoaderImageOptions.ts @@ -4,6 +4,7 @@ import { LoadRequestFunction } from './LoadRequestFunction'; export interface DICOMLoaderImageOptions { useRGBA?: boolean; useNativeDataType?: boolean; + allowFloatRendering?: boolean; skipCreateImage?: boolean; preScale?: { enabled: boolean; diff --git a/packages/streaming-image-volume-loader/src/BaseStreamingImageVolume.ts b/packages/streaming-image-volume-loader/src/BaseStreamingImageVolume.ts index 3c7f8ad73e..5e28e943c2 100644 --- a/packages/streaming-image-volume-loader/src/BaseStreamingImageVolume.ts +++ b/packages/streaming-image-volume-loader/src/BaseStreamingImageVolume.ts @@ -8,7 +8,6 @@ import { cache, imageLoader, utilities as csUtils, - utilities, ProgressiveRetrieveImages, canRenderFloatTextures, } from '@cornerstonejs/core'; @@ -21,9 +20,9 @@ import type { import { scaleArray, autoLoad } from './helpers'; const requestTypeDefault = Enums.RequestType.Prefetch; -const { ProgressiveIterator } = csUtils; +const { ProgressiveIterator, imageRetrieveMetadataProvider, hasFloatRescale } = + csUtils; const { ImageQualityStatus } = Enums; -const { imageRetrieveMetadataProvider } = utilities; /** * Streaming Image Volume Class that extends ImageVolume base class. @@ -205,6 +204,7 @@ export default class BaseStreamingImageVolume const imageIdIndex = this.getImageIdIndex(imageId); const options = this.getLoaderImageOptions(imageId); const scalarData = this.getScalarDataByImageIdIndex(imageIdIndex); + handleArrayBufferLoad(scalarData, image, options); const { scalingParameters } = image.preScale || {}; @@ -398,10 +398,7 @@ export default class BaseStreamingImageVolume typeof scalingParameters.rescaleSlope === 'number' && typeof scalingParameters.rescaleIntercept === 'number'; - const areThereAnyNonIntegerScalingParameter = Object.values( - scalingParameters - ).some((value) => typeof value === 'number' && !Number.isInteger(value)); - + const floatAfterScale = hasFloatRescale(scalingParameters); const allowFloatRendering = canRenderFloatTextures(); /** @@ -420,7 +417,7 @@ export default class BaseStreamingImageVolume // in case where the hardware/os does not support float rendering but the // requested scaling params are not integers, we need to disable pre-scaling - if (!allowFloatRendering && areThereAnyNonIntegerScalingParameter) { + if (!allowFloatRendering && floatAfterScale) { this.isPreScaled = false; } @@ -663,6 +660,10 @@ export default class BaseStreamingImageVolume image, scalingParametersToUse: Types.ScalingParameters ) { + if (!image.preScale.enabled) { + return image.getPixelData().slice(0); + } + const imageIsAlreadyScaled = image.preScale?.scaled; const noScalingParametersToUse = !scalingParametersToUse || diff --git a/packages/tools/src/utilities/stackPrefetch/stackContextPrefetch.ts b/packages/tools/src/utilities/stackPrefetch/stackContextPrefetch.ts index 5aa37595da..f68d6231a1 100644 --- a/packages/tools/src/utilities/stackPrefetch/stackContextPrefetch.ts +++ b/packages/tools/src/utilities/stackPrefetch/stackContextPrefetch.ts @@ -5,7 +5,6 @@ import { imageLoadPoolManager, cache, getConfiguration as getCoreConfiguration, - canRenderFloatTextures, } from '@cornerstonejs/core'; import { addToolState, getToolState } from './state'; import { @@ -235,7 +234,6 @@ function prefetch(element) { enabled: true, }, useNativeDataType, - allowFloatRendering: canRenderFloatTextures(), requestType, }; diff --git a/packages/tools/src/utilities/stackPrefetch/stackPrefetch.ts b/packages/tools/src/utilities/stackPrefetch/stackPrefetch.ts index 0a814353d9..4ce68bb114 100644 --- a/packages/tools/src/utilities/stackPrefetch/stackPrefetch.ts +++ b/packages/tools/src/utilities/stackPrefetch/stackPrefetch.ts @@ -5,7 +5,6 @@ import { imageLoadPoolManager, cache, getConfiguration as getCoreConfiguration, - canRenderFloatTextures, } from '@cornerstonejs/core'; import { addToolState, getToolState } from './state'; import { @@ -182,7 +181,6 @@ function prefetch(element) { preScale: { enabled: true, }, - allowFloatRendering: canRenderFloatTextures(), useNativeDataType, requestType, }; From 8dd5c12b84acaf6795ab47dc95cbca4220c01e7d Mon Sep 17 00:00:00 2001 From: Alireza Date: Wed, 17 Apr 2024 17:15:40 -0400 Subject: [PATCH 13/16] Refactor code to allow float rendering in DICOMLoaderImageOptions, createImage, stackPrefetch, stackContextPrefetch, loadImageToCanvas, generateVolumePropsFromImageIds, hasFloatScalingParameters, and hasFloatRescale --- packages/core/src/index.ts | 2 ++ .../src/utilities/generateVolumePropsFromImageIds.ts | 4 ++-- .../{hasFloatRescale.ts => hasFloatScalingParameters.ts} | 4 +++- packages/core/src/utilities/index.ts | 4 ++-- .../src/BaseStreamingImageVolume.ts | 9 ++++++--- 5 files changed, 15 insertions(+), 8 deletions(-) rename packages/core/src/utilities/{hasFloatRescale.ts => hasFloatScalingParameters.ts} (81%) diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 3e33b57292..6283fd71f1 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -42,6 +42,7 @@ import { getConfiguration, setConfiguration, getWebWorkerManager, + canRenderFloatTextures, } from './init'; // Classes @@ -89,6 +90,7 @@ export { getConfiguration, setConfiguration, getWebWorkerManager, + canRenderFloatTextures, // enums Enums, CONSTANTS, diff --git a/packages/core/src/utilities/generateVolumePropsFromImageIds.ts b/packages/core/src/utilities/generateVolumePropsFromImageIds.ts index ed9681e8e7..bea94b38bf 100644 --- a/packages/core/src/utilities/generateVolumePropsFromImageIds.ts +++ b/packages/core/src/utilities/generateVolumePropsFromImageIds.ts @@ -11,7 +11,7 @@ import createUint8SharedArray from './createUint8SharedArray'; import getScalingParameters from './getScalingParameters'; import makeVolumeMetadata from './makeVolumeMetadata'; import sortImageIdsAndGetSpacing from './sortImageIdsAndGetSpacing'; -import { hasFloatRescale } from './hasFloatRescale'; +import { hasFloatScalingParameters } from './hasFloatScalingParameters'; import { ImageVolumeProps, Mat3, Point3 } from '../types'; import cache from '../cache'; import { Events } from '../enums'; @@ -41,7 +41,7 @@ function generateVolumePropsFromImageIds( // The prescale is ALWAYS used with modality LUT, so we can assume that // if the rescale slope is not an integer, we need to use Float32 - const floatAfterScale = hasFloatRescale(scalingParameters); + const floatAfterScale = hasFloatScalingParameters(scalingParameters); const canRenderFloat = canRenderFloatTextures(); const { diff --git a/packages/core/src/utilities/hasFloatRescale.ts b/packages/core/src/utilities/hasFloatScalingParameters.ts similarity index 81% rename from packages/core/src/utilities/hasFloatRescale.ts rename to packages/core/src/utilities/hasFloatScalingParameters.ts index 40bdfa75a8..60a4d6f15f 100644 --- a/packages/core/src/utilities/hasFloatRescale.ts +++ b/packages/core/src/utilities/hasFloatScalingParameters.ts @@ -5,7 +5,9 @@ import { ScalingParameters } from '../types'; * @param scalingParameters - The scaling parameters to check. * @returns True if the scaling parameters contain a float rescale value, false otherwise. */ -export const hasFloatRescale = (scalingParameters: ScalingParameters) => { +export const hasFloatScalingParameters = ( + scalingParameters: ScalingParameters +): boolean => { const hasFloatRescale = Object.values(scalingParameters).some( (value) => typeof value === 'number' && !Number.isInteger(value) ); diff --git a/packages/core/src/utilities/index.ts b/packages/core/src/utilities/index.ts index 450d625207..ab92c94a93 100644 --- a/packages/core/src/utilities/index.ts +++ b/packages/core/src/utilities/index.ts @@ -71,7 +71,7 @@ import convertToGrayscale from './convertToGrayscale'; import getViewportImageIds from './getViewportImageIds'; import { getRandomSampleFromArray } from './getRandomSampleFromArray'; import { getVolumeId } from './getVolumeId'; -import { hasFloatRescale } from './hasFloatRescale'; +import { hasFloatScalingParameters } from './hasFloatScalingParameters'; // name spaces import * as planar from './planar'; @@ -163,5 +163,5 @@ export { getRandomSampleFromArray, getVolumeId, color, - hasFloatRescale, + hasFloatScalingParameters, }; diff --git a/packages/streaming-image-volume-loader/src/BaseStreamingImageVolume.ts b/packages/streaming-image-volume-loader/src/BaseStreamingImageVolume.ts index 5e28e943c2..6325d7c341 100644 --- a/packages/streaming-image-volume-loader/src/BaseStreamingImageVolume.ts +++ b/packages/streaming-image-volume-loader/src/BaseStreamingImageVolume.ts @@ -20,8 +20,11 @@ import type { import { scaleArray, autoLoad } from './helpers'; const requestTypeDefault = Enums.RequestType.Prefetch; -const { ProgressiveIterator, imageRetrieveMetadataProvider, hasFloatRescale } = - csUtils; +const { + ProgressiveIterator, + imageRetrieveMetadataProvider, + hasFloatScalingParameters, +} = csUtils; const { ImageQualityStatus } = Enums; /** @@ -398,7 +401,7 @@ export default class BaseStreamingImageVolume typeof scalingParameters.rescaleSlope === 'number' && typeof scalingParameters.rescaleIntercept === 'number'; - const floatAfterScale = hasFloatRescale(scalingParameters); + const floatAfterScale = hasFloatScalingParameters(scalingParameters); const allowFloatRendering = canRenderFloatTextures(); /** From ea0144618f950c8cd98a9f3a3e83b24de8bfdd6f Mon Sep 17 00:00:00 2001 From: Alireza Date: Wed, 17 Apr 2024 17:16:50 -0400 Subject: [PATCH 14/16] api --- common/reviews/api/core.api.md | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/common/reviews/api/core.api.md b/common/reviews/api/core.api.md index 7360ea38a9..8b6b8eacb1 100644 --- a/common/reviews/api/core.api.md +++ b/common/reviews/api/core.api.md @@ -264,6 +264,9 @@ function cancelLoadImage(imageId: string): void; // @public (undocumented) function cancelLoadImages(imageIds: Array): void; +// @public (undocumented) +export function canRenderFloatTextures(): boolean; + // @public (undocumented) function clamp(value: number, min: number, max: number): number; @@ -1110,6 +1113,9 @@ function getVolumeViewportScrollInfo(viewport: IVolumeViewport, volumeId: string // @public (undocumented) export function getWebWorkerManager(): any; +// @public (undocumented) +const hasFloatScalingParameters: (scalingParameters: ScalingParameters) => boolean; + // @public (undocumented) function hasNaNValues(input: number[] | number): boolean; @@ -3097,7 +3103,6 @@ export class StackViewport extends Viewport implements IStackViewport, IImagesLo useRGBA: boolean; transferSyntaxUID: any; useNativeDataType: boolean; - allowFloatRendering: boolean; priority: number; requestType: RequestType; additionalDetails: { @@ -3507,7 +3512,8 @@ declare namespace utilities { getViewportImageIds, getRandomSampleFromArray, getVolumeId, - color + color, + hasFloatScalingParameters } } export { utilities } From b22dc747c790e0850a82e4c06124b686b1f3eae9 Mon Sep 17 00:00:00 2001 From: Alireza Date: Wed, 17 Apr 2024 17:22:52 -0400 Subject: [PATCH 15/16] update --- packages/core/src/init.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/core/src/init.ts b/packages/core/src/init.ts index 9bce0867ef..11d2e8a6e5 100644 --- a/packages/core/src/init.ts +++ b/packages/core/src/init.ts @@ -132,7 +132,13 @@ async function init(configuration = config): Promise { config.rendering.useNorm16Texture = _hasNorm16TextureSupport(); if (!config.rendering.useNorm16Texture) { - config.rendering.preferSizeOverAccuracy = true; + if (configuration.rendering?.preferSizeOverAccuracy) { + config.rendering.preferSizeOverAccuracy = true; + } else { + console.log( + 'norm16 texture not supported, you can turn on the preferSizeOverAccuracy flag to use native data type, but be aware of the inaccuracy of the rendering in high bits' + ); + } } } } From 9ebbcf0c253e241052c1527144fea30d42b744c5 Mon Sep 17 00:00:00 2001 From: Alireza Date: Wed, 17 Apr 2024 17:24:19 -0400 Subject: [PATCH 16/16] update --- packages/core/src/init.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/core/src/init.ts b/packages/core/src/init.ts index 11d2e8a6e5..3f4a3951a6 100644 --- a/packages/core/src/init.ts +++ b/packages/core/src/init.ts @@ -202,7 +202,15 @@ function setPreferSizeOverAccuracy(status: boolean): void { */ function canRenderFloatTextures(): boolean { const isMobile = config.isMobile; - return !isMobileIOS() && !isMobile; + if (!isMobile) { + return true; + } + + if (isMobileIOS()) { + return false; + } + + return true; } /**