Skip to content

fix(labelmap) : Display labelmap on volume3d viewport #1993

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions packages/core/src/RenderingEngine/BaseVolumeViewport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1940,6 +1940,30 @@ abstract class BaseVolumeViewport extends Viewport {
public getAllVolumeIds(): string[] {
return Array.from(this.volumeIds);
}

/**
* Gets the blend mode for the volume viewport. If filterActorUIDs is provided,
* it will return the blend mode for the first matching actor. Otherwise, it returns
* the blend mode of the first actor.
*
* @param filterActorUIDs - Optional array of actor UIDs to filter by
* @returns The blend mode of the matched actor
*/
public getBlendMode(filterActorUIDs?: string[]): BlendModes {
const actorEntries = this.getActors();
const actorForBlend =
filterActorUIDs?.length > 0
? actorEntries.find((actorEntry) =>
filterActorUIDs.includes(actorEntry.uid)
)
: actorEntries[0];

return (
actorForBlend?.blendMode ||
// @ts-ignore vtk incorrect typing
actorForBlend?.actor.getMapper().getBlendMode()
);
}
}

export default BaseVolumeViewport;
24 changes: 0 additions & 24 deletions packages/core/src/RenderingEngine/VolumeViewport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -245,30 +245,6 @@ class VolumeViewport extends BaseVolumeViewport {
this.resetCamera();
}

/**
* Gets the blend mode for the volume viewport. If filterActorUIDs is provided,
* it will return the blend mode for the first matching actor. Otherwise, it returns
* the blend mode of the first actor.
*
* @param filterActorUIDs - Optional array of actor UIDs to filter by
* @returns The blend mode of the matched actor
*/
public getBlendMode(filterActorUIDs?: string[]): BlendModes {
const actorEntries = this.getActors();
const actorForBlend =
filterActorUIDs?.length > 0
? actorEntries.find((actorEntry) =>
filterActorUIDs.includes(actorEntry.uid)
)
: actorEntries[0];

return (
actorForBlend?.blendMode ||
// @ts-ignore vtk incorrect typing
actorForBlend?.actor.getMapper().getBlendMode()
);
}

/**
* Sets the blend mode for actors in the volume viewport. Can optionally filter which
* actors to apply the blend mode to using filterActorUIDs.
Expand Down
3 changes: 2 additions & 1 deletion packages/core/src/utilities/getViewportImageIds.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { VolumeViewport } from '../RenderingEngine';
import BaseVolumeViewport from '../RenderingEngine/BaseVolumeViewport';
import cache from '../cache/cache';
import type { IViewport, IStackViewport } from '../types';

Expand All @@ -9,7 +10,7 @@ import type { IViewport, IStackViewport } from '../types';
* @returns An array of image IDs.
*/
function getViewportImageIds(viewport: IViewport) {
if (viewport instanceof VolumeViewport) {
if (viewport instanceof BaseVolumeViewport) {
const volume = cache.getVolume(viewport.getVolumeId());
return volume.imageIds;
} else if ((viewport as IStackViewport).getImageIds) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import type { Types } from '@cornerstonejs/core';
import { volumeLoader, imageLoader, VolumeViewport } from '@cornerstonejs/core';
import {
volumeLoader,
imageLoader,
BaseVolumeViewport,
} from '@cornerstonejs/core';
import { utilities } from '@cornerstonejs/tools';
import * as cornerstoneTools from '@cornerstonejs/tools';
import type { Types as ToolsTypes } from '@cornerstonejs/tools';
Expand Down Expand Up @@ -70,7 +74,7 @@ async function computeLabelmapFromContourSegmentation(
| ToolsTypes.LabelmapSegmentationDataStack
> {
const isVolume = options.viewport
? options.viewport instanceof VolumeViewport
? options.viewport instanceof BaseVolumeViewport
: true;

if (isVolume && !options.viewport) {
Expand Down Expand Up @@ -111,7 +115,7 @@ async function computeLabelmapFromSurfaceSegmentation(
| ToolsTypes.LabelmapSegmentationDataStack
> {
const { viewport } = options;
const isVolume = viewport ? viewport instanceof VolumeViewport : true;
const isVolume = viewport ? viewport instanceof BaseVolumeViewport : true;

const segmentIndices = options.segmentIndices?.length
? options.segmentIndices
Expand Down
2 changes: 1 addition & 1 deletion packages/tools/examples/labelmapMIP/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ async function run() {
},
{
viewportId: viewportId2,
type: ViewportType.ORTHOGRAPHIC,
type: ViewportType.VOLUME_3D,
element: element2,
defaultOptions: {
orientation: Enums.OrientationAxis.SAGITTAL,
Expand Down
256 changes: 256 additions & 0 deletions packages/tools/examples/labelmapMIPVolume3D/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,256 @@
import type { Types } from '@cornerstonejs/core';
import {
RenderingEngine,
Enums,
setVolumesForViewports,
volumeLoader,
} from '@cornerstonejs/core';
import * as cornerstone from '@cornerstonejs/core';
import {
initDemo,
createImageIdsAndCacheMetaData,
setTitleAndDescription,
setPetColorMapTransferFunctionForVolumeActor,
setPetTransferFunctionForVolumeActor,
addButtonToToolbar,
addSegmentIndexDropdown,
} from '../../../../utils/demo/helpers';
import * as cornerstoneTools from '@cornerstonejs/tools';
import { fillVolumeLabelmapWithMockData } from '../../../../utils/test/testUtils';
import { SegmentationRepresentations } from '../../src/enums';
import { triggerSegmentationDataModified } from '../../src/stateManagement/segmentation/triggerSegmentationEvents';
import { BrushTool, SegmentSelectTool } from '../../src/tools';

// This is for debugging purposes
console.warn(
'Click on index.ts to open source code for this example --------->'
);

const {
ToolGroupManager,
Enums: csToolsEnums,
segmentation,
VolumeRotateTool,
StackScrollTool,
} = cornerstoneTools;

const { ViewportType, BlendModes } = Enums;
const { MouseBindings } = csToolsEnums;

// 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
const segmentationId = 'MY_SEGMENTATION_ID';
const toolGroupId = 'MY_TOOLGROUP_ID';
// Create the viewports
const viewportId1 = 'CT_LEFT';
const viewportId2 = 'CT_MIP';

// ======== Set up page ======== //
setTitleAndDescription(
'Labelmap Rendering over MIP data on Volume3D viewport',
'Here we demonstrate rendering of a labelmap over MIP data, use the brush tool to drawn segmentation',
);

const size = '500px';
const content = document.getElementById('content');
const viewportGrid = document.createElement('div');

viewportGrid.style.display = 'flex';
viewportGrid.style.display = 'flex';
viewportGrid.style.flexDirection = 'row';

const element1 = document.createElement('div');
const element2 = document.createElement('div');
element1.style.width = size;
element1.style.height = size;
element2.style.width = size;
element2.style.height = size;

const instructions = document.createElement('p');
instructions.innerText = `
Left Click: Use BrushSphere Segmentation Tool.
Mouse wheel: Scroll Stack
`;

content.append(instructions);

viewportGrid.appendChild(element1);
viewportGrid.appendChild(element2);

content.appendChild(viewportGrid);

addButtonToToolbar({
title: 'Add Labelmap Representation',
onClick: () => {
segmentation.addLabelmapRepresentationToViewport(viewportId2, [
{
segmentationId,
config: {
blendMode: BlendModes.LABELMAP_EDGE_PROJECTION_BLEND,
},
},
]);
},
});

/**
* Runs the demo
*/
async function run() {
// Init Cornerstone and related libraries
await initDemo();
cornerstoneTools.addTool(VolumeRotateTool);
cornerstoneTools.addTool(StackScrollTool);
cornerstoneTools.addTool(SegmentSelectTool);
cornerstoneTools.addTool(BrushTool);

// Define tool groups to add the segmentation display tool to
const toolGroup = ToolGroupManager.createToolGroup(toolGroupId);
const toolGroup2 = ToolGroupManager.createToolGroup('mipToolGroup');

toolGroup2.addTool(VolumeRotateTool.toolName);
toolGroup.addTool(StackScrollTool.toolName);

[toolGroup, toolGroup2].forEach((toolGroup) => {
toolGroup?.addTool(SegmentSelectTool.toolName);
toolGroup?.addToolInstance('SphereBrush', BrushTool.toolName, {
activeStrategy: 'FILL_INSIDE_SPHERE',
});
});

// Get Cornerstone imageIds for the source data 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.879445243400782656317561081015',
wadoRsRoot: 'https://d33do7qe4w26qo.cloudfront.net/dicomweb',
});

// Define a volume in memory
const volume = await volumeLoader.createAndCacheVolume(volumeId, {
imageIds,
});

// Instantiate a rendering engine
const renderingEngineId = 'myRenderingEngine';
const renderingEngine = new RenderingEngine(renderingEngineId);

const viewportInputArray = [
{
viewportId: viewportId1,
type: ViewportType.ORTHOGRAPHIC,
element: element1,
defaultOptions: {
orientation: Enums.OrientationAxis.CORONAL,
background: <Types.Point3>[0.2, 0, 0.2],
},
},
{
viewportId: viewportId2,
type: ViewportType.VOLUME_3D,
element: element2,
defaultOptions: {
orientation: Enums.OrientationAxis.SAGITTAL,
background: <Types.Point3>[0.2, 0, 0.2],
},
},
];

renderingEngine.setViewports(viewportInputArray);

toolGroup.addViewport(viewportId1, renderingEngineId);
toolGroup2.addViewport(viewportId2, renderingEngineId);

toolGroup.setToolActive(StackScrollTool.toolName, {
bindings: [{ mouseButton: MouseBindings.Wheel }],
});
toolGroup.setToolActive('SphereBrush', {
bindings: [{ mouseButton: MouseBindings.Primary }],
});

// toolGroup.setToolActive(SegmentSelectTool.toolName);

toolGroup2.setToolActive(VolumeRotateTool.toolName, {
bindings: [{ mouseButton: MouseBindings.Wheel }],
});

toolGroup2.setToolActive(SegmentSelectTool.toolName);

// Set the volume to load
volume.load();

await setVolumesForViewports(renderingEngine, [{ volumeId }], [viewportId1]);

await setVolumesForViewports(
renderingEngine,
[
{
volumeId: volumeId,
callback: setPetTransferFunctionForVolumeActor,
blendMode: BlendModes.MAXIMUM_INTENSITY_BLEND,
slabThickness: 50000,
},
],
[viewportId2]
);

// Render the image
renderingEngine.renderViewports([viewportId1, viewportId2]);

// Add some segmentations based on the source data volume
// ============================= //

// Create a segmentation of the same resolution as the source data
volumeLoader.createAndCacheDerivedLabelmapVolume(volumeId, {
volumeId: segmentationId,
});

segmentation.config.style.setStyle(
{
type: csToolsEnums.SegmentationRepresentations.Labelmap,
viewportId: viewportId1,
},
{
// fillAlpha: 0.0,
activeSegmentOutlineWidthDelta: 3,
}
);

segmentation.config.style.setStyle(
{
type: csToolsEnums.SegmentationRepresentations.Labelmap,
viewportId: viewportId2,
},
{

activeSegmentOutlineWidthDelta: 2,
}
);

// Add the segmentations to state
segmentation.addSegmentations([
{
segmentationId,
representation: {
type: SegmentationRepresentations.Labelmap,
data: {
volumeId: segmentationId,
},
},
},
]);

addSegmentIndexDropdown('MY_SEGMENTATION_ID', [1, 2, 3, 4]);


await segmentation.addLabelmapRepresentationToViewport(viewportId1, [
{ segmentationId },
]);

triggerSegmentationDataModified(segmentationId);
}

run();
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {
VolumeViewport,
getEnabledElementByViewportId,
StackViewport,
BaseVolumeViewport,
} from '@cornerstonejs/core';

import type { SegmentationDataModifiedEventType } from '../../../types/EventTypes';
Expand All @@ -25,7 +26,7 @@ const onLabelmapSegmentationDataModified = function (

const hasVolumeViewport = viewportIds.some((viewportId) => {
const { viewport } = getEnabledElementByViewportId(viewportId);
return viewport instanceof VolumeViewport;
return viewport instanceof BaseVolumeViewport;
});

const hasStackViewport = viewportIds.some((viewportId) => {
Expand All @@ -38,7 +39,7 @@ const onLabelmapSegmentationDataModified = function (
viewportIds.forEach((viewportId) => {
const { viewport } = getEnabledElementByViewportId(viewportId);

if (viewport instanceof VolumeViewport) {
if (viewport instanceof BaseVolumeViewport) {
// For combined stack and volume scenarios in the rendering engine, updating only affected
// slices is not ideal. Stack indices (e.g., 0 for just one image) don't
// correspond to image indices in the volume. In this case, we update all slices.
Expand Down
Loading
Loading