diff --git a/common/reviews/api/core.api.md b/common/reviews/api/core.api.md index 38bfa33f51..a20979089c 100644 --- a/common/reviews/api/core.api.md +++ b/common/reviews/api/core.api.md @@ -8,6 +8,7 @@ import { mat4 } from 'gl-matrix'; import { vec3 } from 'gl-matrix'; import type vtkActor from '@kitware/vtk.js/Rendering/Core/Actor'; import type { vtkCamera } from '@kitware/vtk.js/Rendering/Core/Camera'; +import vtkColorTransferFunction from '@kitware/vtk.js/Rendering/Core/ColorTransferFunction'; import type { vtkImageData } from '@kitware/vtk.js/Common/DataModel/ImageData'; import vtkImageSlice from '@kitware/vtk.js/Rendering/Core/ImageSlice'; import type { vtkObject } from '@kitware/vtk.js/interfaces'; @@ -69,6 +70,8 @@ export abstract class BaseVolumeViewport extends Viewport implements IVolumeView // (undocumented) getIntensityFromWorld(point: Point3): number; // (undocumented) + getProperties: () => VolumeViewportProperties; + // (undocumented) getSlabThickness(): number; // (undocumented) hasImageURI: (imageURI: string) => boolean; @@ -85,7 +88,7 @@ export abstract class BaseVolumeViewport extends Viewport implements IVolumeView // (undocumented) setOrientation(orientation: OrientationAxis, immediate?: boolean): void; // (undocumented) - setProperties({ voiRange }?: VolumeViewportProperties, volumeId?: string, suppressEvents?: boolean): void; + setProperties({ voiRange, VOILUTFunction }?: VolumeViewportProperties, volumeId?: string, suppressEvents?: boolean): void; // (undocumented) setSlabThickness(slabThickness: number, filterActorUIDs?: string[]): void; // (undocumented) @@ -461,9 +464,15 @@ function createAndCacheVolume(volumeId: string, options: VolumeLoaderOptions): P // @public (undocumented) function createFloat32SharedArray(length: number): Float32Array; +// @public (undocumented) +function createLinearRGBTransferFunction(voiRange: VOIRange): vtkColorTransferFunction; + // @public (undocumented) function createLocalVolume(options: LocalVolumeOptions, volumeId: string, preventCache?: boolean): ImageVolume; +// @public (undocumented) +function createSigmoidRGBTransferFunction(voiRange: VOIRange, approximationNodes?: number): vtkColorTransferFunction; + // @public (undocumented) function createUint8SharedArray(length: number): Uint8Array; @@ -511,7 +520,8 @@ declare namespace Enums { OrientationAxis, SharedArrayBufferModes, GeometryType, - ContourType + ContourType, + VOILUTFunctionType } } export { Enums } @@ -710,6 +720,9 @@ function getViewportsWithImageURI(imageURI: string, renderingEngineId?: string): // @public (undocumented) function getViewportsWithVolumeId(volumeId: string, renderingEngineId?: string): Array; +// @public (undocumented) +function getVoiFromSigmoidRGBTransferFunction(cfun: vtkColorTransferFunction): [number, number]; + // @public (undocumented) function getVolumeActorCorners(volumeActor: any): Array; @@ -993,6 +1006,8 @@ interface IImage { // (undocumented) voiLUT?: CPUFallbackLUT; // (undocumented) + voiLUTFunction: string; + // (undocumented) width: number; // (undocumented) windowCenter: number[] | number; @@ -1611,7 +1626,7 @@ interface IVolumeViewport extends IViewport { // (undocumented) getIntensityFromWorld(point: Point3): number; // (undocumented) - getProperties: () => any; + getProperties: () => VolumeViewportProperties; // (undocumented) getSlabThickness(): number; // (undocumented) @@ -1685,6 +1700,7 @@ type Metadata = { Columns: number; Rows: number; voiLut: Array; + VOILUTFunction: string; }; declare namespace metaData { @@ -2033,7 +2049,7 @@ export class StackViewport extends Viewport implements IStackViewport { // (undocumented) setImageIdIndex(imageIdIndex: number): Promise; // (undocumented) - setProperties({ voiRange, invert, interpolationType, rotation, }?: StackViewportProperties, suppressEvents?: boolean): void; + setProperties({ voiRange, VOILUTFunction, invert, interpolationType, rotation, }?: StackViewportProperties, suppressEvents?: boolean): void; // (undocumented) setStack(imageIds: Array, currentImageIdIndex?: number): Promise; // (undocumented) @@ -2058,6 +2074,7 @@ type StackViewportNewStackEventDetail = { // @public (undocumented) type StackViewportProperties = { voiRange?: VOIRange; + VOILUTFunction?: VOILUTFunctionType; invert?: boolean; interpolationType?: InterpolationType; rotation?: number; @@ -2179,6 +2196,9 @@ function unregisterAllImageLoaders(): void; declare namespace utilities { export { invertRgbTransferFunction, + createSigmoidRGBTransferFunction, + getVoiFromSigmoidRGBTransferFunction, + createLinearRGBTransferFunction, scaleRGBTransferFunction as scaleRgbTransferFunction, triggerEvent, imageIdToURI, @@ -2405,6 +2425,14 @@ type VOI = { windowCenter: number; }; +// @public (undocumented) +enum VOILUTFunctionType { + // (undocumented) + LINEAR = "LINEAR", + // (undocumented) + SAMPLED_SIGMOID = "SIGMOID" +} + // @public (undocumented) type VoiModifiedEvent = CustomEvent_2; @@ -2413,6 +2441,7 @@ type VoiModifiedEventDetail = { viewportId: string; range: VOIRange; volumeId?: string; + VOILUTFunction?: VOILUTFunctionType; }; // @public (undocumented) @@ -2523,6 +2552,7 @@ export class VolumeViewport extends BaseVolumeViewport { // @public (undocumented) type VolumeViewportProperties = { voiRange?: VOIRange; + VOILUTFunction?: VOILUTFunctionType; }; declare namespace windowLevel { diff --git a/common/reviews/api/streaming-image-volume-loader.api.md b/common/reviews/api/streaming-image-volume-loader.api.md index 33fa8f556a..a3ef43ab08 100644 --- a/common/reviews/api/streaming-image-volume-loader.api.md +++ b/common/reviews/api/streaming-image-volume-loader.api.md @@ -707,6 +707,7 @@ interface IImage { lastRenderTime?: number; }; voiLUT?: CPUFallbackLUT; + voiLUTFunction: string; width: number; windowCenter: number[] | number; windowWidth: number[] | number; @@ -1115,8 +1116,7 @@ interface IVolumeViewport extends IViewport { getFrameOfReferenceUID: () => string; getImageData(volumeId?: string): IImageData | undefined; getIntensityFromWorld(point: Point3): number; - // (undocumented) - getProperties: () => any; + getProperties: () => VolumeViewportProperties; getSlabThickness(): number; hasImageURI: (imageURI: string) => boolean; hasVolumeId: (volumeId: string) => boolean; @@ -1181,6 +1181,7 @@ type Metadata = { Columns: number; Rows: number; voiLut: Array; + VOILUTFunction: string; }; // @public (undocumented) @@ -1299,6 +1300,7 @@ type StackViewportNewStackEventDetail = { // @public type StackViewportProperties = { voiRange?: VOIRange; + VOILUTFunction?: VOILUTFunctionType; invert?: boolean; interpolationType?: InterpolationType; rotation?: number; @@ -1411,6 +1413,15 @@ type VOI = { windowCenter: number; }; +// @public +enum VOILUTFunctionType { + // (undocumented) + LINEAR = 'LINEAR', + // (undocumented) + SAMPLED_SIGMOID = 'SIGMOID', // SIGMOID is sampled in 1024 even steps so we call it SAMPLED_SIGMOID + // EXACT_LINEAR = 'EXACT_LINEAR', TODO: Add EXACT_LINEAR option from DICOM NEMA +} + // @public type VoiModifiedEvent = CustomEvent_2; @@ -1419,6 +1430,7 @@ type VoiModifiedEventDetail = { viewportId: string; range: VOIRange; volumeId?: string; + VOILUTFunction?: VOILUTFunctionType; }; // @public (undocumented) @@ -1495,6 +1507,7 @@ type VolumeNewImageEventDetail = { // @public type VolumeViewportProperties = { voiRange?: VOIRange; + VOILUTFunction?: VOILUTFunctionType; }; // (No @packageDocumentation comment for this package) diff --git a/common/reviews/api/tools.api.md b/common/reviews/api/tools.api.md index 86488e7f02..5d7e4fc92e 100644 --- a/common/reviews/api/tools.api.md +++ b/common/reviews/api/tools.api.md @@ -2280,6 +2280,7 @@ interface IImage { lastRenderTime?: number; }; voiLUT?: CPUFallbackLUT; + voiLUTFunction: string; width: number; windowCenter: number[] | number; windowWidth: number[] | number; @@ -2844,8 +2845,7 @@ interface IVolumeViewport extends IViewport { getFrameOfReferenceUID: () => string; getImageData(volumeId?: string): IImageData | undefined; getIntensityFromWorld(point: Point3): number; - // (undocumented) - getProperties: () => any; + getProperties: () => VolumeViewportProperties; getSlabThickness(): number; hasImageURI: (imageURI: string) => boolean; hasVolumeId: (volumeId: string) => boolean; @@ -3146,6 +3146,7 @@ type Metadata = { Columns: number; Rows: number; voiLut: Array; + VOILUTFunction: string; }; // @public (undocumented) @@ -4498,6 +4499,7 @@ type StackViewportNewStackEventDetail = { // @public type StackViewportProperties = { voiRange?: VOIRange; + VOILUTFunction?: VOILUTFunctionType; invert?: boolean; interpolationType?: InterpolationType; rotation?: number; @@ -5122,6 +5124,7 @@ type VoiModifiedEventDetail = { viewportId: string; range: VOIRange; volumeId?: string; + VOILUTFunction?: VOILUTFunctionType; }; // @public (undocumented) @@ -5209,6 +5212,7 @@ export class VolumeRotateMouseWheelTool extends BaseTool { // @public type VolumeViewportProperties = { voiRange?: VOIRange; + VOILUTFunction?: VOILUTFunctionType; }; // @public (undocumented) diff --git a/packages/core/examples/stackVoiSigmoid/index.ts b/packages/core/examples/stackVoiSigmoid/index.ts new file mode 100644 index 0000000000..6ad402b276 --- /dev/null +++ b/packages/core/examples/stackVoiSigmoid/index.ts @@ -0,0 +1,154 @@ +import { + RenderingEngine, + Types, + Enums, + getRenderingEngine, +} from '@cornerstonejs/core'; +import { + initDemo, + createImageIdsAndCacheMetaData, + setTitleAndDescription, + addButtonToToolbar, +} from '../../../../utils/demo/helpers'; +import * as cornerstoneTools from '@cornerstonejs/tools'; + +const { + WindowLevelTool, + ToolGroupManager, + Enums: csToolsEnums, +} = cornerstoneTools; + +const { MouseBindings } = csToolsEnums; +const toolGroupId = 'STACK_TOOL_GROUP_ID'; + +// This is for debugging purposes +console.warn( + 'Click on index.ts to open source code for this example --------->' +); + +const { ViewportType } = Enums; +const renderingEngineId = 'myRenderingEngine'; +const viewportId = 'STACK_VP'; + +// ======== Set up page ======== // +setTitleAndDescription( + 'Sigmoid VOI Stack', + 'This example shows how to set a Sigmoid VOI on a Stack viewport.' +); + +const content = document.getElementById('content'); +const element = document.createElement('div'); +element.id = 'cornerstone-element'; +element.style.width = '500px'; +element.style.height = '500px'; + +content.appendChild(element); +// ============================= // + +addButtonToToolbar({ + title: 'Set Linear VOI', + onClick: () => { + // Get the rendering engine + const renderingEngine = getRenderingEngine(renderingEngineId); + + // Get the stack viewport + const viewport = ( + renderingEngine.getViewport(viewportId) + ); + + // Set a range to highlight bones + viewport.setProperties({ VOILUTFunction: Enums.VOILUTFunctionType.LINEAR }); + + viewport.render(); + }, +}); + +addButtonToToolbar({ + title: 'Set Sigmoid VOI', + onClick: () => { + // Get the rendering engine + const renderingEngine = getRenderingEngine(renderingEngineId); + + // Get the stack viewport + const viewport = ( + renderingEngine.getViewport(viewportId) + ); + + // Set a range to highlight bones + viewport.setProperties({ + VOILUTFunction: Enums.VOILUTFunctionType.SAMPLED_SIGMOID, + }); + + viewport.render(); + }, +}); + +/** + * Runs the demo + */ +async function run() { + // Init Cornerstone and related libraries + await initDemo(); + + // Add tools to Cornerstone3D + cornerstoneTools.addTool(WindowLevelTool); + + // Define a tool group, which defines how mouse events map to tool commands for + // Any viewport using the group + const toolGroup = ToolGroupManager.createToolGroup(toolGroupId); + + // Add the tools to the tool group + toolGroup.addTool(WindowLevelTool.toolName); + + // Set the initial state of the tools, here we set one tool active on left click. + // This means left click will draw that tool. + toolGroup.setToolActive(WindowLevelTool.toolName, { + bindings: [ + { + mouseButton: MouseBindings.Primary, // Left Click + }, + ], + }); + + // Get Cornerstone imageIds and fetch metadata into RAM + const imageIds = await createImageIdsAndCacheMetaData({ + StudyInstanceUID: + '1.3.6.1.4.1.9590.100.1.2.85935434310203356712688695661986996009', + SeriesInstanceUID: + '1.3.6.1.4.1.9590.100.1.2.374115997511889073021386151921807063992', + wadoRsRoot: 'https://server.dcmjs.org/dcm4chee-arc/aets/DCM4CHEE/rs', + }); + + // Instantiate a rendering engine + const renderingEngine = new RenderingEngine(renderingEngineId); + + toolGroup.addViewport(viewportId, renderingEngineId); + + // Create a stack viewport + const viewportInput = { + viewportId, + type: ViewportType.STACK, + element, + defaultOptions: { + background: [0.2, 0, 0.2], + }, + }; + + renderingEngine.enableElement(viewportInput); + + // Get the stack viewport that was created + const viewport = ( + renderingEngine.getViewport(viewportId) + ); + + // Define a stack containing a single image + const stack = [imageIds[0]]; + + // Set the stack on the viewport + await viewport.setStack(stack); + + // Render the image + viewport.render(); +} + +run(); diff --git a/packages/core/examples/volumeVoiSigmoid/index.ts b/packages/core/examples/volumeVoiSigmoid/index.ts new file mode 100644 index 0000000000..0eb5c5f9c9 --- /dev/null +++ b/packages/core/examples/volumeVoiSigmoid/index.ts @@ -0,0 +1,165 @@ +import { + RenderingEngine, + Types, + Enums, + volumeLoader, + getRenderingEngine, +} from '@cornerstonejs/core'; +import { + initDemo, + createImageIdsAndCacheMetaData, + setTitleAndDescription, + addButtonToToolbar, +} from '../../../../utils/demo/helpers'; + +import * as cornerstoneTools from '@cornerstonejs/tools'; + +const { + WindowLevelTool, + ToolGroupManager, + Enums: csToolsEnums, +} = cornerstoneTools; + +const { MouseBindings } = csToolsEnums; +const toolGroupId = 'STACK_TOOL_GROUP_ID'; +const renderingEngineId = 'myRenderingEngine'; +const viewportId = 'CT_SAGITTAL_STACK'; + +// This is for debugging purposes +console.warn( + 'Click on index.ts to open source code for this example --------->' +); + +const { ViewportType } = Enums; + +// ======== Set up page ======== // +setTitleAndDescription( + 'Sigmoid VOI Volume', + 'This example shows how to set a Sigmoid VOI on a Volume viewport.' +); + +addButtonToToolbar({ + title: 'Set Linear VOI', + onClick: () => { + // Get the rendering engine + const renderingEngine = getRenderingEngine(renderingEngineId); + + // Get the volume viewport + const viewport = ( + renderingEngine.getViewport(viewportId) + ); + + // Set a range to highlight bones + viewport.setProperties({ VOILUTFunction: Enums.VOILUTFunctionType.LINEAR }); + + viewport.render(); + }, +}); + +addButtonToToolbar({ + title: 'Set Sigmoid VOI', + onClick: () => { + // Get the rendering engine + const renderingEngine = getRenderingEngine(renderingEngineId); + + // Get the volume viewport + const viewport = ( + renderingEngine.getViewport(viewportId) + ); + + // Set a range to highlight bones + viewport.setProperties({ + VOILUTFunction: Enums.VOILUTFunctionType.SAMPLED_SIGMOID, + }); + + viewport.render(); + }, +}); + +const content = document.getElementById('content'); +const element = document.createElement('div'); +element.id = 'cornerstone-element'; +element.style.width = '500px'; +element.style.height = '500px'; + +content.appendChild(element); +// ============================= // + +/** + * Runs the demo + */ +async function run() { + // Init Cornerstone and related libraries + await initDemo(); + + cornerstoneTools.addTool(WindowLevelTool); + // Define a tool group, which defines how mouse events map to tool commands for + // Any viewport using the group + const toolGroup = ToolGroupManager.createToolGroup(toolGroupId); + + // Add the tools to the tool group + toolGroup.addTool(WindowLevelTool.toolName); + + // Set the initial state of the tools, here we set one tool active on left click. + // This means left click will draw that tool. + toolGroup.setToolActive(WindowLevelTool.toolName, { + bindings: [ + { + mouseButton: MouseBindings.Primary, // Left Click + }, + ], + }); + + // Get Cornerstone imageIds and fetch metadata into RAM + const imageIds = await createImageIdsAndCacheMetaData({ + StudyInstanceUID: + '1.3.6.1.4.1.14519.5.2.1.7009.2403.334240657131972136850343327463', + SeriesInstanceUID: + '1.3.6.1.4.1.14519.5.2.1.7009.2403.226151125820845824875394858561', + wadoRsRoot: 'https://d3t6nz73ql33tx.cloudfront.net/dicomweb', + }); + + // Instantiate a rendering engine + const renderingEngine = new RenderingEngine(renderingEngineId); + toolGroup.addViewport(viewportId, renderingEngineId); + + // Create a stack viewport + const viewportInput = { + viewportId, + type: ViewportType.ORTHOGRAPHIC, + element, + defaultOptions: { + orientation: Enums.OrientationAxis.SAGITTAL, + background: [0.2, 0, 0.2], + }, + }; + + renderingEngine.enableElement(viewportInput); + toolGroup.addViewport(viewportId, renderingEngineId); + + // Get the stack viewport that was created + const viewport = ( + renderingEngine.getViewport(viewportId) + ); + + // Define a unique id for the volume + const volumeName = 'CT_VOLUME_ID'; // Id of the volume less loader prefix + const volumeLoaderScheme = 'cornerstoneStreamingImageVolume'; // Loader id which defines which volume loader to use + const volumeId = `${volumeLoaderScheme}:${volumeName}`; // VolumeId with loader id + volume id + + // Define a volume in memory + const volume = await volumeLoader.createAndCacheVolume(volumeId, { + imageIds, + }); + + // Set the volume to load + volume.load(); + + // Set the volume on the viewport + viewport.setVolumes([{ volumeId }]); + + // Render the image + viewport.render(); +} + +run(); diff --git a/packages/core/src/RenderingEngine/BaseVolumeViewport.ts b/packages/core/src/RenderingEngine/BaseVolumeViewport.ts index 47b8dece4d..de78952880 100644 --- a/packages/core/src/RenderingEngine/BaseVolumeViewport.ts +++ b/packages/core/src/RenderingEngine/BaseVolumeViewport.ts @@ -18,12 +18,25 @@ import type { ActorEntry, FlipDirection, VolumeViewportProperties, + VOIRange, } from '../types'; import type { ViewportInput } from '../types/IViewport'; import type IVolumeViewport from '../types/IVolumeViewport'; -import { Events, BlendModes, OrientationAxis } from '../enums'; +import { + Events, + BlendModes, + OrientationAxis, + VOILUTFunctionType, +} from '../enums'; import eventTarget from '../eventTarget'; -import { actorIsA, imageIdToURI, triggerEvent } from '../utilities'; +import { + actorIsA, + imageIdToURI, + triggerEvent, + createSigmoidRGBTransferFunction, + getVoiFromSigmoidRGBTransferFunction, + createLinearRGBTransferFunction, +} from '../utilities'; import type { vtkSlabCamera as vtkSlabCameraType } from './vtkClasses/vtkSlabCamera'; import { VoiModifiedEventDetail } from '../types/EventTypes'; import { RENDERING_DEFAULTS } from '../constants'; @@ -41,6 +54,10 @@ abstract class BaseVolumeViewport extends Viewport implements IVolumeViewport { useCPURendering = false; private _FrameOfReferenceUID: string; + // Viewport Properties + // TODO: similar to setVoi, this is only applicable to first volume + private VOILUTFunction: VOILUTFunctionType; + constructor(props: ViewportInput) { super(props); @@ -149,6 +166,28 @@ abstract class BaseVolumeViewport extends Viewport implements IVolumeViewport { } } + /** + * Sets the properties for the volume viewport on the volume + * Sets the VOILUTFunction property for the volume viewport on the volume + * + * @param VOILUTFunction - Sets the voi mode (LINEAR or SAMPLED_SIGMOID) + * @param volumeId - The volume id to set the properties for (if undefined, the first volume) + * @param suppressEvents - If true, the viewport will not emit events + */ + private setVOILUTFunction( + voiLUTFunction: VOILUTFunctionType, + volumeId?: string, + suppressEvents?: boolean + ): void { + // make sure the VOI LUT function is valid in the VOILUTFunctionType which is enum + if (Object.values(VOILUTFunctionType).indexOf(voiLUTFunction) === -1) { + voiLUTFunction = VOILUTFunctionType.LINEAR; + } + const { voiRange } = this.getProperties(); + this.VOILUTFunction = voiLUTFunction; + this.setVOI(voiRange, volumeId, suppressEvents); + } + /** * Sets the properties for the volume viewport on the volume * (if fusion, it sets it for the first volume in the fusion) @@ -157,8 +196,8 @@ abstract class BaseVolumeViewport extends Viewport implements IVolumeViewport { * @param volumeId - The volume id to set the properties for (if undefined, the first volume) * @param suppressEvents - If true, the viewport will not emit events */ - public setProperties( - { voiRange }: VolumeViewportProperties = {}, + private setVOI( + voiRange: VOIRange, volumeId?: string, suppressEvents = false ): void { @@ -188,25 +227,87 @@ abstract class BaseVolumeViewport extends Viewport implements IVolumeViewport { volumeId = actorEntries[0].uid; } - if (!voiRange) { - return; + let voiRangeToUse = voiRange; + if (typeof voiRangeToUse === 'undefined') { + const imageData = volumeActor.getMapper().getInputData(); + const range = imageData.getPointData().getScalars().getRange(); + const maxVoiRange = { lower: range[0], upper: range[1] }; + voiRangeToUse = maxVoiRange; } - // Todo: later when we have more properties, refactor the setVoiRange code below - const { lower, upper } = voiRange; - volumeActor.getProperty().getRGBTransferFunction(0).setRange(lower, upper); + // scaling logic here + // https://github.com/Kitware/vtk-js/blob/c6f2e12cddfe5c0386a73f0793eb6d9ab20d573e/Sources/Rendering/OpenGL/VolumeMapper/index.js#L957-L972 + if (this.VOILUTFunction === VOILUTFunctionType.SAMPLED_SIGMOID) { + const cfun = createSigmoidRGBTransferFunction(voiRangeToUse); + volumeActor.getProperty().setRGBTransferFunction(0, cfun); + } else { + const cfun = createLinearRGBTransferFunction(voiRangeToUse); + volumeActor.getProperty().setRGBTransferFunction(0, cfun); + } if (!suppressEvents) { const eventDetail: VoiModifiedEventDetail = { viewportId: this.id, range: voiRange, volumeId: volumeId, + VOILUTFunction: this.VOILUTFunction, }; triggerEvent(this.element, Events.VOI_MODIFIED, eventDetail); } } + /** + * Sets the properties for the volume viewport on the volume + * (if fusion, it sets it for the first volume in the fusion) + * + * @param voiRange - Sets the lower and upper voi + * @param VOILUTFunction - Sets the voi mode (LINEAR, or SAMPLED_SIGMOID) + * @param volumeId - The volume id to set the properties for (if undefined, the first volume) + * @param suppressEvents - If true, the viewport will not emit events + */ + public setProperties( + { voiRange, VOILUTFunction }: VolumeViewportProperties = {}, + volumeId?: string, + suppressEvents = false + ): void { + if (voiRange) { + this.setVOI(voiRange, volumeId, suppressEvents); + } + + if (VOILUTFunction) { + this.setVOILUTFunction(VOILUTFunction, volumeId, suppressEvents); + } + } + + /** + * Retrieve the viewport properties + * @returns viewport properties including voi, interpolation type: TODO: slabThickness, invert, rotation, flip + */ + public getProperties = (): VolumeViewportProperties => { + const actorEntries = this.getActors(); + const voiRanges = actorEntries.map((actorEntry) => { + const volumeActor = actorEntry.actor as vtkVolume; + const volumeId = actorEntry.uid; + const cfun = volumeActor.getProperty().getRGBTransferFunction(0); + let lower, upper; + if (this.VOILUTFunction === 'SIGMOID') { + [lower, upper] = getVoiFromSigmoidRGBTransferFunction(cfun); + } else { + // @ts-ignore: vtk d ts problem + [lower, upper] = cfun.getRange(); + } + return { + volumeId, + voiRange: { lower, upper }, + }; + }); + return { + voiRange: voiRanges[0].voiRange, // TODO: handle multiple actors instead of just first. + VOILUTFunction: this.VOILUTFunction, + }; + }; + /** * Creates volume actors for all volumes defined in the `volumeInputArray`. * For each entry, if a `callback` is supplied, it will be called with the new volume actor as input. diff --git a/packages/core/src/RenderingEngine/StackViewport.ts b/packages/core/src/RenderingEngine/StackViewport.ts index 33a1ff3c99..9fa45f3cde 100644 --- a/packages/core/src/RenderingEngine/StackViewport.ts +++ b/packages/core/src/RenderingEngine/StackViewport.ts @@ -6,7 +6,6 @@ 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 vtkColorTransferFunction from '@kitware/vtk.js/Rendering/Core/ColorTransferFunction'; import * as metaData from '../metaData'; import Viewport from './Viewport'; import eventTarget from '../eventTarget'; @@ -15,6 +14,7 @@ import { triggerEvent, isEqual, invertRgbTransferFunction, + createSigmoidRGBTransferFunction, windowLevel as windowLevelUtil, imageIdToURI, isImageActor, @@ -47,6 +47,7 @@ import { getColormap } from './helpers/cpuFallback/colors/index'; import { loadAndCacheImage } from '../loaders/imageLoader'; import imageLoadPoolManager from '../requestPool/imageLoadPoolManager'; import InterpolationType from '../enums/InterpolationType'; +import VOILUTFunctionType from '../enums/VOILUTFunctionType'; import canvasToPixel from './helpers/cpuFallback/rendering/canvasToPixel'; import pixelToCanvas from './helpers/cpuFallback/rendering/pixelToCanvas'; import getDefaultViewport from './helpers/cpuFallback/rendering/getDefaultViewport'; @@ -66,6 +67,7 @@ import cache from '../cache'; import correctShift from './helpers/cpuFallback/rendering/correctShift'; import { ImageActor } from '../types/IActor'; import isRgbaSourceRgbDest from './helpers/isRgbaSourceRgbDest'; +import createLinearRGBTransferFunction from '../utilities/createLinearRGBTransferFunction'; const EPSILON = 1; // Slice Thickness @@ -78,6 +80,7 @@ interface ImagePixelModule { pixelRepresentation: string; windowWidth: number; windowCenter: number; + voiLUTFunction: VOILUTFunctionType; modality: string; } @@ -133,6 +136,7 @@ class StackViewport extends Viewport implements IStackViewport { // Viewport Properties private voiRange: VOIRange; + private VOILUTFunction: VOILUTFunctionType; private initialVOIRange: VOIRange; private invert = false; private interpolationType: InterpolationType; @@ -397,9 +401,9 @@ class StackViewport extends Viewport implements IStackViewport { const voiLutModule = metaData.get('voiLutModule', imageId); - let windowWidth, windowCenter; + let windowWidth, windowCenter, voiLUTFunction; if (voiLutModule) { - ({ windowWidth, windowCenter } = voiLutModule); + ({ windowWidth, windowCenter, voiLUTFunction } = voiLutModule); if (Array.isArray(windowWidth)) { windowWidth = windowWidth[0]; @@ -408,6 +412,12 @@ class StackViewport extends Viewport implements IStackViewport { if (Array.isArray(windowCenter)) { windowCenter = windowCenter[0]; } + + // when cornerstoneWADOImageLoader uses cornerstonejs/core types + // this marshalling step can be removed. + if (Object.values(VOILUTFunctionType).indexOf(voiLUTFunction) === -1) { + voiLUTFunction = VOILUTFunctionType.LINEAR; + } } const { modality } = metaData.get('generalSeriesModule', imageId); @@ -438,6 +448,7 @@ class StackViewport extends Viewport implements IStackViewport { pixelRepresentation, windowWidth, windowCenter, + voiLUTFunction, modality, }, }; @@ -546,6 +557,7 @@ class StackViewport extends Viewport implements IStackViewport { public setProperties( { voiRange, + VOILUTFunction, invert, interpolationType, rotation, @@ -558,6 +570,10 @@ class StackViewport extends Viewport implements IStackViewport { this.setVOI(voiRange, suppressEvents); } + if (typeof VOILUTFunction !== 'undefined') { + this.setVOILUTFunction(VOILUTFunction, suppressEvents); + } + if (typeof invert !== 'undefined') { this.setInvertColor(invert); } @@ -582,6 +598,7 @@ class StackViewport extends Viewport implements IStackViewport { return { voiRange: this.voiRange, rotation: this.getRotation(), + VOILUTFunction: this.VOILUTFunction, interpolationType: this.interpolationType, invert: this.invert, }; @@ -896,6 +913,25 @@ class StackViewport extends Viewport implements IStackViewport { triggerEvent(this.element, Events.CAMERA_MODIFIED, eventDetail); } + private setVOILUTFunction( + voiLUTFunction: VOILUTFunctionType, + suppressEvents?: boolean + ): void { + if (this.useCPURendering) { + throw new Error('VOI LUT function is not supported in CPU rendering'); + } + + // make sure the VOI LUT function is valid in the VOILUTFunctionType which is enum + if (Object.values(VOILUTFunctionType).indexOf(voiLUTFunction) === -1) { + voiLUTFunction = VOILUTFunctionType.LINEAR; + } + + this.VOILUTFunction = voiLUTFunction; + + const { voiRange } = this.getProperties(); + this.setVOI(voiRange, suppressEvents); + } + private setInterpolationType(interpolationType: InterpolationType): void { if (this.useCPURendering) { this.setInterpolationTypeCPU(interpolationType); @@ -1010,6 +1046,7 @@ class StackViewport extends Viewport implements IStackViewport { } private setVOICPU(voiRange: VOIRange, suppressEvents?: boolean): void { + // TODO: Account for VOILUTFunction const { viewport, image } = this._cpuFallbackEnabledElement; if (!viewport || !image) { @@ -1069,21 +1106,30 @@ class StackViewport extends Viewport implements IStackViewport { } const { actor } = defaultActor; const imageActor = actor as ImageActor; - let voiRangeToUse = voiRange; if (typeof voiRangeToUse === 'undefined') { const imageData = imageActor.getMapper().getInputData(); const range = imageData.getPointData().getScalars().getRange(); - voiRangeToUse = { lower: range[0], upper: range[1] }; + const maxVoiRange = { lower: range[0], upper: range[1] }; + voiRangeToUse = maxVoiRange; } - const { windowWidth, windowCenter } = windowLevelUtil.toWindowLevel( - voiRangeToUse.lower, - voiRangeToUse.upper - ); - - imageActor.getProperty().setColorWindow(windowWidth); - imageActor.getProperty().setColorLevel(windowCenter); + // scaling logic here + // https://github.com/Kitware/vtk-js/blob/master/Sources/Rendering/OpenGL/ImageMapper/index.js#L540-L549 + imageActor.getProperty().setUseLookupTableScalarRange(true); + if (this.VOILUTFunction === VOILUTFunctionType.SAMPLED_SIGMOID) { + const cfun = createSigmoidRGBTransferFunction(voiRangeToUse); + if (this.invert) { + invertRgbTransferFunction(cfun); + } + imageActor.getProperty().setRGBTransferFunction(0, cfun); + } else { + const cfun = createLinearRGBTransferFunction(voiRangeToUse); + if (this.invert) { + invertRgbTransferFunction(cfun); + } + imageActor.getProperty().setRGBTransferFunction(0, cfun); + } this.voiApplied = true; this.voiRange = voiRangeToUse; @@ -1092,6 +1138,7 @@ class StackViewport extends Viewport implements IStackViewport { const eventDetail: VoiModifiedEventDetail = { viewportId: this.id, range: voiRangeToUse, + VOILUTFunction: this.VOILUTFunction, }; triggerEvent(this.element, Events.VOI_MODIFIED, eventDetail); @@ -1815,7 +1862,8 @@ class StackViewport extends Viewport implements IStackViewport { activeCamera.setFreezeFocalPoint(true); // set voi for the first time - const { windowCenter, windowWidth } = imagePixelModule; + const { windowCenter, windowWidth, voiLUTFunction } = imagePixelModule; + let voiRange = typeof windowCenter === 'number' && typeof windowWidth === 'number' ? windowLevelUtil.toLowHighRange(windowWidth, windowCenter) @@ -1839,6 +1887,14 @@ class StackViewport extends Viewport implements IStackViewport { // In that case we want to keep the applied VOI range. voiRange = this.voiRange; } + + // make sure the VOI LUT function is valid in the VOILUTFunctionType which is enum + if (Object.values(VOILUTFunctionType).indexOf(voiLUTFunction) === -1) { + this.VOILUTFunction = VOILUTFunctionType.LINEAR; + } else { + this.VOILUTFunction = voiLUTFunction; + } + this.setProperties({ voiRange }); // At the moment it appears that vtkImageSlice actors do not automatically @@ -1846,20 +1902,10 @@ class StackViewport extends Viewport implements IStackViewport { // Note: the 1024 here is what VTK would normally do to resample a color transfer function // before it is put into the GPU. Setting it with a length of 1024 allows us to // avoid that resampling step. - const cfun = vtkColorTransferFunction.newInstance(); - let lower = 0; - let upper = 1024; - if ( - voiRange && - voiRange.lower !== undefined && - voiRange.upper !== undefined - ) { - lower = voiRange.lower; - upper = voiRange.upper; + if (actor.getProperty().getRGBTransferFunction(0) === null) { + const cfun = createLinearRGBTransferFunction(voiRange); + actor.getProperty().setRGBTransferFunction(0, cfun); } - cfun.addRGBPoint(lower, 0.0, 0.0, 0.0); - cfun.addRGBPoint(upper, 1.0, 1.0, 1.0); - actor.getProperty().setRGBTransferFunction(0, cfun); let invert = false; if (imagePixelModule.photometricInterpretation === 'MONOCHROME1') { diff --git a/packages/core/src/enums/VOILUTFunctionType.ts b/packages/core/src/enums/VOILUTFunctionType.ts new file mode 100644 index 0000000000..f354f4b353 --- /dev/null +++ b/packages/core/src/enums/VOILUTFunctionType.ts @@ -0,0 +1,10 @@ +/** + * Interpolation types for image rendering + */ +enum VOILUTFunctionType { + LINEAR = 'LINEAR', + SAMPLED_SIGMOID = 'SIGMOID', // SIGMOID is sampled in 1024 even steps so we call it SAMPLED_SIGMOID + // EXACT_LINEAR = 'EXACT_LINEAR', TODO: Add EXACT_LINEAR option from DICOM NEMA +} + +export default VOILUTFunctionType; diff --git a/packages/core/src/enums/index.ts b/packages/core/src/enums/index.ts index 406334689d..2286cec95f 100644 --- a/packages/core/src/enums/index.ts +++ b/packages/core/src/enums/index.ts @@ -7,6 +7,7 @@ import OrientationAxis from './OrientationAxis'; import SharedArrayBufferModes from './SharedArrayBufferModes'; import GeometryType from './GeometryType'; import ContourType from './ContourType'; +import VOILUTFunctionType from './VOILUTFunctionType'; export { Events, @@ -18,4 +19,5 @@ export { SharedArrayBufferModes, GeometryType, ContourType, + VOILUTFunctionType, }; diff --git a/packages/core/src/types/EventTypes.ts b/packages/core/src/types/EventTypes.ts index 5c9fbb114c..3a01e13270 100644 --- a/packages/core/src/types/EventTypes.ts +++ b/packages/core/src/types/EventTypes.ts @@ -7,7 +7,7 @@ import type ICamera from './ICamera'; import type IImage from './IImage'; import type IImageVolume from './IImageVolume'; import type { VOIRange } from './voi'; - +import type VOILUTFunctionType from '../enums/VOILUTFunctionType'; /** * CAMERA_MODIFIED Event's data */ @@ -36,6 +36,8 @@ type VoiModifiedEventDetail = { range: VOIRange; /** Unique ID for the volume in the cache */ volumeId?: string; + /** VOILUTFunction */ + VOILUTFunction?: VOILUTFunctionType; }; /** diff --git a/packages/core/src/types/IImage.ts b/packages/core/src/types/IImage.ts index 666f355c9e..eb0628dc07 100644 --- a/packages/core/src/types/IImage.ts +++ b/packages/core/src/types/IImage.ts @@ -39,6 +39,8 @@ interface IImage { windowCenter: number[] | number; /** windowWidth from metadata */ windowWidth: number[] | number; + /** voiLUTFunction from metadata */ + voiLUTFunction: string; /** function that returns the pixelData as an array */ getPixelData: () => Array; getCanvas: () => HTMLCanvasElement; diff --git a/packages/core/src/types/IVolumeViewport.ts b/packages/core/src/types/IVolumeViewport.ts index 2a1a3067da..e03d6aa1d1 100644 --- a/packages/core/src/types/IVolumeViewport.ts +++ b/packages/core/src/types/IVolumeViewport.ts @@ -13,7 +13,10 @@ import { VolumeViewportProperties } from '.'; export default interface IVolumeViewport extends IViewport { useCPURendering: boolean; getFrameOfReferenceUID: () => string; - getProperties: () => any; + /** + * Retrieve the viewport properties + */ + getProperties: () => VolumeViewportProperties; /** * canvasToWorld Returns the world coordinates of the given `canvasPos` * projected onto the plane defined by the `Viewport`'s `vtkCamera`'s focal point diff --git a/packages/core/src/types/Metadata.ts b/packages/core/src/types/Metadata.ts index 86183feeda..d3b462acf7 100644 --- a/packages/core/src/types/Metadata.ts +++ b/packages/core/src/types/Metadata.ts @@ -32,6 +32,8 @@ type Metadata = { Rows: number; /** Window Level/Center for the image */ voiLut: Array; + /** VOILUTFunction for the image which is LINEAR or SAMPLED_SIGMOID */ + VOILUTFunction: string; }; export default Metadata; diff --git a/packages/core/src/types/StackViewportProperties.ts b/packages/core/src/types/StackViewportProperties.ts index 3a32e51bf9..d058624b92 100644 --- a/packages/core/src/types/StackViewportProperties.ts +++ b/packages/core/src/types/StackViewportProperties.ts @@ -1,4 +1,5 @@ import InterpolationType from '../enums/InterpolationType'; +import VOILUTFunctionType from '../enums/VOILUTFunctionType'; import { VOIRange } from './voi'; /** @@ -7,6 +8,8 @@ import { VOIRange } from './voi'; type StackViewportProperties = { /** voi range (upper, lower) for the viewport */ voiRange?: VOIRange; + /** VOILUTFunction type which is LINEAR or SAMPLED_SIGMOID */ + VOILUTFunction?: VOILUTFunctionType; /** invert flag - whether the image is inverted */ invert?: boolean; /** interpolation type - linear or nearest neighbor */ diff --git a/packages/core/src/types/VolumeViewportProperties.ts b/packages/core/src/types/VolumeViewportProperties.ts index 49d08523fc..fc51657e5d 100644 --- a/packages/core/src/types/VolumeViewportProperties.ts +++ b/packages/core/src/types/VolumeViewportProperties.ts @@ -1,4 +1,5 @@ import { VOIRange } from './voi'; +import VOILUTFunctionType from '../enums/VOILUTFunctionType'; /** * Stack Viewport Properties @@ -6,6 +7,8 @@ import { VOIRange } from './voi'; type VolumeViewportProperties = { /** voi range (upper, lower) for the viewport */ voiRange?: VOIRange; + /** VOILUTFunction type which is LINEAR or SAMPLED_SIGMOID */ + VOILUTFunction?: VOILUTFunctionType; }; export default VolumeViewportProperties; diff --git a/packages/core/src/utilities/createLinearRGBTransferFunction.ts b/packages/core/src/utilities/createLinearRGBTransferFunction.ts new file mode 100644 index 0000000000..d837c9e899 --- /dev/null +++ b/packages/core/src/utilities/createLinearRGBTransferFunction.ts @@ -0,0 +1,22 @@ +import vtkColorTransferFunction from '@kitware/vtk.js/Rendering/Core/ColorTransferFunction'; +import { VOIRange } from '../types'; + +export default function createLinearRGBTransferFunction( + voiRange: VOIRange +): vtkColorTransferFunction { + const cfun = vtkColorTransferFunction.newInstance(); + let lower = 0; + let upper = 1024; + if ( + voiRange && + voiRange.lower !== undefined && + voiRange.upper !== undefined + ) { + lower = voiRange.lower; + upper = voiRange.upper; + } + cfun.addRGBPoint(lower, 0.0, 0.0, 0.0); + cfun.addRGBPoint(upper, 1.0, 1.0, 1.0); + + return cfun; +} diff --git a/packages/core/src/utilities/createSigmoidRGBTransferFunction.ts b/packages/core/src/utilities/createSigmoidRGBTransferFunction.ts new file mode 100644 index 0000000000..57d98f7802 --- /dev/null +++ b/packages/core/src/utilities/createSigmoidRGBTransferFunction.ts @@ -0,0 +1,63 @@ +import vtkColorTransferFunction from '@kitware/vtk.js/Rendering/Core/ColorTransferFunction'; +import vtkDataArray from '@kitware/vtk.js/Common/Core/DataArray'; +import { VOIRange } from '../types'; +import { windowLevel as windowLevelUtil } from '.'; + +/** + * A utility that can be used to generate an Sigmoid RgbTransferFunction. + * Sigmoid transfer functions are used in the dicom specification: + * https://dicom.nema.org/medical/dicom/2018b/output/chtml/part03/sect_C.11.2.html + * + * @example + * Setting an RGB Transfer function from the viewport: + * ``` + * const sigmoidRGBTransferFunction = createSigmoidRGBTransferFunction(0, 255, { lower: 0, upper: 255} ); + * viewport + * .getActor() + * .getProperty() + * .setRGBTransferFunction(0, sigmoidRGBTransferFunction); + * ``` + * + * @see {@link https://kitware.github.io/vtk-js/api/Rendering_Core_ColorTransferFunction.html|VTK.js: ColorTransferFunction} + * @param rgbTransferFunction + */ +export default function createSigmoidRGBTransferFunction( + voiRange: VOIRange, + approximationNodes = 1024 // humans can precieve no more than 900 shades of gray doi: 10.1007/s10278-006-1052-3 +): vtkColorTransferFunction { + const { windowWidth, windowCenter } = windowLevelUtil.toWindowLevel( + voiRange.lower, + voiRange.upper + ); + + // Function is defined by dicom spec + // https://dicom.nema.org/medical/dicom/2018b/output/chtml/part03/sect_C.11.2.html + const sigmoid = (x: number, wc: number, ww: number) => { + return 1 / (1 + Math.exp((-4 * (x - wc)) / ww)); + }; + + // This function is the analytical inverse of the dicom spec sigmoid function + // for values y = [0, 1] exclusive. We use this to perform better sampling of + // points for the LUT as some images can have 2^16 unique values. This method + // can be deprecated if vtk supports LUTFunctions rather than look up tables + // or if vtk supports logistic scale. It currently only supports linear and + // log10 scaling which can be set on the vtkColorTransferFunction + const logit = (y: number, wc: number, ww: number) => { + return wc - (ww / 4) * Math.log((1 - y) / y); + }; + + // we slice out the first and last value to avoid 0 and 1 Infinity values + const range = [...Array(approximationNodes + 2).keys()] + .map((v) => v / (approximationNodes + 2)) + .slice(1, -1); + const table = range.reduce((res, y) => { + const x = logit(y, windowCenter, windowWidth); + return res.concat(x, y, y, y, 0.5, 0.0); + }, []); + + const cfun = vtkColorTransferFunction.newInstance(); + cfun.buildFunctionFromArray( + vtkDataArray.newInstance({ values: table, numberOfComponents: 6 }) + ); + return cfun; +} diff --git a/packages/core/src/utilities/getVoiFromSigmoidRGBTransferFunction.ts b/packages/core/src/utilities/getVoiFromSigmoidRGBTransferFunction.ts new file mode 100644 index 0000000000..60c2e9d895 --- /dev/null +++ b/packages/core/src/utilities/getVoiFromSigmoidRGBTransferFunction.ts @@ -0,0 +1,23 @@ +import vtkColorTransferFunction from '@kitware/vtk.js/Rendering/Core/ColorTransferFunction'; + +export default function getVoiFromSigmoidRGBTransferFunction( + cfun: vtkColorTransferFunction +): [number, number] { + let cfunRange = []; + // @ts-ignore: vtk d ts problem + const [lower, upper] = cfun.getRange(); + cfun.getTable(lower, upper, 1024, cfunRange); + cfunRange = cfunRange.filter((v, k) => k % 3 === 0); + const cfunDomain = [...Array(1024).keys()].map((v, k) => { + return lower + ((upper - lower) / (1024 - 1)) * k; + }); + const y1 = cfunRange[256]; + const logy1 = Math.log((1 - y1) / y1); + const x1 = cfunDomain[256]; + const y2 = cfunRange[256 * 3]; + const logy2 = Math.log((1 - y2) / y2); + const x2 = cfunDomain[256 * 3]; + const ww = Math.round((4 * (x2 - x1)) / (logy1 - logy2)); + const wc = Math.round(x1 + (ww * logy1) / 4); + return [Math.round(wc - ww / 2), Math.round(wc + ww / 2)]; +} diff --git a/packages/core/src/utilities/index.ts b/packages/core/src/utilities/index.ts index 895efe0f3b..1171e2a8fa 100644 --- a/packages/core/src/utilities/index.ts +++ b/packages/core/src/utilities/index.ts @@ -1,4 +1,7 @@ import csUtils from './invertRgbTransferFunction'; +import createSigmoidRGBTransferFunction from './createSigmoidRGBTransferFunction'; +import getVoiFromSigmoidRGBTransferFunction from './getVoiFromSigmoidRGBTransferFunction'; +import createLinearRGBTransferFunction from './createLinearRGBTransferFunction'; import scaleRgbTransferFunction from './scaleRgbTransferFunction'; import triggerEvent from './triggerEvent'; import uuidv4 from './uuidv4'; @@ -40,6 +43,9 @@ import * as windowLevel from './windowLevel'; export { csUtils as invertRgbTransferFunction, + createSigmoidRGBTransferFunction, + getVoiFromSigmoidRGBTransferFunction, + createLinearRGBTransferFunction, scaleRgbTransferFunction, triggerEvent, imageIdToURI, diff --git a/packages/streaming-image-volume-loader/src/StreamingImageVolume.ts b/packages/streaming-image-volume-loader/src/StreamingImageVolume.ts index a46c5e124e..ae88cb3e80 100644 --- a/packages/streaming-image-volume-loader/src/StreamingImageVolume.ts +++ b/packages/streaming-image-volume-loader/src/StreamingImageVolume.ts @@ -35,7 +35,6 @@ export default class StreamingImageVolume extends ImageVolume { streamingProperties: Types.IStreamingVolumeProperties ) { super(imageVolumeProperties); - this.imageIds = streamingProperties.imageIds; this.loadStatus = streamingProperties.loadStatus; @@ -52,7 +51,7 @@ export default class StreamingImageVolume extends ImageVolume { const pixelsPerImage = this.dimensions[0] * this.dimensions[1] * numComponents; - const { PhotometricInterpretation, voiLut } = this.metadata; + const { PhotometricInterpretation, voiLut, VOILUTFunction } = this.metadata; let windowCenter = []; let windowWidth = []; @@ -79,6 +78,7 @@ export default class StreamingImageVolume extends ImageVolume { spacing: this.spacing, dimensions: this.dimensions, PhotometricInterpretation, + voiLUTFunction: VOILUTFunction, invert: PhotometricInterpretation === 'MONOCHROME1', }; } @@ -191,7 +191,6 @@ export default class StreamingImageVolume extends ImageVolume { const { cachedFrames } = loadStatus; const { imageIds, vtkOpenGLTexture, imageData, metadata, volumeId } = this; - const { FrameOfReferenceUID } = metadata; loadStatus.loading = true; @@ -656,6 +655,7 @@ export default class StreamingImageVolume extends ImageVolume { dimensions, spacing, invert, + voiLUTFunction, } = this._cornerstoneImageMetaData; // 1. Grab the buffer and it's type @@ -707,6 +707,7 @@ export default class StreamingImageVolume extends ImageVolume { intercept, windowCenter, windowWidth, + voiLUTFunction, color, numComps: numComponents, rows: dimensions[0], diff --git a/packages/streaming-image-volume-loader/src/helpers/makeVolumeMetadata.ts b/packages/streaming-image-volume-loader/src/helpers/makeVolumeMetadata.ts index 437d8011be..f752939994 100644 --- a/packages/streaming-image-volume-loader/src/helpers/makeVolumeMetadata.ts +++ b/packages/streaming-image-volume-loader/src/helpers/makeVolumeMetadata.ts @@ -28,8 +28,10 @@ export default function makeVolumeMetadata( const voiLutModule = metaData.get('voiLutModule', imageId0); // voiLutModule is not always present + let voiLUTFunction; if (voiLutModule) { const { windowWidth, windowCenter } = voiLutModule; + voiLUTFunction = voiLutModule?.voiLUTFunction; if (Array.isArray(windowWidth)) { for (let i = 0; i < windowWidth.length; i++) { @@ -81,6 +83,7 @@ export default function makeVolumeMetadata( Rows: rows, // This is a reshaped object and not a dicom tag: voiLut, + VOILUTFunction: voiLUTFunction, SeriesInstanceUID: seriesInstanceUID, }; } diff --git a/packages/tools/src/tools/StackRotateTool.ts b/packages/tools/src/tools/StackRotateTool.ts index 7bc9b13ac6..ba4432e12d 100644 --- a/packages/tools/src/tools/StackRotateTool.ts +++ b/packages/tools/src/tools/StackRotateTool.ts @@ -54,7 +54,7 @@ class StackRotateTool extends BaseTool { if (Number.isNaN(angle)) return; - const { rotation } = viewport.getProperties(); + const { rotation } = (viewport as Types.IStackViewport).getProperties(); viewport.setProperties({ rotation: rotation + angle }); viewport.render(); diff --git a/packages/tools/src/tools/WindowLevelTool.ts b/packages/tools/src/tools/WindowLevelTool.ts index 8381e25511..b710651ffa 100644 --- a/packages/tools/src/tools/WindowLevelTool.ts +++ b/packages/tools/src/tools/WindowLevelTool.ts @@ -1,8 +1,6 @@ import { BaseTool } from './base'; import { getEnabledElement, - Enums, - triggerEvent, VolumeViewport, StackViewport, utilities, @@ -40,13 +38,11 @@ class WindowLevelTool extends BaseTool { mouseDragCallback(evt: EventTypes.InteractionEventType) { const { element, deltaPoints } = evt.detail; const enabledElement = getEnabledElement(element); - const { renderingEngine, viewportId, viewport } = enabledElement; + const { renderingEngine, viewport } = enabledElement; let volumeId, - volumeActor, lower, upper, - rgbTransferFunction, modality, newRange, viewportsContainingVolumeUID; @@ -55,14 +51,12 @@ class WindowLevelTool extends BaseTool { if (viewport instanceof VolumeViewport) { const targetId = this.getTargetId(viewport as Types.IVolumeViewport); volumeId = targetId.split('volumeId:')[1]; - const actorEntry = viewport.getActor(volumeId); - volumeActor = actorEntry.actor as Types.VolumeActor; - rgbTransferFunction = volumeActor.getProperty().getRGBTransferFunction(0); viewportsContainingVolumeUID = utilities.getViewportsWithVolumeId( volumeId, renderingEngine.id ); - [lower, upper] = rgbTransferFunction.getRange(); + const properties = viewport.getProperties(); + ({ lower, upper } = properties.voiRange); const volume = cache.getVolume(volumeId); modality = volume.metadata.Modality; isPreScaled = volume.scaling && Object.keys(volume.scaling).length > 0; @@ -97,12 +91,6 @@ class WindowLevelTool extends BaseTool { }); } - const eventDetail: Types.EventTypes.VoiModifiedEventDetail = { - volumeId, - viewportId, - range: newRange, - }; - if (viewport instanceof StackViewport) { viewport.setProperties({ voiRange: newRange, @@ -112,14 +100,16 @@ class WindowLevelTool extends BaseTool { return; } - // Only trigger event for volume since the stack event is triggered inside - // the stackViewport, Todo: we need the setProperties API on the volume viewport - triggerEvent(element, Enums.Events.VOI_MODIFIED, eventDetail); - rgbTransferFunction.setRange(newRange.lower, newRange.upper); + if (viewport instanceof VolumeViewport) { + viewport.setProperties({ + voiRange: newRange, + }); - viewportsContainingVolumeUID.forEach((vp) => { - vp.render(); - }); + viewportsContainingVolumeUID.forEach((vp) => { + vp.render(); + }); + return; + } } getPTNewRange({ deltaPointsCanvas, lower, upper, clientHeight }) { diff --git a/utils/ExampleRunner/example-info.json b/utils/ExampleRunner/example-info.json index 9fab3a786c..a0c6efdac0 100644 --- a/utils/ExampleRunner/example-info.json +++ b/utils/ExampleRunner/example-info.json @@ -26,6 +26,10 @@ "name": "Stack Viewport API", "description": "Demonstrates how to interact with a Stack viewport (e.g. Set VOI Range, Next/Previous Images, Flip H/V, Rotate, Invert, Zoom/Pan, Reset)" }, + "stackVoiSigmoid": { + "name": "Stack Sigmoid LUT", + "description": "Demonstrates the Sigmoid LUT Function instead of Linear" + }, "stackEvents": { "name": "Stack Viewport Events", "description": "Demonstrates the Events that are fired during interaction with a Stack Viewport" @@ -42,6 +46,10 @@ "name": "Volume Viewport API", "description": "Demonstrates how to interact with a Volume viewport (e.g. Set VOI Range, Change Camera Position / Orientation, Change Slab Thickness, Flip H/V, Rotate, Invert, Zoom/Pan, Reset)" }, + "volumeVoiSigmoid": { + "name": "Volume Sigmoid LUT", + "description": "Demonstrates the Sigmoid LUT Function instead of Linear" + }, "volumeViewport3D": { "name": "3D Volume Rendering", "description": "Demonstrates how to 3D render a volume and apply a preset"