From 0833bf6ea63aa5aebc592482f604d4deb9cb8655 Mon Sep 17 00:00:00 2001 From: Ouwen Huang Date: Sat, 4 Feb 2023 03:36:20 +0000 Subject: [PATCH 1/2] feat: added support for 16 bit textures. Adds 16 bit texture support to cornerstone3D - CSWIL will decide on array type to match native storage format. In the case native format is unsigned int, if the scaling params are negative, prescale = true will return back a signed int. - Streaming Volume does require the array buffer type to be predetermined (targetBuffer must be specified). This is because all incoming slices need to adhere to one data type. - Block Height is set to 1 in the case norm16 is used due to this chromium bug: https://bugs.chromium.org/p/chromium/issues/detail?id=1408247. In the case preferSizeOverAccuracy is used block height for texture transfers can be calculated as usual. TODO: - CSWIL needs to implement scaling logic. When a buffer type is not provided native is used (using signed versions of array buffers in the case scale is negative). When a buffer type is provided scaling will occur with possible over/underflows. Co-authored-by: sedghi --- common/reviews/api/core.api.md | 45 +++++++- .../api/streaming-image-volume-loader.api.md | 35 +++++- common/reviews/api/tools.api.md | 43 +++++-- .../core/src/RenderingEngine/StackViewport.ts | 50 ++++---- .../helpers/createVolumeMapper.ts | 9 +- .../helpers/setDefaultVolumeVOI.ts | 16 --- .../vtkClasses/vtkStreamingOpenGLTexture.js | 42 ++++++- .../vtkStreamingOpenGLVolumeMapper.js | 4 + packages/core/src/index.ts | 6 + packages/core/src/init.ts | 107 +++++++++++++++--- packages/core/src/loaders/volumeLoader.ts | 22 +++- .../core/src/types/Cornerstone3DConfig.ts | 31 +++++ packages/core/src/types/IImageData.ts | 2 +- packages/core/src/types/IVolume.ts | 2 +- packages/core/src/types/index.ts | 5 +- .../src/utilities/createInt16SharedArray.ts | 43 +++++++ .../src/utilities/createUInt16SharedArray.ts | 43 +++++++ .../src/utilities/deepMerge.ts} | 16 +-- .../core/src/utilities/getScalarDataType.ts | 31 +++++ .../src/utilities/getScalingParameters.ts | 35 ++++++ packages/core/src/utilities/index.ts | 10 ++ .../core/src/utilities/loadImageToCanvas.ts | 5 - .../src/BaseStreamingImageVolume.ts | 30 ++++- .../src/StreamingDynamicImageVolume.ts | 2 +- .../cornerstoneStreamingImageVolumeLoader.ts | 91 +++++++++------ .../src/helpers/scaleArray.ts | 4 +- .../src/store/ToolGroupManager/ToolGroup.ts | 4 +- packages/tools/src/tools/WindowLevelTool.ts | 6 + packages/tools/src/tools/base/BaseTool.ts | 8 +- .../displayTools/Contour/contourDisplay.ts | 4 +- .../displayTools/Labelmap/labelmapDisplay.ts | 3 +- .../displayTools/SegmentationDisplayTool.ts | 9 +- .../src/tools/segmentation/PaintFillTool.ts | 2 +- packages/tools/src/utilities/index.ts | 2 - .../createLabelmapVolumeForViewport.ts | 4 +- .../utilities/stackPrefetch/stackPrefetch.ts | 5 - .../initCornerstoneDICOMImageLoader.js | 3 + 37 files changed, 620 insertions(+), 159 deletions(-) create mode 100644 packages/core/src/types/Cornerstone3DConfig.ts create mode 100644 packages/core/src/utilities/createInt16SharedArray.ts create mode 100644 packages/core/src/utilities/createUInt16SharedArray.ts rename packages/{tools/src/utilities/deepMerge.js => core/src/utilities/deepMerge.ts} (81%) create mode 100644 packages/core/src/utilities/getScalarDataType.ts create mode 100644 packages/core/src/utilities/getScalingParameters.ts diff --git a/common/reviews/api/core.api.md b/common/reviews/api/core.api.md index 5f640876f5..9632a9006c 100644 --- a/common/reviews/api/core.api.md +++ b/common/reviews/api/core.api.md @@ -178,6 +178,16 @@ enum ContourType { OPEN_PLANAR = "OPEN_PLANAR" } +// @public (undocumented) +type Cornerstone3DConfig = { + detectGPU: any; + rendering: { + preferSizeOverAccuracy: boolean; + useNorm16Texture: boolean; + useCPURendering: boolean; + }; +}; + // @public (undocumented) interface CPUFallbackColormap { // (undocumented) @@ -464,6 +474,9 @@ function createAndCacheVolume(volumeId: string, options: VolumeLoaderOptions): P // @public (undocumented) function createFloat32SharedArray(length: number): Float32Array; +// @public (undocumented) +function createInt16SharedArray(length: number): Int16Array; + // @public (undocumented) function createLinearRGBTransferFunction(voiRange: VOIRange): vtkColorTransferFunction; @@ -473,6 +486,9 @@ function createLocalVolume(options: LocalVolumeOptions, volumeId: string, preven // @public (undocumented) function createSigmoidRGBTransferFunction(voiRange: VOIRange, approximationNodes?: number): vtkColorTransferFunction; +// @public (undocumented) +function createUint16SharedArray(length: number): Uint16Array; + // @public (undocumented) function createUint8SharedArray(length: number): Uint8Array; @@ -490,6 +506,9 @@ interface CustomEvent_2 extends Event { initCustomEvent(typeArg: string, canBubbleArg: boolean, cancelableArg: boolean, detailArg: T): void; } +// @public (undocumented) +const deepMerge: (target?: {}, source?: {}, optionsArgument?: any) => any; + // @public (undocumented) type ElementDisabledEvent = CustomEvent_2; @@ -660,6 +679,9 @@ function getClosestImageId(imageVolume: IImageVolume, worldPos: Point3, viewPlan // @public (undocumented) function getClosestStackImageIndexForPoint(point: Point3, viewport: IStackViewport): number | null; +// @public (undocumented) +export function getConfiguration(): Cornerstone3DConfig; + // @public (undocumented) export function getEnabledElement(element: HTMLDivElement | undefined): IEnabledElement | undefined; @@ -693,6 +715,12 @@ export function getRenderingEngines(): IRenderingEngine[] | undefined; // @public (undocumented) function getRuntimeId(context?: unknown, separator?: string, max?: number): string; +// @public (undocumented) +function getScalarDataType(scalingParameters: ScalingParameters, scalarData?: any): string; + +// @public (undocumented) +function getScalingParameters(imageId: string): ScalingParameters; + // @public (undocumented) export function getShouldUseCPURendering(): boolean; @@ -1054,7 +1082,7 @@ interface IImageData { }; }; // (undocumented) - scalarData: Float32Array; + scalarData: Float32Array | Uint16Array | Uint8Array | Int16Array; // (undocumented) scaling?: Scaling; // (undocumented) @@ -1324,7 +1352,7 @@ type ImageVolumeModifiedEventDetail = { function indexWithinDimensions(index: Point3, dimensions: Point3): boolean; // @public (undocumented) -export function init(defaultConfiguration?: {}): Promise; +export function init(configuration?: {}): Promise; // @public (undocumented) enum InterpolationType { @@ -1937,6 +1965,9 @@ type ScalingParameters = { suvbsa?: number; }; +// @public (undocumented) +export function setConfiguration(c: Cornerstone3DConfig): void; + // @public (undocumented) export class Settings { constructor(base?: Settings); @@ -2150,6 +2181,7 @@ export function triggerEvent(el: EventTarget, type: string, detail?: unknown): b declare namespace Types { export { + Cornerstone3DConfig, ICamera, IStackViewport, IVolumeViewport, @@ -2246,6 +2278,8 @@ declare namespace utilities { isOpposite, createFloat32SharedArray, createUint8SharedArray, + createUint16SharedArray, + createInt16SharedArray, windowLevel, getClosestImageId, getSpacingInNormalDirection, @@ -2270,7 +2304,10 @@ declare namespace utilities { spatialRegistrationMetadataProvider, getViewportImageCornersInWorld, hasNaNValues, - applyPreset + applyPreset, + deepMerge, + getScalingParameters, + getScalarDataType } } export { utilities } @@ -2558,7 +2595,7 @@ type VolumeNewImageEventDetail = { }; // @public (undocumented) -type VolumeScalarData = Float32Array | Uint8Array; +type VolumeScalarData = Float32Array | Uint8Array | Uint16Array | Int16Array; // @public (undocumented) export class VolumeViewport extends BaseVolumeViewport { diff --git a/common/reviews/api/streaming-image-volume-loader.api.md b/common/reviews/api/streaming-image-volume-loader.api.md index 125d5262e0..9984e17c4c 100644 --- a/common/reviews/api/streaming-image-volume-loader.api.md +++ b/common/reviews/api/streaming-image-volume-loader.api.md @@ -76,6 +76,37 @@ enum ContourType { OPEN_PLANAR = 'OPEN_PLANAR', } +// @public (undocumented) +type Cornerstone3DConfig = { + detectGPU: any; + rendering: { + // vtk.js supports 8bit integer textures and 32bit float textures. + // However, if the client has norm16 textures (it can be seen by visiting + // the webGl report at https://webglreport.com/?v=2), vtk will be default + // to use it to improve memory usage. However, if the client don't have + // it still another level of optimization can happen by setting the + // preferSizeOverAccuracy since it will reduce the size of the texture to half + // float at the cost of accuracy in rendering. This is a tradeoff that the + // client can decide. + // + // Read more in the following Pull Request: + // 1. HalfFloat: https://github.com/Kitware/vtk-js/pull/2046 + // 2. Norm16: https://github.com/Kitware/vtk-js/pull/2058 + preferSizeOverAccuracy: boolean; + // Whether the EXT_texture_norm16 extension is supported by the browser. + // WebGL 2 report (link: https://webglreport.com/?v=2) can be used to check + // if the browser supports this extension. + // In case the browser supports this extension, instead of using 32bit float + // textures, 16bit float textures will be used to reduce the memory usage where + // possible. + // Norm16 may not work currently due to the two active bugs in chrome + safari + // https://bugs.chromium.org/p/chromium/issues/detail?id=1408247 + // https://bugs.webkit.org/show_bug.cgi?id=252039 + useNorm16Texture: boolean; + useCPURendering: boolean; + }; +}; + // @public (undocumented) export function cornerstoneStreamingDynamicImageVolumeLoader(volumeId: string, options: { imageIds: string[]; @@ -744,7 +775,7 @@ interface IImageData { suvbw?: number; }; }; - scalarData: Float32Array; + scalarData: Float32Array | Uint16Array | Uint8Array | Int16Array; scaling?: Scaling; spacing: Point3; } @@ -1525,7 +1556,7 @@ type VolumeNewImageEventDetail = { }; // @public (undocumented) -type VolumeScalarData = Float32Array | Uint8Array; +type VolumeScalarData = Float32Array | Uint8Array | Uint16Array | Int16Array; // @public type VolumeViewportProperties = { diff --git a/common/reviews/api/tools.api.md b/common/reviews/api/tools.api.md index 167865a1ac..c71f418e10 100644 --- a/common/reviews/api/tools.api.md +++ b/common/reviews/api/tools.api.md @@ -796,6 +796,37 @@ function copyPoints(points: ITouchPoints): ITouchPoints; // @public (undocumented) function copyPointsList(points: ITouchPoints[]): ITouchPoints[]; +// @public (undocumented) +type Cornerstone3DConfig = { + detectGPU: any; + rendering: { + // vtk.js supports 8bit integer textures and 32bit float textures. + // However, if the client has norm16 textures (it can be seen by visiting + // the webGl report at https://webglreport.com/?v=2), vtk will be default + // to use it to improve memory usage. However, if the client don't have + // it still another level of optimization can happen by setting the + // preferSizeOverAccuracy since it will reduce the size of the texture to half + // float at the cost of accuracy in rendering. This is a tradeoff that the + // client can decide. + // + // Read more in the following Pull Request: + // 1. HalfFloat: https://github.com/Kitware/vtk-js/pull/2046 + // 2. Norm16: https://github.com/Kitware/vtk-js/pull/2058 + preferSizeOverAccuracy: boolean; + // Whether the EXT_texture_norm16 extension is supported by the browser. + // WebGL 2 report (link: https://webglreport.com/?v=2) can be used to check + // if the browser supports this extension. + // In case the browser supports this extension, instead of using 32bit float + // textures, 16bit float textures will be used to reduce the memory usage where + // possible. + // Norm16 may not work currently due to the two active bugs in chrome + safari + // https://bugs.chromium.org/p/chromium/issues/detail?id=1408247 + // https://bugs.webkit.org/show_bug.cgi?id=252039 + useNorm16Texture: boolean; + useCPURendering: boolean; + }; +}; + // @public (undocumented) const CORNERSTONE_COLOR_LUT: number[][]; @@ -1080,9 +1111,9 @@ function createLabelmapVolumeForViewport(input: { segmentationId?: string; options?: { volumeId?: string; - scalarData?: Float32Array | Uint8Array; + scalarData?: Float32Array | Uint8Array | Uint16Array | Int16Array; targetBuffer?: { - type: 'Float32Array' | 'Uint8Array'; + type: 'Float32Array' | 'Uint8Array' | 'Uint16Array' | 'Int8Array'; }; metadata?: any; dimensions?: Types_2.Point3; @@ -1289,9 +1320,6 @@ function debounce(func: Function, wait?: number, options?: { trailing?: boolean; }): Function; -// @public (undocumented) -function deepmerge(target?: {}, source?: {}, optionsArgument?: any): any; - // @public (undocumented) const _default: { filterAnnotationsWithinSlice: typeof filterAnnotationsWithinSlice; @@ -2353,7 +2381,7 @@ interface IImageData { suvbw?: number; }; }; - scalarData: Float32Array; + scalarData: Float32Array | Uint16Array | Uint8Array | Int16Array; scaling?: Scaling; spacing: Point3; } @@ -5077,7 +5105,6 @@ declare namespace utilities { viewportFilters, drawing_2 as drawing, debounce, - deepmerge as deepMerge, dynamicVolume, throttle, orientation_2 as orientation, @@ -5278,7 +5305,7 @@ export class VolumeRotateMouseWheelTool extends BaseTool { } // @public (undocumented) -type VolumeScalarData = Float32Array | Uint8Array; +type VolumeScalarData = Float32Array | Uint8Array | Uint16Array | Int16Array; // @public type VolumeViewportProperties = { diff --git a/packages/core/src/RenderingEngine/StackViewport.ts b/packages/core/src/RenderingEngine/StackViewport.ts index b0dfcc66d8..735f5d39cf 100644 --- a/packages/core/src/RenderingEngine/StackViewport.ts +++ b/packages/core/src/RenderingEngine/StackViewport.ts @@ -6,6 +6,7 @@ import vtkCamera from '@kitware/vtk.js/Rendering/Core/Camera'; import { vec2, vec3, mat4 } from 'gl-matrix'; import vtkImageMapper from '@kitware/vtk.js/Rendering/Core/ImageMapper'; import vtkImageSlice from '@kitware/vtk.js/Rendering/Core/ImageSlice'; +import { getConfiguration } from '../init'; import * as metaData from '../metaData'; import Viewport from './Viewport'; import eventTarget from '../eventTarget'; @@ -1419,15 +1420,23 @@ class StackViewport extends Viewport implements IStackViewport { bitsAllocated, numComps, numVoxels, + TypedArray, }): void { let pixelArray; + const { useNorm16Texture, preferSizeOverAccuracy } = + getConfiguration().rendering; + const use16BitDataType = useNorm16Texture || preferSizeOverAccuracy; + switch (bitsAllocated) { case 8: pixelArray = new Uint8Array(numVoxels * numComps); break; - case 16: - pixelArray = new Float32Array(numVoxels * numComps); + if (use16BitDataType) { + pixelArray = new TypedArray(numVoxels * numComps); + } else { + pixelArray = new Float32Array(numVoxels * numComps); + } break; case 24: @@ -1516,13 +1525,13 @@ class StackViewport extends Viewport implements IStackViewport { if (!imageData) { return false; } - const [xSpacing, ySpacing] = imageData.getSpacing(); const [xVoxels, yVoxels] = imageData.getDimensions(); const imagePlaneModule = this._getImagePlaneModule(image.imageId); const direction = imageData.getDirection(); const rowCosines = direction.slice(0, 3); const columnCosines = direction.slice(3, 6); + const dataType = imageData.getPointData().getScalars().getDataType(); // using spacing, size, and direction only for now return ( @@ -1533,7 +1542,8 @@ class StackViewport extends Viewport implements IStackViewport { xVoxels === image.columns && yVoxels === image.rows && isEqual(imagePlaneModule.rowCosines, rowCosines) && - isEqual(imagePlaneModule.columnCosines, columnCosines) + isEqual(imagePlaneModule.columnCosines, columnCosines) && + dataType === image.getPixelData().constructor.name ); } @@ -1557,7 +1567,11 @@ class StackViewport extends Viewport implements IStackViewport { // from the loaded Cornerstone image const pixelData = image.getPixelData(); const scalars = this._imageData.getPointData().getScalars(); - const scalarData = scalars.getData() as Uint8Array | Float32Array; + const scalarData = scalars.getData() as + | Uint8Array + | Float32Array + | Uint16Array + | Int16Array; if (image.rgba || isRgbaSourceRgbDest(pixelData, scalarData)) { if (!image.rgba) { @@ -1715,19 +1729,10 @@ class StackViewport extends Viewport implements IStackViewport { ); } - // Todo: Note that eventually all viewport data is converted into Float32Array, - // we use it here for the purpose of scaling for now. - const type = 'Float32Array'; - const priority = -5; const requestType = RequestType.Interaction; const additionalDetails = { imageId }; const options = { - targetBuffer: { - type, - offset: null, - length: null, - }, preScale: { enabled: true, }, @@ -1802,20 +1807,16 @@ class StackViewport extends Viewport implements IStackViewport { ); } - // Todo: Note that eventually all viewport data is converted into Float32Array, - // we use it here for the purpose of scaling for now. - const type = 'Float32Array'; - + /** + * CSWIL will automatically choose the array type when no targetBuffer + * is provided. When CSWIL is initialized, the use16bit should match + * the settings of cornerstone3D (either preferSizeOverAccuracy or norm16 + * textures need to be enabled) + */ const priority = -5; const requestType = RequestType.Interaction; const additionalDetails = { imageId }; - const options = { - targetBuffer: { - type, - offset: null, - length: null, - }, preScale: { enabled: true, }, @@ -1939,6 +1940,7 @@ class StackViewport extends Viewport implements IStackViewport { bitsAllocated, numComps, numVoxels, + TypedArray: image.getPixelData().constructor, }); // Set the scalar data of the vtkImageData object from the Cornerstone diff --git a/packages/core/src/RenderingEngine/helpers/createVolumeMapper.ts b/packages/core/src/RenderingEngine/helpers/createVolumeMapper.ts index 90e815f2b8..8d52c4b68f 100644 --- a/packages/core/src/RenderingEngine/helpers/createVolumeMapper.ts +++ b/packages/core/src/RenderingEngine/helpers/createVolumeMapper.ts @@ -1,5 +1,5 @@ import { vtkSharedVolumeMapper } from '../vtkClasses'; - +import { getConfiguration } from '../../init'; /** * Given an imageData and a vtkOpenGLTexture, it creates a "shared" vtk volume mapper * from which various volume actors can be created. @@ -16,6 +16,10 @@ export default function createVolumeMapper( ): any { const volumeMapper = vtkSharedVolumeMapper.newInstance(); + if (getConfiguration().rendering.preferSizeOverAccuracy) { + volumeMapper.setPreferSizeOverAccuracy(true); + } + volumeMapper.setInputData(imageData); const spacing = imageData.getSpacing(); @@ -24,10 +28,9 @@ export default function createVolumeMapper( const sampleDistance = (spacing[0] + spacing[1] + spacing[2]) / 6; // This is to allow for good pixel level image quality. + // Todo: why we are setting this to 4000? Is this a good number? it should be configurable volumeMapper.setMaximumSamplesPerRay(4000); - volumeMapper.setSampleDistance(sampleDistance); - volumeMapper.setScalarTexture(vtkOpenGLTexture); return volumeMapper; diff --git a/packages/core/src/RenderingEngine/helpers/setDefaultVolumeVOI.ts b/packages/core/src/RenderingEngine/helpers/setDefaultVolumeVOI.ts index 23a6681cc2..564be44668 100644 --- a/packages/core/src/RenderingEngine/helpers/setDefaultVolumeVOI.ts +++ b/packages/core/src/RenderingEngine/helpers/setDefaultVolumeVOI.ts @@ -131,16 +131,6 @@ async function getVOIFromMinMax(imageVolume: IImageVolume): Promise { const voxelsPerImage = scalarData.length / numImages; const bytePerPixel = scalarData.BYTES_PER_ELEMENT; - let type; - - if (scalarData instanceof Uint8Array) { - type = 'Uint8Array'; - } else if (scalarData instanceof Float32Array) { - type = 'Float32Array'; - } else { - throw new Error('Unsupported array type'); - } - const scalingParameters: ScalingParameters = { rescaleSlope: modalityLutModule.rescaleSlope, rescaleIntercept: modalityLutModule.rescaleIntercept, @@ -162,12 +152,6 @@ async function getVOIFromMinMax(imageVolume: IImageVolume): Promise { const byteOffset = imageIdIndex * bytesPerImage; const options = { - targetBuffer: { - arrayBuffer: scalarData.buffer, - offset: byteOffset, - length: voxelsPerImage, - type, - }, priority: PRIORITY, requestType: REQUEST_TYPE, preScale: { diff --git a/packages/core/src/RenderingEngine/vtkClasses/vtkStreamingOpenGLTexture.js b/packages/core/src/RenderingEngine/vtkClasses/vtkStreamingOpenGLTexture.js index 49614fbb29..e61017e3fc 100644 --- a/packages/core/src/RenderingEngine/vtkClasses/vtkStreamingOpenGLTexture.js +++ b/packages/core/src/RenderingEngine/vtkClasses/vtkStreamingOpenGLTexture.js @@ -1,5 +1,7 @@ import macro from '@kitware/vtk.js/macros'; import vtkOpenGLTexture from '@kitware/vtk.js/Rendering/OpenGL/Texture'; +import HalfFloat from '@kitware/vtk.js/Common/Core/HalfFloat'; +import { getConfiguration } from '../../init'; /** * vtkStreamingOpenGLTexture - A dervied class of the core vtkOpenGLTexture. @@ -21,7 +23,8 @@ function vtkStreamingOpenGLTexture(publicAPI, model) { depth, numComps, dataType, - data + data, + preferSizeOverAccuracy ) => { model.inputDataType = dataType; model.inputNumComps = numComps; @@ -32,7 +35,8 @@ function vtkStreamingOpenGLTexture(publicAPI, model) { depth, numComps, dataType, - data + data, + preferSizeOverAccuracy ); }; @@ -40,7 +44,7 @@ function vtkStreamingOpenGLTexture(publicAPI, model) { * This function updates the GPU texture memory to match the current * representation of data held in RAM. * - * @param {Float32Array|Uint8Array} data The data array which has been updated. + * @param {Float32Array|Uint8Array|Int16Array|Uint16Array} data The data array which has been updated. */ publicAPI.update3DFromRaw = (data) => { const { updatedFrames } = model; @@ -48,7 +52,6 @@ function vtkStreamingOpenGLTexture(publicAPI, model) { if (!updatedFrames.length) { return; } - model._openGLRenderWindow.activateTexture(publicAPI); publicAPI.createTexture(); publicAPI.bind(); @@ -62,6 +65,9 @@ function vtkStreamingOpenGLTexture(publicAPI, model) { } else if (data instanceof Int16Array) { bytesPerVoxel = 2; TypedArrayConstructor = Int16Array; + } else if (data instanceof Uint16Array) { + bytesPerVoxel = 2; + TypedArrayConstructor = Uint16Array; } else if (data instanceof Float32Array) { bytesPerVoxel = 4; TypedArrayConstructor = Float32Array; @@ -129,6 +135,15 @@ function vtkStreamingOpenGLTexture(publicAPI, model) { // Cap to actual frame height: blockHeight = Math.min(blockHeight, model.height); + const { useNorm16Texture, preferSizeOverAccuracy } = + getConfiguration().rendering; + // TODO: there is currently a bug in chrome and safari which requires + // blockheight = 1 for norm16 textures: + // https://bugs.chromium.org/p/chromium/issues/detail?id=1408247 + // https://bugs.webkit.org/show_bug.cgi?id=252039 + if (useNorm16Texture && !preferSizeOverAccuracy) { + blockHeight = 1; + } const multiRowBlockLength = rowLength * blockHeight; const multiRowBlockLengthInBytes = multiRowBlockLength * bytesPerVoxel; @@ -142,13 +157,28 @@ function vtkStreamingOpenGLTexture(publicAPI, model) { for (let block = 0; block < normalBlocks; block++) { const yOffset = block * blockHeight; - // Dataview of block - const dataView = new TypedArrayConstructor( + let dataView = new TypedArrayConstructor( buffer, zOffset + block * multiRowBlockLengthInBytes, multiRowBlockLength ); + if ( + model.useHalfFloat && + (TypedArrayConstructor === Uint16Array || + TypedArrayConstructor === Int16Array) + ) { + // in the case we want to use halfFloat rendering (preferSizeOverAccuracy = true), + // we need to convert uint16 and int16 into fp16 format. + // This is the step where precision is lost for streaming volume viewport. + for (let idx = 0; idx < dataView.length; idx++) { + dataView[idx] = HalfFloat.toHalf(dataView[idx]); + } + if (TypedArrayConstructor === Int16Array) { + dataView = new Uint16Array(dataView); + } + } + gl.texSubImage3D( model.target, // target 0, // mipMap level (always zero) diff --git a/packages/core/src/RenderingEngine/vtkClasses/vtkStreamingOpenGLVolumeMapper.js b/packages/core/src/RenderingEngine/vtkClasses/vtkStreamingOpenGLVolumeMapper.js index 661bb12dbd..27125fc242 100644 --- a/packages/core/src/RenderingEngine/vtkClasses/vtkStreamingOpenGLVolumeMapper.js +++ b/packages/core/src/RenderingEngine/vtkClasses/vtkStreamingOpenGLVolumeMapper.js @@ -185,6 +185,10 @@ function vtkStreamingOpenGLVolumeMapper(publicAPI, model) { } if (shouldReset) { + model.scalarTexture.setOglNorm16Ext( + model.context.getExtension('EXT_texture_norm16') + ); + model.scalarTexture.releaseGraphicsResources(model._openGLRenderWindow); model.scalarTexture.resetFormatAndType(); diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 38d7170b12..d41c280bc0 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -35,6 +35,8 @@ import { setUseSharedArrayBuffer, resetUseCPURendering, resetUseSharedArrayBuffer, + getConfiguration, + setConfiguration, } from './init'; // Classes @@ -58,8 +60,12 @@ import { export type { Types }; export { + // init init, isCornerstoneInitialized, + // configs + getConfiguration, + setConfiguration, // enums Enums, CONSTANTS, diff --git a/packages/core/src/init.ts b/packages/core/src/init.ts index e658ad5ac4..9a90457f44 100644 --- a/packages/core/src/init.ts +++ b/packages/core/src/init.ts @@ -1,24 +1,61 @@ import { getGPUTier } from 'detect-gpu'; import { SharedArrayBufferModes } from './enums'; - let csRenderInitialized = false; -let useCPURendering = false; let useSharedArrayBuffer = true; -let sharedArrayBufferMode = SharedArrayBufferModes.TRUE; +let sharedArrayBufferMode = SharedArrayBufferModes.AUTO; +import { deepMerge } from './utilities'; +import { Cornerstone3DConfig } from './types'; +// TODO: move sharedArrayBuffer into config. +// TODO: change config into a class with methods to better control get/set +const defaultConfig = { + detectGPU: {}, + rendering: { + preferSizeOverAccuracy: true, + useCPURendering: false, + useNorm16Texture: _hasNorm16TextureSupport(), + }, + // cache + // ... +}; -// https://developer.mozilla.org/en-US/docs/Web/API/WebGL_API/By_example/Detect_WebGL -function hasActiveWebGLContext() { +let config = { + detectGPU: {}, + rendering: { + preferSizeOverAccuracy: true, + useCPURendering: false, + useNorm16Texture: _hasNorm16TextureSupport(), + }, + // cache + // ... +}; + +function _getGLContext(): RenderingContext { // Create canvas element. The canvas is not added to the // document itself, so it is never displayed in the // browser window. const canvas = document.createElement('canvas'); // Get WebGLRenderingContext from canvas element. const gl = - canvas.getContext('webgl') || canvas.getContext('experimental-webgl'); + canvas.getContext('webgl2') || + canvas.getContext('webgl') || + canvas.getContext('experimental-webgl'); + + return gl; +} + +// https://developer.mozilla.org/en-US/docs/Web/API/WebGL_API/By_example/Detect_WebGL +function _hasActiveWebGLContext() { + const gl = _getGLContext(); // Report the result. - if (gl && gl instanceof WebGLRenderingContext) { - return true; + if (gl && (gl as WebGL2RenderingContext).getExtension) { + const ext = (gl as WebGL2RenderingContext).getExtension( + 'EXT_texture_norm16' + ); + + if (ext) { + return true; + } } return false; @@ -26,6 +63,7 @@ function hasActiveWebGLContext() { function hasSharedArrayBuffer() { try { + /*eslint-disable no-constant-condition */ if (new SharedArrayBuffer(0)) { return true; } else { @@ -36,27 +74,47 @@ function hasSharedArrayBuffer() { } } +function _hasNorm16TextureSupport() { + const gl = _getGLContext(); + + if (gl) { + const ext = (gl as WebGL2RenderingContext).getExtension( + 'EXT_texture_norm16' + ); + + if (ext) { + return true; + } + } + + return false; +} + /** * Initialize the cornerstone-core. If the browser has a webgl context and * the detected gpu (by detect-gpu library) indicates the GPU is not low end we * will use webgl GPU rendering. Otherwise we will use cpu rendering. * - * @param defaultConfiguration - A configuration object + * @param configuration - A configuration object * @returns A promise that resolves to true cornerstone has been initialized successfully. * @category Initialization */ -async function init(defaultConfiguration = {}): Promise { +async function init(configuration = {}): Promise { if (csRenderInitialized) { return csRenderInitialized; } + // merge configs + config = deepMerge(defaultConfig, configuration); + // detectGPU - const hasWebGLContext = hasActiveWebGLContext(); + const hasWebGLContext = _hasActiveWebGLContext(); if (!hasWebGLContext) { - useCPURendering = true; console.log('CornerstoneRender: GPU not detected, using CPU rendering'); + config.rendering.useCPURendering = true; } else { const gpuTier = await getGPUTier(); + config.detectGPU = gpuTier; console.log( 'CornerstoneRender: Using detect-gpu to get the GPU benchmark:', gpuTier @@ -65,7 +123,7 @@ async function init(defaultConfiguration = {}): Promise { console.log( 'CornerstoneRender: GPU is not powerful enough, using CPU rendering' ); - useCPURendering = true; + config.rendering.useCPURendering = true; } else { console.log('CornerstoneRender: using GPU rendering'); } @@ -86,7 +144,7 @@ async function init(defaultConfiguration = {}): Promise { * */ function setUseCPURendering(status: boolean): void { - useCPURendering = status; + config.rendering.useCPURendering = status; csRenderInitialized = true; } @@ -97,7 +155,7 @@ function setUseCPURendering(status: boolean): void { * */ function resetUseCPURendering(): void { - useCPURendering = !hasActiveWebGLContext(); + config.rendering.useCPURendering = !_hasActiveWebGLContext(); } /** @@ -107,7 +165,7 @@ function resetUseCPURendering(): void { * */ function getShouldUseCPURendering(): boolean { - return useCPURendering; + return config.rendering.useCPURendering; } function setUseSharedArrayBuffer(mode: SharedArrayBufferModes | boolean): void { @@ -160,6 +218,21 @@ function isCornerstoneInitialized(): boolean { return csRenderInitialized; } +/** + * This function returns a copy of the config object. This is used to prevent the + * config object from being modified by other parts of the program. + * @returns A copy of the config object. + */ +function getConfiguration(): Cornerstone3DConfig { + // return a copy + // return JSON.parse(JSON.stringify(config)); + return config; +} + +function setConfiguration(c: Cornerstone3DConfig) { + config = c; +} + export { init, getShouldUseCPURendering, @@ -169,4 +242,6 @@ export { setUseSharedArrayBuffer, resetUseCPURendering, resetUseSharedArrayBuffer, + getConfiguration, + setConfiguration, }; diff --git a/packages/core/src/loaders/volumeLoader.ts b/packages/core/src/loaders/volumeLoader.ts index 9eea03fd87..c97d81cdfd 100644 --- a/packages/core/src/loaders/volumeLoader.ts +++ b/packages/core/src/loaders/volumeLoader.ts @@ -21,12 +21,12 @@ interface VolumeLoaderOptions { interface DerivedVolumeOptions { volumeId: string; targetBuffer?: { - type: 'Float32Array' | 'Uint8Array'; + type: 'Float32Array' | 'Uint8Array' | 'Uint16Array' | 'Int16Array'; sharedArrayBuffer?: boolean; }; } interface LocalVolumeOptions { - scalarData: Float32Array | Uint8Array; + scalarData: Float32Array | Uint8Array | Uint16Array | Int16Array; metadata: Metadata; dimensions: Point3; spacing: Point3; @@ -243,7 +243,8 @@ export async function createAndCacheVolume( * is given, it will be used to generate the intensity values for the derivedVolume. * Finally, it will save the volume in the cache. * @param referencedVolumeId - the volumeId from which the new volume will get its metadata - * @param options - DerivedVolumeOptions {uid: derivedVolumeUID, targetBuffer: { type: FLOAT32Array | Uint8Array}, scalarData: if provided} + * @param options - DerivedVolumeOptions {uid: derivedVolumeUID, targetBuffer: { type: Float32Array | Uint8Array | + * Uint16Array | Uint32Array }, scalarData: if provided} * * @returns ImageVolume */ @@ -280,6 +281,12 @@ export async function createAndCacheDerivedVolume( } else if (targetBuffer.type === 'Uint8Array') { numBytes = scalarLength; TypedArray = Uint8Array; + } else if (targetBuffer.type === 'Uint16Array') { + numBytes = scalarLength * 2; + TypedArray = Uint16Array; + } else if (targetBuffer.type === 'Int16Array') { + numBytes = scalarLength * 2; + TypedArray = Uint16Array; } else { throw new Error('TargetBuffer should be Float32Array or Uint8Array'); } @@ -360,10 +367,15 @@ export function createLocalVolume( if ( !scalarData || - !(scalarData instanceof Uint8Array || scalarData instanceof Float32Array) + !( + scalarData instanceof Uint8Array || + scalarData instanceof Float32Array || + scalarData instanceof Uint16Array || + scalarData instanceof Int16Array + ) ) { throw new Error( - 'To use createLocalVolume you should pass scalarData of type Uint8Array or Float32Array' + 'To use createLocalVolume you should pass scalarData of type Uint8Array, Uint16Array, Int16Array or Float32Array' ); } diff --git a/packages/core/src/types/Cornerstone3DConfig.ts b/packages/core/src/types/Cornerstone3DConfig.ts new file mode 100644 index 0000000000..f11582f5e7 --- /dev/null +++ b/packages/core/src/types/Cornerstone3DConfig.ts @@ -0,0 +1,31 @@ +type Cornerstone3DConfig = { + detectGPU: any; + rendering: { + // vtk.js supports 8bit integer textures and 32bit float textures. + // However, if the client has norm16 textures (it can be seen by visiting + // the webGl report at https://webglreport.com/?v=2), vtk will be default + // to use it to improve memory usage. However, if the client don't have + // it still another level of optimization can happen by setting the + // preferSizeOverAccuracy since it will reduce the size of the texture to half + // float at the cost of accuracy in rendering. This is a tradeoff that the + // client can decide. + // + // Read more in the following Pull Request: + // 1. HalfFloat: https://github.com/Kitware/vtk-js/pull/2046 + // 2. Norm16: https://github.com/Kitware/vtk-js/pull/2058 + preferSizeOverAccuracy: boolean; + // Whether the EXT_texture_norm16 extension is supported by the browser. + // WebGL 2 report (link: https://webglreport.com/?v=2) can be used to check + // if the browser supports this extension. + // In case the browser supports this extension, instead of using 32bit float + // textures, 16bit float textures will be used to reduce the memory usage where + // possible. + // Norm16 may not work currently due to the two active bugs in chrome + safari + // https://bugs.chromium.org/p/chromium/issues/detail?id=1408247 + // https://bugs.webkit.org/show_bug.cgi?id=252039 + useNorm16Texture: boolean; + useCPURendering: boolean; + }; +}; + +export default Cornerstone3DConfig; diff --git a/packages/core/src/types/IImageData.ts b/packages/core/src/types/IImageData.ts index 59b0a7b5c3..47d967c38a 100644 --- a/packages/core/src/types/IImageData.ts +++ b/packages/core/src/types/IImageData.ts @@ -15,7 +15,7 @@ interface IImageData { /** image origin */ origin: Point3; /** image scalarData which stores the array of pixelData */ - scalarData: Float32Array; + scalarData: Float32Array | Uint16Array | Uint8Array | Int16Array; /** vtkImageData object */ imageData: vtkImageData; /** image metadata - currently only modality */ diff --git a/packages/core/src/types/IVolume.ts b/packages/core/src/types/IVolume.ts index 0170097549..5648e7e7e4 100644 --- a/packages/core/src/types/IVolume.ts +++ b/packages/core/src/types/IVolume.ts @@ -3,7 +3,7 @@ import type Point3 from './Point3'; import type Metadata from './Metadata'; import Mat3 from './Mat3'; -type VolumeScalarData = Float32Array | Uint8Array; +type VolumeScalarData = Float32Array | Uint8Array | Uint16Array | Int16Array; /** * Cornerstone ImageVolume interface. diff --git a/packages/core/src/types/index.ts b/packages/core/src/types/index.ts index a0dc4ecd54..f0e28a8814 100644 --- a/packages/core/src/types/index.ts +++ b/packages/core/src/types/index.ts @@ -1,5 +1,5 @@ // @see: https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-8.html#-type-only-imports-and-export - +import type Cornerstone3DConfig from './Cornerstone3DConfig'; import type ICamera from './ICamera'; import type IEnabledElement from './IEnabledElement'; import type ICache from './ICache'; @@ -75,6 +75,9 @@ import type { IContourSet } from './IContourSet'; import type { IContour } from './IContour'; export type { + // config + Cornerstone3DConfig, + // ICamera, IStackViewport, IVolumeViewport, diff --git a/packages/core/src/utilities/createInt16SharedArray.ts b/packages/core/src/utilities/createInt16SharedArray.ts new file mode 100644 index 0000000000..e895c5e7a1 --- /dev/null +++ b/packages/core/src/utilities/createInt16SharedArray.ts @@ -0,0 +1,43 @@ +import global from '../global'; +/** + * A helper function that creates a new Int16 that utilized a shared + * array buffer. This allows the array to be updated simultaneously in + * workers or the main thread. Depending on the system (the CPU, the OS, the Browser) + * it can take a while until the change is propagated to all contexts. + * + * @see {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/SharedArrayBuffer|MDN: SharedArrayBuffer} + * @remarks + * We use SharedArrayBuffers in our ImageCache class. It's what allows us to + * stream data to build a volume. It's important to note that SharedArrayBuffer + * does not work out of the box for all web browsers. In some, it is disabled + * behind a flag; in others, it has been removed entirely. + * + * @example + * Creating an array for a Volume with known dimensions: + * ``` + * const dimensions = [512, 512, 25]; + * const scalarData = createInt16SharedArray(dimensions[0] * dimensions[1] * dimensions[2]); + * ``` + * + * @param length - frame size * number of frames + * @returns a Int8Array with an underlying SharedArrayBuffer + * @public + */ +function createInt16SharedArray(length: number): Int16Array { + if (!window.crossOriginIsolated) { + throw new Error( + 'Your page is NOT cross-origin isolated, see https://developer.mozilla.org/en-US/docs/Web/API/crossOriginIsolated' + ); + } + if (window.SharedArrayBuffer === undefined) { + throw new Error( + 'SharedArrayBuffer is NOT supported in your browser see https://developer.chrome.com/blog/enabling-shared-array-buffer/' + ); + } + + const sharedArrayBuffer = new SharedArrayBuffer(length * 2); + + return new Int16Array(sharedArrayBuffer); +} + +export default createInt16SharedArray; diff --git a/packages/core/src/utilities/createUInt16SharedArray.ts b/packages/core/src/utilities/createUInt16SharedArray.ts new file mode 100644 index 0000000000..6f2ae8c154 --- /dev/null +++ b/packages/core/src/utilities/createUInt16SharedArray.ts @@ -0,0 +1,43 @@ +import global from '../global'; +/** + * A helper function that creates a new Uint16 that utilized a shared + * array buffer. This allows the array to be updated simultaneously in + * workers or the main thread. Depending on the system (the CPU, the OS, the Browser) + * it can take a while until the change is propagated to all contexts. + * + * @see {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/SharedArrayBuffer|MDN: SharedArrayBuffer} + * @remarks + * We use SharedArrayBuffers in our ImageCache class. It's what allows us to + * stream data to build a volume. It's important to note that SharedArrayBuffer + * does not work out of the box for all web browsers. In some, it is disabled + * behind a flag; in others, it has been removed entirely. + * + * @example + * Creating an array for a Volume with known dimensions: + * ``` + * const dimensions = [512, 512, 25]; + * const scalarData = createUint16SharedArray(dimensions[0] * dimensions[1] * dimensions[2]); + * ``` + * + * @param length - frame size * number of frames + * @returns a Uint8Array with an underlying SharedArrayBuffer + * @public + */ +function createUint16SharedArray(length: number): Uint16Array { + if (!window.crossOriginIsolated) { + throw new Error( + 'Your page is NOT cross-origin isolated, see https://developer.mozilla.org/en-US/docs/Web/API/crossOriginIsolated' + ); + } + if (window.SharedArrayBuffer === undefined) { + throw new Error( + 'SharedArrayBuffer is NOT supported in your browser see https://developer.chrome.com/blog/enabling-shared-array-buffer/' + ); + } + + const sharedArrayBuffer = new SharedArrayBuffer(length * 2); + + return new Uint16Array(sharedArrayBuffer); +} + +export default createUint16SharedArray; diff --git a/packages/tools/src/utilities/deepMerge.js b/packages/core/src/utilities/deepMerge.ts similarity index 81% rename from packages/tools/src/utilities/deepMerge.js rename to packages/core/src/utilities/deepMerge.ts index 5775964eb8..b54fd83b87 100644 --- a/packages/tools/src/utilities/deepMerge.js +++ b/packages/core/src/utilities/deepMerge.ts @@ -18,7 +18,7 @@ const cloneIfNecessary = (value, optionsArgument) => { const clone = optionsArgument && optionsArgument.clone === true; return clone && isMergeableObject(value) - ? deepmerge(emptyTarget(value), value, optionsArgument) + ? deepMerge(emptyTarget(value), value, optionsArgument) : value; }; @@ -29,7 +29,7 @@ const defaultArrayMerge = (target, source, optionsArgument) => { if (typeof destination[i] === 'undefined') { destination[i] = cloneIfNecessary(e, optionsArgument); } else if (isMergeableObject(e)) { - destination[i] = deepmerge(target[i], e, optionsArgument); + destination[i] = deepMerge(target[i], e, optionsArgument); } else if (target.indexOf(e) === -1) { destination.push(cloneIfNecessary(e, optionsArgument)); } @@ -50,7 +50,7 @@ const mergeObject = (target, source, optionsArgument) => { if (!isMergeableObject(source[key]) || !target[key]) { destination[key] = cloneIfNecessary(source[key], optionsArgument); } else { - destination[key] = deepmerge(target[key], source[key], optionsArgument); + destination[key] = deepMerge(target[key], source[key], optionsArgument); } }); @@ -59,12 +59,12 @@ const mergeObject = (target, source, optionsArgument) => { /** * Merge two objects, recursively merging any objects that are arrays - * @param [target] - The target object. - * @param [source] - The source object to merge into the target object. - * @param [optionsArgument] - The options object. + * @param target - The target object. + * @param source - The source object to merge into the target object. + * @param optionsArgument - The options object. * @returns The merged object. */ -const deepmerge = (target = {}, source = {}, optionsArgument = undefined) => { +const deepMerge = (target = {}, source = {}, optionsArgument = undefined) => { const array = Array.isArray(source); const options = optionsArgument || { arrayMerge: defaultArrayMerge }; const arrayMerge = options.arrayMerge || defaultArrayMerge; @@ -78,4 +78,4 @@ const deepmerge = (target = {}, source = {}, optionsArgument = undefined) => { return mergeObject(target, source, optionsArgument); }; -export default deepmerge; +export default deepMerge; diff --git a/packages/core/src/utilities/getScalarDataType.ts b/packages/core/src/utilities/getScalarDataType.ts new file mode 100644 index 0000000000..e25ff4b473 --- /dev/null +++ b/packages/core/src/utilities/getScalarDataType.ts @@ -0,0 +1,31 @@ +import { ScalingParameters } from '../types'; + +/** + * If the scalar data is a Uint8Array, return 'Uint8Array'. If the scalar data is a + * Float32Array, return 'Float32Array'. If the scalar data is a Int16Array, return + * 'Int16Array'. If the scalar data is a Uint16Array, return 'Uint16Array'. If the + * scalar data is none of the above, throw an error. + * @param {ScalingParameters} scalingParameters - { + * @param {any} [scalarData] - The data to be converted. + * @returns The data type of the scalar data. + */ +export default function getScalarDataType( + scalingParameters: ScalingParameters, + scalarData?: any +): string { + let type; + + if (scalarData && scalarData instanceof Uint8Array) { + type = 'Uint8Array'; + } else if (scalarData instanceof Float32Array) { + type = 'Float32Array'; + } else if (scalarData instanceof Int16Array) { + type = 'Int16Array'; + } else if (scalarData instanceof Uint16Array) { + type = 'Uint16Array'; + } else { + throw new Error('Unsupported array type'); + } + + return type; +} diff --git a/packages/core/src/utilities/getScalingParameters.ts b/packages/core/src/utilities/getScalingParameters.ts new file mode 100644 index 0000000000..cfa46e7652 --- /dev/null +++ b/packages/core/src/utilities/getScalingParameters.ts @@ -0,0 +1,35 @@ +import { get as metaDataGet } from '../metaData'; +import { ScalingParameters } from '../types'; + +/** + * It returns the scaling parameters for the image with the given imageId. This can be + * used to get passed (as an option) to the imageLoader in order to apply scaling to the image inside + * the imageLoader. + * @param imageId - The imageId of the image + * @returns ScalingParameters + */ +export default function getScalingParameters( + imageId: string +): ScalingParameters { + const modalityLutModule = metaDataGet('modalityLutModule', imageId) || {}; + const generalSeriesModule = metaDataGet('generalSeriesModule', imageId) || {}; + + const { modality } = generalSeriesModule; + + const scalingParameters = { + rescaleSlope: modalityLutModule.rescaleSlope, + rescaleIntercept: modalityLutModule.rescaleIntercept, + modality, + }; + + const suvFactor = metaDataGet('scalingModule', imageId) || {}; + + return { + ...scalingParameters, + ...(modality === 'PT' && { + suvbw: suvFactor.suvbw, + suvbsa: suvFactor.suvbsa, + suvlbm: suvFactor.suvlbm, + }), + }; +} diff --git a/packages/core/src/utilities/index.ts b/packages/core/src/utilities/index.ts index 1171e2a8fa..e5dca7dc56 100644 --- a/packages/core/src/utilities/index.ts +++ b/packages/core/src/utilities/index.ts @@ -13,6 +13,8 @@ import isEqual from './isEqual'; import isOpposite from './isOpposite'; import createUint8SharedArray from './createUint8SharedArray'; import createFloat32SharedArray from './createFloat32SharedArray'; +import createUint16SharedArray from './createUInt16SharedArray'; +import createInt16SharedArray from './createInt16SharedArray'; import getClosestImageId from './getClosestImageId'; import getSpacingInNormalDirection from './getSpacingInNormalDirection'; import getTargetVolumeAndSpacingInNormalDir from './getTargetVolumeAndSpacingInNormalDir'; @@ -36,6 +38,9 @@ import spatialRegistrationMetadataProvider from './spatialRegistrationMetadataPr import getViewportImageCornersInWorld from './getViewportImageCornersInWorld'; import hasNaNValues from './hasNaNValues'; import applyPreset from './applyPreset'; +import deepMerge from './deepMerge'; +import getScalingParameters from './getScalingParameters'; +import getScalarDataType from './getScalarDataType'; // name spaces import * as planar from './planar'; @@ -58,6 +63,8 @@ export { isOpposite, createFloat32SharedArray, createUint8SharedArray, + createUint16SharedArray, + createInt16SharedArray, windowLevel, getClosestImageId, getSpacingInNormalDirection, @@ -83,4 +90,7 @@ export { getViewportImageCornersInWorld, hasNaNValues, applyPreset, + deepMerge, + getScalingParameters, + getScalarDataType, }; diff --git a/packages/core/src/utilities/loadImageToCanvas.ts b/packages/core/src/utilities/loadImageToCanvas.ts index 0bf55adc56..dcdbc06968 100644 --- a/packages/core/src/utilities/loadImageToCanvas.ts +++ b/packages/core/src/utilities/loadImageToCanvas.ts @@ -58,11 +58,6 @@ export default function loadImageToCanvas( // 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: 'Float32Array', - offset: null, - length: null, - }, preScale: { enabled: true, }, diff --git a/packages/streaming-image-volume-loader/src/BaseStreamingImageVolume.ts b/packages/streaming-image-volume-loader/src/BaseStreamingImageVolume.ts index 362fc0f1d5..ba3f5e6885 100644 --- a/packages/streaming-image-volume-loader/src/BaseStreamingImageVolume.ts +++ b/packages/streaming-image-volume-loader/src/BaseStreamingImageVolume.ts @@ -268,6 +268,10 @@ export default class BaseStreamingImageVolume extends ImageVolume { type = 'Uint8Array'; } else if (scalarData instanceof Float32Array) { type = 'Float32Array'; + } else if (scalarData instanceof Uint16Array) { + type = 'Uint16Array'; + } else if (scalarData instanceof Int16Array) { + type = 'Int16Array'; } else { throw new Error('Unsupported array type'); } @@ -455,18 +459,40 @@ export default class BaseStreamingImageVolume extends ImageVolume { const offset = options.targetBuffer.offset; // in bytes const length = options.targetBuffer.length; // in frames + const pixelData = image.pixelData + ? image.pixelData + : image.getPixelData(); + try { if (scalarData instanceof Float32Array) { const bytesInFloat = 4; - const floatView = new Float32Array(image.pixelData); + const floatView = new Float32Array(pixelData); if (floatView.length !== length) { throw 'Error pixelData length does not match frame length'; } + // since set is based on the underlying type, + // we need to divide the offset bytes by the byte type scalarData.set(floatView, offset / bytesInFloat); } + if (scalarData instanceof Int16Array) { + const bytesInInt16 = 2; + const intView = new Int16Array(pixelData); + if (intView.length !== length) { + throw 'Error pixelData length does not match frame length'; + } + scalarData.set(intView, offset / bytesInInt16); + } + if (scalarData instanceof Uint16Array) { + const bytesInUint16 = 2; + const intView = new Uint16Array(pixelData); + if (intView.length !== length) { + throw 'Error pixelData length does not match frame length'; + } + scalarData.set(intView, offset / bytesInUint16); + } if (scalarData instanceof Uint8Array) { const bytesInUint8 = 1; - const intView = new Uint8Array(image.pixelData); + const intView = new Uint8Array(pixelData); if (intView.length !== length) { throw 'Error pixelData length does not match frame length'; } diff --git a/packages/streaming-image-volume-loader/src/StreamingDynamicImageVolume.ts b/packages/streaming-image-volume-loader/src/StreamingDynamicImageVolume.ts index ca07a29ee8..8ab75399be 100644 --- a/packages/streaming-image-volume-loader/src/StreamingDynamicImageVolume.ts +++ b/packages/streaming-image-volume-loader/src/StreamingDynamicImageVolume.ts @@ -5,7 +5,7 @@ type TimePoint = { /** imageIds of each timepoint */ imageIds: Array; /** volume scalar data */ - scalarData: Float32Array | Uint8Array; + scalarData: Float32Array | Uint8Array | Uint16Array | Int16Array; }; /** diff --git a/packages/streaming-image-volume-loader/src/cornerstoneStreamingImageVolumeLoader.ts b/packages/streaming-image-volume-loader/src/cornerstoneStreamingImageVolumeLoader.ts index 03d5ad5db3..20456ae3f6 100644 --- a/packages/streaming-image-volume-loader/src/cornerstoneStreamingImageVolumeLoader.ts +++ b/packages/streaming-image-volume-loader/src/cornerstoneStreamingImageVolumeLoader.ts @@ -5,13 +5,20 @@ import { imageLoader, imageLoadPoolManager, getShouldUseSharedArrayBuffer, + getConfiguration, + utilities as csUtils, } from '@cornerstonejs/core'; import type { Types } from '@cornerstonejs/core'; import { vec3 } from 'gl-matrix'; import { makeVolumeMetadata, sortImageIdsAndGetSpacing } from './helpers'; import StreamingImageVolume from './StreamingImageVolume'; -const { createUint8SharedArray, createFloat32SharedArray } = utilities; +const { + createUint8SharedArray, + createFloat32SharedArray, + createUint16SharedArray, + createInt16SharedArray, +} = utilities; interface IVolumeLoader { promise: Promise; @@ -43,6 +50,10 @@ function cornerstoneStreamingImageVolumeLoader( ); } + const { useNorm16Texture, preferSizeOverAccuracy } = + getConfiguration().rendering; + const use16BitDataType = useNorm16Texture || preferSizeOverAccuracy; + async function getStreamingImageVolume() { /** * Check if we are using the `wadouri:` scheme, and if so, preload first, @@ -86,6 +97,19 @@ function cornerstoneStreamingImageVolumeLoader( const volumeMetadata = makeVolumeMetadata(imageIds); + // For a streaming volume, the data type cannot rely on cswil to load + // the proper array buffer type. This is because the target buffer container + // must be decided ahead of time. + // TODO: move this logic into CSWIL to avoid logic duplication. + // We check if scaling parameters are negative we choose Int16 instead of + // Uint16 for cases where BitsAllocated is 16. + const imageIdIndex = Math.floor(imageIds.length / 2); + const imageId = imageIds[imageIdIndex]; + const scalingParameters = csUtils.getScalingParameters(imageId); + const hasNegativeRescale = + scalingParameters.rescaleIntercept < 0 || + scalingParameters.rescaleSlope < 0; + const { BitsAllocated, PixelRepresentation, @@ -127,60 +151,63 @@ function cornerstoneStreamingImageVolumeLoader( ...scanAxisNormal, ] as Types.Mat3; const signed = PixelRepresentation === 1; - - // Check if it fits in the cache before we allocate data - // TODO Improve this when we have support for more types - // NOTE: We use 4 bytes per voxel as we are using Float32. - const bytesPerVoxel = BitsAllocated === 16 ? 4 : 1; - const sizeInBytesPerComponent = - bytesPerVoxel * dimensions[0] * dimensions[1] * dimensions[2]; - - let numComponents = 1; - if (PhotometricInterpretation === 'RGB') { - numComponents = 3; - } - - const sizeInBytes = sizeInBytesPerComponent * numComponents; - - // check if there is enough space in unallocated + image Cache - const isCacheable = cache.isCacheable(sizeInBytes); - if (!isCacheable) { - throw new Error(Enums.Events.CACHE_SIZE_EXCEEDED); - } - - cache.decacheIfNecessaryUntilBytesAvailable(sizeInBytes); - + const numComponents = PhotometricInterpretation === 'RGB' ? 3 : 1; const useSharedArrayBuffer = getShouldUseSharedArrayBuffer(); const length = dimensions[0] * dimensions[1] * dimensions[2]; + const handleCache = (sizeInBytes) => { + if (!cache.isCacheable(sizeInBytes)) { + throw new Error(Enums.Events.CACHE_SIZE_EXCEEDED); + } + cache.decacheIfNecessaryUntilBytesAvailable(sizeInBytes); + }; - let scalarData; + let scalarData, sizeInBytes; switch (BitsAllocated) { case 8: if (signed) { throw new Error( '8 Bit signed images are not yet supported by this plugin.' ); - } else { - scalarData = useSharedArrayBuffer - ? createUint8SharedArray(length) - : new Uint8Array(length); } - + sizeInBytes = length; + handleCache(sizeInBytes); + scalarData = useSharedArrayBuffer + ? createUint8SharedArray(length) + : new Uint8Array(length); break; case 16: + sizeInBytes = length * 2; + if (use16BitDataType && (signed || hasNegativeRescale)) { + handleCache(sizeInBytes); + scalarData = useSharedArrayBuffer + ? createInt16SharedArray(length) + : new Int16Array(length); + break; + } + + if (use16BitDataType && !signed && !hasNegativeRescale) { + handleCache(sizeInBytes); + scalarData = useSharedArrayBuffer + ? createUint16SharedArray(length) + : new Uint16Array(length); + break; + } + sizeInBytes = length * 4; + handleCache(sizeInBytes); scalarData = useSharedArrayBuffer ? createFloat32SharedArray(length) : new Float32Array(length); - break; case 24: + sizeInBytes = length * numComponents; + handleCache(sizeInBytes); + // hacky because we don't support alpha channel in dicom scalarData = useSharedArrayBuffer ? createUint8SharedArray(length * numComponents) : new Uint8Array(length * numComponents); - break; } diff --git a/packages/streaming-image-volume-loader/src/helpers/scaleArray.ts b/packages/streaming-image-volume-loader/src/helpers/scaleArray.ts index 725b713ad8..756a418dcf 100644 --- a/packages/streaming-image-volume-loader/src/helpers/scaleArray.ts +++ b/packages/streaming-image-volume-loader/src/helpers/scaleArray.ts @@ -8,9 +8,9 @@ import type { Types } from '@cornerstonejs/core'; * @returns The array is being scaled */ export default function scaleArray( - array: Float32Array | Uint8Array, + array: Float32Array | Uint8Array | Uint16Array | Int16Array, scalingParameters: Types.ScalingParameters -): Float32Array | Uint8Array { +): Float32Array | Uint8Array | Uint16Array | Int16Array { const arrayLength = array.length; const { rescaleSlope, rescaleIntercept, suvbw } = scalingParameters; diff --git a/packages/tools/src/store/ToolGroupManager/ToolGroup.ts b/packages/tools/src/store/ToolGroupManager/ToolGroup.ts index 969ad375a2..4c6fa125fe 100644 --- a/packages/tools/src/store/ToolGroupManager/ToolGroup.ts +++ b/packages/tools/src/store/ToolGroupManager/ToolGroup.ts @@ -6,6 +6,7 @@ import { getRenderingEngines, getEnabledElementByIds, Settings, + utilities as csUtils, } from '@cornerstonejs/core'; import type { Types } from '@cornerstonejs/core'; import { state } from '../index'; @@ -19,7 +20,6 @@ import { import { MouseCursor, SVGMouseCursor } from '../../cursors'; import { initElementCursor } from '../../cursors/elementCursor'; -import deepmerge from '../../utilities/deepMerge'; const { Active, Passive, Enabled, Disabled } = ToolModes; @@ -607,7 +607,7 @@ export default class ToolGroup implements IToolGroup { if (overwrite) { _configuration = configuration; } else { - _configuration = deepmerge( + _configuration = csUtils.deepMerge( this._toolInstances[toolName].configuration, configuration ); diff --git a/packages/tools/src/tools/WindowLevelTool.ts b/packages/tools/src/tools/WindowLevelTool.ts index 28b82197c4..677c68de8f 100644 --- a/packages/tools/src/tools/WindowLevelTool.ts +++ b/packages/tools/src/tools/WindowLevelTool.ts @@ -220,6 +220,12 @@ class WindowLevelTool extends BaseTool { } else if (scalarData instanceof Uint8Array) { bytesPerVoxel = 1; TypedArrayConstructor = Uint8Array; + } else if (scalarData instanceof Uint16Array) { + bytesPerVoxel = 2; + TypedArrayConstructor = Uint16Array; + } else if (scalarData instanceof Int16Array) { + bytesPerVoxel = 2; + TypedArrayConstructor = Int16Array; } const buffer = scalarData.buffer; diff --git a/packages/tools/src/tools/base/BaseTool.ts b/packages/tools/src/tools/base/BaseTool.ts index 1a330e54ac..00068d187e 100644 --- a/packages/tools/src/tools/base/BaseTool.ts +++ b/packages/tools/src/tools/base/BaseTool.ts @@ -1,6 +1,5 @@ import { StackViewport, VolumeViewport, utilities } from '@cornerstonejs/core'; import { Types } from '@cornerstonejs/core'; -import deepMerge from '../../utilities/deepMerge'; import { ToolModes } from '../../enums'; import { InteractionTypes, ToolProps, PublicToolProps } from '../../types'; @@ -37,7 +36,7 @@ abstract class BaseTool implements IBaseTool { public mode: ToolModes; constructor(toolProps: PublicToolProps, defaultToolProps: ToolProps) { - const initialProps = deepMerge(defaultToolProps, toolProps); + const initialProps = utilities.deepMerge(defaultToolProps, toolProps); const { configuration = {}, @@ -87,7 +86,10 @@ abstract class BaseTool implements IBaseTool { * @param configuration - toolConfiguration */ public setConfiguration(newConfiguration: Record): void { - this.configuration = deepMerge(this.configuration, newConfiguration); + this.configuration = utilities.deepMerge( + this.configuration, + newConfiguration + ); } /** diff --git a/packages/tools/src/tools/displayTools/Contour/contourDisplay.ts b/packages/tools/src/tools/displayTools/Contour/contourDisplay.ts index da9c6e2fd5..1d6472fe02 100644 --- a/packages/tools/src/tools/displayTools/Contour/contourDisplay.ts +++ b/packages/tools/src/tools/displayTools/Contour/contourDisplay.ts @@ -4,6 +4,7 @@ import { Types, utilities, Enums, + utilities as csUtils, } from '@cornerstonejs/core'; import * as SegmentationState from '../../../stateManagement/segmentation/segmentationState'; @@ -16,7 +17,6 @@ import { ToolGroupSpecificRepresentation, } from '../../../types/SegmentationStateTypes'; -import { deepMerge } from '../../../utilities'; import removeContourFromElement from './removeContourFromElement'; import { addContourToElement, @@ -62,7 +62,7 @@ async function addSegmentationRepresentation( // the first one const currentToolGroupConfig = SegmentationConfig.getToolGroupSpecificConfig(toolGroupId); - const mergedConfig = deepMerge( + const mergedConfig = csUtils.deepMerge( currentToolGroupConfig, toolGroupSpecificConfig ); diff --git a/packages/tools/src/tools/displayTools/Labelmap/labelmapDisplay.ts b/packages/tools/src/tools/displayTools/Labelmap/labelmapDisplay.ts index c9f7b17ce1..efcf9336d5 100644 --- a/packages/tools/src/tools/displayTools/Labelmap/labelmapDisplay.ts +++ b/packages/tools/src/tools/displayTools/Labelmap/labelmapDisplay.ts @@ -21,7 +21,6 @@ import { import addLabelmapToElement from './addLabelmapToElement'; -import { deepMerge } from '../../../utilities'; import removeLabelmapFromElement from './removeLabelmapFromElement'; const MAX_NUMBER_COLORS = 255; @@ -78,7 +77,7 @@ async function addSegmentationRepresentation( const currentToolGroupConfig = SegmentationConfig.getToolGroupSpecificConfig(toolGroupId); - const mergedConfig = deepMerge( + const mergedConfig = utilities.deepMerge( currentToolGroupConfig, toolGroupSpecificConfig ); diff --git a/packages/tools/src/tools/displayTools/SegmentationDisplayTool.ts b/packages/tools/src/tools/displayTools/SegmentationDisplayTool.ts index 375376a812..40f44252d7 100644 --- a/packages/tools/src/tools/displayTools/SegmentationDisplayTool.ts +++ b/packages/tools/src/tools/displayTools/SegmentationDisplayTool.ts @@ -1,5 +1,9 @@ import { BaseTool } from '../base'; -import { getEnabledElementByIds, Types } from '@cornerstonejs/core'; +import { + getEnabledElementByIds, + Types, + utilities as csUtils, +} from '@cornerstonejs/core'; import Representations from '../../enums/SegmentationRepresentations'; import { getSegmentationRepresentations } from '../../stateManagement/segmentation/segmentationState'; import { labelmapDisplay } from './Labelmap'; @@ -10,7 +14,6 @@ import { getToolGroup } from '../../store/ToolGroupManager'; import { PublicToolProps, ToolProps } from '../../types'; -import { deepMerge } from '../../utilities'; import { SegmentationRepresentationConfig, ToolGroupSpecificRepresentation, @@ -182,7 +185,7 @@ class SegmentationDisplayTool extends BaseTool { const globalConfig = segmentationConfig.getGlobalConfig(); // merge two configurations and override the global config - const mergedConfig = deepMerge(globalConfig, toolGroupConfig); + const mergedConfig = csUtils.deepMerge(globalConfig, toolGroupConfig); return mergedConfig; } diff --git a/packages/tools/src/tools/segmentation/PaintFillTool.ts b/packages/tools/src/tools/segmentation/PaintFillTool.ts index 282c8e4384..5915884e6d 100644 --- a/packages/tools/src/tools/segmentation/PaintFillTool.ts +++ b/packages/tools/src/tools/segmentation/PaintFillTool.ts @@ -183,7 +183,7 @@ class PaintFillTool extends BaseTool { }; private generateHelpers = ( - scalarData: Float32Array | Uint8Array, + scalarData: Float32Array | Uint8Array | Uint16Array | Int16Array, dimensions: Types.Point3, seedIndex3D: Types.Point3, fixedDimension = 2 diff --git a/packages/tools/src/utilities/index.ts b/packages/tools/src/utilities/index.ts index f42ce92273..9c7c844747 100644 --- a/packages/tools/src/utilities/index.ts +++ b/packages/tools/src/utilities/index.ts @@ -5,7 +5,6 @@ import { // Lodash/common JS functionality import debounce from './debounce'; -import deepMerge from './deepMerge'; import throttle from './throttle'; import isObject from './isObject'; import clip from './clip'; @@ -43,7 +42,6 @@ export { viewportFilters, drawing, debounce, - deepMerge, dynamicVolume, throttle, orientation, diff --git a/packages/tools/src/utilities/segmentation/createLabelmapVolumeForViewport.ts b/packages/tools/src/utilities/segmentation/createLabelmapVolumeForViewport.ts index e30fa54dc8..a7be313a04 100644 --- a/packages/tools/src/utilities/segmentation/createLabelmapVolumeForViewport.ts +++ b/packages/tools/src/utilities/segmentation/createLabelmapVolumeForViewport.ts @@ -24,9 +24,9 @@ export default async function createLabelmapVolumeForViewport(input: { segmentationId?: string; options?: { volumeId?: string; - scalarData?: Float32Array | Uint8Array; + scalarData?: Float32Array | Uint8Array | Uint16Array | Int16Array; targetBuffer?: { - type: 'Float32Array' | 'Uint8Array'; + type: 'Float32Array' | 'Uint8Array' | 'Uint16Array' | 'Int8Array'; }; metadata?: any; dimensions?: Types.Point3; diff --git a/packages/tools/src/utilities/stackPrefetch/stackPrefetch.ts b/packages/tools/src/utilities/stackPrefetch/stackPrefetch.ts index 2d27a66027..1ee97a1642 100644 --- a/packages/tools/src/utilities/stackPrefetch/stackPrefetch.ts +++ b/packages/tools/src/utilities/stackPrefetch/stackPrefetch.ts @@ -247,11 +247,6 @@ function prefetch(element) { // 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: 'Float32Array', - offset: null, - length: null, - }, preScale: { enabled: true, }, diff --git a/utils/demo/helpers/initCornerstoneDICOMImageLoader.js b/utils/demo/helpers/initCornerstoneDICOMImageLoader.js index 77f7dafd78..eb7411ba68 100644 --- a/utils/demo/helpers/initCornerstoneDICOMImageLoader.js +++ b/utils/demo/helpers/initCornerstoneDICOMImageLoader.js @@ -6,6 +6,8 @@ import cornerstoneWADOImageLoader from 'cornerstone-wado-image-loader'; window.cornerstone = cornerstone; window.cornerstoneTools = cornerstoneTools; +const { preferSizeOverAccuracy, useNorm16Texture } = + cornerstone.getConfiguration().rendering; export default function initCornerstoneDICOMImageLoader() { cornerstoneWADOImageLoader.external.cornerstone = cornerstone; @@ -14,6 +16,7 @@ export default function initCornerstoneDICOMImageLoader() { useWebWorkers: true, decodeConfig: { convertFloatPixelDataToInt: false, + use16BitDataType: preferSizeOverAccuracy || useNorm16Texture, }, }); From 52c9fcaedd28b024415655be29ee52c197e10f9b Mon Sep 17 00:00:00 2001 From: Ouwen Huang Date: Mon, 13 Mar 2023 01:01:05 +0000 Subject: [PATCH 2/2] config: turn default off --- packages/core/src/init.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/core/src/init.ts b/packages/core/src/init.ts index 9a90457f44..07d20cce4d 100644 --- a/packages/core/src/init.ts +++ b/packages/core/src/init.ts @@ -2,7 +2,7 @@ import { getGPUTier } from 'detect-gpu'; import { SharedArrayBufferModes } from './enums'; let csRenderInitialized = false; let useSharedArrayBuffer = true; -let sharedArrayBufferMode = SharedArrayBufferModes.AUTO; +let sharedArrayBufferMode = SharedArrayBufferModes.TRUE; import { deepMerge } from './utilities'; import { Cornerstone3DConfig } from './types'; // TODO: move sharedArrayBuffer into config. @@ -10,9 +10,9 @@ import { Cornerstone3DConfig } from './types'; const defaultConfig = { detectGPU: {}, rendering: { - preferSizeOverAccuracy: true, + preferSizeOverAccuracy: false, useCPURendering: false, - useNorm16Texture: _hasNorm16TextureSupport(), + useNorm16Texture: false, // _hasNorm16TextureSupport(), }, // cache // ... @@ -21,9 +21,9 @@ const defaultConfig = { let config = { detectGPU: {}, rendering: { - preferSizeOverAccuracy: true, + preferSizeOverAccuracy: false, useCPURendering: false, - useNorm16Texture: _hasNorm16TextureSupport(), + useNorm16Texture: false, // _hasNorm16TextureSupport(), }, // cache // ...