OHIF with Cornerstone3D 2.0 (#4347)

This commit is contained in:
Alireza authored and GitHub committed 2024-11-06 07:15:27 -05:00
1 parent 8981a61b8c
commit 7ce7127d7c
803 files changed
+32760 -15893

No files matched your search

@@ -46,9 +46,9 @@
},
"dependencies": {
"@babel/runtime": "^7.20.13",
"@cornerstonejs/adapters": "^1.86.0",
"@cornerstonejs/core": "^1.86.0",
"@kitware/vtk.js": "30.4.1",
"@cornerstonejs/adapters": "^2.1.13",
"@cornerstonejs/core": "^2.1.13",
"@kitware/vtk.js": "32.1.0",
"react-color": "^2.19.3"
}
}
@@ -97,7 +97,7 @@ function _getDisplaySetsFromSeries(
};
// Does not get the referenced volumeId during parametric displaySet creation because the
// referenced displaySet is still not avaialble (getDisplaySetByUID returns `undefined`).
// referenced displaySet is still not available (getDisplaySetByUID returns `undefined`).
displaySet.getReferencedVolumeId = () => {
if (displaySet.referencedVolumeId) {
return displaySet.referencedVolumeId;
@@ -119,6 +119,22 @@ function _getDisplaySetsFromSeries(
return [displaySet];
}
const getRangeFromPixelData = (pixelData: Float32Array) => {
let lowest = pixelData[0];
let highest = pixelData[0];
for (let i = 1; i < pixelData.length; i++) {
if (pixelData[i] < lowest) {
lowest = pixelData[i];
}
if (pixelData[i] > highest) {
highest = pixelData[i];
}
}
return [lowest, highest];
};
async function _load(
displaySet,
servicesManager: AppTypes.ServicesManager,
@@ -132,11 +148,11 @@ async function _load(
return volumeLoadObject.promise;
}
displaySet.loadStatus.loading = true;
displaySet.loading = true;
displaySet.isLoaded = false;
// We don't want to fire multiple loads, so we'll wait for the first to finish
// and also return the same promise to any other callers.
// loadPromises[SOPInstanceUID] = new Promise(async (resolve, reject) => {
const promise = _loadParametricMap({
extensionManager,
displaySet,
@@ -149,18 +165,26 @@ async function _load(
promise
.then(() => {
displaySet.loadStatus.loading = false;
displaySet.loadStatus.loaded = true;
displaySet.loading = false;
displaySet.isLoaded = true;
// Broadcast that loading is complete
servicesManager.services.segmentationService._broadcastEvent(
servicesManager.services.segmentationService.EVENTS.SEGMENTATION_LOADING_COMPLETE,
{
pmapDisplaySet: displaySet,
}
);
})
.catch(err => {
displaySet.loadStatus.loading = false;
displaySet.loading = false;
displaySet.isLoaded = false;
throw err;
});
return promise;
}
async function _loadParametricMap({ extensionManager, displaySet, headers }: withAppTypes) {
async function _loadParametricMap({ displaySet, headers }: withAppTypes) {
const arrayBuffer = await dicomLoaderService.findDicomDataPromise(displaySet, null, headers);
const referencedVolumeId = displaySet.getReferencedVolumeId();
const cachedReferencedVolume = cache.getVolume(referencedVolumeId);
@@ -189,9 +213,12 @@ async function _loadParametricMap({ extensionManager, displaySet, headers }: wit
},
});
derivedVolume.getScalarData().set(pixelData);
const range = derivedVolume.imageData.getPointData().getScalars().getRange();
const newPixelData = new TypedArrayConstructor(pixelData.length);
for (let i = 0; i < pixelData.length; i++) {
newPixelData[i] = pixelData[i] * 100;
}
derivedVolume.voxelManager.setCompleteScalarDataArray(newPixelData);
const range = getRangeFromPixelData(newPixelData);
const windowLevel = csUtils.windowLevel.toWindowLevel(range[0], range[1]);
derivedVolume.metadata.voiLut = [windowLevel];
@@ -1,6 +0,0 @@
function createPMAPToolGroupAndAddTools(ToolGroupService, customizationService, toolGroupId) {
const { tools } = customizationService.get('cornerstone.overlayViewportTools') ?? {};
return ToolGroupService.createToolGroupAndAddTools(toolGroupId, tools);
}
export default createPMAPToolGroupAndAddTools;
@@ -1,16 +1,19 @@
import PropTypes from 'prop-types';
import React, { useCallback, useEffect, useRef } from 'react';
import { useViewportGrid } from '@ohif/ui';
import createPMAPToolGroupAndAddTools from '../utils/initPMAPToolGroup';
const PMAP_TOOLGROUP_BASE_NAME = 'PMAPToolGroup';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { useViewportGrid, LoadingIndicatorTotalPercent } from '@ohif/ui';
function OHIFCornerstonePMAPViewport(props: withAppTypes) {
const { displaySets, viewportOptions, displaySetOptions, servicesManager, extensionManager } =
props;
const {
displaySets,
children,
viewportOptions,
displaySetOptions,
servicesManager,
extensionManager,
} = props;
const viewportId = viewportOptions.viewportId;
const { displaySetService, toolGroupService, customizationService } = servicesManager.services;
const toolGroupId = `${PMAP_TOOLGROUP_BASE_NAME}-${viewportId}`;
const { displaySetService, segmentationService, uiNotificationService } =
servicesManager.services;
// PMAP viewport will always have a single display set
if (displaySets.length !== 1) {
@@ -32,6 +35,24 @@ function OHIFCornerstonePMAPViewport(props: withAppTypes) {
metadata: referencedDisplaySetMetadata,
};
const [pmapIsLoading, setPmapIsLoading] = useState(!pmapDisplaySet.isLoaded);
// Add effect to listen for loading complete
useEffect(() => {
const { unsubscribe } = segmentationService.subscribe(
segmentationService.EVENTS.SEGMENTATION_LOADING_COMPLETE,
evt => {
if (evt.pmapDisplaySet?.displaySetInstanceUID === pmapDisplaySet.displaySetInstanceUID) {
setPmapIsLoading(false);
}
}
);
return () => {
unsubscribe();
};
}, [pmapDisplaySet]);
const getCornerstoneViewport = useCallback(() => {
const { displaySet: referencedDisplaySet } = referencedDisplaySetRef.current;
const { component: Component } = extensionManager.getModuleEntry(
@@ -46,12 +67,25 @@ function OHIFCornerstonePMAPViewport(props: withAppTypes) {
Object.assign(pmapDisplaySetOptions.options, {
colormap: {
name: 'rainbow',
name: 'rainbow_2',
opacity: [
{ value: 0, opacity: 0.5 },
{ value: 1, opacity: 1 },
{ value: 0, opacity: 0 },
{ value: 0.25, opacity: 0.25 },
{ value: 0.5, opacity: 0.5 },
{ value: 0.75, opacity: 0.75 },
{ value: 0.9, opacity: 0.99 },
],
},
voi: {
windowCenter: 50,
windowWidth: 100,
},
});
uiNotificationService.show({
title: 'Parametric Map',
type: 'warning',
message: 'The values are multiplied by 100 in the viewport for better visibility',
});
return (
@@ -61,7 +95,6 @@ function OHIFCornerstonePMAPViewport(props: withAppTypes) {
displaySets={[referencedDisplaySet, pmapDisplaySet]}
viewportOptions={{
viewportType: 'volume',
toolGroupId: toolGroupId,
orientation: viewportOptions.orientation,
viewportId: viewportOptions.viewportId,
}}
@@ -73,7 +106,6 @@ function OHIFCornerstonePMAPViewport(props: withAppTypes) {
displaySetOptions,
props,
pmapDisplaySet,
toolGroupId,
viewportOptions.orientation,
viewportOptions.viewportId,
]);
@@ -98,23 +130,33 @@ function OHIFCornerstonePMAPViewport(props: withAppTypes) {
};
}, [activeViewportId, displaySetService, viewportGridService, viewports]);
useEffect(() => {
let toolGroup = toolGroupService.getToolGroup(toolGroupId);
let childrenWithProps = null;
if (toolGroup) {
return;
}
// This creates a custom tool group which has the lifetime of this view only
toolGroup = createPMAPToolGroupAndAddTools(toolGroupService, customizationService, toolGroupId);
return () => toolGroupService.destroyToolGroup(toolGroupId);
}, [customizationService, toolGroupId, toolGroupService]);
if (children && children.length) {
childrenWithProps = children.map((child, index) => {
return (
child &&
React.cloneElement(child, {
viewportId,
key: index,
})
);
});
}
return (
<>
<div className="relative flex h-full w-full flex-row overflow-hidden">
{pmapIsLoading && (
<LoadingIndicatorTotalPercent
className="h-full w-full"
totalNumbers={null}
percentComplete={null}
loadingText="Loading Parametric Map..."
/>
)}
{getCornerstoneViewport()}
{childrenWithProps}
</div>
</>
);
@@ -0,0 +1,55 @@
import { SegmentationRepresentations } from '@cornerstonejs/tools/enums';
const commandsModule = ({ commandsManager, servicesManager }: withAppTypes) => {
const services = servicesManager.services;
const { displaySetService, viewportGridService } = services;
const actions = {
hydrateRTSDisplaySet: ({ displaySet, viewportId }) => {
if (displaySet.Modality !== 'RTSTRUCT') {
throw new Error('Display set is not an RTSTRUCT');
}
const referencedDisplaySet = displaySetService.getDisplaySetByUID(
displaySet.referencedDisplaySetInstanceUID
);
// update the previously stored segmentationPresentation with the new viewportId
// presentation so that when we put the referencedDisplaySet back in the viewport
// it will have the correct segmentation representation hydrated
commandsManager.runCommand('updateStoredSegmentationPresentation', {
displaySet: displaySet,
type: SegmentationRepresentations.Contour,
});
// update the previously stored positionPresentation with the new viewportId
// presentation so that when we put the referencedDisplaySet back in the viewport
// it will be in the correct position zoom and pan
commandsManager.runCommand('updateStoredPositionPresentation', {
viewportId,
displaySetInstanceUID: referencedDisplaySet.displaySetInstanceUID,
});
viewportGridService.setDisplaySetsForViewport({
viewportId,
displaySetInstanceUIDs: [referencedDisplaySet.displaySetInstanceUID],
});
},
};
const definitions = {
hydrateRTSDisplaySet: {
commandFn: actions.hydrateRTSDisplaySet,
storeContexts: [],
options: {},
},
};
return {
actions,
definitions,
defaultContext: 'cornerstone-dicom-rt',
};
};
export default commandsModule;
@@ -72,22 +72,27 @@ function _getDisplaySetsFromSeries(
displaySet.referencedImages = instance.ReferencedSeriesSequence.ReferencedInstanceSequence;
displaySet.referencedSeriesInstanceUID = referencedSeries.SeriesInstanceUID;
displaySet.getReferenceDisplaySet = () => {
const { displaySetService } = servicesManager.services;
const referencedDisplaySets = displaySetService.getDisplaySetsForSeries(
displaySet.referencedSeriesInstanceUID
const { displaySetService } = servicesManager.services;
const referencedDisplaySets = displaySetService.getDisplaySetsForSeries(
displaySet.referencedSeriesInstanceUID
);
if (!referencedDisplaySets || referencedDisplaySets.length === 0) {
// Instead of throwing error, subscribe to display sets added
const { unsubscribe } = displaySetService.subscribe(
displaySetService.EVENTS.DISPLAY_SETS_ADDED,
({ displaySetsAdded }) => {
const addedDisplaySet = displaySetsAdded[0];
if (addedDisplaySet.SeriesInstanceUID === displaySet.referencedSeriesInstanceUID) {
displaySet.referencedDisplaySetInstanceUID = addedDisplaySet.displaySetInstanceUID;
unsubscribe();
}
}
);
if (!referencedDisplaySets || referencedDisplaySets.length === 0) {
throw new Error('Referenced DisplaySet is missing for the RT');
}
} else {
const referencedDisplaySet = referencedDisplaySets[0];
displaySet.referencedDisplaySetInstanceUID = referencedDisplaySet.displaySetInstanceUID;
return referencedDisplaySet;
};
}
displaySet.load = ({ headers }) => _load(displaySet, servicesManager, extensionManager, headers);
@@ -111,19 +116,13 @@ function _load(rtDisplaySet, servicesManager: AppTypes.ServicesManager, extensio
// and also return the same promise to any other callers.
loadPromises[SOPInstanceUID] = new Promise(async (resolve, reject) => {
if (!rtDisplaySet.structureSet) {
const structureSet = await loadRTStruct(
extensionManager,
rtDisplaySet,
rtDisplaySet.getReferenceDisplaySet(),
headers
);
const structureSet = await loadRTStruct(extensionManager, rtDisplaySet, headers);
rtDisplaySet.structureSet = structureSet;
}
const suppressEvents = true;
segmentationService
.createSegmentationForRTDisplaySet(rtDisplaySet, null, suppressEvents)
.createSegmentationForRTDisplaySet(rtDisplaySet)
.then(() => {
rtDisplaySet.loading = false;
resolve();
@@ -2,6 +2,7 @@ import { id } from './id';
import React from 'react';
import { Types } from '@ohif/core';
import getSopClassHandlerModule from './getSopClassHandlerModule';
import getCommandsModule from './getCommandsModule';
const Component = React.lazy(() => {
return import(/* webpackPrefetch: true */ './viewports/OHIFCornerstoneRTViewport');
@@ -24,6 +25,7 @@ const extension: Types.Extensions.Extension = {
* You ID can be anything you want, but it should be unique.
*/
id,
getCommandsModule,
/**
* PanelModule should provide a list of panels that will be available in OHIF
@@ -89,12 +89,7 @@ async function checkAndLoadContourData(instance, datasource) {
});
}
export default async function loadRTStruct(
extensionManager,
rtStructDisplaySet,
referencedDisplaySet,
headers
) {
export default async function loadRTStruct(extensionManager, rtStructDisplaySet, headers) {
const utilityModule = extensionManager.getModuleEntry(
'@ohif/extension-cornerstone.utilityModule.common'
);
@@ -102,8 +97,6 @@ export default async function loadRTStruct(
const { bulkDataURI } = dataSource.getConfig?.() || {};
const { dicomLoaderService } = utilityModule.exports;
const imageIdSopInstanceUidPairs =
_getImageIdSopInstanceUidPairsForDisplaySet(referencedDisplaySet);
// Set here is loading is asynchronous.
// If this function throws its set back to false.
@@ -149,8 +142,7 @@ export default async function loadRTStruct(
const contourPoints = [];
for (let c = 0; c < ContourSequenceArray.length; c++) {
const { ContourImageSequence, ContourData, NumberOfContourPoints, ContourGeometricType } =
ContourSequenceArray[c];
const { ContourData, NumberOfContourPoints, ContourGeometricType } = ContourSequenceArray[c];
let isSupported = false;
@@ -194,24 +186,6 @@ export default async function loadRTStruct(
return structureSet;
}
const _getImageId = (imageIdSopInstanceUidPairs, sopInstanceUID) => {
const imageIdSopInstanceUidPairsEntry = imageIdSopInstanceUidPairs.find(
imageIdSopInstanceUidPairsEntry =>
imageIdSopInstanceUidPairsEntry.sopInstanceUID === sopInstanceUID
);
return imageIdSopInstanceUidPairsEntry ? imageIdSopInstanceUidPairsEntry.imageId : null;
};
function _getImageIdSopInstanceUidPairsForDisplaySet(referencedDisplaySet) {
return referencedDisplaySet.images.map(image => {
return {
imageId: image.imageId,
sopInstanceUID: image.SOPInstanceUID,
};
});
}
function _setROIContourMetadata(
structureSet,
StructureSetROISequence,
@@ -10,7 +10,6 @@ function promptHydrateRT({
servicesManager,
rtDisplaySet,
viewportId,
toolGroupId = 'default',
preHydrateCallbacks,
hydrateRTDisplaySet,
}: withAppTypes) {
@@ -27,16 +26,13 @@ function promptHydrateRT({
callback();
});
window.setTimeout(async () => {
const isHydrated = await hydrateRTDisplaySet({
rtDisplaySet,
viewportId,
toolGroupId,
servicesManager,
});
const isHydrated = await hydrateRTDisplaySet({
rtDisplaySet,
viewportId,
servicesManager,
});
resolve(isHydrated);
}, 0);
resolve(isHydrated);
}
});
}
@@ -4,7 +4,9 @@ import { useViewportGrid, LoadingIndicatorTotalPercent, ViewportActionArrows } f
import promptHydrateRT from '../utils/promptHydrateRT';
import _getStatusComponent from './_getStatusComponent';
import createRTToolGroupAndAddTools from '../utils/initRTToolGroup';
import { SegmentationRepresentations } from '@cornerstonejs/tools/enums';
const RT_TOOLGROUP_BASE_NAME = 'RTToolGroup';
@@ -62,7 +64,10 @@ function OHIFCornerstoneRTViewport(props: withAppTypes) {
const { viewports, activeViewportId } = viewportGrid;
const referencedDisplaySet = rtDisplaySet.getReferenceDisplaySet();
const referencedDisplaySetInstanceUID = rtDisplaySet.referencedDisplaySetInstanceUID;
const referencedDisplaySet = displaySetService.getDisplaySetByUID(
referencedDisplaySetInstanceUID
);
const referencedDisplaySetMetadata = _getReferencedDisplaySetMetadata(referencedDisplaySet);
referencedDisplaySetRef.current = {
@@ -91,12 +96,15 @@ function OHIFCornerstoneRTViewport(props: withAppTypes) {
});
}, [viewportGrid]);
const hydrateRTDisplaySet = ({ rtDisplaySet, viewportId }) => {
commandsManager.runCommand('loadSegmentationDisplaySetsForViewport', {
displaySets: [rtDisplaySet],
viewportId,
});
};
const hydrateRTDisplaySet = useCallback(
({ rtDisplaySet, viewportId }) => {
commandsManager.runCommand('hydrateRTSDisplaySet', {
displaySet: rtDisplaySet,
viewportId,
});
},
[commandsManager]
);
const getCornerstoneViewport = useCallback(() => {
const { component: Component } = extensionManager.getModuleEntry(
@@ -143,10 +151,10 @@ function OHIFCornerstoneRTViewport(props: withAppTypes) {
newSelectedSegmentIndex = numberOfSegments - 1;
}
segmentationService.jumpToSegmentCenter(segmentationId, newSelectedSegmentIndex, toolGroupId);
segmentationService.jumpToSegmentCenter(segmentationId, newSelectedSegmentIndex, viewportId);
setSelectedSegment(newSelectedSegmentIndex);
},
[selectedSegment]
[selectedSegment, segmentationService]
);
useEffect(() => {
@@ -243,7 +251,7 @@ function OHIFCornerstoneRTViewport(props: withAppTypes) {
return () => {
// remove the segmentation representations if seg displayset changed
segmentationService.removeSegmentationRepresentationFromToolGroup(toolGroupId);
segmentationService.removeSegmentationRepresentations(viewportId);
toolGroupService.destroyToolGroup(toolGroupId);
};
@@ -254,11 +262,27 @@ function OHIFCornerstoneRTViewport(props: withAppTypes) {
return () => {
// remove the segmentation representations if seg displayset changed
segmentationService.removeSegmentationRepresentationFromToolGroup(toolGroupId);
segmentationService.removeSegmentationRepresentations(viewportId);
referencedDisplaySetRef.current = null;
};
}, [rtDisplaySet]);
const onStatusClick = useCallback(async () => {
// Before hydrating a RT and make it added to all viewports in the grid
// that share the same frameOfReferenceUID, we need to store the viewport grid
// presentation state, so that we can restore it after hydrating the RT. This is
// required if the user has changed the viewport (other viewport than RT viewport)
// presentation state (w/l and invert) and then opens the RT. If we don't store
// the presentation state, the viewport will be reset to the default presentation
storePresentationState();
const isHydrated = await hydrateRTDisplaySet({
rtDisplaySet,
viewportId,
});
setIsHydrated(isHydrated);
}, [hydrateRTDisplaySet, rtDisplaySet, storePresentationState, viewportId]);
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
let childrenWithProps = null;
@@ -282,24 +306,8 @@ function OHIFCornerstoneRTViewport(props: withAppTypes) {
});
}
const onStatusClick = useCallback(async () => {
// Before hydrating a RT and make it added to all viewports in the grid
// that share the same frameOfReferenceUID, we need to store the viewport grid
// presentation state, so that we can restore it after hydrating the RT. This is
// required if the user has changed the viewport (other viewport than RT viewport)
// presentation state (w/l and invert) and then opens the RT. If we don't store
// the presentation state, the viewport will be reset to the default presentation
storePresentationState();
const isHydrated = await hydrateRTDisplaySet({
rtDisplaySet,
viewportId,
});
setIsHydrated(isHydrated);
}, [hydrateRTDisplaySet, rtDisplaySet, storePresentationState, viewportId]);
useEffect(() => {
viewportActionCornersService.setComponents([
viewportActionCornersService.addComponents([
{
viewportId,
id: 'viewportStatusComponent',
@@ -318,7 +326,7 @@ function OHIFCornerstoneRTViewport(props: withAppTypes) {
key="actionArrows"
onArrowsClick={onSegmentChange}
className={
viewportId === activeViewportId ? 'visible' : 'invisible group-hover:visible'
viewportId === activeViewportId ? 'visible' : 'invisible group-hover/pane:visible'
}
></ViewportActionArrows>
),
@@ -46,9 +46,9 @@
},
"dependencies": {
"@babel/runtime": "^7.20.13",
"@cornerstonejs/adapters": "^1.86.0",
"@cornerstonejs/core": "^1.86.0",
"@kitware/vtk.js": "30.4.1",
"@cornerstonejs/adapters": "^2.1.13",
"@cornerstonejs/core": "^2.1.13",
"@kitware/vtk.js": "32.1.0",
"react-color": "^2.19.3"
}
}
@@ -14,18 +14,22 @@ import vtkImageMarchingSquares from '@kitware/vtk.js/Filters/General/ImageMarchi
import vtkDataArray from '@kitware/vtk.js/Common/Core/DataArray';
import vtkImageData from '@kitware/vtk.js/Common/DataModel/ImageData';
import {
updateViewportsForSegmentationRendering,
getUpdatedViewportsForSegmentation,
getTargetViewport,
} from './utils/hydrationUtils';
const { segmentation: segmentationUtils } = utilities;
const { datasetToBlob } = dcmjs.data;
const getTargetViewport = ({ viewportId, viewportGridService }) => {
const { viewports, activeViewportId } = viewportGridService.getState();
const targetViewportId = viewportId || activeViewportId;
const viewport = viewports.get(targetViewportId);
return viewport;
};
const {
Cornerstone3D: {
Segmentation: { generateLabelMaps2DFrom3D, generateSegmentation },
Segmentation: { generateSegmentation },
},
} = adaptersSEG;
@@ -42,87 +46,14 @@ const commandsModule = ({
extensionManager,
}: Types.Extensions.ExtensionParams): Types.Extensions.CommandsModule => {
const {
uiNotificationService,
segmentationService,
uiDialogService,
displaySetService,
viewportGridService,
toolGroupService,
cornerstoneViewportService,
} = servicesManager.services;
} = servicesManager.services as AppTypes.Services;
const actions = {
/**
* Retrieves a list of viewports that require updates in preparation for segmentation rendering.
* This function evaluates viewports based on their compatibility with the provided segmentation's
* frame of reference UID and appends them to the updated list if they should render the segmentation.
*
* @param {Object} params - Parameters for the function.
* @param params.viewportId - the ID of the viewport to be updated.
* @param params.servicesManager - The services manager
* @param params.referencedDisplaySetInstanceUID - Optional UID for the referenced display set instance.
*
* @returns {Array} Returns an array of viewports that require updates for segmentation rendering.
*/
getUpdatedViewportsForSegmentation,
/**
* Creates an empty segmentation for a specified viewport.
* It first checks if the display set associated with the viewport is reconstructable.
* If not, it raises a notification error. Otherwise, it creates a new segmentation
* for the display set after handling the necessary steps for making the viewport
* a volume viewport first
*
* @param {Object} params - Parameters for the function.
* @param params.viewportId - the target viewport ID.
*
*/
createEmptySegmentationForViewport: async ({ viewportId }) => {
const viewport = getTargetViewport({ viewportId, viewportGridService });
// Todo: add support for multiple display sets
const displaySetInstanceUID = viewport.displaySetInstanceUIDs[0];
const displaySet = displaySetService.getDisplaySetByUID(displaySetInstanceUID);
if (!displaySet.isReconstructable) {
uiNotificationService.show({
title: 'Segmentation',
message: 'Segmentation is not supported for non-reconstructible displaysets yet',
type: 'error',
});
return;
}
updateViewportsForSegmentationRendering({
viewportId,
servicesManager,
displaySet,
loadFn: async () => {
const currentSegmentations = segmentationService.getSegmentations();
const segmentationId = await segmentationService.createSegmentationForDisplaySet(
displaySetInstanceUID,
{ label: `Segmentation ${currentSegmentations.length + 1}` }
);
const toolGroupId = viewport.viewportOptions.toolGroupId;
await segmentationService.addSegmentationRepresentationToToolGroup(
toolGroupId,
segmentationId
);
// Add only one segment for now
segmentationService.addSegment(segmentationId, {
toolGroupId,
segmentIndex: 1,
properties: {
label: 'Segment 1',
},
});
return segmentationId;
},
});
},
/**
* Loads segmentations for a specified viewport.
* The function prepares the viewport for rendering, then loads the segmentation details.
@@ -134,106 +65,30 @@ const commandsModule = ({
*
*/
loadSegmentationsForViewport: async ({ segmentations, viewportId }) => {
updateViewportsForSegmentationRendering({
viewportId,
servicesManager,
loadFn: async () => {
// Todo: handle adding more than one segmentation
const viewport = getTargetViewport({ viewportId, viewportGridService });
const displaySetInstanceUID = viewport.displaySetInstanceUIDs[0];
const segmentation = segmentations[0];
const segmentationId = segmentation.id;
const label = segmentation.label;
const segments = segmentation.segments;
delete segmentation.segments;
await segmentationService.createSegmentationForDisplaySet(displaySetInstanceUID, {
segmentationId,
label,
});
if (segmentation.scalarData) {
const labelmapVolume = segmentationService.getLabelmapVolume(segmentationId);
labelmapVolume.scalarData.set(segmentation.scalarData);
}
segmentationService.addOrUpdateSegmentation(segmentation);
const toolGroupId = viewport.viewportOptions.toolGroupId;
await segmentationService.addSegmentationRepresentationToToolGroup(
toolGroupId,
segmentationId
);
segments.forEach(segment => {
if (segment === null) {
return;
}
segmentationService.addSegment(segmentationId, {
segmentIndex: segment.segmentIndex,
toolGroupId,
properties: {
color: segment.color,
label: segment.label,
opacity: segment.opacity,
isLocked: segment.isLocked,
visibility: segment.isVisible,
active: segmentation.activeSegmentIndex === segment.segmentIndex,
},
});
});
if (segmentation.centroidsIJK) {
segmentationService.setCentroids(segmentation.id, segmentation.centroidsIJK);
}
return segmentationId;
},
});
},
/**
* Loads segmentation display sets for a specified viewport.
* Depending on the modality of the display set (SEG or RTSTRUCT),
* it chooses the appropriate service function to create
* the segmentation for the display set.
* The function then prepares the viewport for rendering segmentation.
*
* @param {Object} params - Parameters for the function.
* @param params.viewportId - ID of the viewport where the segmentation display sets should be loaded.
* @param params.displaySets - Array of display sets to be loaded for segmentation.
*
*/
loadSegmentationDisplaySetsForViewport: async ({ viewportId, displaySets }) => {
// Todo: handle adding more than one segmentation
const displaySet = displaySets[0];
const referencedDisplaySet = displaySetService.getDisplaySetByUID(
displaySet.referencedDisplaySetInstanceUID
);
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
const initialSliceIndex = viewport.getSliceIndex();
const viewport = getTargetViewport({ viewportId, viewportGridService });
const displaySetInstanceUID = viewport.displaySetInstanceUIDs[0];
updateViewportsForSegmentationRendering({
viewportId,
servicesManager,
displaySet,
loadFn: async () => {
const segDisplaySet = displaySet;
const suppressEvents = false;
const serviceFunction =
segDisplaySet.Modality === 'SEG'
? 'createSegmentationForSEGDisplaySet'
: 'createSegmentationForRTDisplaySet';
const segmentation = segmentations[0];
const segmentationId = segmentation.segmentationId;
const label = segmentation.config.label;
const segments = segmentation.config.segments;
const boundFn = segmentationService[serviceFunction].bind(segmentationService);
const segmentationId = await boundFn(segDisplaySet, null, suppressEvents);
const segmentation = segmentationService.getSegmentation(segmentationId);
segmentation.description = `S${referencedDisplaySet.SeriesNumber}: ${referencedDisplaySet.SeriesDescription}`;
return segmentationId;
},
initialSliceIndex,
const displaySet = displaySetService.getDisplaySetByUID(displaySetInstanceUID);
await segmentationService.createLabelmapForDisplaySet(displaySet, {
segmentationId,
segments,
label,
});
segmentationService.addOrUpdateSegmentation(segmentation);
await segmentationService.addSegmentationRepresentation(viewport.viewportId, {
segmentationId,
});
return segmentationId;
},
/**
* Generates a segmentation from a given segmentation ID.
@@ -251,25 +106,61 @@ const commandsModule = ({
generateSegmentation: ({ segmentationId, options = {} }) => {
const segmentation = cornerstoneToolsSegmentation.state.getSegmentation(segmentationId);
const { referencedVolumeId } = segmentation.representationData.LABELMAP;
const { imageIds } = segmentation.representationData.Labelmap;
const segmentationVolume = cache.getVolume(segmentationId);
const referencedVolume = cache.getVolume(referencedVolumeId);
const referencedImages = referencedVolume.getCornerstoneImages();
const segImages = imageIds.map(imageId => cache.getImage(imageId));
const referencedImages = segImages.map(image => cache.getImage(image.referencedImageId));
const labelmapObj = generateLabelMaps2DFrom3D(segmentationVolume);
const labelmaps2D = [];
// Generate fake metadata as an example
labelmapObj.metadata = [];
let z = 0;
for (const segImage of segImages) {
const segmentsOnLabelmap = new Set();
const pixelData = segImage.getPixelData();
const { rows, columns } = segImage;
// Use a single pass through the pixel data
for (let i = 0; i < pixelData.length; i++) {
const segment = pixelData[i];
if (segment !== 0) {
segmentsOnLabelmap.add(segment);
}
}
labelmaps2D[z++] = {
segmentsOnLabelmap: Array.from(segmentsOnLabelmap),
pixelData,
rows,
columns,
};
}
const allSegmentsOnLabelmap = labelmaps2D.map(labelmap => labelmap.segmentsOnLabelmap);
const labelmap3D = {
segmentsOnLabelmap: Array.from(new Set(allSegmentsOnLabelmap.flat())),
metadata: [],
labelmaps2D,
};
const segmentationInOHIF = segmentationService.getSegmentation(segmentationId);
segmentationInOHIF.segments.forEach(segment => {
const representations = segmentationService.getRepresentationsForSegmentation(segmentationId);
Object.entries(segmentationInOHIF.segments).forEach(([segmentIndex, segment]) => {
// segmentation service already has a color for each segment
if (!segment) {
return;
}
const segmentIndex = segment.segmentIndex;
const { label, color } = segment;
const { label } = segment;
const firstRepresentation = representations[0];
const color = segmentationService.getSegmentColor(
firstRepresentation.viewportId,
segmentationId,
segment.segmentIndex
);
const RecommendedDisplayCIELabValue = dcmjs.data.Colors.rgb2DICOMLAB(
color.slice(0, 3).map(value => value / 255)
@@ -292,12 +183,12 @@ const commandsModule = ({
CodeMeaning: 'Tissue',
},
};
labelmapObj.metadata[segmentIndex] = segmentMetadata;
labelmap3D.metadata[segmentIndex] = segmentMetadata;
});
const generatedSegmentation = generateSegmentation(
referencedImages,
labelmapObj,
labelmap3D,
metaData,
options
);
@@ -445,18 +336,19 @@ const commandsModule = ({
};
const definitions = {
getUpdatedViewportsForSegmentation: {
commandFn: actions.getUpdatedViewportsForSegmentation,
},
/**
* Obsolete?
*/
loadSegmentationDisplaySetsForViewport: {
commandFn: actions.loadSegmentationDisplaySetsForViewport,
},
/**
* Obsolete?
*/
loadSegmentationsForViewport: {
commandFn: actions.loadSegmentationsForViewport,
},
createEmptySegmentationForViewport: {
commandFn: actions.createEmptySegmentationForViewport,
},
generateSegmentation: {
commandFn: actions.generateSegmentation,
},
@@ -20,6 +20,17 @@ const segProtocol: Types.HangingProtocol.Protocol = {
viewportType: 'stack',
toolGroupId: 'default',
allowUnmatchedView: true,
syncGroups: [
{
type: 'hydrateseg',
id: 'sameFORId',
source: true,
target: true,
// options: {
// matchingRules: ['sameFOR'],
// },
},
],
},
displaySets: [
{
@@ -52,7 +63,20 @@ const segProtocol: Types.HangingProtocol.Protocol = {
},
viewports: [
{
viewportOptions: { allowUnmatchedView: true },
viewportOptions: {
allowUnmatchedView: true,
syncGroups: [
{
type: 'hydrateseg',
id: 'sameFORId',
source: true,
target: true,
// options: {
// matchingRules: ['sameFOR'],
// },
},
],
},
displaySets: [
{
id: 'segDisplaySetId',
@@ -1,6 +1,6 @@
import { utils } from '@ohif/core';
import { metaData, cache, triggerEvent, eventTarget } from '@cornerstonejs/core';
import { CONSTANTS } from '@cornerstonejs/tools';
import { metaData, triggerEvent, eventTarget } from '@cornerstonejs/core';
import { CONSTANTS, segmentation as cstSegmentation } from '@cornerstonejs/tools';
import { adaptersSEG, Enums } from '@cornerstonejs/adapters';
import { SOPClassHandlerId } from './id';
@@ -70,28 +70,32 @@ function _getDisplaySetsFromSeries(
displaySet.referencedImages = instance.ReferencedSeriesSequence.ReferencedInstanceSequence;
displaySet.referencedSeriesInstanceUID = referencedSeries.SeriesInstanceUID;
const { displaySetService } = servicesManager.services;
const referencedDisplaySets = displaySetService.getDisplaySetsForSeries(
displaySet.referencedSeriesInstanceUID
);
displaySet.getReferenceDisplaySet = () => {
const { displaySetService } = servicesManager.services;
const referencedDisplaySets = displaySetService.getDisplaySetsForSeries(
displaySet.referencedSeriesInstanceUID
const referencedDisplaySet = referencedDisplaySets[0];
if (!referencedDisplaySet) {
// subscribe to display sets added which means at some point it will be available
const { unsubscribe } = displaySetService.subscribe(
displaySetService.EVENTS.DISPLAY_SETS_ADDED,
({ displaySetsAdded }) => {
// here we can also do a little bit of search, since sometimes DICOM SEG
// does not contain the referenced display set uid , and we can just
// see which of the display sets added is more similar and assign it
// to the referencedDisplaySet
const addedDisplaySet = displaySetsAdded[0];
if (addedDisplaySet.SeriesInstanceUID === displaySet.referencedSeriesInstanceUID) {
displaySet.referencedDisplaySetInstanceUID = addedDisplaySet.displaySetInstanceUID;
unsubscribe();
}
}
);
if (!referencedDisplaySets || referencedDisplaySets.length === 0) {
throw new Error('Referenced DisplaySet is missing for the SEG');
}
const referencedDisplaySet = referencedDisplaySets[0];
} else {
displaySet.referencedDisplaySetInstanceUID = referencedDisplaySet.displaySetInstanceUID;
// Todo: this needs to be able to work with other reference volumes (other than streaming) such as nifti, etc.
displaySet.referencedVolumeURI = referencedDisplaySet.displaySetInstanceUID;
const referencedVolumeId = `cornerstoneStreamingImageVolume:${displaySet.referencedVolumeURI}`;
displaySet.referencedVolumeId = referencedVolumeId;
return referencedDisplaySet;
};
}
displaySet.load = async ({ headers }) =>
await _load(displaySet, servicesManager, extensionManager, headers);
@@ -111,7 +115,7 @@ function _load(
if (
(segDisplaySet.loading || segDisplaySet.isLoaded) &&
loadPromises[SOPInstanceUID] &&
_segmentationExists(segDisplaySet, segmentationService)
_segmentationExists(segDisplaySet)
) {
return loadPromises[SOPInstanceUID];
}
@@ -135,9 +139,8 @@ function _load(
}
}
const suppressEvents = true;
segmentationService
.createSegmentationForSEGDisplaySet(segDisplaySet, null, suppressEvents)
.createSegmentationForSEGDisplaySet(segDisplaySet)
.then(() => {
segDisplaySet.loading = false;
resolve();
@@ -166,20 +169,20 @@ async function _loadSegments({
const { dicomLoaderService } = utilityModule.exports;
const arrayBuffer = await dicomLoaderService.findDicomDataPromise(segDisplaySet, null, headers);
const cachedReferencedVolume = cache.getVolume(segDisplaySet.referencedVolumeId);
const referencedDisplaySet = servicesManager.services.displaySetService.getDisplaySetByUID(
segDisplaySet.referencedDisplaySetInstanceUID
);
if (!cachedReferencedVolume) {
throw new Error(
'Referenced Volume is missing for the SEG, and stack viewport SEG is not supported yet'
);
if (!referencedDisplaySet) {
throw new Error('referencedDisplaySet is missing for SEG');
}
const { imageIds } = cachedReferencedVolume;
const { instances: images } = referencedDisplaySet;
const imageIds = images.map(({ imageId }) => imageId);
// Todo: what should be defaults here
const tolerance = 0.001;
const skipOverlapping = true;
eventTarget.addEventListener(Enums.Events.SEGMENTATION_LOAD_PROGRESS, evt => {
const { percentComplete } = evt.detail;
segmentationService._broadcastEvent(segmentationService.EVENTS.SEGMENT_LOADING_COMPLETE, {
@@ -231,9 +234,8 @@ async function _loadSegments({
Object.assign(segDisplaySet, results);
}
function _segmentationExists(segDisplaySet, segmentationService: AppTypes.SegmentationService) {
// This should be abstracted with the CornerstoneCacheService
return segmentationService.getSegmentation(segDisplaySet.displaySetInstanceUID);
function _segmentationExists(segDisplaySet) {
return cstSegmentation.state.getSegmentation(segDisplaySet.displaySetInstanceUID);
}
function getSopClassHandlerModule({ servicesManager, extensionManager }) {
@@ -8,7 +8,7 @@ export function getToolbarModule({ servicesManager }: withAppTypes) {
// forcing the button to have black background since initially
// it is designed for the toolbox not the toolbar on top
// we should then branch the buttonSectionId to have different styles
const segmentations = segmentationService.getSegmentations();
const segmentations = segmentationService.getSegmentationRepresentations(viewportId);
if (!segmentations?.length) {
return {
disabled: true,
@@ -3,7 +3,6 @@ import React from 'react';
import getSopClassHandlerModule from './getSopClassHandlerModule';
import getHangingProtocolModule from './getHangingProtocolModule';
import getPanelModule from './getPanelModule';
import getCommandsModule from './commandsModule';
import { getToolbarModule } from './getToolbarModule';
@@ -28,13 +27,6 @@ const extension = {
* You ID can be anything you want, but it should be unique.
*/
id,
/**
* PanelModule should provide a list of panels that will be available in OHIF
* for Modes to consume and render. Each panel is defined by a {name,
* iconName, iconLabel, label, component} object. Example of a panel module
* is the StudyBrowserPanel that is provided by the default extension in OHIF.
*/
getPanelModule,
getCommandsModule,
getToolbarModule,
getViewportModule({ servicesManager, extensionManager, commandsManager }) {
@@ -1,393 +0,0 @@
import { createReportAsync } from '@ohif/extension-default';
import React, { useEffect, useState, useCallback } from 'react';
import PropTypes from 'prop-types';
import { SegmentationGroupTable, SegmentationGroupTableExpanded } from '@ohif/ui';
import { SegmentationPanelMode } from '../types/segmentation';
import callInputDialog from './callInputDialog';
import callColorPickerDialog from './colorPickerDialog';
import { useTranslation } from 'react-i18next';
import { Separator } from '@ohif/ui-next';
const components = {
[SegmentationPanelMode.Expanded]: SegmentationGroupTableExpanded,
[SegmentationPanelMode.Dropdown]: SegmentationGroupTable,
};
export default function PanelSegmentation({
servicesManager,
commandsManager,
extensionManager,
configuration,
}: withAppTypes) {
const {
segmentationService,
viewportGridService,
uiDialogService,
displaySetService,
cornerstoneViewportService,
} = servicesManager.services;
const { t } = useTranslation('PanelSegmentation');
const [selectedSegmentationId, setSelectedSegmentationId] = useState(null);
const [addSegmentationClassName, setAddSegmentationClassName] = useState('');
const [segmentationConfiguration, setSegmentationConfiguration] = useState(
segmentationService.getConfiguration()
);
const [segmentations, setSegmentations] = useState(() => segmentationService.getSegmentations());
useEffect(() => {
// ~~ Subscription
const added = segmentationService.EVENTS.SEGMENTATION_ADDED;
const updated = segmentationService.EVENTS.SEGMENTATION_UPDATED;
const removed = segmentationService.EVENTS.SEGMENTATION_REMOVED;
const subscriptions = [];
[added, updated, removed].forEach(evt => {
const { unsubscribe } = segmentationService.subscribe(evt, () => {
const segmentations = segmentationService.getSegmentations();
setSegmentations(segmentations);
setSegmentationConfiguration(segmentationService.getConfiguration());
});
subscriptions.push(unsubscribe);
});
return () => {
subscriptions.forEach(unsub => {
unsub();
});
};
}, []);
// temporary measure to not allow add segmentation when the selected viewport
// is stack viewport
useEffect(() => {
const handleActiveViewportChange = viewportId => {
const displaySetUIDs = viewportGridService.getDisplaySetsUIDsForViewport(
viewportId || viewportGridService.getActiveViewportId()
);
if (!displaySetUIDs) {
return;
}
const isReconstructable =
displaySetUIDs?.some(displaySetUID => {
const displaySet = displaySetService.getDisplaySetByUID(displaySetUID);
return displaySet?.isReconstructable;
}) || false;
if (isReconstructable) {
setAddSegmentationClassName('');
} else {
setAddSegmentationClassName('ohif-disabled');
}
};
// Handle initial state
handleActiveViewportChange();
const changedGrid = viewportGridService.EVENTS.ACTIVE_VIEWPORT_ID_CHANGED;
const ready = viewportGridService.EVENTS.VIEWPORTS_READY;
const subsGrid = [];
[ready, changedGrid].forEach(evt => {
const { unsubscribe } = viewportGridService.subscribe(evt, ({ viewportId }) => {
handleActiveViewportChange(viewportId);
});
subsGrid.push(unsubscribe);
});
const changedData = cornerstoneViewportService.EVENTS.VIEWPORT_DATA_CHANGED;
const subsData = [];
[changedData].forEach(evt => {
const { unsubscribe } = cornerstoneViewportService.subscribe(evt, () => {
handleActiveViewportChange();
});
subsData.push(unsubscribe);
});
// Clean up
return () => {
subsGrid.forEach(unsub => unsub());
subsData.forEach(unsub => unsub());
};
}, []);
const getToolGroupIds = segmentationId => {
const toolGroupIds = segmentationService.getToolGroupIdsWithSegmentation(segmentationId);
return toolGroupIds;
};
const onSegmentationAdd = async () => {
commandsManager.runCommand('createEmptySegmentationForViewport', {
viewportId: viewportGridService.getActiveViewportId(),
});
};
const onSegmentationClick = (segmentationId: string) => {
segmentationService.setActiveSegmentationForToolGroup(segmentationId);
};
const onSegmentationDelete = (segmentationId: string) => {
segmentationService.remove(segmentationId);
};
const onSegmentAdd = segmentationId => {
segmentationService.addSegment(segmentationId);
};
const onSegmentClick = (segmentationId, segmentIndex) => {
segmentationService.setActiveSegment(segmentationId, segmentIndex);
const toolGroupIds = getToolGroupIds(segmentationId);
toolGroupIds.forEach(toolGroupId => {
// const toolGroupId =
segmentationService.setActiveSegmentationForToolGroup(segmentationId, toolGroupId);
segmentationService.jumpToSegmentCenter(segmentationId, segmentIndex, toolGroupId);
});
};
const onSegmentEdit = (segmentationId, segmentIndex) => {
const segmentation = segmentationService.getSegmentation(segmentationId);
const segment = segmentation.segments[segmentIndex];
const { label } = segment;
callInputDialog(uiDialogService, label, (label, actionId) => {
if (label === '') {
return;
}
segmentationService.setSegmentLabel(segmentationId, segmentIndex, label);
});
};
const onSegmentationEdit = segmentationId => {
const segmentation = segmentationService.getSegmentation(segmentationId);
const { label } = segmentation;
callInputDialog(uiDialogService, label, (label, actionId) => {
if (label === '') {
return;
}
segmentationService.addOrUpdateSegmentation(
{
id: segmentationId,
label,
},
false, // suppress event
true // notYetUpdatedAtSource
);
});
};
const onSegmentColorClick = (segmentationId, segmentIndex) => {
const segmentation = segmentationService.getSegmentation(segmentationId);
const segment = segmentation.segments[segmentIndex];
const { color, opacity } = segment;
const rgbaColor = {
r: color[0],
g: color[1],
b: color[2],
a: opacity / 255.0,
};
callColorPickerDialog(uiDialogService, rgbaColor, (newRgbaColor, actionId) => {
if (actionId === 'cancel') {
return;
}
segmentationService.setSegmentRGBAColor(segmentationId, segmentIndex, [
newRgbaColor.r,
newRgbaColor.g,
newRgbaColor.b,
newRgbaColor.a * 255.0,
]);
});
};
const onSegmentDelete = (segmentationId, segmentIndex) => {
segmentationService.removeSegment(segmentationId, segmentIndex);
};
// segment hide
const onToggleSegmentVisibility = (segmentationId, segmentIndex) => {
const segmentation = segmentationService.getSegmentation(segmentationId);
const segmentInfo = segmentation.segments[segmentIndex];
const isVisible = !segmentInfo.isVisible;
const toolGroupIds = getToolGroupIds(segmentationId);
// Todo: right now we apply the visibility to all tool groups
toolGroupIds.forEach(toolGroupId => {
segmentationService.setSegmentVisibility(
segmentationId,
segmentIndex,
isVisible,
toolGroupId
);
});
};
const onToggleSegmentLock = (segmentationId, segmentIndex) => {
segmentationService.toggleSegmentLocked(segmentationId, segmentIndex);
};
const onToggleSegmentationVisibility = segmentationId => {
segmentationService.toggleSegmentationVisibility(segmentationId);
const segmentation = segmentationService.getSegmentation(segmentationId);
const isVisible = segmentation.isVisible;
const segments = segmentation.segments;
const toolGroupIds = getToolGroupIds(segmentationId);
toolGroupIds.forEach(toolGroupId => {
segments.forEach((segment, segmentIndex) => {
segmentationService.setSegmentVisibility(
segmentationId,
segmentIndex,
isVisible,
toolGroupId
);
});
});
};
const _setSegmentationConfiguration = useCallback(
(segmentationId, key, value) => {
segmentationService.setConfiguration({
segmentationId,
[key]: value,
});
},
[segmentationService]
);
const onSegmentationDownload = segmentationId => {
commandsManager.runCommand('downloadSegmentation', {
segmentationId,
});
};
const storeSegmentation = async segmentationId => {
const datasources = extensionManager.getActiveDataSource();
const displaySetInstanceUIDs = await createReportAsync({
servicesManager,
getReport: () =>
commandsManager.runCommand('storeSegmentation', {
segmentationId,
dataSource: datasources[0],
}),
reportType: 'Segmentation',
});
// Show the exported report in the active viewport as read only (similar to SR)
if (displaySetInstanceUIDs) {
// clear the segmentation that we exported, similar to the storeMeasurement
// where we remove the measurements and prompt again the user if they would like
// to re-read the measurements in a SR read only viewport
segmentationService.remove(segmentationId);
viewportGridService.setDisplaySetsForViewport({
viewportId: viewportGridService.getActiveViewportId(),
displaySetInstanceUIDs,
});
}
};
const onSegmentationDownloadRTSS = segmentationId => {
commandsManager.runCommand('downloadRTSS', {
segmentationId,
});
};
const SegmentationGroupTableComponent =
components[configuration?.segmentationPanelMode] || SegmentationGroupTable;
const allowAddSegment = configuration?.addSegment;
const onSegmentationAddWrapper =
configuration?.onSegmentationAdd && typeof configuration?.onSegmentationAdd === 'function'
? configuration?.onSegmentationAdd
: onSegmentationAdd;
return (
<>
<SegmentationGroupTableComponent
title={t('Segmentations')}
segmentations={segmentations}
disableEditing={configuration.disableEditing}
activeSegmentationId={selectedSegmentationId || ''}
onSegmentationAdd={onSegmentationAddWrapper}
addSegmentationClassName={addSegmentationClassName}
showAddSegment={allowAddSegment}
onSegmentationClick={onSegmentationClick}
onSegmentationDelete={onSegmentationDelete}
onSegmentationDownload={onSegmentationDownload}
onSegmentationDownloadRTSS={onSegmentationDownloadRTSS}
storeSegmentation={storeSegmentation}
onSegmentationEdit={onSegmentationEdit}
onSegmentClick={onSegmentClick}
onSegmentEdit={onSegmentEdit}
onSegmentAdd={onSegmentAdd}
onSegmentColorClick={onSegmentColorClick}
onSegmentDelete={onSegmentDelete}
onToggleSegmentVisibility={onToggleSegmentVisibility}
onToggleSegmentLock={onToggleSegmentLock}
onToggleSegmentationVisibility={onToggleSegmentationVisibility}
showDeleteSegment={true}
segmentationConfig={{ initialConfig: segmentationConfiguration }}
setRenderOutline={value =>
_setSegmentationConfiguration(selectedSegmentationId, 'renderOutline', value)
}
setOutlineOpacityActive={value =>
_setSegmentationConfiguration(selectedSegmentationId, 'outlineOpacity', value)
}
setRenderFill={value =>
_setSegmentationConfiguration(selectedSegmentationId, 'renderFill', value)
}
setRenderInactiveSegmentations={value =>
_setSegmentationConfiguration(
selectedSegmentationId,
'renderInactiveSegmentations',
value
)
}
setOutlineWidthActive={value =>
_setSegmentationConfiguration(selectedSegmentationId, 'outlineWidthActive', value)
}
setFillAlpha={value =>
_setSegmentationConfiguration(selectedSegmentationId, 'fillAlpha', value)
}
setFillAlphaInactive={value =>
_setSegmentationConfiguration(selectedSegmentationId, 'fillAlphaInactive', value)
}
/>
</>
);
}
PanelSegmentation.propTypes = {
commandsManager: PropTypes.shape({
runCommand: PropTypes.func.isRequired,
}),
servicesManager: PropTypes.shape({
services: PropTypes.shape({
segmentationService: PropTypes.shape({
getSegmentation: PropTypes.func.isRequired,
getSegmentations: PropTypes.func.isRequired,
toggleSegmentationVisibility: PropTypes.func.isRequired,
subscribe: PropTypes.func.isRequired,
EVENTS: PropTypes.object.isRequired,
}).isRequired,
}).isRequired,
}).isRequired,
};
@@ -1,62 +0,0 @@
import React from 'react';
import { Input, Dialog, ButtonEnums } from '@ohif/ui';
function callInputDialog(uiDialogService, label, callback) {
const dialogId = 'enter-segment-label';
const onSubmitHandler = ({ action, value }) => {
switch (action.id) {
case 'save':
callback(value.label, action.id);
break;
case 'cancel':
callback('', action.id);
break;
}
uiDialogService.dismiss({ id: dialogId });
};
if (uiDialogService) {
uiDialogService.create({
id: dialogId,
centralize: true,
isDraggable: false,
showOverlay: true,
content: Dialog,
contentProps: {
title: 'Segment',
value: { label },
noCloseButton: true,
onClose: () => uiDialogService.dismiss({ id: dialogId }),
actions: [
{ id: 'cancel', text: 'Cancel', type: ButtonEnums.type.secondary },
{ id: 'save', text: 'Confirm', type: ButtonEnums.type.primary },
],
onSubmit: onSubmitHandler,
body: ({ value, setValue }) => {
return (
<Input
label="Enter the segment label"
labelClassName="text-white text-[14px] leading-[1.2]"
autoFocus
className="border-primary-main bg-black"
type="text"
value={value.label}
onChange={event => {
event.persist();
setValue(value => ({ ...value, label: event.target.value }));
}}
onKeyPress={event => {
if (event.key === 'Enter') {
onSubmitHandler({ value, action: { id: 'save' } });
}
}}
/>
);
},
},
});
}
}
export default callInputDialog;
@@ -1,33 +0,0 @@
// Todo: use defaults in cs3d
const initialState = {
renderOutline: true,
renderFill: true,
outlineOpacity: 0.9,
outlineWidth: 3,
fillOpacity: 0.9,
fillOpacityInactive: 0.8,
renderInactiveSegmentations: true,
};
const reducer = (state, action) => {
switch (action.type) {
case 'RENDER_OUTLINE':
return { ...state, renderOutline: action.payload.value };
case 'RENDER_FILL':
return { ...state, renderFill: action.payload.value };
case 'SET_OUTLINE_OPACITY':
return { ...state, outlineOpacity: action.payload.value };
case 'SET_OUTLINE_WIDTH':
return { ...state, outlineWidth: action.payload.value };
case 'SET_FILL_OPACITY':
return { ...state, fillOpacity: action.payload.value };
case 'SET_FILL_OPACITY_INACTIVE':
return { ...state, fillOpacityInactive: action.payload.value };
case 'RENDER_INACTIVE_SEGMENTATIONS':
return { ...state, renderInactiveSegmentations: action.payload.value };
default:
return state;
}
};
export { initialState, reducer };
@@ -1,203 +0,0 @@
import { Enums, cache } from '@cornerstonejs/core';
/**
* Updates the viewports in preparation for rendering segmentations.
* Evaluates each viewport to determine which need modifications,
* then for those viewports, changes them to a volume type and ensures
* they are ready for segmentation rendering.
*
* @param {Object} params - Parameters for the function.
* @param params.viewportId - ID of the viewport to be updated.
* @param params.loadFn - Function to load the segmentation data.
* @param params.servicesManager - The services manager.
* @param params.displaySet - the display set.
* @param params.initialSliceIndex - The initial slice index.
*
* @returns Returns true upon successful update of viewports for segmentation rendering.
*/
async function updateViewportsForSegmentationRendering({
viewportId,
loadFn,
servicesManager,
displaySet,
initialSliceIndex = null,
}: {
viewportId: string;
loadFn: () => Promise<string>;
servicesManager: AppTypes.ServicesManager;
displaySet?: any;
initialSliceIndex?: number;
}) {
const { cornerstoneViewportService, segmentationService, viewportGridService } =
servicesManager.services;
const viewport = getTargetViewport({ viewportId, viewportGridService });
const targetViewportId = viewport.viewportOptions.viewportId;
const referencedDisplaySetInstanceUID =
displaySet?.referencedDisplaySetInstanceUID || viewport?.displaySetInstanceUIDs[0];
const updatedViewports = getUpdatedViewportsForSegmentation({
viewportId,
servicesManager,
displaySet,
});
// create Segmentation callback which needs to be waited until
// the volume is created (if coming from stack)
const createSegmentationForVolume = async () => {
const segmentationId = await loadFn();
segmentationService.hydrateSegmentation(segmentationId);
};
// the reference volume that is used to draw the segmentation. so check if the
// volume exists in the cache (the target Viewport is already a volume viewport)
const volumeExists = Array.from(cache._volumeCache.keys()).some(volumeId =>
volumeId.includes(referencedDisplaySetInstanceUID)
);
updatedViewports.forEach(async viewport => {
viewport.viewportOptions = {
...viewport.viewportOptions,
viewportType: displaySet?.Modality === 'RTSTRUCT' ? 'stack' : 'volume',
needsRerendering: true,
};
const viewportId = viewport.viewportId;
// maintain the prehydration slice on the target viewport only
if (viewportId === targetViewportId) {
viewport.viewportOptions.initialImageOptions = {
index: initialSliceIndex,
useOnce: true,
};
}
const csViewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
const prevCamera = csViewport.getCamera();
// only run the createSegmentationForVolume for the targetViewportId
// since the rest will get handled by cornerstoneViewportService
if ((volumeExists || displaySet.Modality === 'RTSTRUCT') && viewportId === targetViewportId) {
await createSegmentationForVolume();
return;
}
const createNewSegmentationWhenVolumeMounts = async evt => {
const isTheActiveViewportVolumeMounted = evt.detail.volumeActors?.find(ac =>
ac.uid.includes(referencedDisplaySetInstanceUID)
);
// Note: make sure to re-grab the viewport since it might have changed
// during the time it took for the volume to be mounted, for instance
// the stack viewport has been changed to a volume viewport
const volumeViewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
volumeViewport.setCamera(prevCamera);
volumeViewport.element.removeEventListener(
Enums.Events.VOLUME_VIEWPORT_NEW_VOLUME,
createNewSegmentationWhenVolumeMounts
);
if (!isTheActiveViewportVolumeMounted) {
// it means it is one of those other updated viewports so just update the camera
return;
}
if (viewportId === targetViewportId) {
await createSegmentationForVolume();
}
};
csViewport.element.addEventListener(
Enums.Events.VOLUME_VIEWPORT_NEW_VOLUME,
createNewSegmentationWhenVolumeMounts
);
});
// Set the displaySets for the viewports that require to be updated
viewportGridService.setDisplaySetsForViewports(updatedViewports);
return true;
}
const getTargetViewport = ({ viewportId, viewportGridService }) => {
const { viewports, activeViewportId } = viewportGridService.getState();
const targetViewportId = viewportId || activeViewportId;
const viewport = viewports.get(targetViewportId);
return viewport;
};
/**
* Retrieves a list of viewports that require updates in preparation for segmentation rendering.
* This function evaluates viewports based on their compatibility with the provided segmentation's
* frame of reference UID and appends them to the updated list if they should render the segmentation.
*
* @param {Object} params - Parameters for the function.
* @param params.viewportId - the ID of the viewport to be updated.
* @param params.servicesManager - The services manager
* @param params.displaySet - the display set.
*
* @returns {Array} Returns an array of viewports that require updates for segmentation rendering.
*/
function getUpdatedViewportsForSegmentation({
viewportId,
servicesManager,
displaySet,
}: withAppTypes) {
const { hangingProtocolService, displaySetService, segmentationService, viewportGridService } =
servicesManager.services;
const { viewports, isHangingProtocolLayout } = viewportGridService.getState();
const viewport = getTargetViewport({ viewportId, viewportGridService });
const targetViewportId = viewport.viewportOptions.viewportId;
const displaySetInstanceUIDs = viewports.get(targetViewportId).displaySetInstanceUIDs;
const referenceDisplaySetInstanceUID =
displaySet?.referencedDisplaySetInstanceUID || displaySetInstanceUIDs[0];
const referencedDisplaySet = displaySetService.getDisplaySetByUID(referenceDisplaySetInstanceUID);
const segmentationFrameOfReferenceUID = referencedDisplaySet.instances[0].FrameOfReferenceUID;
const updatedViewports = hangingProtocolService.getViewportsRequireUpdate(
targetViewportId,
referenceDisplaySetInstanceUID,
isHangingProtocolLayout
);
viewports.forEach((viewport, viewportId) => {
if (
targetViewportId === viewportId ||
updatedViewports.find(v => v.viewportId === viewportId)
) {
return;
}
const shouldDisplaySeg = segmentationService.shouldRenderSegmentation(
viewport.displaySetInstanceUIDs,
segmentationFrameOfReferenceUID
);
if (shouldDisplaySeg) {
updatedViewports.push({
viewportId,
displaySetInstanceUIDs: viewport.displaySetInstanceUIDs,
viewportOptions: {
viewportType: displaySet?.Modality === 'RTSTRUCT' ? 'stack' : 'volume',
needsRerendering: true,
},
});
}
});
return updatedViewports.filter(v => v.viewportOptions?.viewportType !== 'volume3d');
}
export {
updateViewportsForSegmentationRendering,
getUpdatedViewportsForSegmentation,
getTargetViewport,
};
@@ -11,7 +11,7 @@ function promptHydrateSEG({
segDisplaySet,
viewportId,
preHydrateCallbacks,
hydrateSEGDisplaySet,
hydrateCallback,
}: withAppTypes) {
const { uiViewportDialogService } = servicesManager.services;
const extensionManager = servicesManager._extensionManager;
@@ -28,7 +28,7 @@ function promptHydrateSEG({
});
window.setTimeout(async () => {
const isHydrated = await hydrateSEGDisplaySet({
const isHydrated = await hydrateCallback({
segDisplaySet,
viewportId,
});
@@ -1,10 +1,11 @@
import PropTypes from 'prop-types';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { LoadingIndicatorTotalPercent, useViewportGrid, ViewportActionArrows } from '@ohif/ui';
import createSEGToolGroupAndAddTools from '../utils/initSEGToolGroup';
import promptHydrateSEG from '../utils/promptHydrateSEG';
import _getStatusComponent from './_getStatusComponent';
import { useSegmentationPresentationStore } from '@ohif/extension-cornerstone';
import { SegmentationRepresentations } from '@cornerstonejs/tools/enums';
const SEG_TOOLGROUP_BASE_NAME = 'SEGToolGroup';
@@ -37,7 +38,6 @@ function OHIFCornerstoneSEGViewport(props: withAppTypes) {
}
const segDisplaySet = displaySets[0];
const [viewportGrid, viewportGridService] = useViewportGrid();
// States
@@ -61,7 +61,11 @@ function OHIFCornerstoneSEGViewport(props: withAppTypes) {
const { viewports, activeViewportId } = viewportGrid;
const referencedDisplaySet = segDisplaySet.getReferenceDisplaySet();
const referencedDisplaySetInstanceUID = segDisplaySet.referencedDisplaySetInstanceUID;
const referencedDisplaySet = displaySetService.getDisplaySetByUID(
referencedDisplaySetInstanceUID
);
const referencedDisplaySetMetadata = _getReferencedDisplaySetMetadata(
referencedDisplaySet,
segDisplaySet
@@ -98,18 +102,17 @@ function OHIFCornerstoneSEGViewport(props: withAppTypes) {
'@ohif/extension-cornerstone.viewportModule.cornerstone'
);
const { displaySet: referencedDisplaySet } = referencedDisplaySetRef.current;
// Todo: jump to the center of the first segment
return (
<Component
{...props}
displaySets={[referencedDisplaySet, segDisplaySet]}
displaySets={[segDisplaySet]}
viewportOptions={{
viewportType: 'volume',
viewportType: viewportOptions.viewportType,
toolGroupId: toolGroupId,
orientation: viewportOptions.orientation,
viewportId: viewportOptions.viewportId,
presentationIds: viewportOptions.presentationIds,
}}
onElementEnabled={evt => {
props.onElementEnabled?.(evt);
@@ -139,12 +142,35 @@ function OHIFCornerstoneSEGViewport(props: withAppTypes) {
newSelectedSegmentIndex = numberOfSegments - 1;
}
segmentationService.jumpToSegmentCenter(segmentationId, newSelectedSegmentIndex, toolGroupId);
segmentationService.jumpToSegmentCenter(segmentationId, newSelectedSegmentIndex, viewportId);
setSelectedSegment(newSelectedSegmentIndex);
},
[selectedSegment]
);
const hydrateSEG = useCallback(() => {
// update the previously stored segmentationPresentation with the new viewportId
// presentation so that when we put the referencedDisplaySet back in the viewport
// it will have the correct segmentation representation hydrated
commandsManager.runCommand('updateStoredSegmentationPresentation', {
displaySet: segDisplaySet,
type: SegmentationRepresentations.Labelmap,
});
// update the previously stored positionPresentation with the new viewportId
// presentation so that when we put the referencedDisplaySet back in the viewport
// it will be in the correct position zoom and pan
commandsManager.runCommand('updateStoredPositionPresentation', {
viewportId,
displaySetInstanceUID: referencedDisplaySet.displaySetInstanceUID,
});
viewportGridService.setDisplaySetsForViewport({
viewportId,
displaySetInstanceUIDs: [referencedDisplaySet.displaySetInstanceUID],
});
}, [commandsManager, viewportId, referencedDisplaySet, segDisplaySet]);
useEffect(() => {
if (segIsLoading) {
return;
@@ -155,15 +181,18 @@ function OHIFCornerstoneSEGViewport(props: withAppTypes) {
viewportId,
segDisplaySet,
preHydrateCallbacks: [storePresentationState],
hydrateSEGDisplaySet,
hydrateCallback: hydrateSEG,
}).then(isHydrated => {
if (isHydrated) {
setIsHydrated(true);
}
});
}, [servicesManager, viewportId, segDisplaySet, segIsLoading]);
}, [servicesManager, viewportId, segDisplaySet, segIsLoading, hydrateSEG]);
useEffect(() => {
// on new seg display set, remove all segmentations from all viewports
segmentationService.clearSegmentationRepresentations(viewportId);
const { unsubscribe } = segmentationService.subscribe(
segmentationService.EVENTS.SEGMENTATION_LOADING_COMPLETE,
evt => {
@@ -222,6 +251,9 @@ function OHIFCornerstoneSEGViewport(props: withAppTypes) {
if (toolGroup) {
return;
}
// always start fresh for this viewport since it is special type of viewport
// that should only show one segmentation at a time.
segmentationService.clearSegmentationRepresentations(viewportId);
// This creates a custom tool group which has the lifetime of this view
// only, and does NOT interfere with currently displayed segmentations.
@@ -229,33 +261,14 @@ function OHIFCornerstoneSEGViewport(props: withAppTypes) {
return () => {
// remove the segmentation representations if seg displayset changed
segmentationService.removeSegmentationRepresentationFromToolGroup(toolGroupId);
// e.g., another seg displayset is dragged into the viewport
segmentationService.clearSegmentationRepresentations(viewportId);
// Only destroy the viewport specific implementation
toolGroupService.destroyToolGroup(toolGroupId);
};
}, []);
useEffect(() => {
setIsHydrated(segDisplaySet.isHydrated);
return () => {
// remove the segmentation representations if seg displayset changed
segmentationService.removeSegmentationRepresentationFromToolGroup(toolGroupId);
referencedDisplaySetRef.current = null;
};
}, [segDisplaySet]);
const hydrateSEGDisplaySet = useCallback(
({ segDisplaySet, viewportId }) => {
commandsManager.runCommand('loadSegmentationDisplaySetsForViewport', {
displaySets: [segDisplaySet],
viewportId,
});
},
[commandsManager]
);
const onStatusClick = useCallback(async () => {
// Before hydrating a SEG and make it added to all viewports in the grid
// that share the same frameOfReferenceUID, we need to store the viewport grid
@@ -264,16 +277,11 @@ function OHIFCornerstoneSEGViewport(props: withAppTypes) {
// presentation state (w/l and invert) and then opens the SEG. If we don't store
// the presentation state, the viewport will be reset to the default presentation
storePresentationState();
const isHydrated = await hydrateSEGDisplaySet({
segDisplaySet,
viewportId,
});
setIsHydrated(isHydrated);
}, [hydrateSEGDisplaySet, segDisplaySet, storePresentationState, viewportId]);
hydrateSEG();
}, [storePresentationState, hydrateSEG]);
useEffect(() => {
viewportActionCornersService.setComponents([
viewportActionCornersService.addComponents([
{
viewportId,
id: 'viewportStatusComponent',
@@ -292,7 +300,7 @@ function OHIFCornerstoneSEGViewport(props: withAppTypes) {
key="actionArrows"
onArrowsClick={onSegmentChange}
className={
viewportId === activeViewportId ? 'visible' : 'invisible group-hover:visible'
viewportId === activeViewportId ? 'visible' : 'invisible group-hover/pane:visible'
}
></ViewportActionArrows>
),
@@ -332,18 +340,6 @@ function OHIFCornerstoneSEGViewport(props: withAppTypes) {
});
}
const {
PatientID,
PatientName,
PatientSex,
PatientAge,
SliceThickness,
ManufacturerModelName,
StudyDate,
SeriesDescription,
SpacingBetweenSlices,
} = referencedDisplaySetRef.current.metadata;
return (
<>
<div className="relative flex h-full w-full flex-row overflow-hidden">
@@ -362,13 +358,6 @@ function OHIFCornerstoneSEGViewport(props: withAppTypes) {
);
}
OHIFCornerstoneSEGViewport.propTypes = {
displaySets: PropTypes.arrayOf(PropTypes.object),
viewportId: PropTypes.string.isRequired,
dataSource: PropTypes.object,
children: PropTypes.node,
};
function _getReferencedDisplaySetMetadata(referencedDisplaySet, segDisplaySet) {
const { SharedFunctionalGroupsSequence } = segDisplaySet.instance;
+3 -3
View File
@@ -46,9 +46,9 @@
},
"dependencies": {
"@babel/runtime": "^7.20.13",
"@cornerstonejs/adapters": "^1.86.0",
"@cornerstonejs/core": "^1.86.0",
"@cornerstonejs/tools": "^1.86.0",
"@cornerstonejs/adapters": "^2.1.13",
"@cornerstonejs/core": "^2.1.13",
"@cornerstonejs/tools": "^2.1.13",
"classnames": "^2.3.2"
}
}
@@ -15,8 +15,15 @@ const MEASUREMENT_TRACKING_EXTENSION_ID = '@ohif/extension-measurement-tracking'
const SR_TOOLGROUP_BASE_NAME = 'SRToolGroup';
function OHIFCornerstoneSRMeasurementViewport(props: withAppTypes) {
const { children, dataSource, displaySets, viewportOptions, servicesManager, extensionManager } =
props;
const {
commandsManager,
children,
dataSource,
displaySets,
viewportOptions,
servicesManager,
extensionManager,
} = props;
const [appConfig] = useAppConfig();
@@ -63,6 +70,7 @@ function OHIFCornerstoneSRMeasurementViewport(props: withAppTypes) {
trackedMeasurements = tracked?.[0];
sendTrackedMeasurementsEvent = tracked?.[1];
}
if (!sendTrackedMeasurementsEvent) {
// if no panels from measurement-tracking extension is used, this code will run
trackedMeasurements = null;
@@ -72,6 +80,7 @@ function OHIFCornerstoneSRMeasurementViewport(props: withAppTypes) {
{ servicesManager, extensionManager, appConfig },
displaySetInstanceUID
);
const displaySets = displaySetService.getDisplaySetsForSeries(SeriesInstanceUIDs[0]);
if (displaySets.length) {
viewportGridService.setDisplaySetsForViewports([
@@ -84,6 +93,11 @@ function OHIFCornerstoneSRMeasurementViewport(props: withAppTypes) {
};
}
/**
* Todo: what is this, not sure what it does regarding the react aspect,
* it is updating a local variable? which is not state.
*/
const [isLocked, setIsLocked] = useState(trackedMeasurements?.context?.trackedSeries?.length > 0);
/**
* Store the tracking identifiers per viewport in order to be able to
* show the SR measurements on the referenced image on the correct viewport,
@@ -297,17 +311,12 @@ function OHIFCornerstoneSRMeasurementViewport(props: withAppTypes) {
updateSR();
}, [measurementSelected, element, setTrackingIdentifiers, srDisplaySet]);
/**
* Todo: what is this, not sure what it does regarding the react aspect,
* it is updating a local variable? which is not state.
*/
const [isLocked, setIsLocked] = useState(trackedMeasurements?.context?.trackedSeries?.length > 0);
useEffect(() => {
setIsLocked(trackedMeasurements?.context?.trackedSeries?.length > 0);
}, [trackedMeasurements]);
useEffect(() => {
viewportActionCornersService.setComponents([
viewportActionCornersService.addComponents([
{
viewportId,
id: 'viewportStatusComponent',
+2 -2
View File
@@ -12,7 +12,7 @@ import {
utilities as csToolsUtils,
} from '@cornerstonejs/tools';
import { Types, MeasurementService } from '@ohif/core';
import { StackViewport } from '@cornerstonejs/core';
import { StackViewport, utilities as csUtils } from '@cornerstonejs/core';
import { Enums as CSExtensionEnums } from '@ohif/extension-cornerstone';
import DICOMSRDisplayTool from './tools/DICOMSRDisplayTool';
import SCOORD3DPointTool from './tools/SCOORD3DPointTool';
@@ -95,7 +95,7 @@ export default function init({
const imageIndex = (currentViewport as StackViewport)
.getImageIds()
.indexOf(referencedImageId);
csToolsUtils.jumpToSlice(currentViewport.element, { imageIndex });
csUtils.jumpToSlice(currentViewport.element, { imageIndex });
} catch (error) {
console.warn('Unable to jump to image based on measurement coordinate', error);
}
@@ -45,11 +45,19 @@ export default function hydrateStructuredReport(
displaySetInstanceUID
) {
const annotationManager = CsAnnotation.state.getAnnotationManager();
const disableEditing = appConfig?.disableEditing;
const dataSource = extensionManager.getActiveDataSource()[0];
const { measurementService, displaySetService, customizationService } = servicesManager.services;
const codingValues = customizationService.getCustomization('codingValues', {});
const { disableEditing } = customizationService.getCustomization(
'PanelMeasurement.disableEditing',
{
id: 'default.disableEditing',
disableEditing: false,
}
);
const displaySet = displaySetService.getDisplaySetByUID(displaySetInstanceUID);
// TODO -> We should define a strict versioning somewhere.
@@ -42,9 +42,8 @@
},
"dependencies": {
"@babel/runtime": "^7.20.13",
"@cornerstonejs/core": "^1.86.0",
"@cornerstonejs/streaming-image-volume-loader": "^1.82.4",
"@cornerstonejs/tools": "^1.86.0",
"@cornerstonejs/core": "^2.1.13",
"@cornerstonejs/tools": "^2.1.13",
"classnames": "^2.3.2"
}
}
@@ -110,7 +110,12 @@ function _getTimePointsData(volume) {
return { timePoints, timePointsUnit };
}
function _getSegmentationData(segmentation, volumesTimePointsCache, displaySetService) {
function _getSegmentationData(
segmentation,
volumesTimePointsCache,
{ servicesManager }: { servicesManager: AppTypes.ServicesManager }
) {
const { displaySetService, segmentationService, viewportGridService } = servicesManager.services;
const displaySets = displaySetService.getActiveDisplaySets();
const dynamic4DDisplaySet = displaySets.find(displaySet => {
@@ -145,8 +150,11 @@ function _getSegmentationData(segmentation, volumesTimePointsCache, displaySetSe
referencedDynamicVolume.imageIds[0]
);
const segmentationVolume = segmentationService.getLabelmapVolume(segmentation.segmentationId);
const maskVolumeId = segmentationVolume?.volumeId;
const [timeData, _] = csToolsUtils.dynamicVolume.getDataInTime(referencedDynamicVolume, {
maskVolumeId: segmentation.id,
maskVolumeId,
}) as number[][];
const pixelCount = timeData.length;
@@ -155,27 +163,15 @@ function _getSegmentationData(segmentation, volumesTimePointsCache, displaySetSe
return [];
}
// since we only use one segmentation representation per segmentationId
// it is fine to pick the first one
const segmentationRepresentations = csTools.segmentation.state.getSegmentationIdRepresentations(
segmentation.id
);
const segmentationRepresentationUID =
segmentationRepresentations[0].segmentationRepresentationUID;
const toolGroupId = csTools.segmentation.state.getToolGroupIdFromSegmentationRepresentationUID(
segmentationRepresentationUID
);
// Todo: this is useless we should be able to grab color with just segRepUID and segmentIndex
const color = csTools.segmentation.config.color.getColorForSegmentIndex(
toolGroupId,
segmentationRepresentationUID,
1 // segmentIndex
);
// const color = csTools.segmentation.config.color.getSegmentIndexColor(
// segmentationRepresentationUID,
// 1 // segmentIndex
// );
const viewportId = viewportGridService.getActiveViewportId();
const color = segmentationService.getSegmentColor(viewportId, segmentation.segmentationId, 1);
const hexColor = cs.utilities.color.rgbToHex(...color);
const hexColor = cs.utilities.color.rgbToHex(color[0], color[1], color[2]);
let timePointsData = volumesTimePointsCache.get(referencedDynamicVolume);
if (!timePointsData) {
@@ -219,14 +215,14 @@ function _getSegmentationData(segmentation, volumesTimePointsCache, displaySetSe
};
}
function _getInstanceFromSegmentations(segmentations, displaySetService) {
function _getInstanceFromSegmentations(segmentations, { servicesManager }) {
if (!segmentations.length) {
return;
}
const volumesTimePointsCache = new WeakMap();
const segmentationsData = segmentations.map(segmentation =>
_getSegmentationData(segmentation, volumesTimePointsCache, displaySetService)
_getSegmentationData(segmentation, volumesTimePointsCache, { servicesManager })
);
const { date: seriesDate, time: seriesTime } = _getDateTimeStr();
@@ -266,10 +262,11 @@ function _getInstanceFromSegmentations(segmentations, displaySetService) {
}
function updateSegmentationsChartDisplaySet({ servicesManager }: withAppTypes): void {
const { segmentationService, displaySetService } = servicesManager.services;
debugger;
const { segmentationService } = servicesManager.services;
const segmentations = segmentationService.getSegmentations();
const { seriesMetadata, instance } =
_getInstanceFromSegmentations(segmentations, displaySetService) ?? {};
_getInstanceFromSegmentations(segmentations, { servicesManager }) ?? {};
if (seriesMetadata && instance) {
// An event is triggered after adding the instance and the displaySet is created
@@ -84,11 +84,11 @@ const commandsModule = ({ commandsManager, servicesManager }: withAppTypes) => {
const stdDeviation = Math.sqrt(variance);
return stdDeviation;
}
// Iterate through each segmentation to get the timeData and ijkCoords
segmentations.forEach((segmentation, segmentationIndex) => {
segmentations.forEach(segmentation => {
const volume = segmentationService.getLabelmapVolume(segmentation.segmentationId);
const [timeData, ijkCoords] = utilities.dynamicVolume.getDataInTime(dynamicVolume, {
maskVolumeId: segmentation.id,
maskVolumeId: volume.volumeId,
}) as number[][];
if (summaryStats) {
@@ -113,17 +113,33 @@ const commandsModule = ({ commandsManager, servicesManager }: withAppTypes) => {
for (let timeIndex = 0; timeIndex < maxLength; timeIndex++) {
// for each voxel in the ROI, get the value at the current time point
const voxelValues = [];
let sum = 0;
let minValue = Infinity;
let maxValue = -Infinity;
let minIndex = 0;
let maxIndex = 0;
// Single pass through the data to collect all needed values
for (let voxelIndex = 0; voxelIndex < numVoxels; voxelIndex++) {
voxelValues.push(timeData[voxelIndex][timeIndex]);
const value = timeData[voxelIndex][timeIndex];
voxelValues.push(value);
sum += value;
if (value < minValue) {
minValue = value;
minIndex = voxelIndex;
}
if (value > maxValue) {
maxValue = value;
maxIndex = voxelIndex;
}
}
mean.push(voxelValues.reduce((acc, value) => acc + value, 0) / numVoxels);
const minimum = Math.min(...voxelValues);
min.push(minimum);
minIJK.push(ijkCoords[voxelValues.indexOf(minimum)]);
const maximum = Math.max(...voxelValues);
max.push(maximum);
maxIJK.push(ijkCoords[voxelValues.indexOf(maximum)]);
mean.push(sum / numVoxels);
min.push(minValue);
minIJK.push(ijkCoords[minIndex]);
max.push(maxValue);
maxIJK.push(ijkCoords[maxIndex]);
std.push(calculateStandardDeviation(voxelValues));
}
@@ -340,30 +356,19 @@ const commandsModule = ({ commandsManager, servicesManager }: withAppTypes) => {
throw new Error('No reference display set found based on the dynamic data');
}
const segmentationId = await segmentationService.createSegmentationForDisplaySet(
referenceDisplaySet.displaySetInstanceUID,
{ label }
const displaySet = displaySetService.getDisplaySetByUID(
referenceDisplaySet.displaySetInstanceUID
);
// Add Segmentation to all toolGroupIds in the viewer
const toolGroupIds = Array.from(
viewports.values(),
viewport => viewport.viewportOptions.toolGroupId
);
const segmentationId = await segmentationService.createLabelmapForDisplaySet(displaySet, {
label,
});
const representationType = LABELMAP;
const firstViewport = viewports.values().next().value;
for (const toolGroupId of toolGroupIds) {
const hydrateSegmentation = true;
await segmentationService.addSegmentationRepresentationToToolGroup(
toolGroupId,
segmentationId,
hydrateSegmentation,
representationType
);
segmentationService.setActiveSegmentationForToolGroup(segmentationId, toolGroupId);
}
await segmentationService.addSegmentationRepresentation(firstViewport.viewportId, {
segmentationId,
});
return segmentationId;
},
@@ -147,8 +147,8 @@ function getFusionViewports() {
name: DEFAULT_COLORMAP,
opacity: [
{ value: 0, opacity: 0 },
{ value: 0.1, opacity: 0.3 },
{ value: 1, opacity: 0.3 },
{ value: 0.1, opacity: 0.8 },
{ value: 1, opacity: 0.8 },
],
},
};
@@ -191,6 +191,15 @@ function getFusionViewports() {
syncInvertState: false,
},
},
{
type: 'hydrateseg',
id: 'sameFORId',
source: true,
target: true,
options: {
matchingRules: ['sameFOR'],
},
},
],
},
displaySets: [
@@ -240,6 +249,15 @@ function getFusionViewports() {
syncInvertState: false,
},
},
{
type: 'hydrateseg',
id: 'sameFORId',
source: true,
target: true,
options: {
matchingRules: ['sameFOR'],
},
},
],
},
displaySets: [
@@ -289,6 +307,15 @@ function getFusionViewports() {
syncInvertState: false,
},
},
{
type: 'hydrateseg',
id: 'sameFORId',
source: true,
target: true,
options: {
matchingRules: ['sameFOR'],
},
},
],
},
displaySets: [
@@ -1,10 +1,11 @@
import React from 'react';
import { DynamicDataPanel } from './panels';
import { Toolbox } from '@ohif/ui-next';
import { PanelSegmentation } from '@ohif/extension-cornerstone';
import DynamicExport from './panels/DynamicExport';
function getPanelModule({ commandsManager, extensionManager, servicesManager }) {
const wrappedDynamicDataPanel = ({}) => {
function getPanelModule({ commandsManager, extensionManager, servicesManager, configuration }) {
const wrappedDynamicDataPanel = () => {
return (
<DynamicDataPanel
commandsManager={commandsManager}
@@ -14,7 +15,7 @@ function getPanelModule({ commandsManager, extensionManager, servicesManager })
);
};
const wrappedDynamicToolbox = ({}) => {
const wrappedDynamicSegmentation = () => {
return (
<>
<Toolbox
@@ -24,18 +25,17 @@ function getPanelModule({ commandsManager, extensionManager, servicesManager })
buttonSectionId="dynamic-toolbox"
title="Threshold Tools"
/>
</>
);
};
const wrappedDynamicExport = () => {
return (
<>
<DynamicExport
commandsManager={commandsManager}
<PanelSegmentation
servicesManager={servicesManager}
commandsManager={commandsManager}
extensionManager={extensionManager}
/>
configuration={configuration}
>
<DynamicExport
servicesManager={servicesManager}
commandsManager={commandsManager}
/>
</PanelSegmentation>
</>
);
};
@@ -49,18 +49,11 @@ function getPanelModule({ commandsManager, extensionManager, servicesManager })
component: wrappedDynamicDataPanel,
},
{
name: 'dynamic-toolbox',
iconName: 'tab-4d',
iconLabel: '4D Workflow',
label: 'Dynamic Toolbox',
component: wrappedDynamicToolbox,
},
{
name: 'dynamic-export',
iconName: 'tab-4d',
iconLabel: '4D Workflow',
label: '4D Workflow',
component: wrappedDynamicExport,
name: 'dynamic-segmentation',
iconName: 'tab-segmentation',
iconLabel: 'Segmentation',
label: 'Segmentation',
component: wrappedDynamicSegmentation,
},
];
}
@@ -1,6 +1,5 @@
import React from 'react';
import PanelGenerateImage from './PanelGenerateImage';
import { Separator } from '@ohif/ui-next';
function DynamicDataPanel({ servicesManager, commandsManager, tab }: withAppTypes) {
return (
@@ -1,73 +1,52 @@
import React, { useState, useEffect } from 'react';
import { ActionButtons } from '@ohif/ui';
import { useTranslation } from 'react-i18next';
import React from 'react';
import { Button, Icons } from '@ohif/ui-next';
import { useSegmentations } from '@ohif/extension-cornerstone';
function DynamicExport({ commandsManager, servicesManager, extensionManager }: withAppTypes) {
const { segmentationService } = servicesManager.services;
const { t } = useTranslation('dynamicExport');
function DynamicExport({ commandsManager, servicesManager }: withAppTypes) {
const segmentations = useSegmentations({ servicesManager });
const [segmentations, setSegmentations] = useState(() => segmentationService.getSegmentations());
const actions = [
{
label: 'Export Time Data',
onClick: () => {
commandsManager.runCommand('exportTimeReportCSV', {
segmentations,
options: {
filename: 'TimeData.csv',
},
});
},
disabled: !segmentations?.length,
},
{
label: 'Export ROI Stats',
onClick: () => {
commandsManager.runCommand('exportTimeReportCSV', {
segmentations,
summaryStats: true,
options: {
filename: 'ROIStats.csv',
},
});
},
disabled: !segmentations?.length,
},
];
/**
* Update UI based on segmentation changes (added, removed, updated)
*/
useEffect(() => {
// ~~ Subscription
const added = segmentationService.EVENTS.SEGMENTATION_ADDED;
const updated = segmentationService.EVENTS.SEGMENTATION_UPDATED;
const removed = segmentationService.EVENTS.SEGMENTATION_REMOVED;
const subscriptions = [];
[added, updated, removed].forEach(evt => {
const { unsubscribe } = segmentationService.subscribe(evt, () => {
const segmentations = segmentationService.getSegmentations();
setSegmentations(segmentations);
});
subscriptions.push(unsubscribe);
});
return () => {
subscriptions.forEach(unsub => {
unsub();
});
};
}, []);
if (!segmentations?.length) {
return null;
}
return (
<div>
<div className="mt-3 flex justify-center px-2">
<ActionButtons
actions={actions}
t={t}
/>
<div className="flex gap-2">
<div className="flex h-8 w-full items-center rounded pr-0.5">
<Button
size="sm"
variant="ghost"
className="pl-1.5"
onClick={() => {
commandsManager.runCommand('exportTimeReportCSV', {
segmentations,
options: {
filename: 'TimeData.csv',
},
});
}}
>
<Icons.Export />
<span className="pl-1">Time Data</span>
</Button>
</div>
<div className="flex h-8 w-full items-center rounded pr-0.5">
<Button
size="sm"
variant="ghost"
className="pl-1.5"
onClick={() => {
commandsManager.runCommand('exportTimeReportCSV', {
segmentations,
summaryStats: true,
options: {
filename: 'ROIStats.csv',
},
});
}}
>
<Icons.Export />
<span className="pl-1">ROI Stats</span>
</Button>
</div>
</div>
);
@@ -1,6 +1,5 @@
import React, { useEffect, useState } from 'react';
import {
InputDoubleRange,
Button,
PanelSection,
ButtonGroup,
@@ -10,6 +9,8 @@ import {
Tooltip,
} from '@ohif/ui';
import { DoubleSlider } from '@ohif/ui-next';
import { Enums } from '@cornerstonejs/core';
const controlClassNames = {
@@ -55,21 +56,15 @@ const DynamicVolumeControls = ({
const [computeViewMode, setComputeViewMode] = useState(Enums.DynamicOperatorType.SUM);
const [sliderRangeValues, setSliderRangeValues] = useState([framesLength / 4, framesLength / 2]);
useEffect(() => {
setSliderRangeValues([framesLength / 4, framesLength / 2]);
}, [framesLength]);
const [sliderRangeValues, setSliderRangeValues] = useState([0, framesLength - 1]);
const handleSliderChange = newValues => {
onDoubleRangeChange(newValues);
if (newValues[0] === sliderRangeValues[0] && newValues[1] === sliderRangeValues[1]) {
return;
}
setSliderRangeValues(newValues);
};
const formatLabel = value => Math.round(value);
return (
<div className="flex select-none flex-col">
<PanelSection
@@ -134,7 +129,7 @@ const DynamicVolumeControls = ({
}
/>
<ButtonGroup
className={`mt-2 w-full `}
className={`mt-2 w-full`}
separated={true}
>
<button
@@ -156,15 +151,15 @@ const DynamicVolumeControls = ({
{Enums.DynamicOperatorType.SUBTRACT.toString().toUpperCase()}
</button>
</ButtonGroup>
<div className="w-ful mt-2">
<InputDoubleRange
values={sliderRangeValues}
onChange={handleSliderChange}
minValue={0}
showLabel={true}
allowNumberEdit={true}
maxValue={framesLength}
<div className="mt-2 w-full">
<DoubleSlider
min={0}
max={framesLength - 1}
step={1}
defaultValue={sliderRangeValues}
onValueChange={handleSliderChange}
formatLabel={formatLabel}
className="w-full"
/>
</div>
<Button
@@ -216,14 +211,14 @@ function FrameControls({
>
<Icon
name={getPlayPauseIconName()}
className=" active:text-primary-light hover:bg-customblue-300 h-[24px] w-[24px] cursor-pointer text-white"
className="active:text-primary-light hover:bg-customblue-300 h-[24px] w-[24px] cursor-pointer text-white"
/>
</IconButton>
<InputNumber
value={currentFrameIndex}
onChange={onFrameChange}
minValue={0}
maxValue={framesLength}
maxValue={framesLength - 1}
label="Frame"
{...controlClassNames}
/>
@@ -1,8 +1,6 @@
import React, { useEffect, useState, useRef } from 'react';
import PropTypes from 'prop-types';
import { useCine, useViewportGrid } from '@ohif/ui';
import { cache, utilities as csUtils, volumeLoader, eventTarget } from '@cornerstonejs/core';
import { Enums } from '@cornerstonejs/streaming-image-volume-loader';
import { utilities as csUtils, volumeLoader, eventTarget, Enums, cache } from '@cornerstonejs/core';
import { utilities as cstUtils } from '@cornerstonejs/tools';
import DynamicVolumeControls from './DynamicVolumeControls';
@@ -16,8 +14,8 @@ export default function PanelGenerateImage({ servicesManager, commandsManager }:
const [{ activeViewportId }] = useViewportGrid();
//
const [timePointsRange, setTimePointsRange] = useState([]);
const [timePointsRangeToUseForGenerate, setTimePointsRangeToUseForGenerate] = useState([]);
const [timePointsRange, setTimePointsRange] = useState([0, 0]);
const [timePointsRangeToUseForGenerate, setTimePointsRangeToUseForGenerate] = useState([0, 0]);
const [computedDisplaySet, setComputedDisplaySet] = useState(null);
const [dynamicVolume, setDynamicVolume] = useState(null);
const [frameRate, setFrameRate] = useState(20);
@@ -31,47 +29,62 @@ export default function PanelGenerateImage({ servicesManager, commandsManager }:
const computedVolumeId = `cornerstoneStreamingImageVolume:${uuidComputedVolume.current}`;
useEffect(() => {
const evt = cornerstoneViewportService.EVENTS.VIEWPORT_DATA_CHANGED;
const viewportDataChangedEvt = cornerstoneViewportService.EVENTS.VIEWPORT_DATA_CHANGED;
const cineStateChangedEvt = servicesManager.services.cineService.EVENTS.CINE_STATE_CHANGED;
const { unsubscribe } = cornerstoneViewportService.subscribe(evt, evtDetails => {
const viewportDataChangedCallback = evtDetails => {
evtDetails.viewportData.data.forEach(volumeData => {
if (volumeData.volume.isDynamicVolume()) {
if (volumeData.volume?.isDynamicVolume()) {
setDynamicVolume(volumeData.volume);
uuidDynamicVolume.current = volumeData.displaySetInstanceUID;
setTimePointsRange([1, volumeData.volume.numTimePoints]);
const newRange = [1, volumeData.volume.numTimePoints];
setTimePointsRange(newRange);
setTimePointsRangeToUseForGenerate(newRange);
}
});
});
};
const cineStateChangedCallback = evt => {
setIsPlaying(evt.isPlaying);
};
const { unsubscribe: unsubscribeViewportData } = cornerstoneViewportService.subscribe(
viewportDataChangedEvt,
viewportDataChangedCallback
);
const { unsubscribe: unsubscribeCineState } = servicesManager.services.cineService.subscribe(
cineStateChangedEvt,
cineStateChangedCallback
);
return () => {
unsubscribe();
unsubscribeViewportData();
unsubscribeCineState();
};
}, [cornerstoneViewportService, cineService, servicesManager.services.cineService]);
useEffect(() => {
const evt = Enums.Events.DYNAMIC_VOLUME_TIME_POINT_INDEX_CHANGED;
const callback = evt => {
setTimePointRendered(evt.detail.timePointIndex);
};
eventTarget.addEventListener(evt, callback);
return () => {
eventTarget.removeEventListener(evt, callback);
};
}, [cornerstoneViewportService]);
useEffect(() => {
const { unsubscribe } = servicesManager.services.cineService.subscribe(
servicesManager.services.cineService.EVENTS.CINE_STATE_CHANGED,
evt => {
setIsPlaying(evt.isPlaying);
}
);
return () => {
unsubscribe();
};
}, [cineService]);
useEffect(() => {
const displaySetUIDs = viewportGridService.getDisplaySetsUIDsForViewport(activeViewportId);
if (!displaySetUIDs || displaySetUIDs.length === 0) {
if (!displaySetUIDs?.length) {
return;
}
const displaySets = displaySetUIDs.map(displaySetUID =>
displaySetService.getDisplaySetByUID(displaySetUID)
);
const displaySets = displaySetUIDs.map(displaySetService.getDisplaySetByUID);
const dynamicVolumeDisplaySet = displaySets.find(displaySet => displaySet.isDynamicVolume);
if (!dynamicVolumeDisplaySet) {
@@ -88,23 +101,16 @@ export default function PanelGenerateImage({ servicesManager, commandsManager }:
setDynamicVolume(dynamicVolume);
uuidDynamicVolume.current = dynamicVolumeDisplaySet.displaySetInstanceUID;
setTimePointsRange([1, dynamicVolume.numTimePoints]);
}, [activeViewportId, cornerstoneViewportService]);
useEffect(() => {
// ~~ Subscription
const evt = Enums.Events.DYNAMIC_VOLUME_TIME_POINT_INDEX_CHANGED;
const callback = evt => {
setTimePointRendered(evt.detail.timePointIndex);
};
eventTarget.addEventListener(evt, callback);
return () => {
eventTarget.removeEventListener(evt, callback);
};
}, [cornerstoneViewportService]);
const newRange = [1, dynamicVolume.numTimePoints];
setTimePointsRange(newRange);
setTimePointsRangeToUseForGenerate(newRange);
}, [
activeViewportId,
viewportGridService,
displaySetService,
cornerstoneViewportService,
cineService,
]);
function renderGeneratedImage(displaySet) {
commandsManager.runCommand('swapDynamicWithComputedDisplaySet', {
@@ -130,26 +136,19 @@ export default function PanelGenerateImage({ servicesManager, commandsManager }:
let computedVolume = cache.getVolume(computedVolumeId);
if (!computedVolume) {
await createComputedVolume(dynamicVolumeId, computedVolumeId);
computedVolume = cache.getVolume(computedVolumeId);
computedVolume = await volumeLoader.createAndCacheDerivedVolume(dynamicVolumeId, {
volumeId: computedVolumeId,
});
}
const [start, end] = timePointsRangeToUseForGenerate;
const frameNumbers = Array.from({ length: end - start + 1 }, (_, i) => i + start - 1);
const vals = timePointsRangeToUseForGenerate;
const options = {
frameNumbers: operationName === 'SUBTRACT' ? [start, end - 1] : frameNumbers,
targetVolume: computedVolume,
};
const targets = Array.from({ length: vals[1] - vals[0] + 1 }, (_, i) => i + vals[0]);
const dataInTime = cstUtils.dynamicVolume.generateImageFromTimeData(
dynamicVolume,
operationName,
operationName === 'SUBTRACT' ? vals : targets
);
// Add loadStatus.loaded to computed volume and set to true
computedVolume.loadStatus = {};
computedVolume.loadStatus.loaded = true;
// Set computed scalar data to volume
const scalarData = computedVolume.getScalarData();
scalarData.set(dataInTime);
cstUtils.dynamicVolume.updateVolumeFromTimeData(dynamicVolume, operationName, options);
// If computed display set does not exist, create an object to be used as
// the displaySet. If it does exist, update the image data and vtkTexture
@@ -166,6 +165,7 @@ export default function PanelGenerateImage({ servicesManager, commandsManager }:
madeInClient: true,
FrameOfReferenceUID: dynamicVolume.metadata.FrameOfReferenceUID,
isDerived: true,
imageIds: computedVolume.imageIds,
};
setComputedDisplaySet(displaySet);
renderGeneratedImage(displaySet);
@@ -173,19 +173,8 @@ export default function PanelGenerateImage({ servicesManager, commandsManager }:
commandsManager.runCommand('updateVolumeData', {
volume: computedVolume,
});
// Check if viewport is currently displaying the computed volume, if so,
// call render on the viewports to update the image, if not, call
// renderGeneratedImage
// if (!cache.getVolume(dynamicVolumeId)) {
// for (const viewportId of viewports.keys()) {
// const viewportForRendering =
// cornerstoneViewportService.getCornerstoneViewport(viewportId);
// viewportForRendering.render();
// }
// } else {
cornerstoneViewportService.getRenderingEngine().render();
renderGeneratedImage(computedDisplaySet);
// }
}
}
@@ -248,28 +237,7 @@ export default function PanelGenerateImage({ servicesManager, commandsManager }:
onGenerate={onGenerateImage}
onDynamicClick={displayingComputed ? () => renderDynamicImage(computedDisplaySet) : null}
onDoubleRangeChange={handleSliderChange}
initialRangeValues={timePointsRangeToUseForGenerate}
/>
);
}
async function createComputedVolume(dynamicVolumeId, computedVolumeId) {
if (!cache.getVolume(computedVolumeId)) {
const computedVolume = await volumeLoader.createAndCacheDerivedVolume(dynamicVolumeId, {
volumeId: computedVolumeId,
});
return computedVolume;
}
}
PanelGenerateImage.propTypes = {
servicesManager: PropTypes.shape({
services: PropTypes.shape({
measurementService: PropTypes.shape({
getMeasurements: PropTypes.func.isRequired,
subscribe: PropTypes.func.isRequired,
EVENTS: PropTypes.object.isRequired,
VALUE_TYPES: PropTypes.object.isRequired,
}).isRequired,
}).isRequired,
}).isRequired,
};
+5 -6
View File
@@ -38,7 +38,7 @@
"@cornerstonejs/codec-libjpeg-turbo-8bit": "^1.2.2",
"@cornerstonejs/codec-openjpeg": "^1.2.4",
"@cornerstonejs/codec-openjph": "^2.4.5",
"@cornerstonejs/dicom-image-loader": "^1.86.0",
"@cornerstonejs/dicom-image-loader": "^2.1.13",
"@icr/polyseg-wasm": "^0.4.0",
"@ohif/core": "3.9.0-beta.105",
"@ohif/ui": "3.9.0-beta.105",
@@ -55,12 +55,11 @@
},
"dependencies": {
"@babel/runtime": "^7.20.13",
"@cornerstonejs/adapters": "^1.86.0",
"@cornerstonejs/core": "^1.86.0",
"@cornerstonejs/streaming-image-volume-loader": "^1.82.4",
"@cornerstonejs/tools": "^1.86.0",
"@cornerstonejs/adapters": "^2.1.13",
"@cornerstonejs/core": "^2.1.13",
"@cornerstonejs/tools": "^2.1.13",
"@icr/polyseg-wasm": "^0.4.0",
"@kitware/vtk.js": "30.4.1",
"@kitware/vtk.js": "32.1.0",
"html2canvas": "^1.4.1",
"lodash.debounce": "^4.0.8",
"lodash.merge": "^4.6.2",
@@ -1,10 +1,9 @@
import React, { useEffect, useRef, useCallback, useState } from 'react';
import { useResizeDetector } from 'react-resize-detector';
import PropTypes from 'prop-types';
import * as cs3DTools from '@cornerstonejs/tools';
import { Enums, eventTarget, getEnabledElement } from '@cornerstonejs/core';
import { MeasurementService } from '@ohif/core';
import { Notification, useViewportDialog, AllInOneMenu } from '@ohif/ui';
import { AllInOneMenu, Notification, useViewportDialog } from '@ohif/ui';
import type { Types as csTypes } from '@cornerstonejs/core';
import { setEnabledElement } from '../state';
@@ -17,8 +16,10 @@ import type { Types } from '@ohif/core';
import OHIFViewportActionCorners from '../components/OHIFViewportActionCorners';
import { getWindowLevelActionMenu } from '../components/WindowLevelActionMenu/getWindowLevelActionMenu';
import { useAppConfig } from '@state';
import { LutPresentation, PositionPresentation } from '../types/Presentation';
import { getViewportDataOverlaySettingsMenu } from '../components/ViewportDataOverlaySettingMenu';
import { getViewportPresentations } from '../utils/presentations/getViewportPresentations';
import { useSynchronizersStore } from '../stores/useSynchronizersStore';
import ActiveViewportBehavior from '../utils/ActiveViewportBehavior';
const STACK = 'stack';
@@ -28,428 +29,401 @@ const STACK = 'stack';
*/
let cacheJumpToMeasurementEvent;
function areEqual(prevProps, nextProps) {
if (nextProps.needsRerendering) {
return false;
}
if (prevProps.displaySets.length !== nextProps.displaySets.length) {
return false;
}
if (prevProps.viewportOptions.orientation !== nextProps.viewportOptions.orientation) {
return false;
}
if (prevProps.viewportOptions.toolGroupId !== nextProps.viewportOptions.toolGroupId) {
return false;
}
if (prevProps.viewportOptions.viewportType !== nextProps.viewportOptions.viewportType) {
return false;
}
if (nextProps.viewportOptions.needsRerendering) {
return false;
}
const prevDisplaySets = prevProps.displaySets;
const nextDisplaySets = nextProps.displaySets;
if (prevDisplaySets.length !== nextDisplaySets.length) {
return false;
}
for (let i = 0; i < prevDisplaySets.length; i++) {
const prevDisplaySet = prevDisplaySets[i];
const foundDisplaySet = nextDisplaySets.find(
nextDisplaySet =>
nextDisplaySet.displaySetInstanceUID === prevDisplaySet.displaySetInstanceUID
);
if (!foundDisplaySet) {
return false;
}
// check they contain the same image
if (foundDisplaySet.images?.length !== prevDisplaySet.images?.length) {
return false;
}
// check if their imageIds are the same
if (foundDisplaySet.images?.length) {
for (let j = 0; j < foundDisplaySet.images.length; j++) {
if (foundDisplaySet.images[j].imageId !== prevDisplaySet.images[j].imageId) {
return false;
}
}
}
}
return true;
}
// Todo: This should be done with expose of internal API similar to react-vtkjs-viewport
// Then we don't need to worry about the re-renders if the props change.
const OHIFCornerstoneViewport = React.memo((props: withAppTypes) => {
const {
displaySets,
dataSource,
viewportOptions,
displaySetOptions,
servicesManager,
commandsManager,
onElementEnabled,
// eslint-disable-next-line react/prop-types
onElementDisabled,
isJumpToMeasurementDisabled = false,
// Note: you SHOULD NOT use the initialImageIdOrIndex for manipulation
// of the imageData in the OHIFCornerstoneViewport. This prop is used
// to set the initial state of the viewport's first image to render
// eslint-disable-next-line react/prop-types
initialImageIndex,
// if the viewport is part of a hanging protocol layout
// we should not really rely on the old synchronizers and
// you see below we only rehydrate the synchronizers if the viewport
// is not part of the hanging protocol layout. HPs should
// define their own synchronizers. Since the synchronizers are
// viewportId dependent and
// eslint-disable-next-line react/prop-types
isHangingProtocolLayout,
} = props;
const OHIFCornerstoneViewport = React.memo(
(
props: withAppTypes<{
viewportId: string;
displaySets: AppTypes.DisplaySet[];
viewportOptions: AppTypes.ViewportGrid.GridViewportOptions;
initialImageIndex: number;
}>
) => {
const {
displaySets,
dataSource,
viewportOptions,
displaySetOptions,
servicesManager,
commandsManager,
onElementEnabled,
// eslint-disable-next-line react/prop-types
onElementDisabled,
isJumpToMeasurementDisabled = false,
// Note: you SHOULD NOT use the initialImageIdOrIndex for manipulation
// of the imageData in the OHIFCornerstoneViewport. This prop is used
// to set the initial state of the viewport's first image to render
// eslint-disable-next-line react/prop-types
initialImageIndex,
// if the viewport is part of a hanging protocol layout
// we should not really rely on the old synchronizers and
// you see below we only rehydrate the synchronizers if the viewport
// is not part of the hanging protocol layout. HPs should
// define their own synchronizers. Since the synchronizers are
// viewportId dependent and
// eslint-disable-next-line react/prop-types
isHangingProtocolLayout,
} = props;
const viewportId = viewportOptions.viewportId;
const viewportId = viewportOptions.viewportId;
if (!viewportId) {
throw new Error('Viewport ID is required');
}
// Make sure displaySetOptions has one object per displaySet
while (displaySetOptions.length < displaySets.length) {
displaySetOptions.push({});
}
// Since we only have support for dynamic data in volume viewports, we should
// handle this case here and set the viewportType to volume if any of the
// displaySets are dynamic volumes
viewportOptions.viewportType = displaySets.some(ds => ds.isDynamicVolume && ds.isReconstructable)
? 'volume'
: viewportOptions.viewportType;
const [scrollbarHeight, setScrollbarHeight] = useState('100px');
const [enabledVPElement, setEnabledVPElement] = useState(null);
const elementRef = useRef() as React.MutableRefObject<HTMLDivElement>;
const [appConfig] = useAppConfig();
const {
displaySetService,
toolbarService,
toolGroupService,
syncGroupService,
cornerstoneViewportService,
cornerstoneCacheService,
stateSyncService,
viewportActionCornersService,
} = servicesManager.services;
const [viewportDialogState] = useViewportDialog();
// useCallback for scroll bar height calculation
const setImageScrollBarHeight = useCallback(() => {
const scrollbarHeight = `${elementRef.current.clientHeight - 40}px`;
setScrollbarHeight(scrollbarHeight);
}, [elementRef]);
// useCallback for onResize
const onResize = useCallback(() => {
if (elementRef.current) {
cornerstoneViewportService.resize();
setImageScrollBarHeight();
if (!viewportId) {
throw new Error('Viewport ID is required');
}
}, [elementRef]);
const cleanUpServices = useCallback(
viewportInfo => {
const renderingEngineId = viewportInfo.getRenderingEngineId();
const syncGroups = viewportInfo.getSyncGroups();
// Make sure displaySetOptions has one object per displaySet
while (displaySetOptions.length < displaySets.length) {
displaySetOptions.push({});
}
toolGroupService.removeViewportFromToolGroup(viewportId, renderingEngineId);
// Since we only have support for dynamic data in volume viewports, we should
// handle this case here and set the viewportType to volume if any of the
// displaySets are dynamic volumes
viewportOptions.viewportType = displaySets.some(
ds => ds.isDynamicVolume && ds.isReconstructable
)
? 'volume'
: viewportOptions.viewportType;
syncGroupService.removeViewportFromSyncGroup(viewportId, renderingEngineId, syncGroups);
const [scrollbarHeight, setScrollbarHeight] = useState('100px');
const [enabledVPElement, setEnabledVPElement] = useState(null);
const elementRef = useRef() as React.MutableRefObject<HTMLDivElement>;
const [appConfig] = useAppConfig();
viewportActionCornersService.clear(viewportId);
},
[viewportId]
);
const {
displaySetService,
toolbarService,
toolGroupService,
syncGroupService,
cornerstoneViewportService,
segmentationService,
cornerstoneCacheService,
viewportActionCornersService,
} = servicesManager.services;
const elementEnabledHandler = useCallback(
evt => {
// check this is this element reference and return early if doesn't match
if (evt.detail.element !== elementRef.current) {
return;
const [viewportDialogState] = useViewportDialog();
// useCallback for scroll bar height calculation
const setImageScrollBarHeight = useCallback(() => {
const scrollbarHeight = `${elementRef.current.clientHeight - 40}px`;
setScrollbarHeight(scrollbarHeight);
}, [elementRef]);
// useCallback for onResize
const onResize = useCallback(() => {
if (elementRef.current) {
cornerstoneViewportService.resize();
setImageScrollBarHeight();
}
}, [elementRef]);
const { viewportId, element } = evt.detail;
const viewportInfo = cornerstoneViewportService.getViewportInfo(viewportId);
setEnabledElement(viewportId, element);
setEnabledVPElement(element);
const cleanUpServices = useCallback(
viewportInfo => {
const renderingEngineId = viewportInfo.getRenderingEngineId();
const syncGroups = viewportInfo.getSyncGroups();
const renderingEngineId = viewportInfo.getRenderingEngineId();
const toolGroupId = viewportInfo.getToolGroupId();
const syncGroups = viewportInfo.getSyncGroups();
toolGroupService.removeViewportFromToolGroup(viewportId, renderingEngineId);
syncGroupService.removeViewportFromSyncGroup(viewportId, renderingEngineId, syncGroups);
toolGroupService.addViewportToToolGroup(viewportId, renderingEngineId, toolGroupId);
segmentationService.clearSegmentationRepresentations(viewportId);
syncGroupService.addViewportToSyncGroup(viewportId, renderingEngineId, syncGroups);
viewportActionCornersService.clear(viewportId);
},
[
viewportId,
segmentationService,
syncGroupService,
toolGroupService,
viewportActionCornersService,
]
);
const synchronizersStore = stateSyncService.getState().synchronizersStore;
if (synchronizersStore?.[viewportId]?.length && !isHangingProtocolLayout) {
// If the viewport used to have a synchronizer, re apply it again
_rehydrateSynchronizers(synchronizersStore, viewportId, syncGroupService);
}
if (onElementEnabled) {
onElementEnabled(evt);
}
},
[viewportId, onElementEnabled, toolGroupService]
);
// disable the element upon unmounting
useEffect(() => {
cornerstoneViewportService.enableViewport(viewportId, elementRef.current);
eventTarget.addEventListener(Enums.Events.ELEMENT_ENABLED, elementEnabledHandler);
setImageScrollBarHeight();
return () => {
const viewportInfo = cornerstoneViewportService.getViewportInfo(viewportId);
if (!viewportInfo) {
return;
}
cornerstoneViewportService.storePresentation({ viewportId });
// This should be done after the store presentation since synchronizers
// will get cleaned up and they need the viewportInfo to be present
cleanUpServices(viewportInfo);
if (onElementDisabled) {
onElementDisabled(viewportInfo);
}
cornerstoneViewportService.disableElement(viewportId);
eventTarget.removeEventListener(Enums.Events.ELEMENT_ENABLED, elementEnabledHandler);
};
}, []);
// subscribe to displaySet metadata invalidation (updates)
// Currently, if the metadata changes we need to re-render the display set
// for it to take effect in the viewport. As we deal with scaling in the loading,
// we need to remove the old volume from the cache, and let the
// viewport to re-add it which will use the new metadata. Otherwise, the
// viewport will use the cached volume and the new metadata will not be used.
// Note: this approach does not actually end of sending network requests
// and it uses the network cache
useEffect(() => {
const { unsubscribe } = displaySetService.subscribe(
displaySetService.EVENTS.DISPLAY_SET_SERIES_METADATA_INVALIDATED,
async ({
displaySetInstanceUID: invalidatedDisplaySetInstanceUID,
invalidateData,
}: Types.DisplaySetSeriesMetadataInvalidatedEvent) => {
if (!invalidateData) {
const elementEnabledHandler = useCallback(
evt => {
// check this is this element reference and return early if doesn't match
if (evt.detail.element !== elementRef.current) {
return;
}
const { viewportId, element } = evt.detail;
const viewportInfo = cornerstoneViewportService.getViewportInfo(viewportId);
setEnabledElement(viewportId, element);
setEnabledVPElement(element);
const renderingEngineId = viewportInfo.getRenderingEngineId();
const toolGroupId = viewportInfo.getToolGroupId();
const syncGroups = viewportInfo.getSyncGroups();
toolGroupService.addViewportToToolGroup(viewportId, renderingEngineId, toolGroupId);
syncGroupService.addViewportToSyncGroup(viewportId, renderingEngineId, syncGroups);
// we don't need reactivity here so just use state
const { synchronizersStore } = useSynchronizersStore.getState();
if (synchronizersStore?.[viewportId]?.length && !isHangingProtocolLayout) {
// If the viewport used to have a synchronizer, re apply it again
_rehydrateSynchronizers(viewportId, syncGroupService);
}
if (onElementEnabled && typeof onElementEnabled === 'function') {
onElementEnabled(evt);
}
},
[viewportId, onElementEnabled, toolGroupService]
);
// disable the element upon unmounting
useEffect(() => {
cornerstoneViewportService.enableViewport(viewportId, elementRef.current);
eventTarget.addEventListener(Enums.Events.ELEMENT_ENABLED, elementEnabledHandler);
setImageScrollBarHeight();
return () => {
const viewportInfo = cornerstoneViewportService.getViewportInfo(viewportId);
if (viewportInfo.hasDisplaySet(invalidatedDisplaySetInstanceUID)) {
const viewportData = viewportInfo.getViewportData();
const newViewportData = await cornerstoneCacheService.invalidateViewportData(
viewportData,
invalidatedDisplaySetInstanceUID,
dataSource,
displaySetService
);
const keepCamera = true;
cornerstoneViewportService.updateViewport(viewportId, newViewportData, keepCamera);
if (!viewportInfo) {
return;
}
}
);
return () => {
unsubscribe();
};
}, [viewportId]);
useEffect(() => {
// handle the default viewportType to be stack
if (!viewportOptions.viewportType) {
viewportOptions.viewportType = STACK;
}
cornerstoneViewportService.storePresentation({ viewportId });
const loadViewportData = async () => {
const viewportData = await cornerstoneCacheService.createViewportData(
displaySets,
viewportOptions,
dataSource,
initialImageIndex
);
// This should be done after the store presentation since synchronizers
// will get cleaned up and they need the viewportInfo to be present
cleanUpServices(viewportInfo);
// The presentation state will have been stored previously by closing
// a viewport. Otherwise, this viewport will be unchanged and the
// presentation information will be directly carried over.
const state = stateSyncService.getState();
const lutPresentationStore = state.lutPresentationStore as LutPresentation;
const positionPresentationStore = state.positionPresentationStore as PositionPresentation;
if (onElementDisabled && typeof onElementDisabled === 'function') {
onElementDisabled(viewportInfo);
}
const { presentationIds } = viewportOptions;
const presentations = {
positionPresentation: positionPresentationStore[presentationIds?.positionPresentationId],
lutPresentation: lutPresentationStore[presentationIds?.lutPresentationId],
cornerstoneViewportService.disableElement(viewportId);
eventTarget.removeEventListener(Enums.Events.ELEMENT_ENABLED, elementEnabledHandler);
};
let measurement;
if (cacheJumpToMeasurementEvent?.viewportId === viewportId) {
measurement = cacheJumpToMeasurementEvent.measurement;
// Delete the position presentation so that viewport navigates direct
presentations.positionPresentation = null;
cacheJumpToMeasurementEvent = null;
}
}, []);
// Note: This is a hack to get the grid to re-render the OHIFCornerstoneViewport component
// Used for segmentation hydration right now, since the logic to decide whether
// a viewport needs to render a segmentation lives inside the CornerstoneViewportService
// so we need to re-render (force update via change of the needsRerendering) so that React
// does the diffing and decides we should render this again (although the id and element has not changed)
// so that the CornerstoneViewportService can decide whether to render the segmentation or not. Not that we reached here we can turn it off.
if (viewportOptions.needsRerendering) {
viewportOptions.needsRerendering = false;
}
// subscribe to displaySet metadata invalidation (updates)
// Currently, if the metadata changes we need to re-render the display set
// for it to take effect in the viewport. As we deal with scaling in the loading,
// we need to remove the old volume from the cache, and let the
// viewport to re-add it which will use the new metadata. Otherwise, the
// viewport will use the cached volume and the new metadata will not be used.
// Note: this approach does not actually end of sending network requests
// and it uses the network cache
useEffect(() => {
const { unsubscribe } = displaySetService.subscribe(
displaySetService.EVENTS.DISPLAY_SET_SERIES_METADATA_INVALIDATED,
async ({
displaySetInstanceUID: invalidatedDisplaySetInstanceUID,
invalidateData,
}: Types.DisplaySetSeriesMetadataInvalidatedEvent) => {
if (!invalidateData) {
return;
}
cornerstoneViewportService.setViewportData(
viewportId,
viewportData,
viewportOptions,
displaySetOptions,
presentations
const viewportInfo = cornerstoneViewportService.getViewportInfo(viewportId);
if (viewportInfo.hasDisplaySet(invalidatedDisplaySetInstanceUID)) {
const viewportData = viewportInfo.getViewportData();
const newViewportData = await cornerstoneCacheService.invalidateViewportData(
viewportData,
invalidatedDisplaySetInstanceUID,
dataSource,
displaySetService
);
const keepCamera = true;
cornerstoneViewportService.updateViewport(viewportId, newViewportData, keepCamera);
}
}
);
if (measurement) {
cs3DTools.annotation.selection.setAnnotationSelected(measurement.uid);
return () => {
unsubscribe();
};
}, [viewportId]);
useEffect(() => {
// handle the default viewportType to be stack
if (!viewportOptions.viewportType) {
viewportOptions.viewportType = STACK;
}
};
loadViewportData();
}, [viewportOptions, displaySets, dataSource]);
const loadViewportData = async () => {
const viewportData = await cornerstoneCacheService.createViewportData(
displaySets,
viewportOptions,
dataSource,
initialImageIndex
);
/**
* There are two scenarios for jump to click
* 1. Current viewports contain the displaySet that the annotation was drawn on
* 2. Current viewports don't contain the displaySet that the annotation was drawn on
* and we need to change the viewports displaySet for jumping.
* Since measurement_jump happens via events and listeners, the former case is handled
* by the measurement_jump direct callback, but the latter case is handled first by
* the viewportGrid to set the correct displaySet on the viewport, AND THEN we check
* the cache for jumping to see if there is any jump queued, then we jump to the correct slice.
*/
useEffect(() => {
if (isJumpToMeasurementDisabled) {
return;
}
const presentations = getViewportPresentations(viewportId, viewportOptions);
const unsubscribeFromJumpToMeasurementEvents = _subscribeToJumpToMeasurementEvents(
elementRef,
viewportId,
servicesManager
);
let measurement;
if (cacheJumpToMeasurementEvent?.viewportId === viewportId) {
measurement = cacheJumpToMeasurementEvent.measurement;
// Delete the position presentation so that viewport navigates direct
presentations.positionPresentation = null;
cacheJumpToMeasurementEvent = null;
}
_checkForCachedJumpToMeasurementEvents(elementRef, viewportId, displaySets, servicesManager);
// Note: This is a hack to get the grid to re-render the OHIFCornerstoneViewport component
// Used for segmentation hydration right now, since the logic to decide whether
// a viewport needs to render a segmentation lives inside the CornerstoneViewportService
// so we need to re-render (force update via change of the needsRerendering) so that React
// does the diffing and decides we should render this again (although the id and element has not changed)
// so that the CornerstoneViewportService can decide whether to render the segmentation or not. Not that we reached here we can turn it off.
if (viewportOptions.needsRerendering) {
viewportOptions.needsRerendering = false;
}
return () => {
unsubscribeFromJumpToMeasurementEvents();
};
}, [displaySets, elementRef, viewportId]);
cornerstoneViewportService.setViewportData(
viewportId,
viewportData,
viewportOptions,
displaySetOptions,
presentations
);
// Set up the window level action menu in the viewport action corners.
useEffect(() => {
// Doing an === check here because the default config value when not set is true
if (appConfig.addWindowLevelActionMenu === false) {
return;
}
if (measurement) {
cs3DTools.annotation.selection.setAnnotationSelected(measurement.uid);
}
};
// TODO: In the future we should consider using the customization service
// to determine if and in which corner various action components should go.
const wlActionMenu = getWindowLevelActionMenu({
viewportId,
element: elementRef.current,
loadViewportData();
}, [viewportOptions, displaySets, dataSource]);
/**
* There are two scenarios for jump to click
* 1. Current viewports contain the displaySet that the annotation was drawn on
* 2. Current viewports don't contain the displaySet that the annotation was drawn on
* and we need to change the viewports displaySet for jumping.
* Since measurement_jump happens via events and listeners, the former case is handled
* by the measurement_jump direct callback, but the latter case is handled first by
* the viewportGrid to set the correct displaySet on the viewport, AND THEN we check
* the cache for jumping to see if there is any jump queued, then we jump to the correct slice.
*/
useEffect(() => {
if (isJumpToMeasurementDisabled) {
return;
}
const unsubscribeFromJumpToMeasurementEvents = _subscribeToJumpToMeasurementEvents(
elementRef,
viewportId,
servicesManager
);
_checkForCachedJumpToMeasurementEvents(elementRef, viewportId, displaySets, servicesManager);
return () => {
unsubscribeFromJumpToMeasurementEvents();
};
}, [displaySets, elementRef, viewportId, isJumpToMeasurementDisabled, servicesManager]);
// Set up the window level action menu in the viewport action corners.
useEffect(() => {
// Doing an === check here because the default config value when not set is true
if (appConfig.addWindowLevelActionMenu === false) {
return;
}
const location = viewportActionCornersService.LOCATIONS.topRight;
// TODO: In the future we should consider using the customization service
// to determine if and in which corner various action components should go.
viewportActionCornersService.addComponent({
viewportId,
id: 'windowLevelActionMenu',
component: getWindowLevelActionMenu({
viewportId,
element: elementRef.current,
displaySets,
servicesManager,
commandsManager,
location,
verticalDirection: AllInOneMenu.VerticalDirection.TopToBottom,
horizontalDirection: AllInOneMenu.HorizontalDirection.RightToLeft,
}),
location,
});
viewportActionCornersService.addComponent({
viewportId,
id: 'segmentation',
component: getViewportDataOverlaySettingsMenu({
viewportId,
element: elementRef.current,
displaySets,
servicesManager,
commandsManager,
location,
}),
location,
});
}, [
displaySets,
viewportId,
viewportActionCornersService,
servicesManager,
commandsManager,
verticalDirection: AllInOneMenu.VerticalDirection.TopToBottom,
horizontalDirection: AllInOneMenu.HorizontalDirection.RightToLeft,
appConfig,
]);
const { ref: resizeRef } = useResizeDetector({
onResize,
});
viewportActionCornersService.setComponent({
viewportId,
id: 'windowLevelActionMenu',
component: wlActionMenu,
location: viewportActionCornersService.LOCATIONS.topRight,
indexPriority: -100,
});
}, [displaySets, viewportId, viewportActionCornersService, servicesManager, commandsManager]);
const { ref: resizeRef } = useResizeDetector({
onResize,
});
return (
<React.Fragment>
<div className="viewport-wrapper">
<div
className="cornerstone-viewport-element"
style={{ height: '100%', width: '100%' }}
onContextMenu={e => e.preventDefault()}
onMouseDown={e => e.preventDefault()}
ref={el => {
resizeRef.current = el;
elementRef.current = el;
}}
></div>
<CornerstoneOverlays
viewportId={viewportId}
toolBarService={toolbarService}
element={elementRef.current}
scrollbarHeight={scrollbarHeight}
servicesManager={servicesManager}
/>
<CinePlayer
enabledVPElement={enabledVPElement}
viewportId={viewportId}
servicesManager={servicesManager}
/>
</div>
{/* top offset of 24px to account for ViewportActionCorners. */}
<div className="absolute top-[24px] w-full">
{viewportDialogState.viewportId === viewportId && (
<Notification
id="viewport-notification"
message={viewportDialogState.message}
type={viewportDialogState.type}
actions={viewportDialogState.actions}
onSubmit={viewportDialogState.onSubmit}
onOutsideClick={viewportDialogState.onOutsideClick}
onKeyPress={viewportDialogState.onKeyPress}
return (
<React.Fragment>
<div className="viewport-wrapper">
<div
className="cornerstone-viewport-element"
style={{ height: '100%', width: '100%' }}
onContextMenu={e => e.preventDefault()}
onMouseDown={e => e.preventDefault()}
ref={el => {
resizeRef.current = el;
elementRef.current = el;
}}
></div>
<CornerstoneOverlays
viewportId={viewportId}
toolBarService={toolbarService}
element={elementRef.current}
scrollbarHeight={scrollbarHeight}
servicesManager={servicesManager}
/>
)}
</div>
{/* The OHIFViewportActionCorners follows the viewport in the DOM so that it is naturally at a higher z-index.*/}
<OHIFViewportActionCorners viewportId={viewportId} />
</React.Fragment>
);
}, areEqual);
<CinePlayer
enabledVPElement={enabledVPElement}
viewportId={viewportId}
servicesManager={servicesManager}
/>
<ActiveViewportBehavior
viewportId={viewportId}
servicesManager={servicesManager}
/>
</div>
{/* top offset of 24px to account for ViewportActionCorners. */}
<div className="absolute top-[24px] w-full">
{viewportDialogState.viewportId === viewportId && (
<Notification
id="viewport-notification"
message={viewportDialogState.message}
type={viewportDialogState.type}
actions={viewportDialogState.actions}
onSubmit={viewportDialogState.onSubmit}
onOutsideClick={viewportDialogState.onOutsideClick}
onKeyPress={viewportDialogState.onKeyPress}
/>
)}
</div>
{/* The OHIFViewportActionCorners follows the viewport in the DOM so that it is naturally at a higher z-index.*/}
<OHIFViewportActionCorners viewportId={viewportId} />
</React.Fragment>
);
},
areEqual
);
function _subscribeToJumpToMeasurementEvents(elementRef, viewportId, servicesManager) {
const { measurementService, cornerstoneViewportService } = servicesManager.services;
@@ -542,12 +516,15 @@ function _jumpToMeasurement(measurement, targetElementRef, viewportId, servicesM
}
}
function _rehydrateSynchronizers(
synchronizersStore: { [key: string]: unknown },
viewportId: string,
syncGroupService: any
) {
synchronizersStore[viewportId].forEach(synchronizerObj => {
function _rehydrateSynchronizers(viewportId: string, syncGroupService: any) {
const { synchronizersStore } = useSynchronizersStore.getState();
const synchronizers = synchronizersStore[viewportId];
if (!synchronizers) {
return;
}
synchronizers.forEach(synchronizerObj => {
if (!synchronizerObj.id) {
return;
}
@@ -597,17 +574,91 @@ function _rehydrateSynchronizers(
// Component displayName
OHIFCornerstoneViewport.displayName = 'OHIFCornerstoneViewport';
OHIFCornerstoneViewport.propTypes = {
displaySets: PropTypes.array.isRequired,
dataSource: PropTypes.object.isRequired,
viewportOptions: PropTypes.object,
displaySetOptions: PropTypes.arrayOf(PropTypes.any),
servicesManager: PropTypes.object.isRequired,
onElementEnabled: PropTypes.func,
isJumpToMeasurementDisabled: PropTypes.bool,
// Note: you SHOULD NOT use the initialImageIdOrIndex for manipulation
// of the imageData in the OHIFCornerstoneViewport. This prop is used
// to set the initial state of the viewport's first image to render
initialImageIdOrIndex: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
};
function areEqual(prevProps, nextProps) {
if (nextProps.needsRerendering) {
console.debug('OHIFCornerstoneViewport: Rerender caused by: needsRerendering');
return false;
}
if (prevProps.displaySets.length !== nextProps.displaySets.length) {
console.debug('OHIFCornerstoneViewport: Rerender caused by: displaySets length change');
return false;
}
if (prevProps.viewportOptions.orientation !== nextProps.viewportOptions.orientation) {
console.debug('OHIFCornerstoneViewport: Rerender caused by: orientation change');
return false;
}
if (prevProps.viewportOptions.toolGroupId !== nextProps.viewportOptions.toolGroupId) {
console.debug('OHIFCornerstoneViewport: Rerender caused by: toolGroupId change');
return false;
}
if (
nextProps.viewportOptions.viewportType &&
prevProps.viewportOptions.viewportType !== nextProps.viewportOptions.viewportType
) {
console.debug('OHIFCornerstoneViewport: Rerender caused by: viewportType change');
return false;
}
if (nextProps.viewportOptions.needsRerendering) {
console.debug('OHIFCornerstoneViewport: Rerender caused by: viewportOptions.needsRerendering');
return false;
}
const prevDisplaySets = prevProps.displaySets;
const nextDisplaySets = nextProps.displaySets;
if (prevDisplaySets.length !== nextDisplaySets.length) {
console.debug('OHIFCornerstoneViewport: Rerender caused by: displaySets length mismatch');
return false;
}
for (let i = 0; i < prevDisplaySets.length; i++) {
const prevDisplaySet = prevDisplaySets[i];
const foundDisplaySet = nextDisplaySets.find(
nextDisplaySet =>
nextDisplaySet.displaySetInstanceUID === prevDisplaySet.displaySetInstanceUID
);
if (!foundDisplaySet) {
console.debug('OHIFCornerstoneViewport: Rerender caused by: displaySet not found');
return false;
}
// check they contain the same image
if (foundDisplaySet.images?.length !== prevDisplaySet.images?.length) {
console.debug('OHIFCornerstoneViewport: Rerender caused by: images length mismatch');
return false;
}
// check if their imageIds are the same
if (foundDisplaySet.images?.length) {
for (let j = 0; j < foundDisplaySet.images.length; j++) {
if (foundDisplaySet.images[j].imageId !== prevDisplaySet.images[j].imageId) {
console.debug('OHIFCornerstoneViewport: Rerender caused by: imageId mismatch');
return false;
}
}
}
}
return true;
}
// Helper function to check if display sets have changed
function haveDisplaySetsChanged(prevDisplaySets, currentDisplaySets) {
if (prevDisplaySets.length !== currentDisplaySets.length) {
return true;
}
return currentDisplaySets.some((currentDS, index) => {
const prevDS = prevDisplaySets[index];
return currentDS.displaySetInstanceUID !== prevDS.displaySetInstanceUID;
});
}
export default OHIFCornerstoneViewport;
@@ -2,9 +2,9 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { vec3 } from 'gl-matrix';
import PropTypes from 'prop-types';
import { metaData, Enums, utilities } from '@cornerstonejs/core';
import { ImageSliceData } from '@cornerstonejs/core/dist/esm/types';
import type { ImageSliceData } from '@cornerstonejs/core/types';
import { ViewportOverlay } from '@ohif/ui';
import { InstanceMetadata } from '@ohif/core/src/types';
import type { InstanceMetadata } from '@ohif/core/src/types';
import { formatPN, formatDICOMDate, formatDICOMTime, formatNumberPrecision } from './utils';
import { StackViewportData, VolumeViewportData } from '../../types/CornerstoneCacheService';
@@ -45,14 +45,14 @@ const OverlayItemComponents = {
'ohif.overlayItem.instanceNumber': InstanceNumberOverlayItem,
};
const studyDateItem = {
id: 'StudyDate',
customizationType: 'ohif.overlayItem',
label: '',
title: 'Study date',
condition: ({ referenceInstance }) => referenceInstance?.StudyDate,
contentF: ({ referenceInstance, formatters: { formatDate } }) => formatDate(referenceInstance.StudyDate),
contentF: ({ referenceInstance, formatters: { formatDate } }) =>
formatDate(referenceInstance.StudyDate),
};
const seriesDescriptionItem = {
@@ -63,15 +63,19 @@ const seriesDescriptionItem = {
condition: ({ referenceInstance }) => {
return referenceInstance && referenceInstance.SeriesDescription;
},
contentF: ({ referenceInstance }) => referenceInstance.SeriesDescription
contentF: ({ referenceInstance }) => referenceInstance.SeriesDescription,
};
const topLeftItems = { id: 'cornerstoneOverlayTopLeft', items: [studyDateItem, seriesDescriptionItem] };
const topLeftItems = {
id: 'cornerstoneOverlayTopLeft',
items: [studyDateItem, seriesDescriptionItem],
};
const topRightItems = { id: 'cornerstoneOverlayTopRight', items: [] };
const bottomLeftItems = {
id: 'cornerstoneOverlayBottomLeft', items: [
id: 'cornerstoneOverlayBottomLeft',
items: [
{
id: 'WindowLevel',
customizationType: 'ohif.overlayItem.windowLevel',
@@ -79,12 +83,12 @@ const bottomLeftItems = {
{
id: 'ZoomLevel',
customizationType: 'ohif.overlayItem.zoomLevel',
condition: (props) => {
condition: props => {
const activeToolName = props.toolGroupService.getActiveToolForViewport(props.viewportId);
return activeToolName === 'Zoom';
},
},
]
],
};
const bottomRightItems = {
@@ -94,7 +98,7 @@ const bottomRightItems = {
id: 'InstanceNumber',
customizationType: 'ohif.overlayItem.instanceNumber',
},
]
],
};
/**
@@ -145,19 +149,18 @@ function CustomizableViewportOverlay({
// append functionality. This code enables the historical usage, but
// the recommended functionality is to append to the default values in
// cornerstoneOverlay rather than defining individual items.
const topLeftCustomization = customizationService.getCustomization(
'cornerstoneOverlayTopLeft'
) || cornerstoneOverlay?.topLeftItems;
const topRightCustomization = customizationService.getCustomization(
'cornerstoneOverlayTopRight'
) || cornerstoneOverlay?.topRightItems;
const bottomLeftCustomization = customizationService.getCustomization(
'cornerstoneOverlayBottomLeft'
) || cornerstoneOverlay?.bottomLeftItems;
const bottomRightCustomization = customizationService.getCustomization(
'cornerstoneOverlayBottomRight'
) || cornerstoneOverlay?.bottomRightItems;
const topLeftCustomization =
customizationService.getCustomization('cornerstoneOverlayTopLeft') ||
cornerstoneOverlay?.topLeftItems;
const topRightCustomization =
customizationService.getCustomization('cornerstoneOverlayTopRight') ||
cornerstoneOverlay?.topRightItems;
const bottomLeftCustomization =
customizationService.getCustomization('cornerstoneOverlayBottomLeft') ||
cornerstoneOverlay?.bottomLeftItems;
const bottomRightCustomization =
customizationService.getCustomization('cornerstoneOverlayBottomRight') ||
cornerstoneOverlay?.bottomRightItems;
const instanceNumber = useMemo(
() =>
@@ -177,13 +180,12 @@ function CustomizableViewportOverlay({
return {
displaySets,
displaySet,
instance: instances[imageIndex],
instance: instances?.[imageIndex],
instances,
referenceInstance,
};
}, [viewportData, viewportId, instanceNumber, cornerstoneViewportService]);
/**
* Updating the VOI when the viewport changes its voi
*/
@@ -307,7 +309,8 @@ function CustomizableViewportOverlay({
<>
{items.map((item, index) => (
<div key={`${keyPrefix}_${index}`}>
{(!item?.condition || item.condition(props)) && _renderOverlayItem(item, props) || null}
{((!item?.condition || item.condition(props)) && _renderOverlayItem(item, props)) ||
null}
</div>
))}
</>
@@ -316,7 +319,6 @@ function CustomizableViewportOverlay({
[_renderOverlayItem]
);
return (
<ViewportOverlay
topLeft={getContent(topLeftCustomization, 'topLeftOverlayItem')}
@@ -335,7 +337,9 @@ function getDisplaySets(viewportData, displaySetService) {
if (!viewportData?.data?.length) {
return null;
}
const displaySets = viewportData.data.map(datum => displaySetService.getDisplaySetByUID(datum.displaySetInstanceUID)).filter(it => !!it);
const displaySets = viewportData.data
.map(datum => displaySetService.getDisplaySetByUID(datum.displaySetInstanceUID))
.filter(it => !!it);
if (!displaySets.length) {
return null;
}
@@ -382,7 +386,6 @@ function _getInstanceNumberFromStack(viewportData, imageIndex) {
return parseInt(instanceNumber);
}
// Since volume viewports can be in any view direction, they can render
// a reconstructed image which don't have imageIds; therefore, no instance and instanceNumber
// Here we check if viewport is in the acquisition direction and if so, we get the instanceNumber
@@ -400,6 +403,11 @@ function _getInstanceNumberFromVolume(
// Todo: support fusion of acquisition plane which has instanceNumber
const { volume } = volumes[0];
if (!volume) {
return;
}
const { direction, imageIds } = volume;
const cornerstoneViewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
@@ -443,9 +451,10 @@ function OverlayItem(props) {
style={{ color, background }}
title={title}
>
{label ? (<span className="mr-1 shrink-0">{label}</span>) : null}
{label ? <span className="mr-1 shrink-0">{label}</span> : null}
<span className="ml-1 mr-2 shrink-0">{value}</span>
</div>);
</div>
);
}
/**
@@ -1,7 +1,6 @@
import React, { useEffect } from 'react';
import PropTypes from 'prop-types';
import { Enums, VolumeViewport3D } from '@cornerstonejs/core';
import { utilities as csToolsUtils } from '@cornerstonejs/tools';
import { Enums, VolumeViewport3D, utilities as csUtils } from '@cornerstonejs/core';
import { ImageScrollbar } from '@ohif/ui';
function CornerstoneImageScrollbar({
@@ -12,7 +11,9 @@ function CornerstoneImageScrollbar({
setImageSliceData,
scrollbarHeight,
servicesManager,
}: withAppTypes) {
}: withAppTypes<{
element: HTMLElement;
}>) {
const { cineService, cornerstoneViewportService } = servicesManager.services;
const onImageScrollbarChange = (imageIndex, viewportId) => {
@@ -26,7 +27,7 @@ function CornerstoneImageScrollbar({
cineService.setCine({ id: viewportId, isPlaying: false });
}
csToolsUtils.jumpToSlice(viewport.element, {
csUtils.jumpToSlice(viewport.element, {
imageIndex,
debounceLoading: true,
});
@@ -64,7 +65,9 @@ function CornerstoneImageScrollbar({
const updateIndex = event => {
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
if(!viewport || viewport instanceof VolumeViewport3D) return
if (!viewport || viewport instanceof VolumeViewport3D) {
return;
}
const { imageIndex, newImageIdIndex = imageIndex } = event.detail;
const numberOfSlices = viewport.getNumberOfSlices();
// find the index of imageId in the imageIds
@@ -1,14 +1,7 @@
import React, { useEffect, useState, useMemo } from 'react';
import classNames from 'classnames';
import {
metaData,
Enums,
Types,
getEnabledElement,
utilities as csUtils,
} from '@cornerstonejs/core';
import { metaData, Enums, Types, getEnabledElement } from '@cornerstonejs/core';
import { utilities } from '@cornerstonejs/tools';
import PropTypes from 'prop-types';
import { vec3 } from 'gl-matrix';
import './ViewportOrientationMarkers.css';
@@ -31,8 +24,9 @@ function ViewportOrientationMarkers({
useEffect(() => {
const cameraModifiedListener = (evt: Types.EventTypes.CameraModifiedEvent) => {
const { rotation, previousCamera, camera } = evt.detail;
const { previousCamera, camera } = evt.detail;
const { rotation } = camera;
if (rotation !== undefined) {
setRotation(rotation);
}
@@ -74,7 +68,12 @@ function ViewportOrientationMarkers({
return false;
}
({ rowCosines, columnCosines, isDefaultValueSetForColumnCosine, isDefaultValueSetForColumnCosine } = metaData.get('imagePlaneModule', imageId) || {});
({
rowCosines,
columnCosines,
isDefaultValueSetForColumnCosine,
isDefaultValueSetForColumnCosine,
} = metaData.get('imagePlaneModule', imageId) || {});
} else {
if (!element || !getEnabledElement(element)) {
return '';
@@ -90,7 +89,13 @@ function ViewportOrientationMarkers({
rowCosines = viewRight;
}
if (!rowCosines || !columnCosines || rotation === undefined || isDefaultValueSetForRowCosine || isDefaultValueSetForColumnCosine) {
if (
!rowCosines ||
!columnCosines ||
rotation === undefined ||
isDefaultValueSetForRowCosine ||
isDefaultValueSetForColumnCosine
) {
return '';
}
+472 -14
View File
@@ -12,15 +12,23 @@ import {
utilities as cstUtils,
ReferenceLinesTool,
} from '@cornerstonejs/tools';
import { Types as OhifTypes } from '@ohif/core';
import {
callLabelAutocompleteDialog,
showLabelAnnotationPopup,
createReportAsync,
callInputDialog,
colorPickerDialog,
} from '@ohif/extension-default';
import { vec3, mat4 } from 'gl-matrix';
import CornerstoneViewportDownloadForm from './utils/CornerstoneViewportDownloadForm';
import { callLabelAutocompleteDialog, showLabelAnnotationPopup } from './utils/callInputDialog';
import toggleImageSliceSync from './utils/imageSliceSync/toggleImageSliceSync';
import { getFirstAnnotationSelected } from './utils/measurementServiceMappings/utils/selection';
import getActiveViewportEnabledElement from './utils/getActiveViewportEnabledElement';
import toggleVOISliceSync from './utils/toggleVOISliceSync';
import { usePositionPresentationStore, useSegmentationPresentationStore } from './stores';
const toggleSyncFunctions = {
imageSlice: toggleImageSliceSync,
@@ -29,6 +37,7 @@ const toggleSyncFunctions = {
function commandsModule({
servicesManager,
extensionManager,
commandsManager,
}: OhifTypes.Extensions.ExtensionParams): OhifTypes.Extensions.CommandsModule {
const {
@@ -101,7 +110,49 @@ function commandsModule({
commandsManager.run(options, optionsToUse);
},
updateStoredSegmentationPresentation: ({ displaySet, type }) => {
const { setSegmentationPresentation } = useSegmentationPresentationStore.getState();
const referencedDisplaySetInstanceUID = displaySet.referencedDisplaySetInstanceUID;
setSegmentationPresentation(referencedDisplaySetInstanceUID, [
{
segmentationId: displaySet.displaySetInstanceUID,
hydrated: true,
type,
},
]);
},
updateStoredPositionPresentation: ({ viewportId, displaySetInstanceUID }) => {
const presentations = cornerstoneViewportService.getPresentations(viewportId);
const { positionPresentationStore, setPositionPresentation, getPositionPresentationId } =
usePositionPresentationStore.getState();
// Look inside positionPresentationStore and find the key that includes the displaySetInstanceUID
// and the value has viewportId as activeViewportId.
const previousReferencedDisplaySetStoreKey = Object.entries(positionPresentationStore).find(
([key, value]) => key.includes(displaySetInstanceUID) && value.viewportId === viewportId
)?.[0];
if (previousReferencedDisplaySetStoreKey) {
setPositionPresentation(
previousReferencedDisplaySetStoreKey,
presentations.positionPresentation
);
return;
}
// if not found means we have not visited that referencedDisplaySetInstanceUID before
// so we need to grab the positionPresentationId directly from the store,
// Todo: this is really hacky, we should have a better way for this
const positionPresentationId = getPositionPresentationId({
displaySetInstanceUIDs: [displaySetInstanceUID],
viewportId,
});
setPositionPresentation(positionPresentationId, presentations.positionPresentation);
},
getNearbyToolData({ nearbyToolData, element, canvasCoordinates }) {
return nearbyToolData ?? cstUtils.getAnnotationNearPoint(element, canvasCoordinates);
},
@@ -435,9 +486,10 @@ function commandsModule({
viewport.setCamera({ viewUp: rotatedViewUp as CoreTypes.Point3 });
viewport.render();
} else if (viewport.getRotation !== undefined) {
const currentRotation = viewport.getRotation();
const presentation = viewport.getViewPresentation();
const { rotation: currentRotation } = presentation;
const newRotation = (currentRotation + rotation) % 360;
viewport.setProperties({ rotation: newRotation });
viewport.setViewPresentation({ rotation: newRotation });
viewport.render();
}
},
@@ -554,7 +606,7 @@ function commandsModule({
// Set slice to last slice
const options = { imageIndex: jumpIndex };
cstUtils.jumpToSlice(viewport.element, options);
csUtils.jumpToSlice(viewport.element, options);
},
scroll: ({ direction }) => {
const enabledElement = _getActiveViewportEnabledElement();
@@ -566,7 +618,7 @@ function commandsModule({
const { viewport } = enabledElement;
const options = { delta: direction };
cstUtils.scroll(viewport, options);
csUtils.scroll(viewport, options);
},
setViewportColormap: ({
viewportId,
@@ -577,7 +629,6 @@ function commandsModule({
}) => {
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
const actorEntries = viewport.getActors();
let hpOpacity;
// Retrieve active protocol's viewport match details
const { viewportMatchDetails } = hangingProtocolService.getActiveProtocol();
@@ -596,14 +647,8 @@ function commandsModule({
// HP takes priority over the default opacity
colormap = { ...colormap, opacity: hpOpacity || opacity };
const setViewportProperties = (viewport, uid) => {
const actorEntry = actorEntries.find(entry => entry.uid.includes(uid));
const { actor: volumeActor, uid: volumeId } = actorEntry;
viewport.setProperties({ colormap, volumeActor }, volumeId);
};
if (viewport instanceof StackViewport) {
setViewportProperties(viewport, viewportId);
viewport.setProperties({ colormap });
}
if (viewport instanceof VolumeViewport) {
@@ -611,7 +656,9 @@ function commandsModule({
const { viewports } = viewportGridService.getState();
displaySetInstanceUID = viewports.get(viewportId)?.displaySetInstanceUIDs[0];
}
setViewportProperties(viewport, displaySetInstanceUID);
const volumeId = viewport.getVolumeId();
viewport.setProperties({ colormap }, volumeId);
}
if (immediate) {
@@ -827,6 +874,339 @@ function commandsModule({
ins?.resetCrosshairs();
});
},
/**
* Creates a labelmap for the active viewport
*/
createLabelmapForViewport: async ({ viewportId, options = {} }) => {
const { viewportGridService, displaySetService, segmentationService } =
servicesManager.services;
const { viewports } = viewportGridService.getState();
const targetViewportId = viewportId;
const viewport = viewports.get(targetViewportId);
// Todo: add support for multiple display sets
const displaySetInstanceUID =
options.displaySetInstanceUID || viewport.displaySetInstanceUIDs[0];
const segs = segmentationService.getSegmentations();
const label = options.label || `Segmentation ${segs.length + 1}`;
const segmentationId = options.segmentationId || `${csUtils.uuidv4()}`;
const displaySet = displaySetService.getDisplaySetByUID(displaySetInstanceUID);
const generatedSegmentationId = await segmentationService.createLabelmapForDisplaySet(
displaySet,
{
label,
segmentationId,
segments: options.createInitialSegment
? {
1: {
label: 'Segment 1',
active: true,
},
}
: {},
}
);
await segmentationService.addSegmentationRepresentation(viewportId, {
segmentationId,
type: Enums.SegmentationRepresentations.Labelmap,
});
return generatedSegmentationId;
},
/**
* Sets the active segmentation for a viewport
* @param props.segmentationId - The ID of the segmentation to set as active
*/
setActiveSegmentation: ({ segmentationId }) => {
const { viewportGridService, segmentationService } = servicesManager.services;
segmentationService.setActiveSegmentation(
viewportGridService.getActiveViewportId(),
segmentationId
);
},
/**
* Adds a new segment to a segmentation
* @param props.segmentationId - The ID of the segmentation to add the segment to
*/
addSegmentCommand: ({ segmentationId }) => {
const { segmentationService } = servicesManager.services;
segmentationService.addSegment(segmentationId);
},
/**
* Sets the active segment and jumps to its center
* @param props.segmentationId - The ID of the segmentation
* @param props.segmentIndex - The index of the segment to activate
*/
setActiveSegmentAndCenterCommand: ({ segmentationId, segmentIndex }) => {
const { segmentationService } = servicesManager.services;
segmentationService.setActiveSegment(segmentationId, segmentIndex);
segmentationService.jumpToSegmentCenter(segmentationId, segmentIndex);
},
/**
* Toggles the visibility of a segment
* @param props.segmentationId - The ID of the segmentation
* @param props.segmentIndex - The index of the segment
* @param props.type - The type of visibility to toggle
*/
toggleSegmentVisibilityCommand: ({ segmentationId, segmentIndex, type }) => {
const { segmentationService, viewportGridService } = servicesManager.services;
segmentationService.toggleSegmentVisibility(
viewportGridService.getActiveViewportId(),
segmentationId,
segmentIndex,
type
);
},
/**
* Toggles the lock state of a segment
* @param props.segmentationId - The ID of the segmentation
* @param props.segmentIndex - The index of the segment
*/
toggleSegmentLockCommand: ({ segmentationId, segmentIndex }) => {
const { segmentationService } = servicesManager.services;
segmentationService.toggleSegmentLocked(segmentationId, segmentIndex);
},
/**
* Toggles the visibility of a segmentation representation
* @param props.segmentationId - The ID of the segmentation
* @param props.type - The type of representation
*/
toggleSegmentationVisibilityCommand: ({ segmentationId, type }) => {
const { segmentationService, viewportGridService } = servicesManager.services;
segmentationService.toggleSegmentationRepresentationVisibility(
viewportGridService.getActiveViewportId(),
{ segmentationId, type }
);
},
/**
* Downloads a segmentation
* @param props.segmentationId - The ID of the segmentation to download
*/
downloadSegmentationCommand: ({ segmentationId }) => {
const { segmentationService } = servicesManager.services;
segmentationService.downloadSegmentation(segmentationId);
},
/**
* Stores a segmentation and shows it in the viewport
* @param props.segmentationId - The ID of the segmentation to store
*/
storeSegmentationCommand: async ({ segmentationId }) => {
const { segmentationService, viewportGridService } = servicesManager.services;
const datasources = extensionManager.getActiveDataSource();
const displaySetInstanceUIDs = await createReportAsync({
servicesManager,
getReport: () =>
commandsManager.runCommand('storeSegmentation', {
segmentationId,
dataSource: datasources[0],
}),
reportType: 'Segmentation',
});
if (displaySetInstanceUIDs) {
segmentationService.remove(segmentationId);
viewportGridService.setDisplaySetsForViewport({
viewportId: viewportGridService.getActiveViewportId(),
displaySetInstanceUIDs,
});
}
},
/**
* Downloads a segmentation as RTSS
* @param props.segmentationId - The ID of the segmentation
*/
downloadRTSSCommand: ({ segmentationId }) => {
const { segmentationService } = servicesManager.services;
segmentationService.downloadRTSS(segmentationId);
},
/**
* Sets the style for a segmentation
* @param props.segmentationId - The ID of the segmentation
* @param props.type - The type of style
* @param props.key - The style key to set
* @param props.value - The style value
*/
setSegmentationStyleCommand: ({ segmentationId, type, key, value }) => {
const { segmentationService } = servicesManager.services;
segmentationService.setStyle({ type }, { [key]: value });
},
/**
* Deletes a segment from a segmentation
* @param props.segmentationId - The ID of the segmentation
* @param props.segmentIndex - The index of the segment to delete
*/
deleteSegmentCommand: ({ segmentationId, segmentIndex }) => {
const { segmentationService } = servicesManager.services;
segmentationService.removeSegment(segmentationId, segmentIndex);
},
/**
* Deletes an entire segmentation
* @param props.segmentationId - The ID of the segmentation to delete
*/
deleteSegmentationCommand: ({ segmentationId }) => {
const { segmentationService } = servicesManager.services;
segmentationService.remove(segmentationId);
},
/**
* Removes a segmentation from the viewport
* @param props.segmentationId - The ID of the segmentation to remove
*/
removeSegmentationFromViewportCommand: ({ segmentationId }) => {
const { segmentationService, viewportGridService } = servicesManager.services;
segmentationService.removeSegmentationRepresentations(
viewportGridService.getActiveViewportId(),
{ segmentationId }
);
},
/**
* Toggles rendering of inactive segmentations
*/
toggleRenderInactiveSegmentationsCommand: () => {
const { segmentationService, viewportGridService } = servicesManager.services;
const viewportId = viewportGridService.getActiveViewportId();
const renderInactive = segmentationService.getRenderInactiveSegmentations(viewportId);
segmentationService.setRenderInactiveSegmentations(viewportId, !renderInactive);
},
/**
* Sets the fill alpha value for a segmentation type
* @param props.type - The type of segmentation
* @param props.value - The alpha value to set
*/
setFillAlphaCommand: ({ type, value }) => {
const { segmentationService } = servicesManager.services;
segmentationService.setStyle({ type }, { fillAlpha: value });
},
/**
* Sets the outline width for a segmentation type
* @param props.type - The type of segmentation
* @param props.value - The width value to set
*/
setOutlineWidthCommand: ({ type, value }) => {
const { segmentationService } = servicesManager.services;
segmentationService.setStyle({ type }, { outlineWidth: value });
},
/**
* Sets whether to render fill for a segmentation type
* @param props.type - The type of segmentation
* @param props.value - Whether to render fill
*/
setRenderFillCommand: ({ type, value }) => {
const { segmentationService } = servicesManager.services;
segmentationService.setStyle({ type }, { renderFill: value });
},
/**
* Sets whether to render outline for a segmentation type
* @param props.type - The type of segmentation
* @param props.value - Whether to render outline
*/
setRenderOutlineCommand: ({ type, value }) => {
const { segmentationService } = servicesManager.services;
segmentationService.setStyle({ type }, { renderOutline: value });
},
/**
* Sets the fill alpha for inactive segmentations
* @param props.type - The type of segmentation
* @param props.value - The alpha value to set
*/
setFillAlphaInactiveCommand: ({ type, value }) => {
const { segmentationService } = servicesManager.services;
segmentationService.setStyle({ type }, { fillAlphaInactive: value });
},
editSegmentLabel: ({ segmentationId, segmentIndex }) => {
const { segmentationService, uiDialogService } = servicesManager.services;
const segmentation = segmentationService.getSegmentation(segmentationId);
if (!segmentation) {
return;
}
const segment = segmentation.segments[segmentIndex];
const { label } = segment;
callInputDialog(uiDialogService, label, (label, actionId) => {
if (label === '') {
return;
}
segmentationService.setSegmentLabel(segmentationId, segmentIndex, label);
});
},
editSegmentationLabel: ({ segmentationId }) => {
const { segmentationService, uiDialogService } = servicesManager.services;
const segmentation = segmentationService.getSegmentation(segmentationId);
if (!segmentation) {
return;
}
const { label } = segmentation;
callInputDialog(uiDialogService, label, (label, actionId) => {
if (label === '') {
return;
}
segmentationService.addOrUpdateSegmentation({ segmentationId, label });
});
},
editSegmentColor: ({ segmentationId, segmentIndex }) => {
const { segmentationService, uiDialogService, viewportGridService } =
servicesManager.services;
const viewportId = viewportGridService.getActiveViewportId();
const color = segmentationService.getSegmentColor(viewportId, segmentationId, segmentIndex);
const rgbaColor = {
r: color[0],
g: color[1],
b: color[2],
a: color[3] / 255.0,
};
colorPickerDialog(uiDialogService, rgbaColor, (newRgbaColor, actionId) => {
if (actionId === 'cancel') {
return;
}
const color = [newRgbaColor.r, newRgbaColor.g, newRgbaColor.b, newRgbaColor.a * 255.0];
segmentationService.setSegmentColor(viewportId, segmentationId, segmentIndex, color);
});
},
getRenderInactiveSegmentations: () => {
const { segmentationService, viewportGridService } = servicesManager.services;
return segmentationService.getRenderInactiveSegmentations(
viewportGridService.getActiveViewportId()
);
},
};
const definitions = {
@@ -988,6 +1368,84 @@ function commandsModule({
toggleActiveDisabledToolbar: {
commandFn: actions.toggleActiveDisabledToolbar,
},
updateStoredPositionPresentation: {
commandFn: actions.updateStoredPositionPresentation,
},
updateStoredSegmentationPresentation: {
commandFn: actions.updateStoredSegmentationPresentation,
},
createLabelmapForViewport: {
commandFn: actions.createLabelmapForViewport,
},
setActiveSegmentation: {
commandFn: actions.setActiveSegmentation,
},
addSegment: {
commandFn: actions.addSegmentCommand,
},
setActiveSegmentAndCenter: {
commandFn: actions.setActiveSegmentAndCenterCommand,
},
toggleSegmentVisibility: {
commandFn: actions.toggleSegmentVisibilityCommand,
},
toggleSegmentLock: {
commandFn: actions.toggleSegmentLockCommand,
},
toggleSegmentationVisibility: {
commandFn: actions.toggleSegmentationVisibilityCommand,
},
downloadSegmentation: {
commandFn: actions.downloadSegmentationCommand,
},
storeSegmentation: {
commandFn: actions.storeSegmentationCommand,
},
downloadRTSS: {
commandFn: actions.downloadRTSSCommand,
},
setSegmentationStyle: {
commandFn: actions.setSegmentationStyleCommand,
},
deleteSegment: {
commandFn: actions.deleteSegmentCommand,
},
deleteSegmentation: {
commandFn: actions.deleteSegmentationCommand,
},
removeSegmentationFromViewport: {
commandFn: actions.removeSegmentationFromViewportCommand,
},
toggleRenderInactiveSegmentations: {
commandFn: actions.toggleRenderInactiveSegmentationsCommand,
},
setFillAlpha: {
commandFn: actions.setFillAlphaCommand,
},
setOutlineWidth: {
commandFn: actions.setOutlineWidthCommand,
},
setRenderFill: {
commandFn: actions.setRenderFillCommand,
},
setRenderOutline: {
commandFn: actions.setRenderOutlineCommand,
},
setFillAlphaInactive: {
commandFn: actions.setFillAlphaInactiveCommand,
},
editSegmentLabel: {
commandFn: actions.editSegmentLabel,
},
editSegmentationLabel: {
commandFn: actions.editSegmentationLabel,
},
editSegmentColor: {
commandFn: actions.editSegmentColor,
},
getRenderInactiveSegmentations: {
commandFn: actions.getRenderInactiveSegmentations,
},
};
return {
@@ -1,7 +1,6 @@
import React, { useCallback, useEffect, useState, useRef } from 'react';
import { CinePlayer, useCine } from '@ohif/ui';
import { Enums, eventTarget, cache } from '@cornerstonejs/core';
import { Enums as StreamingEnums } from '@cornerstonejs/streaming-image-volume-loader';
import { useAppConfig } from '@state';
function WrappedCinePlayer({
@@ -101,7 +100,7 @@ function WrappedCinePlayer({
return;
}
eventTarget.addEventListener(Enums.Events.STACK_VIEWPORT_NEW_STACK, newDisplaySetHandler);
enabledVPElement.addEventListener(Enums.Events.VIEWPORT_NEW_IMAGE_SET, newDisplaySetHandler);
// this doesn't makes sense that we are listening to this event on viewport element
enabledVPElement.addEventListener(
Enums.Events.VOLUME_VIEWPORT_NEW_VOLUME,
@@ -111,7 +110,10 @@ function WrappedCinePlayer({
return () => {
cineService.setCine({ id: viewportId, isPlaying: false });
eventTarget.removeEventListener(Enums.Events.STACK_VIEWPORT_NEW_STACK, newDisplaySetHandler);
enabledVPElement.removeEventListener(
Enums.Events.VIEWPORT_NEW_IMAGE_SET,
newDisplaySetHandler
);
enabledVPElement.removeEventListener(
Enums.Events.VOLUME_VIEWPORT_NEW_VOLUME,
newDisplaySetHandler
@@ -181,13 +183,13 @@ function RenderCinePlayer({
};
eventTarget.addEventListener(
StreamingEnums.Events.DYNAMIC_VOLUME_TIME_POINT_INDEX_CHANGED,
Enums.Events.DYNAMIC_VOLUME_TIME_POINT_INDEX_CHANGED,
handleTimePointIndexChange
);
return () => {
eventTarget.removeEventListener(
StreamingEnums.Events.DYNAMIC_VOLUME_TIME_POINT_INDEX_CHANGED,
Enums.Events.DYNAMIC_VOLUME_TIME_POINT_INDEX_CHANGED,
handleTimePointIndexChange
);
};
@@ -199,7 +201,7 @@ function RenderCinePlayer({
}
const { volumeId, timePointIndex, numTimePoints, splittingTag } = dynamicInfo || {};
const volume = cache.getVolume(volumeId);
const volume = cache.getVolume(volumeId, true);
volume.timePointIndex = timePointIndex;
setDynamicInfo({ volumeId, timePointIndex, numTimePoints, label: splittingTag });
@@ -207,7 +209,7 @@ function RenderCinePlayer({
const updateDynamicInfo = useCallback(props => {
const { volumeId, timePointIndex } = props;
const volume = cache.getVolume(volumeId);
const volume = cache.getVolume(volumeId, true);
volume.timePointIndex = timePointIndex;
}, []);
@@ -223,6 +225,7 @@ function RenderCinePlayer({
isPlaying: false,
});
cineService.setIsCineEnabled(false);
cineService.setViewportCineClosed(viewportId);
}}
onPlayPauseChange={isPlaying => {
cineService.setCine({
@@ -0,0 +1,138 @@
import React, { useEffect, useState } from 'react';
import { Button, Icons, Separator } from '@ohif/ui-next';
import { SegmentationRepresentations } from '@cornerstonejs/tools/enums';
function ViewportSegmentationMenu({
viewportId,
servicesManager,
}: withAppTypes<{ viewportId: string }>) {
const { segmentationService } = servicesManager.services;
const [activeSegmentations, setActiveSegmentations] = useState([]);
const [availableSegmentations, setAvailableSegmentations] = useState([]);
useEffect(() => {
const updateSegmentations = () => {
const active = segmentationService.getSegmentationRepresentations(viewportId);
setActiveSegmentations(active);
const all = segmentationService.getSegmentations();
const available = all.filter(
seg => !active.some(activeSeg => activeSeg.segmentationId === seg.segmentationId)
);
setAvailableSegmentations(available);
};
updateSegmentations();
const subscriptions = [
segmentationService.EVENTS.SEGMENTATION_MODIFIED,
segmentationService.EVENTS.SEGMENTATION_REMOVED,
segmentationService.EVENTS.SEGMENTATION_REPRESENTATION_MODIFIED,
].map(event => segmentationService.subscribe(event, updateSegmentations));
return () => {
subscriptions.forEach(subscription => subscription.unsubscribe());
};
}, [segmentationService, viewportId]);
const toggleSegmentationRepresentationVisibility = (
segmentationId,
type = SegmentationRepresentations.Labelmap
) => {
segmentationService.toggleSegmentationRepresentationVisibility(viewportId, {
segmentationId,
type,
});
};
const addSegmentationToViewport = segmentationId => {
segmentationService.addSegmentationRepresentation(viewportId, { segmentationId });
};
const removeSegmentationFromViewport = segmentationId => {
segmentationService.removeSegmentationRepresentations(viewportId, {
segmentationId,
});
};
return (
<div className="bg-muted flex h-full w-[262px] flex-col rounded p-3">
<span className="text-muted-foreground mb-2 text-xs font-semibold">Current Viewport</span>
<ul className="space-y-1">
{activeSegmentations.map(segmentation => (
<li
key={segmentation.id}
className="flex items-center text-sm"
>
<Button
variant="ghost"
size="icon"
className="text-muted-foreground mr-2"
onClick={() => removeSegmentationFromViewport(segmentation.segmentationId)}
>
<Icons.Minus className="h-6 w-6" />
</Button>
<span className="text-foreground flex-grow">{segmentation.label}</span>
{segmentation.visible ? (
<Button
variant="ghost"
size="icon"
className="text-muted-foreground"
onClick={() =>
toggleSegmentationRepresentationVisibility(
segmentation.segmentationId,
segmentation.type
)
}
>
<Icons.Hide className="h-6 w-6" />
</Button>
) : (
<Button
variant="ghost"
size="icon"
className="text-muted-foreground"
onClick={() =>
toggleSegmentationRepresentationVisibility(
segmentation.segmentationId,
segmentation.type
)
}
>
<Icons.Show className="h-6 w-6" />
</Button>
)}
</li>
))}
</ul>
{availableSegmentations.length > 0 && (
<>
<Separator className="bg-input mb-3" />
<span className="text-muted-foreground mb-2 text-xs font-semibold">Available</span>
<ul className="space-y-1">
{availableSegmentations.map(segmentation => (
<li
key={segmentation.segmentation.segmentationId}
className="flex items-center text-sm"
>
<Button
variant="ghost"
size="icon"
className="text-muted-foreground mr-2"
onClick={() =>
addSegmentationToViewport(segmentation.segmentation.segmentationId)
}
>
<Icons.Plus className="h-6 w-6" />
</Button>
<span className="text-foreground/60">{segmentation.segmentation.label}</span>
</li>
))}
</ul>
</>
)}
</div>
);
}
export default ViewportSegmentationMenu;
@@ -0,0 +1,101 @@
import React, { ReactNode, useEffect, useState } from 'react';
import { Button, Icons, Popover, PopoverContent, PopoverTrigger } from '@ohif/ui-next';
import ViewportSegmentationMenu from './ViewportSegmentationMenu';
import classNames from 'classnames';
export function ViewportSegmentationMenuWrapper({
viewportId,
displaySets,
servicesManager,
commandsManager,
location,
}: withAppTypes<{
viewportId: string;
element: HTMLElement;
}>): ReactNode {
const { segmentationService, viewportActionCornersService, viewportGridService } =
servicesManager.services;
const [representations, setRepresentations] = useState(null);
useEffect(() => {
const representations = segmentationService.getSegmentationRepresentations(viewportId);
setRepresentations(representations);
}, [viewportId, segmentationService]);
useEffect(() => {
const { unsubscribe } = segmentationService.subscribe(
segmentationService.EVENTS.SEGMENTATION_REPRESENTATION_MODIFIED,
() => {
const representations = segmentationService.getSegmentationRepresentations(viewportId);
setRepresentations(representations);
}
);
return () => {
unsubscribe();
};
}, [viewportId, segmentationService]);
const activeViewportId = viewportGridService.getActiveViewportId();
const isActiveViewport = viewportId === activeViewportId;
const { align, side } = getAlignAndSide(viewportActionCornersService, location);
if (!representations?.length) {
return null;
}
return (
<Popover>
<PopoverTrigger
asChild
className="flex items-center justify-center"
>
<Button
variant="ghost"
size="icon"
>
<Icons.ViewportViews
className={classNames(
'text-highlight',
isActiveViewport ? 'visible' : 'invisible group-hover/pane:visible'
)}
/>
</Button>
</PopoverTrigger>
<PopoverContent
className="border-none bg-transparent p-0 shadow-none"
side={side}
align={align}
alignOffset={-15}
sideOffset={5}
>
<ViewportSegmentationMenu
className="w-full"
viewportId={viewportId}
displaySets={displaySets}
servicesManager={servicesManager}
commandsManager={commandsManager}
/>
</PopoverContent>
</Popover>
);
}
const getAlignAndSide = (viewportActionCornersService, location) => {
const ViewportActionCornersLocations = viewportActionCornersService.LOCATIONS;
switch (location) {
case ViewportActionCornersLocations.topLeft:
return { align: 'start', side: 'bottom' };
case ViewportActionCornersLocations.topRight:
return { align: 'end', side: 'bottom' };
case ViewportActionCornersLocations.bottomLeft:
return { align: 'start', side: 'top' };
case ViewportActionCornersLocations.bottomRight:
return { align: 'end', side: 'top' };
default:
console.debug('Unknown location, defaulting to bottom-start');
return { align: 'start', side: 'bottom' };
}
};
@@ -0,0 +1,11 @@
import React, { ReactNode } from 'react';
import { ViewportSegmentationMenuWrapper } from './ViewportSegmentationMenuWrapper';
export function getViewportDataOverlaySettingsMenu(
props: withAppTypes<{
viewportId: string;
element: HTMLElement;
}>
): ReactNode {
return <ViewportSegmentationMenuWrapper {...props} />;
}
@@ -1,10 +1,15 @@
import React, { useEffect, useCallback, useState, ReactElement } from 'react';
import React, { useEffect, useCallback, useState, ReactElement, useMemo } from 'react';
import PropTypes from 'prop-types';
import debounce from 'lodash.debounce';
import { PanelSection, WindowLevel } from '@ohif/ui';
import vtkColorMaps from '@kitware/vtk.js/Rendering/Core/ColorTransferFunction/ColorMaps';
import { Enums, eventTarget, cache as cs3DCache, utilities as csUtils } from '@cornerstonejs/core';
import { getViewportVolumeHistogram } from './getViewportVolumeHistogram';
import { Enums, eventTarget } from '@cornerstonejs/core';
import { useActiveViewportDisplaySets } from '@ohif/core';
import {
getNodeOpacity,
isPetVolumeWithDefaultOpacity,
isVolumeWithConstantOpacity,
getWindowLevelsData,
} from './utils';
const { Events } = Enums;
@@ -16,19 +21,16 @@ const ViewportWindowLevel = ({
}>): ReactElement => {
const { cornerstoneViewportService } = servicesManager.services;
const [windowLevels, setWindowLevels] = useState([]);
const [cachedHistograms, setCachedHistograms] = useState({});
const [isLoading, setIsLoading] = useState(true);
const displaySets = useActiveViewportDisplaySets({ servicesManager });
/**
* Looks for all viewports that has exactly all volumeIds passed as parameter.
*/
const getViewportsWithVolumeIds = useCallback(
(volumeIds: string[]) => {
const renderingEngine = cornerstoneViewportService.getRenderingEngine();
const viewports = renderingEngine.getVolumeViewports();
return viewports.filter(vp => {
const viewportVolumeIds = vp.getActors().map(actor => actor.uid);
const viewportVolumeIds = vp.getActors().map(actor => actor.referencedId);
return (
volumeIds.length === viewportVolumeIds.length &&
volumeIds.every(volumeId => viewportVolumeIds.includes(volumeId))
@@ -38,97 +40,10 @@ const ViewportWindowLevel = ({
[cornerstoneViewportService]
);
const getNodeOpacity = (volumeActor, nodeIndex) => {
const volumeOpacity = volumeActor.getProperty().getScalarOpacity(0);
const nodeValue = [];
volumeOpacity.getNodeValue(nodeIndex, nodeValue);
return nodeValue[1];
};
/**
* Checks if the opacity applied to the PET volume is something like
* [{x: 0, y: 0}, {x: 0.1, y: [C]}, {x: [ANY], y: [C]}] where C is a
* constant opacity value for all x's greater than 0.1
*/
const isPetVolumeWithDefaultOpacity = (volumeId, volumeActor) => {
const volume = cs3DCache.getVolume(volumeId);
if (!volume) {
return false;
}
const modality = volume.metadata.Modality;
if (modality !== 'PT') {
return false;
}
const volumeOpacity = volumeActor.getProperty().getScalarOpacity(0);
// It must have at least two points (0 and 0.1)
if (volumeOpacity.getSize() < 2) {
return false;
}
const node1Value = [];
const node2Value = [];
volumeOpacity.getNodeValue(0, node1Value);
volumeOpacity.getNodeValue(1, node2Value);
// First node must be (x:0, y:0} and the second one {x:0.1, y:any}
if (node1Value[0] !== 0 || node1Value[1] !== 0 || node2Value[0] !== 0.1) {
return false;
}
const expectedOpacity = node2Value[1];
const opacitySize = volumeOpacity.getSize();
const currentNodeValue = [];
// Any point after 0.1 must have the same opacity
for (let i = 2; i < opacitySize; i++) {
volumeOpacity.getNodeValue(i, currentNodeValue);
if (currentNodeValue[1] !== expectedOpacity) {
return false;
}
}
return true;
};
/**
* Checks if the opacity function has a constance opacity value for all x's
*/
const isVolumeWithConstantOpacity = volumeActor => {
const volumeOpacity = volumeActor.getProperty().getScalarOpacity(0);
const opacitySize = volumeOpacity.getSize();
const firstNodeValue = [];
volumeOpacity.getNodeValue(0, firstNodeValue);
const firstNodeOpacity = firstNodeValue[1];
for (let i = 0; i < opacitySize; i++) {
const currentNodeValue = [];
volumeOpacity.getNodeValue(0, currentNodeValue);
if (currentNodeValue[1] !== firstNodeOpacity) {
return false;
}
}
return true;
};
const getVolumeOpacity = useCallback((viewport, volumeId) => {
const volumeActor = viewport.getActor(volumeId).actor;
const volumeActor = viewport.getActors().find(actor => actor.referencedId === volumeId)?.actor;
if (isPetVolumeWithDefaultOpacity(volumeId, volumeActor)) {
// Get the opacity from the second node at 0.1
return getNodeOpacity(volumeActor, 1);
} else if (isVolumeWithConstantOpacity(volumeActor)) {
return getNodeOpacity(volumeActor, 0);
@@ -137,85 +52,14 @@ const ViewportWindowLevel = ({
return undefined;
}, []);
const getWindowLevelsData = useCallback(
async (viewportId: number) => {
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
if (!viewport) {
return [];
}
const viewportInfo = cornerstoneViewportService.getViewportInfo(viewportId);
const volumeIds = viewport.getActors().map(actor => actor.uid);
const viewportProperties = viewport.getProperties();
const { voiRange } = viewportProperties;
const viewportVoi = voiRange
? {
windowWidth: voiRange.upper - voiRange.lower,
windowCenter: voiRange.lower + (voiRange.upper - voiRange.lower) / 2,
}
: undefined;
const windowLevels = await Promise.all(
volumeIds.map(async (volumeId, volumeIndex) => {
const volume = cs3DCache.getVolume(volumeId);
if (!volume) {
return null;
}
const opacity = getVolumeOpacity(viewport, volumeId);
const { metadata, scaling } = volume;
const modality = metadata.Modality;
// TODO: find a proper way to fix the histogram
const options = {
min: modality === 'PT' ? 0.1 : -999,
max: modality === 'PT' ? 5 : 10000,
};
const histogram =
cachedHistograms[volumeId] ??
(await getViewportVolumeHistogram(viewport, volume, options));
const { voi: displaySetVOI, colormap: displaySetColormap } =
viewportInfo.displaySetOptions[volumeIndex];
let colormap;
if (displaySetColormap) {
colormap =
csUtils.colormap.getColormap(displaySetColormap.name) ??
vtkColorMaps.getPresetByName(displaySetColormap.name);
}
const voi = !volumeIndex ? viewportVoi ?? displaySetVOI : displaySetVOI;
return {
viewportId,
modality,
volumeId,
volumeIndex,
voi,
histogram,
colormap,
step: scaling?.PT ? 0.05 : 1,
opacity,
showOpacitySlider: volumeIndex === 1 && opacity !== undefined,
};
})
);
const data = windowLevels.filter(Boolean);
return data;
},
[cachedHistograms, cornerstoneViewportService, getVolumeOpacity]
);
const updateViewportHistograms = useCallback(() => {
getWindowLevelsData(viewportId).then(windowLevels => {
setWindowLevels(windowLevels);
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
const viewportInfo = cornerstoneViewportService.getViewportInfo(viewportId);
getWindowLevelsData(viewport, viewportInfo, getVolumeOpacity).then(data => {
setWindowLevels(data);
});
}, [viewportId, getWindowLevelsData]);
}, [viewportId, cornerstoneViewportService, getVolumeOpacity]);
const handleCornerstoneVOIModified = useCallback(
e => {
@@ -252,8 +96,8 @@ const ViewportWindowLevel = ({
[windowLevels]
);
const debouncedHandleCornerstoneVOIModified = useCallback(
debounce(handleCornerstoneVOIModified, 100),
const debouncedHandleCornerstoneVOIModified = useMemo(
() => debounce(handleCornerstoneVOIModified, 100),
[handleCornerstoneVOIModified]
);
@@ -280,7 +124,7 @@ const ViewportWindowLevel = ({
return;
}
const viewportVolumeIds = viewport.getActors().map(actor => actor.uid);
const viewportVolumeIds = viewport.getActors().map(actor => actor.referencedId);
const viewports = getViewportsWithVolumeIds(viewportVolumeIds);
viewports.forEach(vp => {
@@ -291,49 +135,50 @@ const ViewportWindowLevel = ({
[getViewportsWithVolumeIds, cornerstoneViewportService]
);
// Listen to windowLevels changes and caches all the new ones
// New function to handle image volume loading completion
const handleImageVolumeLoadingCompleted = useCallback(() => {
setIsLoading(false);
updateViewportHistograms();
}, [updateViewportHistograms]);
// Listen to cornerstone events and set up interval for histogram updates
useEffect(() => {
const newVolumeHistograms = windowLevels
.filter(windowLevel => !cachedHistograms[windowLevel.volumeId] && windowLevel.histogram)
.reduce((volumeHistograms, windowLevel) => {
// If the histogram exists, add it to the volumeHistograms object
if (windowLevel?.histogram) {
volumeHistograms[windowLevel.volumeId] = windowLevel.histogram;
}
return volumeHistograms;
}, {});
if (Object.keys(newVolumeHistograms).length) {
setCachedHistograms(prev => ({ ...prev, ...newVolumeHistograms }));
}
}, [windowLevels, cachedHistograms]);
// Updates the histogram when the viewport index prop has changed
useEffect(() => updateViewportHistograms(), [viewportId, updateViewportHistograms]);
// Listen to cornerstone events on "eventTarget" and at the document level
useEffect(() => {
eventTarget.addEventListener(Events.IMAGE_VOLUME_LOADING_COMPLETED, updateViewportHistograms);
document.addEventListener(Events.VOI_MODIFIED, debouncedHandleCornerstoneVOIModified, true);
eventTarget.addEventListener(
Events.IMAGE_VOLUME_LOADING_COMPLETED,
handleImageVolumeLoadingCompleted
);
const intervalId = setInterval(() => {
if (isLoading) {
updateViewportHistograms();
}
}, 1000);
return () => {
eventTarget.removeEventListener(
Events.IMAGE_VOLUME_LOADING_COMPLETED,
updateViewportHistograms
);
document.removeEventListener(
Events.VOI_MODIFIED,
debouncedHandleCornerstoneVOIModified,
true
);
eventTarget.removeEventListener(
Events.IMAGE_VOLUME_LOADING_COMPLETED,
handleImageVolumeLoadingCompleted
);
clearInterval(intervalId);
};
}, [updateViewportHistograms, debouncedHandleCornerstoneVOIModified]);
}, [
updateViewportHistograms,
debouncedHandleCornerstoneVOIModified,
handleImageVolumeLoadingCompleted,
isLoading,
]);
// Create a memoized version of displaySet IDs for comparison
const displaySetIds = useMemo(() => {
return displaySets?.map(ds => ds.displaySetInstanceUID).sort() || [];
}, [displaySets]);
// Updates the viewport when the context of the viewport has changed. This is
// necessary when moving across different stages because the viewport index
// may not change but the volumes loaded on it may change.
useEffect(() => {
const { unsubscribe } = cornerstoneViewportService.subscribe(
cornerstoneViewportService.EVENTS.VIEWPORT_VOLUMES_CHANGED,
@@ -344,10 +189,15 @@ const ViewportWindowLevel = ({
}
);
// Only update if displaySets actually changed and are loaded
if (displaySetIds.length && !isLoading) {
updateViewportHistograms();
}
return () => {
unsubscribe();
};
}, [viewportId, cornerstoneViewportService, updateViewportHistograms]);
}, [viewportId, cornerstoneViewportService, updateViewportHistograms, displaySetIds, isLoading]);
return (
<PanelSection title="Window Level">
@@ -20,10 +20,6 @@ const workerFn = () => {
const getViewportVolumeHistogram = async (viewport, volume, options?) => {
workerManager.registerWorker('histogram-worker', workerFn, WorkerOptions);
if (!volume?.loadStatus.loaded) {
return undefined;
}
const volumeImageData = viewport.getImageData(volume.volumeId);
if (!volumeImageData) {
@@ -32,12 +28,15 @@ const getViewportVolumeHistogram = async (viewport, volume, options?) => {
let scalarData = volume.scalarData;
let prevTimePoint;
if (volume.numTimePoints > 1) {
prevTimePoint = volume.timePointIndex;
const middleTimePoint = Math.round(volume.numTimePoints / 2);
volume.timePointIndex = middleTimePoint;
scalarData = volume.getScalarData(middleTimePoint);
const targetTimePoint = volume.numTimePoints - 1; // or any other time point you need
scalarData = volume.voxelManager.getTimePointScalarData(targetTimePoint);
} else {
scalarData = volume.voxelManager.getCompleteScalarDataArray();
}
if (!scalarData?.length) {
return undefined;
}
const { dimensions, origin, direction, spacing } = volume;
@@ -50,11 +49,12 @@ const getViewportVolumeHistogram = async (viewport, volume, options?) => {
scalarData,
});
// after we calculate the range let's reset the timePointIndex
if (volume.numTimePoints > 1) {
volume.timePointIndex = prevTimePoint;
}
const { minimum: min, maximum: max } = range;
if (min === Infinity || max === -Infinity) {
return undefined;
}
const calcHistOptions = {
numBins: 256,
min: Math.max(min, options?.min ?? min),
@@ -0,0 +1,153 @@
import { cache as cs3DCache, Types } from '@cornerstonejs/core';
import vtkColorMaps from '@kitware/vtk.js/Rendering/Core/ColorTransferFunction/ColorMaps';
import { utilities as csUtils } from '@cornerstonejs/core';
import { getViewportVolumeHistogram } from './getViewportVolumeHistogram';
/**
* Gets node opacity from volume actor
*/
export const getNodeOpacity = (volumeActor, nodeIndex) => {
const volumeOpacity = volumeActor.getProperty().getScalarOpacity(0);
const nodeValue = [];
volumeOpacity.getNodeValue(nodeIndex, nodeValue);
return nodeValue[1];
};
/**
* Checks if the opacity applied to the PET volume follows a specific pattern
*/
export const isPetVolumeWithDefaultOpacity = (volumeId: string, volumeActor) => {
const volume = cs3DCache.getVolume(volumeId);
if (!volume || volume.metadata.Modality !== 'PT') {
return false;
}
const volumeOpacity = volumeActor.getProperty().getScalarOpacity(0);
if (volumeOpacity.getSize() < 2) {
return false;
}
const node1Value = [];
const node2Value = [];
volumeOpacity.getNodeValue(0, node1Value);
volumeOpacity.getNodeValue(1, node2Value);
if (node1Value[0] !== 0 || node1Value[1] !== 0 || node2Value[0] !== 0.1) {
return false;
}
const expectedOpacity = node2Value[1];
const opacitySize = volumeOpacity.getSize();
const currentNodeValue = [];
for (let i = 2; i < opacitySize; i++) {
volumeOpacity.getNodeValue(i, currentNodeValue);
if (currentNodeValue[1] !== expectedOpacity) {
return false;
}
}
return true;
};
/**
* Checks if volume has constant opacity
*/
export const isVolumeWithConstantOpacity = volumeActor => {
const volumeOpacity = volumeActor.getProperty().getScalarOpacity(0);
const opacitySize = volumeOpacity.getSize();
const firstNodeValue = [];
volumeOpacity.getNodeValue(0, firstNodeValue);
const firstNodeOpacity = firstNodeValue[1];
for (let i = 0; i < opacitySize; i++) {
const currentNodeValue = [];
volumeOpacity.getNodeValue(0, currentNodeValue);
if (currentNodeValue[1] !== firstNodeOpacity) {
return false;
}
}
return true;
};
/**
* Gets window levels data for a viewport
*/
export const getWindowLevelsData = async (
viewport: Types.IStackViewport | Types.IVolumeViewport,
viewportInfo: any,
getVolumeOpacity: (viewport: any, volumeId: string) => number | undefined
) => {
if (!viewport) {
return [];
}
const volumeIds = (viewport as Types.IBaseVolumeViewport).getAllVolumeIds();
const viewportProperties = viewport.getProperties();
const { voiRange } = viewportProperties;
const viewportVoi = voiRange
? {
windowWidth: voiRange.upper - voiRange.lower,
windowCenter: voiRange.lower + (voiRange.upper - voiRange.lower) / 2,
}
: undefined;
const windowLevels = await Promise.all(
volumeIds.map(async (volumeId, volumeIndex) => {
const volume = cs3DCache.getVolume(volumeId);
const opacity = getVolumeOpacity(viewport, volumeId);
const { metadata, scaling } = volume;
const modality = metadata.Modality;
const options = {
min: modality === 'PT' ? 0.1 : -999,
max: modality === 'PT' ? 5 : 10000,
};
const histogram = await getViewportVolumeHistogram(viewport, volume, options);
if (!histogram || histogram.range.min === histogram.range.max) {
return null;
}
if (!viewportInfo.displaySetOptions || !viewportInfo.displaySetOptions[volumeIndex]) {
return null;
}
const { voi: displaySetVOI, colormap: displaySetColormap } =
viewportInfo.displaySetOptions[volumeIndex];
let colormap;
if (displaySetColormap) {
colormap =
csUtils.colormap.getColormap(displaySetColormap.name) ??
vtkColorMaps.getPresetByName(displaySetColormap.name);
}
const voi = !volumeIndex ? (viewportVoi ?? displaySetVOI) : displaySetVOI;
return {
viewportId: viewportInfo.viewportId,
modality,
volumeId,
volumeIndex,
voi,
histogram,
colormap,
step: scaling?.PT ? 0.05 : 1,
opacity,
showOpacitySlider: volumeIndex === 1 && opacity !== undefined,
};
})
);
return windowLevels.filter(Boolean);
};
@@ -1,6 +1,6 @@
import React, { ReactElement, useCallback, useEffect, useRef, useState, useMemo } from 'react';
import { AllInOneMenu, ButtonGroup, SwitchButton } from '@ohif/ui';
import { StackViewport } from '@cornerstonejs/core';
import { StackViewport, Types } from '@cornerstonejs/core';
import { ColormapProps } from '../../types/Colormap';
export function Colormap({
@@ -54,8 +54,10 @@ export function Colormap({
return colormap;
}
const actorEntries = viewport.getActors();
const actorEntry = actorEntries.find(entry => entry.uid.includes(displaySetInstanceUID));
const { colormap } = viewport.getProperties(actorEntry.uid);
const actorEntry = actorEntries?.find(entry =>
entry.referencedId.includes(displaySetInstanceUID)
);
const { colormap } = (viewport as Types.IVolumeViewport).getProperties(actorEntry.referencedId);
if (!colormap) {
return colormaps.find(c => c.Name === 'Grayscale') || colormaps[0];
}
@@ -15,12 +15,12 @@ import { ViewportPreset } from '../../types/ViewportPresets';
import { VolumeViewport3D } from '@cornerstonejs/core';
import { utilities } from '@cornerstonejs/core';
export const nonWLModalities = ['SR', 'SEG', 'SM', 'RTSTRUCT', 'RTPLAN', 'RTDOSE'];
export type WindowLevelActionMenuProps = {
viewportId: string;
element: HTMLElement;
presets: Array<Record<string, Array<WindowLevelPreset>>>;
verticalDirection: AllInOneMenu.VerticalDirection;
horizontalDirection: AllInOneMenu.HorizontalDirection;
colorbarProperties: ColorbarProperties;
displaySets: Array<any>;
volumeRenderingPresets: Array<ViewportPreset>;
@@ -53,8 +53,6 @@ export function WindowLevelActionMenu({
const backgroundColor = viewportInfo.getViewportOptions().background;
const isLight = backgroundColor ? utilities.isEqual(backgroundColor, [1, 1, 1]) : false;
const nonImageModalities = ['SR', 'SEG', 'SM', 'RTSTRUCT', 'RTPLAN', 'RTDOSE'];
const { t } = useTranslation('WindowLevelActionMenu');
const [viewportGrid] = useViewportGrid();
@@ -119,7 +117,7 @@ export function WindowLevelActionMenu({
horizontalDirection={horizontalDirection}
iconClassName={classNames(
// Visible on hover and for the active viewport
activeViewportId === viewportId ? 'visible' : 'invisible group-hover:visible',
activeViewportId === viewportId ? 'visible' : 'invisible group-hover/pane:visible',
'flex shrink-0 cursor-pointer rounded active:text-white text-primary-light',
isLight ? ' hover:bg-secondary-dark' : 'hover:bg-secondary-light/60'
)}
@@ -133,7 +131,7 @@ export function WindowLevelActionMenu({
{!is3DVolume && (
<Colorbar
viewportId={viewportId}
displaySets={displaySets.filter(ds => !nonImageModalities.includes(ds.Modality))}
displaySets={displaySets.filter(ds => !nonWLModalities.includes(ds.Modality))}
commandsManager={commandsManager}
servicesManager={servicesManager}
colorbarProperties={colorbarProperties}
@@ -149,7 +147,7 @@ export function WindowLevelActionMenu({
<Colormap
colormaps={colormaps}
viewportId={viewportId}
displaySets={displaySets.filter(ds => !nonImageModalities.includes(ds.Modality))}
displaySets={displaySets.filter(ds => !nonWLModalities.includes(ds.Modality))}
commandsManager={commandsManager}
servicesManager={servicesManager}
/>
@@ -1,5 +1,5 @@
import React, { ReactNode } from 'react';
import { WindowLevelActionMenu } from './WindowLevelActionMenu';
import { nonWLModalities, WindowLevelActionMenu } from './WindowLevelActionMenu';
export function getWindowLevelActionMenu({
viewportId,
@@ -9,7 +9,11 @@ export function getWindowLevelActionMenu({
commandsManager,
verticalDirection,
horizontalDirection,
}: withAppTypes): ReactNode {
}: withAppTypes<{
viewportId: string;
element: HTMLElement;
displaySets: AppTypes.DisplaySet[];
}>): ReactNode {
const { customizationService } = servicesManager.services;
const { presets } = customizationService.get('cornerstone.windowLevelPresets');
@@ -24,6 +28,14 @@ export function getWindowLevelActionMenu({
return { [displaySet.Modality]: presets[displaySet.Modality] };
});
const modalities = displaySets
.map(displaySet => displaySet.Modality)
.filter(modality => !nonWLModalities.includes(modality));
if (modalities.length === 0) {
return null;
}
return (
<WindowLevelActionMenu
viewportId={viewportId}
@@ -35,8 +35,8 @@ export const ViewportActionCornersContext = createContext(DEFAULT_STATE);
export function ViewportActionCornersProvider({ children, service }) {
const viewportActionCornersReducer = (state, action) => {
switch (action.type) {
case 'SET_ACTION_COMPONENT': {
const { viewportId, id, component, location, indexPriority = 0 } = action.payload;
case 'ADD_ACTION_COMPONENT': {
const { viewportId, id, component, location, indexPriority } = action.payload;
// Get the components at the specified location of the specified viewport.
let locationComponents = state?.[viewportId]?.[location]
? [...state[viewportId][location]]
@@ -56,27 +56,48 @@ export function ViewportActionCornersProvider({ children, service }) {
// Insert the component from the payload but
// do not insert an undefined or null component.
if (component) {
const insertionIndex = locationComponents.findIndex(
component => indexPriority <= component.indexPriority
);
let insertionIndex;
const isRightSide =
location === ViewportActionCornersLocations.topRight ||
location === ViewportActionCornersLocations.bottomRight;
if (indexPriority === undefined) {
// If no indexPriority is provided, add it to the appropriate end
insertionIndex = isRightSide ? 0 : locationComponents.length;
} else {
if (isRightSide) {
insertionIndex = locationComponents.findIndex(
component => indexPriority > component.indexPriority
);
} else {
insertionIndex = locationComponents.findIndex(
component => indexPriority <= component.indexPriority
);
}
if (insertionIndex === -1) {
// If no suitable position found, add to the appropriate end
insertionIndex = isRightSide ? 0 : locationComponents.length;
}
}
const defaultPriority = isRightSide ? Number.MIN_SAFE_INTEGER : Number.MAX_SAFE_INTEGER;
locationComponents = [
...locationComponents.slice(0, insertionIndex),
{
id,
component,
indexPriority,
indexPriority: indexPriority ?? defaultPriority,
},
...locationComponents.slice(insertionIndex + 1),
...locationComponents.slice(insertionIndex),
];
}
return {
...state,
...{
[viewportId]: {
...state[viewportId],
[location]: locationComponents,
},
[viewportId]: {
...state[viewportId],
[location]: locationComponents,
},
};
}
@@ -100,17 +121,17 @@ export function ViewportActionCornersProvider({ children, service }) {
return viewportActionCornersState;
}, [viewportActionCornersState]);
const setComponent = useCallback(
const addComponent = useCallback(
(actionComponentInfo: ActionComponentInfo) => {
dispatch({ type: 'SET_ACTION_COMPONENT', payload: actionComponentInfo });
dispatch({ type: 'ADD_ACTION_COMPONENT', payload: actionComponentInfo });
},
[dispatch]
);
const setComponents = useCallback(
const addComponents = useCallback(
(actionComponentInfos: Array<ActionComponentInfo>) => {
actionComponentInfos.forEach(actionComponentInfo =>
dispatch({ type: 'SET_ACTION_COMPONENT', payload: actionComponentInfo })
dispatch({ type: 'ADD_ACTION_COMPONENT', payload: actionComponentInfo })
);
},
[dispatch]
@@ -120,28 +141,28 @@ export function ViewportActionCornersProvider({ children, service }) {
(viewportId: string) => dispatch({ type: 'CLEAR_ACTION_COMPONENTS', payload: viewportId }),
[dispatch]
);
useEffect(() => {
if (service) {
service.setServiceImplementation({
getState,
setComponent,
setComponents,
addComponent,
addComponents,
clear,
});
}
}, [getState, service, setComponent, setComponents]);
}, [getState, service, addComponent, addComponents, clear]);
// run many of the calls through the service itself since we want to publish events
const api = {
const viewportCornerActions = {
getState,
setComponent: props => service.setComponent(props),
setComponents: props => service.setComponents(props),
addComponent: props => service.addComponent(props),
addComponents: props => service.addComponents(props),
clear: props => service.clear(props),
};
const contextValue = useMemo(
() => [viewportActionCornersState, api],
[viewportActionCornersState, api]
() => [viewportActionCornersState, viewportCornerActions],
[viewportActionCornersState, viewportCornerActions]
);
return (
@@ -1,6 +1,5 @@
import { Enums } from '@cornerstonejs/tools';
import { toolNames } from './initCornerstoneTools';
import DicomUpload from './components/DicomUpload/DicomUpload';
import defaultWindowLevelPresets from './components/WindowLevelActionMenu/defaultWindowLevelPresets';
import { colormaps } from './utils/colormaps';
import { CONSTANTS } from '@cornerstonejs/core';
@@ -23,10 +22,12 @@ const tools = {
toolName: toolNames.Zoom,
bindings: [{ mouseButton: Enums.MouseBindings.Secondary }],
},
{ toolName: toolNames.StackScrollMouseWheel, bindings: [] },
{
toolName: toolNames.StackScroll,
bindings: [{ mouseButton: Enums.MouseBindings.Wheel }],
},
],
enabled: [
{ toolName: toolNames.SegmentationDisplay },
{
toolName: toolNames.PlanarFreehandContourSegmentation,
configuration: {
@@ -38,13 +39,6 @@ const tools = {
function getCustomizationModule() {
return [
{
name: 'cornerstoneDicomUploadComponent',
value: {
id: 'dicomUploadComponent',
component: DicomUpload,
},
},
{
name: 'default',
value: [
@@ -120,7 +114,7 @@ function getCustomizationModule() {
type: 'value',
},
{
value: 'areaUnit',
value: 'areaUnits',
for: ['area'],
type: 'unit',
},
@@ -140,7 +134,7 @@ function getCustomizationModule() {
},
{
displayName: 'Unit',
value: 'areaUnit',
value: 'areaUnits',
type: 'value',
},
],
@@ -170,12 +164,12 @@ function getCustomizationModule() {
type: 'value',
},
{
value: 'modalityUnit',
value: 'pixelValueUnits',
for: ['mean', 'max' /** 'stdDev **/],
type: 'unit',
},
{
value: 'areaUnit',
value: 'areaUnits',
for: ['area'],
type: 'unit',
},
@@ -1,15 +1,11 @@
import React from 'react';
import { useAppConfig } from '@state';
import { Toolbox } from '@ohif/ui-next';
import PanelSegmentation from './panels/PanelSegmentation';
import ActiveViewportWindowLevel from './components/ActiveViewportWindowLevel';
const getPanelModule = ({ commandsManager, servicesManager, extensionManager }: withAppTypes) => {
const { customizationService } = servicesManager.services;
const wrappedPanelSegmentation = ({ configuration }) => {
const [appConfig] = useAppConfig();
return (
<PanelSegmentation
commandsManager={commandsManager}
@@ -17,16 +13,25 @@ const getPanelModule = ({ commandsManager, servicesManager, extensionManager }:
extensionManager={extensionManager}
configuration={{
...configuration,
disableEditing: appConfig.disableEditing,
...customizationService.get('segmentation.panel'),
}}
/>
);
};
const wrappedPanelSegmentationNoHeader = ({ configuration }) => {
return (
<PanelSegmentation
commandsManager={commandsManager}
servicesManager={servicesManager}
extensionManager={extensionManager}
configuration={{
...configuration,
}}
/>
);
};
const wrappedPanelSegmentationWithTools = ({ configuration }) => {
const [appConfig] = useAppConfig();
return (
<>
<Toolbox
@@ -45,8 +50,6 @@ const getPanelModule = ({ commandsManager, servicesManager, extensionManager }:
extensionManager={extensionManager}
configuration={{
...configuration,
disableEditing: appConfig.disableEditing,
...customizationService.get('segmentation.panel'),
}}
/>
</>
@@ -54,6 +57,12 @@ const getPanelModule = ({ commandsManager, servicesManager, extensionManager }:
};
return [
{
name: 'activeViewportWindowLevel',
component: () => {
return <ActiveViewportWindowLevel servicesManager={servicesManager} />;
},
},
{
name: 'panelSegmentation',
iconName: 'tab-segmentation',
@@ -61,6 +70,13 @@ const getPanelModule = ({ commandsManager, servicesManager, extensionManager }:
label: 'Segmentation',
component: wrappedPanelSegmentation,
},
{
name: 'panelSegmentationNoHeader',
iconName: 'tab-segmentation',
iconLabel: 'Segmentation',
label: 'Segmentation',
component: wrappedPanelSegmentationNoHeader,
},
{
name: 'panelSegmentationWithTools',
iconName: 'tab-segmentation',
@@ -89,7 +89,7 @@ function _getDisplaySetsFromSeries(instances, servicesManager, extensionManager)
const displaySet = {
plugin: 'microscopy',
Modality: 'SM',
viewportType: csEnums.ViewportType.WholeSlide,
viewportType: csEnums.ViewportType.WHOLE_SLIDE,
altImageText: 'Microscopy',
displaySetInstanceUID: utils.guid(),
SOPInstanceUID,
+55 -49
View File
@@ -6,6 +6,12 @@ const getToggledClassName = (isToggled: boolean) => {
: '!text-common-bright hover:!bg-primary-dark hover:text-primary-light';
};
const getDisabledState = (disabledText?: string) => ({
disabled: true,
className: '!text-common-bright ohif-disabled',
disabledText: disabledText ?? 'Not available on the current viewport',
});
export default function getToolbarModule({ commandsManager, servicesManager }: withAppTypes) {
const {
toolGroupService,
@@ -20,6 +26,52 @@ export default function getToolbarModule({ commandsManager, servicesManager }: w
return [
// functions/helpers to be used by the toolbar buttons to decide if they should
// enabled or not
{
name: 'evaluate.viewport.supported',
evaluate: ({ viewportId, unsupportedViewportTypes, disabledText }) => {
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
if (viewport && unsupportedViewportTypes?.includes(viewport.type)) {
return getDisabledState(disabledText);
}
return undefined;
},
},
{
name: 'evaluate.modality.supported',
evaluate: ({ viewportId, unsupportedModalities, supportedModalities, disabledText }) => {
const displaySetUIDs = viewportGridService.getDisplaySetsUIDsForViewport(viewportId);
if (!displaySetUIDs?.length) {
return;
}
const displaySets = displaySetUIDs.map(displaySetService.getDisplaySetByUID);
// Check for unsupported modalities (exclusion)
if (unsupportedModalities?.length) {
const hasUnsupportedModality = displaySets.some(displaySet =>
unsupportedModalities.includes(displaySet?.Modality)
);
if (hasUnsupportedModality) {
return getDisabledState(disabledText);
}
}
// Check for supported modalities (inclusion)
if (supportedModalities?.length) {
const hasAnySupportedModality = displaySets.some(displaySet =>
supportedModalities.includes(displaySet?.Modality)
);
if (!hasAnySupportedModality) {
return getDisabledState(disabledText || 'Tool not available for this modality');
}
}
},
},
{
name: 'evaluate.cornerstoneTool',
evaluate: ({ viewportId, button, toolNames, disabledText }) => {
@@ -32,11 +84,7 @@ export default function getToolbarModule({ commandsManager, servicesManager }: w
const toolName = toolbarService.getToolNameForButton(button);
if (!toolGroup || (!toolGroup.hasTool(toolName) && !toolNames)) {
return {
disabled: true,
className: '!text-common-bright ohif-disabled',
disabledText: disabledText ?? 'Not available on the current viewport',
};
return getDisabledState(disabledText);
}
const isPrimaryActive = toolNames
@@ -172,40 +220,6 @@ export default function getToolbarModule({ commandsManager, servicesManager }: w
};
},
},
{
name: 'evaluate.not3D',
evaluate: ({ viewportId, disabledText }) => {
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
if (viewport?.type === 'volume3d') {
return {
disabled: true,
className: '!text-common-bright ohif-disabled',
disabledText: disabledText ?? 'Not available on the current viewport',
};
}
},
},
{
name: 'evaluate.isUS',
evaluate: ({ viewportId, disabledText }) => {
const displaySetUIDs = viewportGridService.getDisplaySetsUIDsForViewport(viewportId);
if (!displaySetUIDs?.length) {
return;
}
const displaySets = displaySetUIDs.map(displaySetService.getDisplaySetByUID);
const isUS = displaySets.some(displaySet => displaySet?.Modality === 'US');
if (!isUS) {
return {
disabled: true,
className: '!text-common-bright ohif-disabled',
disabledText: disabledText ?? 'Not available on the current viewport',
};
}
},
},
{
name: 'evaluate.viewportProperties.toggle',
evaluate: ({ viewportId, button }) => {
@@ -254,11 +268,7 @@ export default function getToolbarModule({ commandsManager, servicesManager }: w
});
if (!areReconstructable) {
return {
disabled: true,
className: '!text-common-bright ohif-disabled',
disabledText: disabledText ?? 'Not available on the current viewport',
};
return getDisabledState(disabledText);
}
const isMpr = protocol?.id === 'mpr';
@@ -288,11 +298,7 @@ function _evaluateToggle({
const toolName = toolbarService.getToolNameForButton(button);
if (!toolGroup.hasTool(toolName)) {
return {
disabled: true,
className: '!text-common-bright ohif-disabled',
disabledText: disabledText ?? 'Not available on the current viewport',
};
return getDisabledState(disabledText);
}
const isOff = offModes.includes(toolGroup.getToolOptions(toolName).mode);
@@ -0,0 +1,219 @@
import { useState, useEffect } from 'react';
import debounce from 'lodash.debounce';
import { roundNumber } from '@ohif/core/src/utils';
import {
SegmentationData,
SegmentationRepresentation,
} from '../services/SegmentationService/SegmentationService';
const excludedModalities = ['SM', 'OT', 'DOC', 'ECG'];
function mapSegmentationToDisplay(segmentation, customizationService) {
const { label, segments } = segmentation;
// Get the readable text mapping once
const { readableText: readableTextMap } = customizationService.getCustomization(
'PanelSegmentation.readableText',
{}
);
// Helper function to recursively map cachedStats to readable display text
function mapStatsToDisplay(stats, indent = 0) {
const primary = [];
const indentation = ' '.repeat(indent);
for (const key in stats) {
if (Object.prototype.hasOwnProperty.call(stats, key)) {
const value = stats[key];
const readableText = readableTextMap?.[key];
if (!readableText) {
continue;
}
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
// Add empty row before category (except for the first category)
if (primary.length > 0) {
primary.push('');
}
// Add category title
primary.push(`${indentation}${readableText}`);
// Recursively handle nested objects
primary.push(...mapStatsToDisplay(value, indent + 1));
} else {
// For non-nested values, don't add empty rows
primary.push(`${indentation}${readableText}: ${roundNumber(value, 2)}`);
}
}
}
return primary;
}
// Get customization for display text mapping
const displayTextMapper = segment => {
const defaultDisplay = {
primary: [],
secondary: [],
};
// If the segment has cachedStats, map it to readable text
if (segment.cachedStats) {
const primary = mapStatsToDisplay(segment.cachedStats);
defaultDisplay.primary = primary;
}
return defaultDisplay;
};
const updatedSegments = {};
Object.entries(segments).forEach(([segmentIndex, segment]) => {
updatedSegments[segmentIndex] = {
...segment,
displayText: displayTextMapper(segment),
};
});
// Map the segments and apply the display text mapper
return {
...segmentation,
label,
segments: updatedSegments,
};
}
/**
* Represents the combination of segmentation data and its representation in a viewport.
*/
type ViewportSegmentationRepresentation = {
segmentationsWithRepresentations: {
representation: SegmentationRepresentation;
segmentation: SegmentationData;
}[];
disabled: boolean;
};
/**
* Custom hook that provides segmentation data and their representations for the active viewport.
* @param options - The options object.
* @param options.servicesManager - The services manager object.
* @param options.subscribeToDataModified - Whether to subscribe to segmentation data modifications.
* @param options.debounceTime - Debounce time in milliseconds for updates.
* @returns An array of segmentation data and their representations for the active viewport.
*/
export function useActiveViewportSegmentationRepresentations({
servicesManager,
subscribeToDataModified = false,
debounceTime = 0,
}: withAppTypes<{ debounceTime?: number }>): ViewportSegmentationRepresentation {
const { segmentationService, viewportGridService, customizationService, displaySetService } =
servicesManager.services;
const [segmentationsWithRepresentations, setSegmentationsWithRepresentations] =
useState<ViewportSegmentationRepresentation>({
segmentationsWithRepresentations: [],
disabled: false,
});
useEffect(() => {
const update = () => {
const viewportId = viewportGridService.getActiveViewportId();
const displaySetUIDs = viewportGridService.getDisplaySetsUIDsForViewport(viewportId);
if (!displaySetUIDs?.length) {
return;
}
const displaySet = displaySetService.getDisplaySetByUID(displaySetUIDs[0]);
if (!displaySet) {
return;
}
if (excludedModalities.includes(displaySet.Modality)) {
setSegmentationsWithRepresentations(prev => ({
segmentationsWithRepresentations: [],
disabled: true,
}));
return;
}
const segmentations = segmentationService.getSegmentations();
if (!segmentations?.length) {
setSegmentationsWithRepresentations(prev => ({
segmentationsWithRepresentations: [],
disabled: false,
}));
return;
}
const representations = segmentationService.getSegmentationRepresentations(viewportId);
const newSegmentationsWithRepresentations = representations.map(representation => {
const segmentation = segmentationService.getSegmentation(representation.segmentationId);
const mappedSegmentation = mapSegmentationToDisplay(segmentation, customizationService);
return {
representation,
segmentation: mappedSegmentation,
};
});
setSegmentationsWithRepresentations({
segmentationsWithRepresentations: newSegmentationsWithRepresentations,
disabled: false,
});
};
const debouncedUpdate =
debounceTime > 0 ? debounce(update, debounceTime, { leading: true, trailing: true }) : update;
update();
const subscriptions = [
segmentationService.subscribe(
segmentationService.EVENTS.SEGMENTATION_MODIFIED,
debouncedUpdate
),
segmentationService.subscribe(
segmentationService.EVENTS.SEGMENTATION_REMOVED,
debouncedUpdate
),
segmentationService.subscribe(
segmentationService.EVENTS.SEGMENTATION_REPRESENTATION_MODIFIED,
debouncedUpdate
),
viewportGridService.subscribe(
viewportGridService.EVENTS.ACTIVE_VIEWPORT_ID_CHANGED,
debouncedUpdate
),
viewportGridService.subscribe(viewportGridService.EVENTS.GRID_STATE_CHANGED, debouncedUpdate),
];
if (subscribeToDataModified) {
subscriptions.push(
segmentationService.subscribe(
segmentationService.EVENTS.SEGMENTATION_DATA_MODIFIED,
debouncedUpdate
)
);
}
return () => {
subscriptions.forEach(subscription => subscription.unsubscribe());
if (debounceTime > 0) {
debouncedUpdate.cancel();
}
};
}, [
segmentationService,
viewportGridService,
customizationService,
displaySetService,
debounceTime,
subscribeToDataModified,
]);
return segmentationsWithRepresentations;
}
@@ -0,0 +1,103 @@
import { useState, useEffect } from 'react';
import debounce from 'lodash.debounce';
function mapMeasurementToDisplay(measurement, displaySetService) {
const { referenceSeriesUID } = measurement;
const displaySets = displaySetService.getDisplaySetsForSeries(referenceSeriesUID);
if (!displaySets[0]?.instances) {
throw new Error('The tracked measurements panel should only be tracking "stack" displaySets.');
}
const { findingSites, finding, label: baseLabel, displayText: baseDisplayText } = measurement;
const firstSite = findingSites?.[0];
const label = baseLabel || finding?.text || firstSite?.text || '(empty)';
// Initialize displayText with the structure used in Length.ts and CobbAngle.ts
const displayText = {
primary: [],
secondary: baseDisplayText?.secondary || [],
};
// Add baseDisplayText to primary if it exists
if (baseDisplayText) {
displayText.primary.push(...baseDisplayText.primary);
}
// Add finding sites to primary
if (findingSites) {
findingSites.forEach(site => {
if (site?.text && site.text !== label) {
displayText.primary.push(site.text);
}
});
}
// Add finding to primary if it's different from the label
if (finding && finding.text && finding.text !== label) {
displayText.primary.push(finding.text);
}
return {
...measurement,
displayText,
label,
};
}
/**
* A custom hook that provides mapped measurements based on the given services and filters.
*
* @param {Object} servicesManager - The services manager object.
* @param {Object} options - The options for filtering and mapping measurements.
* @param {Function} options.measurementFilter - Optional function to filter measurements.
* @param {Object} options.valueTypes - The value types for mapping measurements.
* @returns {Array} An array of mapped and filtered measurements.
*/
export function useMeasurements(servicesManager, { measurementFilter }) {
const { measurementService, displaySetService } = servicesManager.services;
const [displayMeasurements, setDisplayMeasurements] = useState([]);
useEffect(() => {
const updateDisplayMeasurements = () => {
let measurements = measurementService.getMeasurements();
if (measurementFilter) {
measurements = measurements.filter(measurementFilter);
}
const mappedMeasurements = measurements.map(m =>
mapMeasurementToDisplay(m, displaySetService)
);
setDisplayMeasurements(prevMeasurements => {
if (JSON.stringify(prevMeasurements) !== JSON.stringify(mappedMeasurements)) {
return mappedMeasurements;
}
return prevMeasurements;
});
};
const debouncedUpdate = debounce(updateDisplayMeasurements, 100);
updateDisplayMeasurements();
const events = [
measurementService.EVENTS.MEASUREMENT_ADDED,
measurementService.EVENTS.RAW_MEASUREMENT_ADDED,
measurementService.EVENTS.MEASUREMENT_UPDATED,
measurementService.EVENTS.MEASUREMENT_REMOVED,
measurementService.EVENTS.MEASUREMENTS_CLEARED,
];
const subscriptions = events.map(
evt => measurementService.subscribe(evt, debouncedUpdate).unsubscribe
);
return () => {
subscriptions.forEach(unsub => unsub());
debouncedUpdate.cancel();
};
}, [measurementService, measurementFilter, displaySetService]);
return displayMeasurements;
}
@@ -0,0 +1,148 @@
import { useState, useEffect } from 'react';
import debounce from 'lodash.debounce';
import { roundNumber } from '@ohif/core/src/utils';
import { SegmentationData } from '../services/SegmentationService/SegmentationService';
function mapSegmentationToDisplay(segmentation, customizationService) {
const { label, segments } = segmentation;
// Get the readable text mapping once
const { readableText: readableTextMap } = customizationService.getCustomization(
'PanelSegmentation.readableText',
{}
);
// Helper function to recursively map cachedStats to readable display text
function mapStatsToDisplay(stats, indent = 0) {
const primary = [];
const indentation = ' '.repeat(indent);
for (const key in stats) {
if (Object.prototype.hasOwnProperty.call(stats, key)) {
const value = stats[key];
const readableText = readableTextMap?.[key];
if (!readableText) {
continue;
}
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
// Add empty row before category (except for the first category)
if (primary.length > 0) {
primary.push('');
}
// Add category title
primary.push(`${indentation}${readableText}`);
// Recursively handle nested objects
primary.push(...mapStatsToDisplay(value, indent + 1));
} else {
// For non-nested values, don't add empty rows
primary.push(`${indentation}${readableText}: ${roundNumber(value, 2)}`);
}
}
}
return primary;
}
// Get customization for display text mapping
const displayTextMapper = segment => {
const defaultDisplay = {
primary: [],
secondary: [],
};
// If the segment has cachedStats, map it to readable text
if (segment.cachedStats) {
const primary = mapStatsToDisplay(segment.cachedStats);
defaultDisplay.primary = primary;
}
return defaultDisplay;
};
const updatedSegments = {};
Object.entries(segments).forEach(([segmentIndex, segment]) => {
updatedSegments[segmentIndex] = {
...segment,
displayText: displayTextMapper(segment),
};
});
// Map the segments and apply the display text mapper
return {
...segmentation,
label,
segments: updatedSegments,
};
}
/**
* Custom hook that provides segmentation data.
* @param options - The options object.
* @param options.servicesManager - The services manager object.
* @param options.subscribeToDataModified - Whether to subscribe to segmentation data modifications.
* @param options.debounceTime - Debounce time in milliseconds for updates.
* @returns An array of segmentation data.
*/
export function useSegmentations({
servicesManager,
subscribeToDataModified = false,
debounceTime = 0,
}: withAppTypes<{ debounceTime?: number }>): SegmentationData[] {
const { segmentationService, customizationService } = servicesManager.services;
const [segmentations, setSegmentations] = useState<SegmentationData[]>([]);
useEffect(() => {
const update = () => {
const segmentations = segmentationService.getSegmentations();
if (!segmentations?.length) {
setSegmentations([]);
return;
}
const mappedSegmentations = segmentations.map(segmentation =>
mapSegmentationToDisplay(segmentation, customizationService)
);
setSegmentations(mappedSegmentations);
};
const debouncedUpdate =
debounceTime > 0 ? debounce(update, debounceTime, { leading: true, trailing: true }) : update;
update();
const subscriptions = [
segmentationService.subscribe(
segmentationService.EVENTS.SEGMENTATION_MODIFIED,
debouncedUpdate
),
segmentationService.subscribe(
segmentationService.EVENTS.SEGMENTATION_REMOVED,
debouncedUpdate
),
];
if (subscribeToDataModified) {
subscriptions.push(
segmentationService.subscribe(
segmentationService.EVENTS.SEGMENTATION_DATA_MODIFIED,
debouncedUpdate
)
);
}
return () => {
subscriptions.forEach(subscription => subscription.unsubscribe());
if (debounceTime > 0) {
debouncedUpdate.cancel();
}
};
}, [segmentationService, customizationService, debounceTime, subscribeToDataModified]);
return segmentations;
}
+23 -34
View File
@@ -1,5 +1,25 @@
import { Types } from '@ohif/core';
const VOI_SYNC_GROUP = {
type: 'voi',
id: 'mpr',
source: true,
target: true,
options: {
syncColormap: true,
},
};
const HYDRATE_SEG_SYNC_GROUP = {
type: 'hydrateseg',
id: 'sameFORId',
source: true,
target: true,
options: {
matchingRules: ['sameFOR'],
},
};
export const mpr: Types.HangingProtocol.Protocol = {
id: 'mpr',
name: 'MPR',
@@ -10,7 +30,6 @@ export const mpr: Types.HangingProtocol.Protocol = {
modifiedDate: '2023-08-15',
availableTo: {},
editableBy: {},
// Unknown number of priors referenced - so just match any study
numberOfPriorsReferenced: 0,
protocolMatchingRules: [],
imageLoadStrategy: 'nth',
@@ -71,17 +90,7 @@ export const mpr: Types.HangingProtocol.Protocol = {
initialImageOptions: {
preset: 'middle',
},
syncGroups: [
{
type: 'voi',
id: 'mpr',
source: true,
target: true,
options: {
syncColormap: true,
},
},
],
syncGroups: [VOI_SYNC_GROUP, HYDRATE_SEG_SYNC_GROUP],
},
displaySets: [
{
@@ -98,17 +107,7 @@ export const mpr: Types.HangingProtocol.Protocol = {
initialImageOptions: {
preset: 'middle',
},
syncGroups: [
{
type: 'voi',
id: 'mpr',
source: true,
target: true,
options: {
syncColormap: true,
},
},
],
syncGroups: [VOI_SYNC_GROUP, HYDRATE_SEG_SYNC_GROUP],
},
displaySets: [
{
@@ -125,17 +124,7 @@ export const mpr: Types.HangingProtocol.Protocol = {
initialImageOptions: {
preset: 'middle',
},
syncGroups: [
{
type: 'voi',
id: 'mpr',
source: true,
target: true,
options: {
syncColormap: true,
},
},
],
syncGroups: [VOI_SYNC_GROUP, HYDRATE_SEG_SYNC_GROUP],
},
displaySets: [
{
+39 -21
View File
@@ -6,7 +6,6 @@ import {
imageLoadPoolManager,
imageRetrievalPoolManager,
} from '@cornerstonejs/core';
import * as csStreamingImageVolumeLoader from '@cornerstonejs/streaming-image-volume-loader';
import { Enums as cs3DToolsEnums } from '@cornerstonejs/tools';
import { Types } from '@ohif/core';
import Enums from './enums';
@@ -33,17 +32,28 @@ import { id } from './id';
import { measurementMappingUtils } from './utils/measurementServiceMappings';
import type { PublicViewportOptions } from './services/ViewportService/Viewport';
import ImageOverlayViewerTool from './tools/ImageOverlayViewerTool';
import { showLabelAnnotationPopup } from './utils/callInputDialog';
import ViewportActionCornersService from './services/ViewportActionCornersService/ViewportActionCornersService';
import { ViewportActionCornersProvider } from './contextProviders/ViewportActionCornersProvider';
import ActiveViewportWindowLevel from './components/ActiveViewportWindowLevel';
import getSOPInstanceAttributes from './utils/measurementServiceMappings/utils/getSOPInstanceAttributes';
import { findNearbyToolData } from './utils/findNearbyToolData';
import { createFrameViewSynchronizer } from './synchronizers/frameViewSynchronizer';
import { getSopClassHandlerModule } from './getSopClassHandlerModule';
import { getDynamicVolumeInfo } from '@cornerstonejs/core/utilities';
import {
useLutPresentationStore,
usePositionPresentationStore,
useSegmentationPresentationStore,
useSynchronizersStore,
} from './stores';
import { useToggleOneUpViewportGridStore } from '../../default/src/stores/useToggleOneUpViewportGridStore';
import { useActiveViewportSegmentationRepresentations } from './hooks/useActiveViewportSegmentationRepresentations';
import { useMeasurements } from './hooks/useMeasurements';
import getPanelModule from './getPanelModule';
import PanelSegmentation from './panels/PanelSegmentation';
import PanelMeasurement from './panels/PanelMeasurement';
import DicomUpload from './components/DicomUpload/DicomUpload';
import { useSegmentations } from './hooks/useSegmentations';
const { helpers: volumeLoaderHelpers } = csStreamingImageVolumeLoader;
const { getDynamicVolumeInfo } = volumeLoaderHelpers ?? {};
const { imageRetrieveMetadataProvider } = cornerstone.utilities;
const Component = React.lazy(() => {
@@ -84,9 +94,8 @@ const cornerstoneExtension: Types.Extensions.Extension = {
]);
toolbarService.registerEventForToolbarUpdate(segmentationService, [
segmentationService.EVENTS.SEGMENTATION_ADDED,
segmentationService.EVENTS.SEGMENTATION_REMOVED,
segmentationService.EVENTS.SEGMENTATION_UPDATED,
segmentationService.EVENTS.SEGMENTATION_MODIFIED,
]);
toolbarService.registerEventForToolbarUpdate(cornerstone.eventTarget, [
@@ -108,9 +117,9 @@ const cornerstoneExtension: Types.Extensions.Extension = {
// how to define them.
imageRetrieveMetadataProvider.add('stack', stackRetrieveOptions);
},
getPanelModule,
onModeExit: ({ servicesManager }: withAppTypes): void => {
const { cineService } = servicesManager.services;
const { cineService, segmentationService } = servicesManager.services;
// Empty out the image load and retrieval pools to prevent memory leaks
// on the mode exits
Object.values(cs3DEnums.RequestType).forEach(type => {
@@ -121,6 +130,13 @@ const cornerstoneExtension: Types.Extensions.Extension = {
cineService.setIsCineEnabled(false);
enabledElementReset();
useLutPresentationStore.getState().clearLutPresentationStore();
usePositionPresentationStore.getState().clearPositionPresentationStore();
useSynchronizersStore.getState().clearSynchronizersStore();
useToggleOneUpViewportGridStore.getState().clearToggleOneUpViewportGridStore();
useSegmentationPresentationStore.getState().clearSegmentationPresentationStore();
segmentationService.removeAllSegmentations();
},
/**
@@ -145,20 +161,12 @@ const cornerstoneExtension: Types.Extensions.Extension = {
const { syncGroupService } = servicesManager.services;
syncGroupService.registerCustomSynchronizer('frameview', createFrameViewSynchronizer);
servicesManager.services.customizationService.setGlobalCustomization('dicomUploadComponent', {
component: props => <DicomUpload {...props} />,
});
return init.call(this, props);
},
getToolbarModule,
getPanelModule({ servicesManager }) {
return [
{
name: 'activeViewportWindowLevel',
component: () => {
return <ActiveViewportWindowLevel servicesManager={servicesManager} />;
},
},
];
},
getHangingProtocolModule,
getViewportModule({ servicesManager, commandsManager }) {
const ExtendedOHIFCornerstoneViewport = props => {
@@ -196,7 +204,6 @@ const cornerstoneExtension: Types.Extensions.Extension = {
},
getEnabledElement,
dicomLoaderService,
showLabelAnnotationPopup,
},
},
{
@@ -235,6 +242,17 @@ export {
ImageOverlayViewerTool,
getSOPInstanceAttributes,
dicomLoaderService,
// Export all stores
useLutPresentationStore,
usePositionPresentationStore,
useSegmentationPresentationStore,
useSynchronizersStore,
Enums,
useMeasurements,
useActiveViewportSegmentationRepresentations,
useSegmentations,
PanelSegmentation,
PanelMeasurement,
DicomUpload,
};
export default cornerstoneExtension;
+79 -88
View File
@@ -1,4 +1,4 @@
import OHIF, { Types, errorHandler } from '@ohif/core';
import OHIF, { errorHandler } from '@ohif/core';
import React from 'react';
import * as cornerstone from '@cornerstonejs/core';
@@ -13,12 +13,13 @@ import {
getEnabledElement,
Settings,
utilities as csUtilities,
Enums as csEnums,
} from '@cornerstonejs/core';
import {
cornerstoneStreamingImageVolumeLoader,
cornerstoneStreamingDynamicImageVolumeLoader,
} from '@cornerstonejs/streaming-image-volume-loader';
} from '@cornerstonejs/core/loaders';
import RequestTypes from '@cornerstonejs/core/enums/RequestType';
import initWADOImageLoader from './initWADOImageLoader';
import initCornerstoneTools from './initCornerstoneTools';
@@ -33,6 +34,10 @@ import initContextMenu from './initContextMenu';
import initDoubleClick from './initDoubleClick';
import initViewTiming from './utils/initViewTiming';
import { colormaps } from './utils/colormaps';
import { SegmentationRepresentations } from '@cornerstonejs/tools/enums';
import { useLutPresentationStore } from './stores/useLutPresentationStore';
import { usePositionPresentationStore } from './stores/usePositionPresentationStore';
import { useSegmentationPresentationStore } from './stores/useSegmentationPresentationStore';
const { registerColormap } = csUtilities.colormap;
@@ -46,27 +51,12 @@ export default async function init({
servicesManager,
commandsManager,
extensionManager,
appConfig
}: Types.Extensions.ExtensionParams): Promise<void> {
appConfig,
}: withAppTypes): Promise<void> {
// Note: this should run first before initializing the cornerstone
// DO NOT CHANGE THE ORDER
const value = appConfig.useSharedArrayBuffer;
let sharedArrayBufferDisabled = false;
if (value === 'AUTO') {
cornerstone.setUseSharedArrayBuffer(csEnums.SharedArrayBufferModes.AUTO);
} else if (value === 'FALSE' || value === false) {
cornerstone.setUseSharedArrayBuffer(csEnums.SharedArrayBufferModes.FALSE);
sharedArrayBufferDisabled = true;
} else {
cornerstone.setUseSharedArrayBuffer(csEnums.SharedArrayBufferModes.TRUE);
}
await cs3DInit({
rendering: {
preferSizeOverAccuracy: Boolean(appConfig.preferSizeOverAccuracy),
useNorm16Texture: Boolean(appConfig.useNorm16Texture),
},
peerImport: appConfig.peerImport,
});
@@ -99,63 +89,39 @@ export default async function init({
cornerstoneViewportService,
hangingProtocolService,
viewportGridService,
stateSyncService,
studyPrefetcherService,
} = servicesManager.services;
window.services = servicesManager.services;
window.extensionManager = extensionManager;
window.commandsManager = commandsManager;
if (
appConfig.showWarningMessageForCrossOrigin &&
!window.crossOriginIsolated &&
!sharedArrayBufferDisabled
) {
uiNotificationService.show({
title: 'Cross Origin Isolation',
message:
'Cross Origin Isolation is not enabled, read more about it here: https://docs.ohif.org/faq/',
type: 'warning',
});
}
if (appConfig.showCPUFallbackMessage && cornerstone.getShouldUseCPURendering()) {
_showCPURenderingModal(uiModalService, hangingProtocolService);
}
const { getPresentationId: getLutPresentationId } = useLutPresentationStore.getState();
// Stores a map from `lutPresentationId` to a Presentation object so that
// an OHIFCornerstoneViewport can be redisplayed with the same LUT
stateSyncService.register('lutPresentationStore', { clearOnModeExit: true });
const { getPresentationId: getSegmentationPresentationId } =
useSegmentationPresentationStore.getState();
// Stores synchronizers state to be restored
stateSyncService.register('synchronizersStore', { clearOnModeExit: true });
const { getPresentationId: getPositionPresentationId } = usePositionPresentationStore.getState();
// Stores a map from `positionPresentationId` to a Presentation object so that
// an OHIFCornerstoneViewport can be redisplayed with the same position
stateSyncService.register('positionPresentationStore', {
clearOnModeExit: true,
});
// register presentation id providers
viewportGridService.addPresentationIdProvider(
'positionPresentationId',
getPositionPresentationId
);
viewportGridService.addPresentationIdProvider('lutPresentationId', getLutPresentationId);
viewportGridService.addPresentationIdProvider(
'segmentationPresentationId',
getSegmentationPresentationId
);
// Stores the entire ViewportGridService getState when toggling to one up
// (e.g. via a double click) so that it can be restored when toggling back.
stateSyncService.register('toggleOneUpViewportGridStore', {
clearOnModeExit: true,
});
const labelmapRepresentation = cornerstoneTools.Enums.SegmentationRepresentations.Labelmap;
const contourRepresentation = cornerstoneTools.Enums.SegmentationRepresentations.Contour;
cornerstoneTools.segmentation.config.setGlobalRepresentationConfig(labelmapRepresentation, {
fillAlpha: 0.5,
fillAlphaInactive: 0.2,
outlineOpacity: 1,
outlineOpacityInactive: 0.65,
});
cornerstoneTools.segmentation.config.setGlobalRepresentationConfig(contourRepresentation, {
renderFill: false,
});
cornerstoneTools.segmentation.config.style.setStyle(
{ type: SegmentationRepresentations.Contour },
{
renderFill: false,
}
);
const metadataProvider = OHIF.classes.MetadataProvider;
@@ -184,9 +150,10 @@ export default async function init({
// These are set reasonably low to allow for interleaved retrieves and slower
// connections.
imageLoadPoolManager.maxNumRequests = {
interaction: appConfig?.maxNumRequests?.interaction || 10,
thumbnail: appConfig?.maxNumRequests?.thumbnail || 5,
prefetch: appConfig?.maxNumRequests?.prefetch || 5,
[RequestTypes.Interaction]: appConfig?.maxNumRequests?.interaction || 10,
[RequestTypes.Thumbnail]: appConfig?.maxNumRequests?.thumbnail || 5,
[RequestTypes.Prefetch]: appConfig?.maxNumRequests?.prefetch || 5,
[RequestTypes.Compute]: appConfig?.maxNumRequests?.compute || 10,
};
initWADOImageLoader(userAuthenticationService, appConfig, extensionManager);
@@ -201,17 +168,23 @@ export default async function init({
hangingProtocolService.subscribe(
hangingProtocolService.EVENTS.CUSTOM_IMAGE_LOAD_PERFORMED,
volumeInputArrayMap => {
const { lutPresentationStore } = useLutPresentationStore.getState();
const { segmentationPresentationStore } = useSegmentationPresentationStore.getState();
const { positionPresentationStore } = usePositionPresentationStore.getState();
for (const entry of volumeInputArrayMap.entries()) {
const [viewportId, volumeInputArray] = entry;
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
const ohifViewport = cornerstoneViewportService.getViewportInfo(viewportId);
const { lutPresentationStore, positionPresentationStore } = stateSyncService.getState();
const { presentationIds } = ohifViewport.getViewportOptions();
const presentations = {
positionPresentation: positionPresentationStore[presentationIds?.positionPresentationId],
lutPresentation: lutPresentationStore[presentationIds?.lutPresentationId],
segmentationPresentation:
segmentationPresentationStore[presentationIds?.segmentationPresentationId],
};
cornerstoneViewportService.setVolumesForViewport(viewport, volumeInputArray, presentations);
@@ -249,10 +222,6 @@ export default async function init({
handler(detail.error);
};
eventTarget.addEventListener(EVENTS.STACK_VIEWPORT_NEW_STACK, evt => {
const { element } = evt.detail;
cornerstoneTools.utilities.stackContextPrefetch.enable(element);
});
eventTarget.addEventListener(EVENTS.IMAGE_LOAD_FAILED, imageLoadFailedHandler);
eventTarget.addEventListener(EVENTS.IMAGE_LOAD_ERROR, imageLoadFailedHandler);
@@ -269,26 +238,11 @@ export default async function init({
commandsManager.runCommand('resetCrosshairs', { viewportId });
});
// eventTarget.addEventListener(EVENTS.STACK_VIEWPORT_NEW_STACK, toolbarEventListener);
initViewTiming({ element });
}
function elementDisabledHandler(evt) {
const { element } = evt.detail;
// element.removeEventListener(EVENTS.CAMERA_RESET, resetCrosshairs);
// TODO - consider removing the callback when all elements are gone
// eventTarget.removeEventListener(
// EVENTS.STACK_VIEWPORT_NEW_STACK,
// newStackCallback
// );
}
eventTarget.addEventListener(EVENTS.ELEMENT_ENABLED, elementEnabledHandler.bind(null));
eventTarget.addEventListener(EVENTS.ELEMENT_DISABLED, elementDisabledHandler.bind(null));
colormaps.forEach(registerColormap);
// Event listener
@@ -301,8 +255,45 @@ export default async function init({
type: 'error',
});
},
1000
100
);
// Call this function when initializing
initializeWebWorkerProgressHandler(servicesManager.services.uiNotificationService);
}
function initializeWebWorkerProgressHandler(uiNotificationService) {
const activeToasts = new Map();
eventTarget.addEventListener(EVENTS.WEB_WORKER_PROGRESS, ({ detail }) => {
const { progress, type, id } = detail;
const cacheKey = `${type}-${id}`;
if (progress === 0 && !activeToasts.has(cacheKey)) {
const progressPromise = new Promise((resolve, reject) => {
activeToasts.set(cacheKey, { resolve, reject });
});
uiNotificationService.show({
id: cacheKey,
title: `${type}`,
message: `${type}: ${progress}%`,
autoClose: false,
promise: progressPromise,
promiseMessages: {
loading: `Computing...`,
success: `Completed successfully`,
error: 'Web Worker failed',
},
});
} else {
if (progress === 100) {
const { resolve } = activeToasts.get(cacheKey);
resolve({ progress, type });
activeToasts.delete(cacheKey);
}
}
});
}
function CPUModal() {
@@ -1,12 +1,9 @@
import { cache } from '@cornerstonejs/core';
import { cache, Types } from '@cornerstonejs/core';
import { utilities } from '@cornerstonejs/tools';
function _getVolumesFromViewport(viewport) {
return viewport ? viewport.getActors().map(actor => cache.getVolume(actor.uid)) : [];
}
function _getVolumeFromViewport(viewport) {
const volumes = _getVolumesFromViewport(viewport).filter(volume => volume);
function _getVolumeFromViewport(viewport: Types.IBaseVolumeViewport) {
const volumeIds = viewport.getAllVolumeIds();
const volumes = volumeIds.map(id => cache.getVolume(id));
const dynamicVolume = volumes.find(volume => volume.isDynamicVolume());
return dynamicVolume ?? volumes[0];
@@ -2,9 +2,8 @@ import {
PanTool,
WindowLevelTool,
StackScrollTool,
StackScrollMouseWheelTool,
VolumeRotateTool,
ZoomTool,
VolumeRotateMouseWheelTool,
MIPJumpToClickTool,
LengthTool,
RectangleROITool,
@@ -19,7 +18,6 @@ import {
CobbAngleTool,
MagnifyTool,
CrosshairsTool,
SegmentationDisplayTool,
RectangleScissorsTool,
SphereScissorsTool,
CircleScissorsTool,
@@ -47,15 +45,15 @@ export default function initCornerstoneTools(configuration = {}) {
CrosshairsTool.isAnnotation = false;
ReferenceLinesTool.isAnnotation = false;
AdvancedMagnifyTool.isAnnotation = false;
PlanarFreehandContourSegmentationTool.isAnnotation = false;
init(configuration);
addTool(PanTool);
addTool(WindowLevelTool);
addTool(StackScrollMouseWheelTool);
addTool(StackScrollTool);
addTool(VolumeRotateTool);
addTool(ZoomTool);
addTool(ProbeTool);
addTool(VolumeRotateMouseWheelTool);
addTool(MIPJumpToClickTool);
addTool(LengthTool);
addTool(RectangleROITool);
@@ -69,7 +67,6 @@ export default function initCornerstoneTools(configuration = {}) {
addTool(CobbAngleTool);
addTool(MagnifyTool);
addTool(CrosshairsTool);
addTool(SegmentationDisplayTool);
addTool(RectangleScissorsTool);
addTool(SphereScissorsTool);
addTool(CircleScissorsTool);
@@ -108,9 +105,8 @@ const toolNames = {
ArrowAnnotate: ArrowAnnotateTool.toolName,
WindowLevel: WindowLevelTool.toolName,
StackScroll: StackScrollTool.toolName,
StackScrollMouseWheel: StackScrollMouseWheelTool.toolName,
Zoom: ZoomTool.toolName,
VolumeRotateMouseWheel: VolumeRotateMouseWheelTool.toolName,
VolumeRotate: VolumeRotateTool.toolName,
MipJumpToClick: MIPJumpToClickTool.toolName,
Length: LengthTool.toolName,
DragProbe: DragProbeTool.toolName,
@@ -124,7 +120,6 @@ const toolNames = {
CobbAngle: CobbAngleTool.toolName,
Magnify: MagnifyTool.toolName,
Crosshairs: CrosshairsTool.toolName,
SegmentationDisplay: SegmentationDisplayTool.toolName,
Brush: BrushTool.toolName,
PaintFill: PaintFillTool.toolName,
ReferenceLines: ReferenceLinesTool.toolName,
@@ -1,4 +1,4 @@
import { eventTarget } from '@cornerstonejs/core';
import { eventTarget, Types } from '@cornerstonejs/core';
import { Enums, annotation } from '@cornerstonejs/tools';
import { DicomMetadataStore } from '@ohif/core';
@@ -7,6 +7,7 @@ import { toolNames } from './initCornerstoneTools';
import { onCompletedCalibrationLine } from './tools/CalibrationLineTool';
import measurementServiceMappingsFactory from './utils/measurementServiceMappings/measurementServiceMappingsFactory';
import getSOPInstanceAttributes from './utils/measurementServiceMappings/utils/getSOPInstanceAttributes';
import { triggerAnnotationRenderForViewportIds } from '@cornerstonejs/tools/utilities';
const { CORNERSTONE_3D_TOOLS_SOURCE_NAME, CORNERSTONE_3D_TOOLS_SOURCE_VERSION } = CSExtensionEnums;
const { removeAnnotation } = annotation.state;
@@ -344,8 +345,7 @@ const connectMeasurementServiceToTools = (measurementService, cornerstoneViewpor
return;
}
const { uid, label } = measurement;
const { uid, label, isLocked, isVisible } = measurement;
const sourceAnnotation = annotation.state.getAnnotation(uid);
const { data, metadata } = sourceAnnotation;
@@ -361,7 +361,23 @@ const connectMeasurementServiceToTools = (measurementService, cornerstoneViewpor
data.text = label;
}
// Todo: trigger render for annotation
// update the isLocked state
annotation.locking.setAnnotationLocked(uid, isLocked);
// update the isVisible state
annotation.visibility.setAnnotationVisibility(uid, isVisible);
// annotation.config.style.setAnnotationStyles(uid, {
// color: `rgb(${color[0]}, ${color[1]}, ${color[2]})`,
// });
// I don't like this but will fix later
const renderingEngine =
cornerstoneViewportService.getRenderingEngine() as Types.IRenderingEngine;
// Note: We could do a better job by triggering the render on the
// viewport itself, but the removeAnnotation does not include that info...
const viewportIds = renderingEngine.getViewports().map(viewport => viewport.id);
triggerAnnotationRenderForViewportIds(viewportIds);
}
);
@@ -1,47 +1,18 @@
import * as cornerstone from '@cornerstonejs/core';
import { volumeLoader } from '@cornerstonejs/core';
import {
cornerstoneStreamingImageVolumeLoader,
cornerstoneStreamingDynamicImageVolumeLoader,
} from '@cornerstonejs/streaming-image-volume-loader';
import dicomImageLoader, { webWorkerManager } from '@cornerstonejs/dicom-image-loader';
import dicomParser from 'dicom-parser';
} from '@cornerstonejs/core/loaders';
import dicomImageLoader from '@cornerstonejs/dicom-image-loader';
import { errorHandler, utils } from '@ohif/core';
const { registerVolumeLoader } = volumeLoader;
let initialized = false;
function initWebWorkers(appConfig) {
const config = {
maxWebWorkers: Math.min(
Math.max(navigator.hardwareConcurrency - 1, 1),
appConfig.maxNumberOfWebWorkers
),
startWebWorkersOnDemand: true,
taskConfiguration: {
decodeTask: {
initializeCodecsOnStartup: false,
usePDFJS: false,
strict: false,
},
},
};
if (!initialized) {
dicomImageLoader.webWorkerManager.initialize(config);
initialized = true;
}
}
export default function initWADOImageLoader(
userAuthenticationService,
appConfig,
extensionManager
) {
dicomImageLoader.external.cornerstone = cornerstone;
dicomImageLoader.external.dicomParser = dicomParser;
registerVolumeLoader('cornerstoneStreamingImageVolume', cornerstoneStreamingImageVolumeLoader);
registerVolumeLoader(
@@ -49,17 +20,11 @@ export default function initWADOImageLoader(
cornerstoneStreamingDynamicImageVolumeLoader
);
dicomImageLoader.configure({
decodeConfig: {
// !! IMPORTANT !!
// We should set this flag to false, since, by default @cornerstonejs/dicom-image-loader
// will convert everything to integers (to be able to work with cornerstone-2d).
// Until the default is set to true (which is the case for cornerstone3D),
// we should set this flag to false.
convertFloatPixelDataToInt: false,
use16BitDataType:
Boolean(appConfig.useNorm16Texture) || Boolean(appConfig.preferSizeOverAccuracy),
},
dicomImageLoader.init({
maxWebWorkers: Math.min(
Math.max(navigator.hardwareConcurrency - 1, 1),
appConfig.maxNumberOfWebWorkers
),
beforeSend: function (xhr) {
//TODO should be removed in the future and request emitted by DicomWebDataSource
const sourceConfig = extensionManager.getActiveDataSource()?.[0].getConfig() ?? {};
@@ -84,16 +49,8 @@ export default function initWADOImageLoader(
errorHandler.getHTTPErrorHandler(error);
},
});
initWebWorkers(appConfig);
}
export function destroy() {
// Note: we don't want to call .terminate on the webWorkerManager since
// that resets the config
const webWorkers = webWorkerManager.webWorkers;
for (let i = 0; i < webWorkers.length; i++) {
webWorkers[i].worker.terminate();
}
webWorkers.length = 0;
console.debug('Destroying WADO Image Loader');
}
@@ -0,0 +1,147 @@
import React, { useEffect, useRef } from 'react';
import { useViewportGrid } from '@ohif/ui';
import { MeasurementTable } from '@ohif/ui-next';
import debounce from 'lodash.debounce';
import { useMeasurements } from '../hooks/useMeasurements';
import { showLabelAnnotationPopup, colorPickerDialog } from '@ohif/extension-default';
export default function PanelMeasurementTable({
servicesManager,
customHeader,
measurementFilter,
}: withAppTypes): React.ReactNode {
const measurementsPanelRef = useRef(null);
const [viewportGrid] = useViewportGrid();
const { measurementService, customizationService, uiDialogService } = servicesManager.services;
const displayMeasurements = useMeasurements(servicesManager, {
measurementFilter,
});
useEffect(() => {
if (displayMeasurements.length > 0) {
debounce(() => {
measurementsPanelRef.current.scrollTop = measurementsPanelRef.current.scrollHeight;
}, 300)();
}
}, [displayMeasurements.length]);
const onMeasurementItemClickHandler = (uid: string, isActive: boolean) => {
if (isActive) {
return;
}
const measurements = [...displayMeasurements];
const measurement = measurements.find(m => m.uid === uid);
measurements.forEach(m => (m.isActive = m.uid !== uid ? false : true));
measurement.isActive = true;
};
const jumpToImage = (uid: string) => {
measurementService.jumpToMeasurement(viewportGrid.activeViewportId, uid);
onMeasurementItemClickHandler(uid, true);
};
const removeMeasurement = (uid: string) => {
measurementService.remove(uid);
};
const renameMeasurement = (uid: string) => {
jumpToImage(uid);
const labelConfig = customizationService.get('measurementLabels');
const measurement = measurementService.getMeasurement(uid);
showLabelAnnotationPopup(measurement, uiDialogService, labelConfig).then(val => {
measurementService.update(
uid,
{
...val,
},
true
);
});
};
const changeColorMeasurement = (uid: string) => {
const { color } = measurementService.getMeasurement(uid);
const rgbaColor = {
r: color[0],
g: color[1],
b: color[2],
a: color[3] / 255.0,
};
colorPickerDialog(uiDialogService, rgbaColor, (newRgbaColor, actionId) => {
if (actionId === 'cancel') {
return;
}
const color = [newRgbaColor.r, newRgbaColor.g, newRgbaColor.b, newRgbaColor.a * 255.0];
// segmentationService.setSegmentColor(viewportId, segmentationId, segmentIndex, color);
});
};
const toggleLockMeasurement = (uid: string) => {
measurementService.toggleLockMeasurement(uid);
};
const toggleVisibilityMeasurement = (uid: string) => {
measurementService.toggleVisibilityMeasurement(uid);
};
const measurements = displayMeasurements.filter(
dm => dm.measurementType !== measurementService.VALUE_TYPES.POINT && dm.referencedImageId
);
const additionalFindings = displayMeasurements.filter(
dm => dm.measurementType === measurementService.VALUE_TYPES.POINT && dm.referencedImageId
);
return (
<>
<div
className="invisible-scrollbar overflow-y-auto overflow-x-hidden"
ref={measurementsPanelRef}
data-cy={'trackedMeasurements-panel'}
>
<MeasurementTable
title="Measurements"
data={measurements}
onClick={jumpToImage}
onDelete={removeMeasurement}
onToggleVisibility={toggleVisibilityMeasurement}
onToggleLocked={toggleLockMeasurement}
onRename={renameMeasurement}
// onColor={changeColorMeasurement}
>
<MeasurementTable.Header>
{customHeader && (
<>
{typeof customHeader === 'function'
? customHeader({
additionalFindings,
measurements,
})
: customHeader}
</>
)}
</MeasurementTable.Header>
<MeasurementTable.Body />
</MeasurementTable>
{additionalFindings.length > 0 && (
<MeasurementTable
data={additionalFindings}
title="Additional Findings"
onClick={jumpToImage}
onDelete={removeMeasurement}
onToggleVisibility={toggleVisibilityMeasurement}
onToggleLocked={toggleLockMeasurement}
onRename={renameMeasurement}
// onColor={changeColorMeasurement}
>
<MeasurementTable.Body />
</MeasurementTable>
)}
</div>
</>
);
}
@@ -0,0 +1,230 @@
import React from 'react';
import { SegmentationTable } from '@ohif/ui-next';
import { useActiveViewportSegmentationRepresentations } from '../hooks/useActiveViewportSegmentationRepresentations';
import { metaData } from '@cornerstonejs/core';
export default function PanelSegmentation({
servicesManager,
commandsManager,
extensionManager,
configuration,
children,
}: withAppTypes) {
const { customizationService, viewportGridService, displaySetService } = servicesManager.services;
const { segmentationsWithRepresentations, disabled } =
useActiveViewportSegmentationRepresentations({
servicesManager,
});
const handlers = {
onSegmentationAdd: async () => {
const viewportId = viewportGridService.getState().activeViewportId;
commandsManager.run('createLabelmapForViewport', { viewportId });
},
onSegmentationClick: (segmentationId: string) => {
commandsManager.run('setActiveSegmentation', { segmentationId });
},
onSegmentAdd: segmentationId => {
commandsManager.run('addSegment', { segmentationId });
},
onSegmentClick: (segmentationId, segmentIndex) => {
commandsManager.run('setActiveSegmentAndCenter', { segmentationId, segmentIndex });
},
onSegmentEdit: (segmentationId, segmentIndex) => {
commandsManager.run('editSegmentLabel', { segmentationId, segmentIndex });
},
onSegmentationEdit: segmentationId => {
commandsManager.run('editSegmentationLabel', { segmentationId });
},
onSegmentColorClick: (segmentationId, segmentIndex) => {
commandsManager.run('editSegmentColor', { segmentationId, segmentIndex });
},
onSegmentDelete: (segmentationId, segmentIndex) => {
commandsManager.run('deleteSegment', { segmentationId, segmentIndex });
},
onToggleSegmentVisibility: (segmentationId, segmentIndex, type) => {
commandsManager.run('toggleSegmentVisibility', { segmentationId, segmentIndex, type });
},
onToggleSegmentLock: (segmentationId, segmentIndex) => {
commandsManager.run('toggleSegmentLock', { segmentationId, segmentIndex });
},
onToggleSegmentationRepresentationVisibility: (segmentationId, type) => {
commandsManager.run('toggleSegmentationVisibility', { segmentationId, type });
},
onSegmentationDownload: segmentationId => {
commandsManager.run('downloadSegmentation', { segmentationId });
},
storeSegmentation: async segmentationId => {
commandsManager.run('storeSegmentation', { segmentationId });
},
onSegmentationDownloadRTSS: segmentationId => {
commandsManager.run('downloadRTSS', { segmentationId });
},
setStyle: (segmentationId, type, key, value) => {
commandsManager.run('setSegmentationStyle', { segmentationId, type, key, value });
},
toggleRenderInactiveSegmentations: () => {
commandsManager.run('toggleRenderInactiveSegmentations');
},
onSegmentationRemoveFromViewport: segmentationId => {
commandsManager.run('removeSegmentationFromViewport', { segmentationId });
},
onSegmentationDelete: segmentationId => {
commandsManager.run('deleteSegmentation', { segmentationId });
},
setFillAlpha: (type, value) => {
commandsManager.run('setFillAlpha', { type, value });
},
setOutlineWidth: (type, value) => {
commandsManager.run('setOutlineWidth', { type, value });
},
setRenderFill: (type, value) => {
commandsManager.run('setRenderFill', { type, value });
},
setRenderOutline: (type, value) => {
commandsManager.run('setRenderOutline', { type, value });
},
setFillAlphaInactive: (type, value) => {
commandsManager.run('setFillAlphaInactive', { type, value });
},
getRenderInactiveSegmentations: () => {
return commandsManager.run('getRenderInactiveSegmentations');
},
};
const { mode: SegmentationTableMode } = customizationService.getCustomization(
'PanelSegmentation.tableMode',
{
id: 'default.segmentationTable.mode',
mode: 'collapsed',
}
);
// custom onSegmentationAdd if provided
const { onSegmentationAdd } = customizationService.getCustomization(
'PanelSegmentation.onSegmentationAdd',
{
id: 'segmentation.onSegmentationAdd',
onSegmentationAdd: handlers.onSegmentationAdd,
}
);
const { disableEditing } = customizationService.getCustomization(
'PanelSegmentation.disableEditing',
{
id: 'default.disableEditing',
disableEditing: false,
}
);
const { showAddSegment } = customizationService.getCustomization(
'PanelSegmentation.showAddSegment',
{
id: 'default.showAddSegment',
showAddSegment: true,
}
);
const exportOptions = segmentationsWithRepresentations.map(({ segmentation }) => {
const { representationData, segmentationId } = segmentation;
const { Labelmap } = representationData;
const referencedImageIds = Labelmap.referencedImageIds;
const firstImageId = referencedImageIds[0];
const instance = metaData.get('instance', firstImageId);
const { SOPInstanceUID, SeriesInstanceUID } = instance;
const displaySet = displaySetService.getDisplaySetForSOPInstanceUID(
SOPInstanceUID,
SeriesInstanceUID
);
const isExportable = displaySet.isReconstructable;
return {
segmentationId,
isExportable,
};
});
return (
<>
<SegmentationTable
disabled={disabled}
data={segmentationsWithRepresentations}
mode={SegmentationTableMode}
title="Segmentations"
exportOptions={exportOptions}
disableEditing={disableEditing}
onSegmentationAdd={onSegmentationAdd}
onSegmentationClick={handlers.onSegmentationClick}
onSegmentationDelete={handlers.onSegmentationDelete}
showAddSegment={showAddSegment}
onSegmentAdd={handlers.onSegmentAdd}
onSegmentClick={handlers.onSegmentClick}
onSegmentEdit={handlers.onSegmentEdit}
onSegmentationEdit={handlers.onSegmentationEdit}
onSegmentColorClick={handlers.onSegmentColorClick}
onSegmentDelete={handlers.onSegmentDelete}
onToggleSegmentVisibility={handlers.onToggleSegmentVisibility}
onToggleSegmentLock={handlers.onToggleSegmentLock}
onToggleSegmentationRepresentationVisibility={
handlers.onToggleSegmentationRepresentationVisibility
}
onSegmentationDownload={handlers.onSegmentationDownload}
storeSegmentation={handlers.storeSegmentation}
onSegmentationDownloadRTSS={handlers.onSegmentationDownloadRTSS}
setStyle={handlers.setStyle}
toggleRenderInactiveSegmentations={handlers.toggleRenderInactiveSegmentations}
onSegmentationRemoveFromViewport={handlers.onSegmentationRemoveFromViewport}
setFillAlpha={handlers.setFillAlpha}
setOutlineWidth={handlers.setOutlineWidth}
setRenderFill={handlers.setRenderFill}
setRenderOutline={handlers.setRenderOutline}
setFillAlphaInactive={handlers.setFillAlphaInactive}
renderInactiveSegmentations={handlers.getRenderInactiveSegmentations()}
>
{children}
<SegmentationTable.Config />
<SegmentationTable.AddSegmentationRow />
{SegmentationTableMode === 'collapsed' ? (
<SegmentationTable.Collapsed>
<SegmentationTable.SelectorHeader />
<SegmentationTable.AddSegmentRow />
<SegmentationTable.Segments />
</SegmentationTable.Collapsed>
) : (
<SegmentationTable.Expanded>
<SegmentationTable.Header />
{/* <SegmentationTable.AddSegmentRow /> */}
<SegmentationTable.Segments />
</SegmentationTable.Expanded>
)}
</SegmentationTable>
</>
);
}
@@ -48,6 +48,17 @@ export default class ColorbarService extends PubSubService {
super(ColorbarService.EVENTS);
}
/**
* Gets the volume ID for a given identifier by searching through the viewport's volume IDs.
* @param viewport - The viewport instance to search volumes in
* @param searchId - The identifier to search for within volume IDs
* @returns The matching volume ID if found, null otherwise
*/
private getVolumeIdForIdentifier(viewport, searchId: string): string | null {
const volumeIds = viewport.getAllVolumeIds?.() || [];
return volumeIds.length > 0 ? volumeIds.find(id => id.includes(searchId)) || null : null;
}
/**
* Adds a colorbar to a specific viewport identified by `viewportId`, using the provided `displaySetInstanceUIDs` and `options`.
* This method sets up the colorbar, associates it with the viewport, and applies initial configurations based on the provided options.
@@ -59,8 +70,18 @@ export default class ColorbarService extends PubSubService {
public addColorbar(viewportId, displaySetInstanceUIDs, options = {} as ColorbarOptions) {
const renderingEngine = getRenderingEngine(RENDERING_ENGINE_ID);
const viewport = renderingEngine.getViewport(viewportId);
if (!viewport) {
return;
}
const { element } = viewport;
const actorEntries = viewport.getActors();
if (!actorEntries || actorEntries.length === 0) {
return;
}
const { position, width: thickness, activeColormapName, colormaps } = options;
const numContainers = displaySetInstanceUIDs.length;
@@ -74,11 +95,9 @@ export default class ColorbarService extends PubSubService {
);
displaySetInstanceUIDs.forEach((displaySetInstanceUID, index) => {
const actorEntry = actorEntries.find(entry => entry.uid.includes(displaySetInstanceUID));
const volumeId = actorEntry?.uid;
const volumeId = this.getVolumeIdForIdentifier(viewport, displaySetInstanceUID);
const properties = viewport?.getProperties(volumeId);
const colormap = properties?.colormap;
// if there's an initial colormap set, and no colormap on the viewport, set it
if (activeColormapName && !colormap) {
this.setViewportColormap(
viewportId,
@@ -196,9 +215,8 @@ export default class ColorbarService extends PubSubService {
return;
}
const setViewportProperties = (viewport, uid) => {
const actorEntry = actorEntries.find(entry => entry.uid.includes(uid));
const { actor: volumeActor, uid: volumeId } = actorEntry;
viewport.setProperties({ colormap, volumeActor }, volumeId);
const volumeId = this.getVolumeIdForIdentifier(viewport, uid);
viewport.setProperties({ colormap }, volumeId);
};
if (viewport instanceof StackViewport) {
@@ -1,5 +1,5 @@
import { Types } from '@ohif/core';
import { cache as cs3DCache, Enums, volumeLoader, utilities as utils } from '@cornerstonejs/core';
import { cache as cs3DCache, Enums, volumeLoader } from '@cornerstonejs/core';
import getCornerstoneViewportType from '../../utils/getCornerstoneViewportType';
import { StackViewportData, VolumeViewportData } from '../../types/CornerstoneCacheService';
@@ -33,27 +33,11 @@ class CornerstoneCacheService {
public async createViewportData(
displaySets: Types.DisplaySet[],
viewportOptions: Record<string, unknown>,
viewportOptions: AppTypes.ViewportGrid.GridViewportOptions,
dataSource: unknown,
initialImageIndex?: number
): Promise<StackViewportData | VolumeViewportData> {
let viewportType = viewportOptions.viewportType as string;
// Todo: Since Cornerstone 3D currently doesn't support segmentation
// on stack viewport, we should check if whether the the displaySets
// that are about to be displayed are referenced in a segmentation
// as a reference volume, if so, we should hang a volume viewport
// instead of a stack viewport
if (this._shouldRenderSegmentation(displaySets)) {
// if the viewport type is volume 3D, we should let it be as it is
// Todo: in future here we should kick start the conversion of the
// segmentation to closed surface
viewportType =
viewportType === Enums.ViewportType.VOLUME_3D ? Enums.ViewportType.VOLUME_3D : 'volume';
// update viewportOptions to reflect the new viewport type
viewportOptions.viewportType = viewportType;
}
const viewportType = viewportOptions.viewportType as string;
const cs3DViewportType = getCornerstoneViewportType(viewportType, displaySets);
let viewportData: StackViewportData | VolumeViewportData;
@@ -86,7 +70,7 @@ class CornerstoneCacheService {
}
public async invalidateViewportData(
viewportData: VolumeViewportData,
viewportData: VolumeViewportData | StackViewportData,
invalidatedDisplaySetInstanceUID: string,
dataSource,
displaySetService
@@ -176,9 +160,6 @@ class CornerstoneCacheService {
): Promise<StackViewportData> {
const { uiNotificationService } = this.servicesManager.services;
const overlayDisplaySets = displaySets.filter(ds => ds.isOverlayDisplaySet);
const nonOverlayDisplaySets = displaySets.filter(ds => !ds.isOverlayDisplaySet);
// load overlays if they are not loaded
for (const overlayDisplaySet of overlayDisplaySets) {
if (overlayDisplaySet.load && overlayDisplaySet.load instanceof Function) {
const { userAuthenticationService } = this.servicesManager.services;
@@ -187,7 +168,7 @@ class CornerstoneCacheService {
await overlayDisplaySet.load({ headers });
} catch (e) {
uiNotificationService.show({
title: 'Error loading overlayDisplaySet',
title: 'Error loading displaySet',
message: e.message,
type: 'error',
});
@@ -196,43 +177,43 @@ class CornerstoneCacheService {
}
}
const displaySet = nonOverlayDisplaySets[0];
if (displaySet.load && displaySet.load instanceof Function) {
const { userAuthenticationService } = this.servicesManager.services;
const headers = userAuthenticationService.getAuthorizationHeader();
try {
await displaySet.load({ headers });
} catch (e) {
uiNotificationService.show({
title: 'Error loading displaySet',
message: e.message,
type: 'error',
});
console.error(e);
}
}
let stackImageIds = this.stackImageIds.get(displaySet.displaySetInstanceUID);
if (!stackImageIds) {
stackImageIds = this._getCornerstoneStackImageIds(displaySet, dataSource);
this.stackImageIds.set(displaySet.displaySetInstanceUID, stackImageIds);
}
// Ensuring the first non-overlay `displaySet` is always the primary one
const StackViewportData = [displaySet, ...overlayDisplaySets].map(ds => {
const { displaySetInstanceUID, StudyInstanceUID, isCompositeStack } = ds;
const StackViewportData = [];
for (const displaySet of displaySets) {
const { displaySetInstanceUID, StudyInstanceUID, isCompositeStack } = displaySet;
return {
if (displaySet.load && displaySet.load instanceof Function) {
const { userAuthenticationService } = this.servicesManager.services;
const headers = userAuthenticationService.getAuthorizationHeader();
try {
await displaySet.load({ headers });
} catch (e) {
uiNotificationService.show({
title: 'Error loading displaySet',
message: e.message,
type: 'error',
});
console.error(e);
}
}
let stackImageIds = this.stackImageIds.get(displaySet.displaySetInstanceUID);
if (!stackImageIds) {
stackImageIds = this._getCornerstoneStackImageIds(displaySet, dataSource);
// assign imageIds to the displaySet
displaySet.imageIds = stackImageIds;
this.stackImageIds.set(displaySet.displaySetInstanceUID, stackImageIds);
}
StackViewportData.push({
StudyInstanceUID,
displaySetInstanceUID,
isCompositeStack,
imageIds: stackImageIds,
initialImageIndex,
};
});
});
}
return {
viewportType,
@@ -253,6 +234,7 @@ class CornerstoneCacheService {
for (const displaySet of displaySets) {
const { Modality } = displaySet;
const isParametricMap = Modality === 'PMAP';
const isSeg = Modality === 'SEG';
// Don't create volumes for the displaySets that have custom load
// function (e.g., SEG, RT, since they rely on the reference volumes
@@ -295,7 +277,7 @@ class CornerstoneCacheService {
// Parametric maps do not have image ids but they already have volume data
// therefore a new volume should not be created.
if (!isParametricMap && (!volumeImageIds || !volume)) {
if (!isParametricMap && !isSeg && (!volumeImageIds || !volume)) {
volumeImageIds = this._getCornerstoneVolumeImageIds(displaySet, dataSource);
volume = await volumeLoader.createAndCacheVolume(volumeId, {
@@ -303,6 +285,9 @@ class CornerstoneCacheService {
});
this.volumeImageIds.set(displaySet.displaySetInstanceUID, volumeImageIds);
// Add imageIds to the displaySet for volumes
displaySet.imageIds = volumeImageIds;
}
volumeData.push({
@@ -321,39 +306,15 @@ class CornerstoneCacheService {
};
}
private _shouldRenderSegmentation(displaySets) {
const { segmentationService, displaySetService } = this.servicesManager.services;
const viewportDisplaySetInstanceUIDs = displaySets.map(
({ displaySetInstanceUID }) => displaySetInstanceUID
);
// check inside segmentations if any of them are referencing the displaySets
// that are about to be displayed
const segmentations = segmentationService.getSegmentations();
for (const segmentation of segmentations) {
const segDisplaySetInstanceUID = segmentation.displaySetInstanceUID;
const segDisplaySet = displaySetService.getDisplaySetByUID(segDisplaySetInstanceUID);
const instance = segDisplaySet.instances?.[0] || segDisplaySet.instance;
const shouldDisplaySeg = segmentationService.shouldRenderSegmentation(
viewportDisplaySetInstanceUIDs,
instance?.FrameOfReferenceUID || segDisplaySet.FrameOfReferenceUID
);
if (shouldDisplaySeg) {
return true;
}
}
}
private _getCornerstoneStackImageIds(displaySet, dataSource): string[] {
return dataSource.getImageIdsForDisplaySet(displaySet);
}
private _getCornerstoneVolumeImageIds(displaySet, dataSource): string[] {
if (displaySet.imageIds) {
return displaySet.imageIds;
}
const stackImageIds = this._getCornerstoneStackImageIds(displaySet, dataSource);
return stackImageIds;
File diff suppressed because it is too large. Load diff
@@ -1,76 +0,0 @@
import { Enums as csToolsEnums, Types as cstTypes } from '@cornerstonejs/tools';
import { Types } from '@cornerstonejs/core';
type SegmentationConfig = cstTypes.LabelmapTypes.LabelmapConfig & {
renderInactiveSegmentations: boolean;
brushSize: number;
brushThresholdGate: number;
};
type Segment = {
// the label for the segment
label: string;
// the index of the segment in the segmentation
segmentIndex: number;
// the color of the segment
color: Types.Point3;
// the opacity of the segment
opacity: number;
// whether the segment is visible
isVisible: boolean;
// whether the segment is locked
isLocked: boolean;
// display texts
displayText?: string[];
// The name of algorithm used to generate the segment. (0062,0009)
algorithmName?: string;
// Type of algorithm used to generate the segment. (0062,0008)
algorithmType?: string;
};
type Segmentation = {
// active segment index is the index of the segment that is currently being edited.
activeSegmentIndex: number;
// colorLUTIndex is the index of the color LUT that is currently being used.
colorLUTIndex: number;
// if segmentation contains any data (often calculated from labelmap)
cachedStats: Record<string, any>;
// displaySetInstanceUID
displaySetInstanceUID: string;
// displayText is the text that is displayed on the segmentation panel (often derived from the data)
displayText?: string[];
// the id of the segmentation
id: string;
// if the segmentation is the active segmentation being used in the viewer
isActive: boolean;
// if the segmentation is visible in the viewer
isVisible: boolean;
// the frame of reference UID of the segmentation
FrameOfReferenceUID: string;
// the label of the segmentation
label: string;
// the number of segments in the segmentation
segmentCount: number;
// the array of segments with their details, [null, segment1, segment2, ...]
segments: Array<Segment>;
// the set of segments that are locked
segmentsLocked: Array<number>;
// whether the segmentation is hydrated or not (non-hydrated SEG -> temporary segmentation for display in SEG Viewport
// but hydrated SEG -> segmentation that is persisted in the store)
hydrated: boolean;
// the type of the segmentation (e.g., Labelmap etc.)
type: csToolsEnums.SegmentationRepresentations;
// the segmentation representation data
representationData: SegmentationRepresentationData;
};
type LabelmapSegmentationData = {
volumeId: string;
referencedVolumeId?: string;
};
type SegmentationRepresentationData = {
LABELMAP?: LabelmapSegmentationData;
};
export type { SegmentationConfig, Segment, Segmentation };
@@ -2,6 +2,7 @@ import { synchronizers, SynchronizerManager, Synchronizer } from '@cornerstonejs
import { getRenderingEngines, utilities } from '@cornerstonejs/core';
import { pubSubServiceInterface, Types } from '@ohif/core';
import createHydrateSegmentationSynchronizer from './createHydrateSegmentationSynchronizer';
const EVENTS = {
TOOL_GROUP_CREATED: 'event::cornerstone::syncgroupservice:toolgroupcreated',
@@ -27,6 +28,7 @@ const VOI = 'voi';
const ZOOMPAN = 'zoompan';
const STACKIMAGE = 'stackimage';
const IMAGE_SLICE = 'imageslice';
const HYDRATE_SEG = 'hydrateseg';
const asSyncGroup = (syncGroup: string | SyncGroup): SyncGroup =>
typeof syncGroup === 'string' ? { type: syncGroup } : syncGroup;
@@ -51,6 +53,7 @@ export default class SyncGroupService {
// handles both stack and volume viewports
[STACKIMAGE]: synchronizers.createImageSliceSynchronizer,
[IMAGE_SLICE]: synchronizers.createImageSliceSynchronizer,
[HYDRATE_SEG]: createHydrateSegmentationSynchronizer,
};
synchronizersByType: { [key: string]: Synchronizer[] } = {};
@@ -66,18 +69,18 @@ export default class SyncGroupService {
private _createSynchronizer(type: string, id: string, options): Synchronizer | undefined {
// Initialize if not already done
this.synchronizersByType[type] = this.synchronizersByType[type] || [];
const syncCreator = this.synchronizerCreators[type.toLowerCase()];
if (syncCreator) {
const synchronizer = syncCreator(id, options);
// Pass the servicesManager along with other parameters
const synchronizer = syncCreator(id, { ...options, servicesManager: this.servicesManager });
if (synchronizer) {
this.synchronizersByType[type].push(synchronizer);
return synchronizer;
}
} else {
console.warn(`Unknown synchronizer type: ${type}, id: ${id}`);
console.debug(`Unknown synchronizer type: ${type}, id: ${id}`);
}
}
@@ -0,0 +1,79 @@
import { Types, getEnabledElementByViewportId } from '@cornerstonejs/core';
import {
SynchronizerManager,
Synchronizer,
Enums,
Types as ToolsTypes,
} from '@cornerstonejs/tools';
const { createSynchronizer } = SynchronizerManager;
const { SEGMENTATION_REPRESENTATION_ADDED } = Enums.Events;
export default function createHydrateSegmentationSynchronizer(
synchronizerName: string,
{ servicesManager, ...options }: { servicesManager: AppTypes.ServicesManager; options }
): Synchronizer {
const stackImageSynchronizer = createSynchronizer(
synchronizerName,
SEGMENTATION_REPRESENTATION_ADDED,
(synchronizerInstance, sourceViewport, targetViewport, sourceEvent) =>
segmentationRepresentationModifiedCallback(
synchronizerInstance,
sourceViewport,
targetViewport,
sourceEvent,
{ servicesManager, options }
),
{
eventSource: 'eventTarget',
}
);
return stackImageSynchronizer;
}
const segmentationRepresentationModifiedCallback = async (
synchronizerInstance: Synchronizer,
sourceViewport: Types.IViewportId,
targetViewport: Types.IViewportId,
sourceEvent: Event,
{ servicesManager, options }: { servicesManager: AppTypes.ServicesManager; options: unknown }
) => {
const event = sourceEvent as ToolsTypes.EventTypes.SegmentationRepresentationModifiedEventType;
const { segmentationId, viewportId } = event.detail;
const { segmentationService, hangingProtocolService } = servicesManager.services;
const targetViewportId = targetViewport.viewportId;
const { viewport } = getEnabledElementByViewportId(targetViewportId);
const targetFrameOfReferenceUID = viewport.getFrameOfReferenceUID();
if (!targetFrameOfReferenceUID) {
console.debug('No frame of reference UID found for the target viewport');
return;
}
const targetViewportRepresentation = segmentationService.getSegmentationRepresentations(
targetViewportId,
{ segmentationId }
);
if (targetViewportRepresentation.length > 0) {
return;
}
// whatever type the source viewport has, we need to add that to the target viewport
const sourceViewportRepresentation = segmentationService.getSegmentationRepresentations(
sourceViewport.viewportId,
{ segmentationId }
);
const type = sourceViewportRepresentation[0].type;
await segmentationService.addSegmentationRepresentation(targetViewportId, {
segmentationId,
type,
});
};
@@ -7,7 +7,7 @@ export type ActionComponentInfo = {
id: string;
component: ReactNode;
location: ViewportActionCornersLocations;
indexPriority: number;
indexPriority?: number;
};
class ViewportActionCornersService extends PubSubService {
@@ -33,21 +33,21 @@ class ViewportActionCornersService extends PubSubService {
public setServiceImplementation({
getState: getStateImplementation,
setComponent: setComponentImplementation,
setComponents: setComponentsImplementation,
clear: clearImplementation,
addComponent: addComponentImplementation,
addComponents: addComponentsImplementation,
clear: clearComponentsImplementation,
}): void {
if (getStateImplementation) {
this.serviceImplementation._getState = getStateImplementation;
}
if (setComponentImplementation) {
this.serviceImplementation._setComponent = setComponentImplementation;
if (addComponentImplementation) {
this.serviceImplementation._addComponent = addComponentImplementation;
}
if (setComponentsImplementation) {
this.serviceImplementation._setComponents = setComponentsImplementation;
if (addComponentsImplementation) {
this.serviceImplementation._addComponents = addComponentsImplementation;
}
if (clearImplementation) {
this.serviceImplementation._clear = clearImplementation;
if (clearComponentsImplementation) {
this.serviceImplementation._clear = clearComponentsImplementation;
}
}
@@ -55,12 +55,12 @@ class ViewportActionCornersService extends PubSubService {
return this.serviceImplementation._getState();
}
public setComponent(component: ActionComponentInfo) {
this.serviceImplementation._setComponent(component);
public addComponent(component: ActionComponentInfo) {
this.serviceImplementation._addComponent(component);
}
public setComponents(components: Array<ActionComponentInfo>) {
this.serviceImplementation._setComponents(components);
public addComponents(components: Array<ActionComponentInfo>) {
this.serviceImplementation._addComponents(components);
}
public clear(viewportId: string) {
@@ -18,9 +18,20 @@ import { IViewportService } from './IViewportService';
import { RENDERING_ENGINE_ID } from './constants';
import ViewportInfo, { DisplaySetOptions, PublicViewportOptions } from './Viewport';
import { StackViewportData, VolumeViewportData } from '../../types/CornerstoneCacheService';
import { LutPresentation, PositionPresentation, Presentations } from '../../types/Presentation';
import {
LutPresentation,
PositionPresentation,
Presentations,
SegmentationPresentation,
SegmentationPresentationItem,
} from '../../types/Presentation';
import JumpPresets from '../../utils/JumpPresets';
import { ViewportProperties } from '@cornerstonejs/core/types';
import { useLutPresentationStore } from '../../stores/useLutPresentationStore';
import { usePositionPresentationStore } from '../../stores/usePositionPresentationStore';
import { useSynchronizersStore } from '../../stores/useSynchronizersStore';
import { useSegmentationPresentationStore } from '../../stores/useSegmentationPresentationStore';
const EVENTS = {
VIEWPORT_DATA_CHANGED: 'event::cornerstoneViewportService:viewportDataChanged',
@@ -176,11 +187,7 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
* @param presentations - The presentations to apply to the viewport.
* @param viewportInfo - Contains a view reference for immediate application
*/
public setPresentations(
viewportId: string,
presentations?: Presentations,
viewportInfo?: ViewportInfo
): void {
public setPresentations(viewportId: string, presentations: Presentations): void {
const viewport = this.getCornerstoneViewport(viewportId) as
| Types.IStackViewport
| Types.IVolumeViewport;
@@ -189,83 +196,113 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
return;
}
const { lutPresentation, positionPresentation } = presentations;
if (lutPresentation) {
const { presentation } = lutPresentation;
if (viewport instanceof BaseVolumeViewport) {
if (presentation instanceof Map) {
presentation.forEach((properties, volumeId) => {
viewport.setProperties(properties, volumeId);
});
} else {
viewport.setProperties(presentation);
}
} else {
viewport.setProperties(presentation);
}
const { lutPresentation, positionPresentation, segmentationPresentation } = presentations;
// Always set the segmentation presentation first, since there might be some
// lutpresentation states that need to be set on the segmentation
// Todo: i think we should even await this
this._setSegmentationPresentation(viewport, segmentationPresentation);
this._setLutPresentation(viewport, lutPresentation);
this._setPositionPresentation(viewport, { ...positionPresentation, viewportId });
}
/**
* Stores the presentation state for a given viewport inside the
* each store. This is used to persist the presentation state
* across different scenarios e.g., when the viewport is changing the
* display set, or when the viewport is moving to a different layout.
*
* @param viewportId The ID of the viewport.
*/
public storePresentation({ viewportId }) {
const presentationIds = this.getPresentationIds(viewportId);
const { syncGroupService } = this.servicesManager.services;
const synchronizers = syncGroupService.getSynchronizersForViewport(viewportId);
if (!presentationIds || Object.keys(presentationIds).length === 0) {
return null;
}
const viewRef = viewportInfo?.getViewReference() || positionPresentation?.viewReference;
if (viewRef) {
viewport.setViewReference(viewRef);
const { lutPresentationId, positionPresentationId, segmentationPresentationId } =
presentationIds;
const positionPresentation = this._getPositionPresentation(viewportId);
const lutPresentation = this._getLutPresentation(viewportId);
const segmentationPresentation = this._getSegmentationPresentation(viewportId);
const { setLutPresentation } = useLutPresentationStore.getState();
const { setPositionPresentation } = usePositionPresentationStore.getState();
const { setSynchronizers } = useSynchronizersStore.getState();
const { setSegmentationPresentation } = useSegmentationPresentationStore.getState();
if (lutPresentationId) {
setLutPresentation(lutPresentationId, lutPresentation);
}
if (positionPresentation?.position) {
viewport.setViewPresentation(positionPresentation.position);
if (positionPresentationId) {
setPositionPresentation(positionPresentationId, positionPresentation);
}
if (segmentationPresentationId) {
setSegmentationPresentation(segmentationPresentationId, segmentationPresentation);
}
if (synchronizers?.length) {
setSynchronizers(
viewportId,
synchronizers.map(synchronizer => ({
id: synchronizer.id,
sourceViewports: [...synchronizer.getSourceViewports()],
targetViewports: [...synchronizer.getTargetViewports()],
}))
);
}
}
/**
* Retrieves the position presentation information for a given viewport.
* @param viewportId The ID of the viewport.
* @returns The position presentation object containing various properties
* such as ID, viewport type, initial image index, view plane normal, view up, zoom, and pan.
* Retrieves the presentations for a given viewport.
* @param viewportId - The ID of the viewport.
* @returns The presentations for the viewport.
*/
public getPositionPresentation(viewportId: string): PositionPresentation {
public getPresentations(viewportId: string): Presentations {
const positionPresentation = this._getPositionPresentation(viewportId);
const lutPresentation = this._getLutPresentation(viewportId);
const segmentationPresentation = this._getSegmentationPresentation(viewportId);
return {
positionPresentation,
lutPresentation,
segmentationPresentation,
};
}
private getPresentationIds(viewportId: string): AppTypes.PresentationIds | null {
const viewportInfo = this.viewportsById.get(viewportId);
if (!viewportInfo) {
return;
return null;
}
const presentationIds = viewportInfo.getPresentationIds();
if (!presentationIds) {
return;
}
const { positionPresentationId } = presentationIds;
return viewportInfo.getPresentationIds();
}
private _getPositionPresentation(viewportId: string): PositionPresentation {
const csViewport = this.getCornerstoneViewport(viewportId);
if (!csViewport) {
return;
}
const viewportInfo = this.viewportsById.get(viewportId);
return {
id: positionPresentationId,
viewportType: viewportInfo.getViewportType(),
viewReference: csViewport instanceof VolumeViewport3D ? null : csViewport.getViewReference(),
position: csViewport.getViewPresentation({ pan: true, zoom: true }),
viewPresentation: csViewport.getViewPresentation({ pan: true, zoom: true }),
viewportId,
};
}
/**
* Retrieves the LUT (Lookup Table) presentation for a given viewport.
* @param viewportId The ID of the viewport.
* @returns The LUT presentation object, or undefined if the viewport does not exist.
*/
public getLutPresentation(viewportId: string): LutPresentation {
const viewportInfo = this.viewportsById.get(viewportId);
if (!viewportInfo) {
return;
}
const presentationIds = viewportInfo.getPresentationIds();
if (!presentationIds) {
return;
}
const { lutPresentationId } = presentationIds;
private _getLutPresentation(viewportId: string): LutPresentation {
const csViewport = this.getCornerstoneViewport(viewportId) as
| Types.IStackViewport
| Types.IVolumeViewport;
@@ -275,116 +312,39 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
}
const cleanProperties = properties => {
if (properties.isComputedVOI) {
delete properties.voiRange;
delete properties.VOILUTFunction;
if (properties?.isComputedVOI) {
delete properties?.voiRange;
delete properties?.VOILUTFunction;
}
return properties;
};
const presentation =
const properties =
csViewport instanceof BaseVolumeViewport
? new Map()
: cleanProperties(csViewport.getProperties());
if (presentation instanceof Map) {
csViewport.getActors().forEach(({ uid: volumeId }) => {
const properties = cleanProperties(csViewport.getProperties(volumeId));
presentation.set(volumeId, properties);
if (properties instanceof Map) {
const volumeIds = (csViewport as Types.IBaseVolumeViewport).getAllVolumeIds();
volumeIds?.forEach(volumeId => {
const csProps = cleanProperties(csViewport.getProperties(volumeId));
properties.set(volumeId, csProps);
});
}
return {
id: lutPresentationId,
viewportType: viewportInfo.getViewportType(),
presentation,
};
}
/**
* Retrieves the presentations for a given viewport.
* @param viewportId - The ID of the viewport.
* @returns The presentations for the viewport.
*/
public getPresentations(viewportId: string): Presentations {
const viewportInfo = this.viewportsById.get(viewportId);
if (!viewportInfo) {
return;
}
const positionPresentation = this.getPositionPresentation(viewportId);
const lutPresentation = this.getLutPresentation(viewportId);
return {
positionPresentation,
lutPresentation,
viewportType: viewportInfo.getViewportType(),
properties,
};
}
/**
* Stores the presentation state for a given viewport inside the
* stateSyncService. This is used to persist the presentation state
* across different scenarios e.g., when the viewport is changing the
* display set, or when the viewport is moving to a different layout.
*
* @param viewportId The ID of the viewport.
*/
public storePresentation({ viewportId }) {
let presentations = null as Presentations;
try {
presentations = this.getPresentations(viewportId);
if (!presentations?.positionPresentation && !presentations?.lutPresentation) {
return;
}
} catch (error) {
console.warn(error);
return;
}
private _getSegmentationPresentation(viewportId: string): SegmentationPresentation {
const { segmentationService } = this.servicesManager.services;
const { stateSyncService, syncGroupService } = this.servicesManager.services;
const synchronizers = syncGroupService.getSynchronizersForViewport(viewportId);
const { positionPresentationStore, synchronizersStore, lutPresentationStore } =
stateSyncService.getState();
const { lutPresentation, positionPresentation } = presentations;
const { id: positionPresentationId } = positionPresentation;
const { id: lutPresentationId } = lutPresentation;
const updateStore = (store, id, value) => ({ ...store, [id]: value });
const newState = {} as { [key: string]: any };
if (lutPresentationId) {
newState.lutPresentationStore = updateStore(
lutPresentationStore,
lutPresentationId,
lutPresentation
);
}
if (positionPresentationId) {
newState.positionPresentationStore = updateStore(
positionPresentationStore,
positionPresentationId,
positionPresentation
);
}
if (synchronizers?.length) {
newState.synchronizersStore = updateStore(
synchronizersStore,
viewportId,
synchronizers.map(synchronizer => ({
id: synchronizer.id,
sourceViewports: [...synchronizer.getSourceViewports()],
targetViewports: [...synchronizer.getTargetViewports()],
}))
);
}
stateSyncService.store(newState);
const presentation = segmentationService.getPresentation(viewportId);
return presentation;
}
/**
@@ -423,6 +383,11 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
// and we would lose the presentation.
this.storePresentation({ viewportId: viewportInfo.getViewportId() });
// Todo: i don't like this here, move it
this.servicesManager.services.segmentationService.clearSegmentationRepresentations(
viewportInfo.getViewportId()
);
if (!viewportInfo) {
throw new Error('element is not enabled for the given viewportId');
}
@@ -431,8 +396,7 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
// since those are the newly set ones, we set them here so that it handles defaults
const displaySetOptions = viewportInfo.setPublicDisplaySetOptions(publicDisplaySetOptions);
// Specify an over-ride for the viewport type, even though it is in the public
// viewport options, because the one in the public viewport options is a suggestion
// for initial view, whereas the one in viewportData is a requirement based on the
// viewport options, because the one in the viewportData is a requirement based on the
// type of data being displayed.
const viewportOptions = viewportInfo.setPublicViewportOptions(
publicViewportOptions,
@@ -647,13 +611,21 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
properties.colormap = colormap ?? properties.colormap;
}
this._handleOverlays(viewport);
viewport.element.addEventListener(csEnums.Events.VIEWPORT_NEW_IMAGE_SET, evt => {
const { element } = evt.detail;
if (!imageIds?.length) {
return;
}
if (element !== viewport.element) {
return;
}
return viewport.setStack(imageIds, initialImageIndexToUse).then(() => {
csToolsUtils.stackContextPrefetch.enable(element);
});
let imageIdsToSet = imageIds;
const res = this._processExtraDisplaySetsForViewport(viewport);
imageIdsToSet = res?.imageIds ?? imageIdsToSet;
return viewport.setStack(imageIdsToSet, initialImageIndexToUse).then(() => {
viewport.setProperties({ ...properties });
this.setPresentations(viewport.id, presentations, viewportInfo);
if (displayArea) {
@@ -702,7 +674,7 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
const lastSliceIndex = numberOfSlices - 1;
if (imageIndex !== undefined) {
return csToolsUtils.clip(imageIndex, 0, lastSliceIndex);
return csUtils.clip(imageIndex, 0, lastSliceIndex);
}
if (preset === JumpPresets.First) {
@@ -764,7 +736,6 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
const displaySetOptions = displaySetOptionsArray[index];
const { volumeId } = volume;
volumeInputArray.push({
imageIds,
volumeId,
@@ -775,8 +746,7 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
this.viewportsDisplaySets.set(viewport.id, displaySetInstanceUIDs);
const volumesNotLoaded = volumeToLoad.filter(volume => !volume.loadStatus.loaded);
const volumesNotLoaded = volumeToLoad.filter(volume => !volume.loadStatus?.loaded);
if (volumesNotLoaded.length) {
if (hangingProtocolService.getShouldPerformCustomImageLoad()) {
// delegate the volume loading to the hanging protocol service if it has a custom image load strategy
@@ -787,7 +757,7 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
}
volumesNotLoaded.forEach(volume => {
if (!volume.loadStatus.loading) {
if (!volume.loadStatus?.loading && volume.load instanceof Function) {
volume.load();
}
});
@@ -798,8 +768,7 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
}
public async setVolumesForViewport(viewport, volumeInputArray, presentations) {
const { displaySetService, toolGroupService, viewportGridService } =
this.servicesManager.services;
const { displaySetService, viewportGridService } = this.servicesManager.services;
const viewportInfo = this.getViewportInfo(viewport.id);
const displaySetOptions = viewportInfo.getDisplaySetOptions();
@@ -811,7 +780,7 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
const { volumeId } = volumeInput;
const displaySetOption = displaySetOptions[index];
const { voi, voiInverted, colormap, displayPreset } = displaySetOption;
const properties = {};
const properties = {} as ViewportProperties;
if (voi && (voi.windowWidth || voi.windowCenter)) {
const { lower, upper } = csUtils.windowLevel.toLowHighRange(
@@ -836,22 +805,21 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
return { properties, volumeId };
});
// For SEG and RT viewports
this._processExtraDisplaySetsForViewport(viewport);
await viewport.setVolumes(volumeInputArray);
volumesProperties.forEach(({ properties, volumeId }) => {
viewport.setProperties(properties, volumeId);
});
this.setPresentations(viewport.id, presentations, viewportInfo);
this._handleOverlays(viewport);
const toolGroup = toolGroupService.getToolGroupForViewport(viewport.id);
csToolsUtils.segmentation.triggerSegmentationRender(toolGroup.id);
const imageIndex = this._getInitialImageIndexForViewport(viewportInfo);
if (imageIndex !== undefined) {
csToolsUtils.jumpToSlice(viewport.element, {
csUtils.jumpToSlice(viewport.element, {
imageIndex,
});
}
@@ -863,7 +831,9 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
});
}
private _handleOverlays(viewport: Types.IStackViewport | Types.IVolumeViewport) {
private _processExtraDisplaySetsForViewport(
viewport: Types.IStackViewport | Types.IVolumeViewport
) {
const { displaySetService } = this.servicesManager.services;
// load any secondary displaySets
@@ -877,104 +847,41 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
displaySet?.isOverlayDisplaySet && ['SEG', 'RTSTRUCT'].includes(displaySet.Modality)
);
if (segOrRTSOverlayDisplaySet) {
this.addOverlayRepresentationForDisplaySet(segOrRTSOverlayDisplaySet, viewport);
} else {
// If the displaySet is not a SEG displaySet we assume it is a primary displaySet
// and we can look into hydrated segmentations to check if any of them are
// associated with the primary displaySet
// get segmentations only returns the hydrated segmentations
this._addSegmentationRepresentationToToolGroupIfNecessary(displaySetInstanceUIDs, viewport);
// if it is only the overlay displaySet, then we need to get the reference
// displaySet imageIds and set them as the imageIds for the viewport,
// here we can do some logic if the reference is missing
// then find the most similar match of displaySet instead
if (!segOrRTSOverlayDisplaySet) {
return;
}
const referenceDisplaySet = displaySetService.getDisplaySetByUID(
segOrRTSOverlayDisplaySet.referencedDisplaySetInstanceUID
);
const imageIds = referenceDisplaySet.images.map(image => image.imageId);
this.addOverlayRepresentationForDisplaySet(segOrRTSOverlayDisplaySet, viewport);
return { imageIds };
}
private _addSegmentationRepresentationToToolGroupIfNecessary(
displaySetInstanceUIDs: string[],
viewport: any
private addOverlayRepresentationForDisplaySet(
displaySet: OhifTypes.DisplaySet,
viewport: Types.IViewport
) {
const { segmentationService, toolGroupService } = this.servicesManager.services;
const toolGroup = toolGroupService.getToolGroupForViewport(viewport.id);
// this only returns hydrated segmentations
const segmentations = segmentationService.getSegmentations();
for (const segmentation of segmentations) {
const toolGroupSegmentationRepresentations =
segmentationService.getSegmentationRepresentationsForToolGroup(toolGroup.id) || [];
// if there is already a segmentation representation for this segmentation
// for this toolGroup, don't bother at all
const isSegmentationInToolGroup = toolGroupSegmentationRepresentations.find(
representation => representation.segmentationId === segmentation.id
);
if (isSegmentationInToolGroup) {
continue;
}
// otherwise, check if the hydrated segmentations are in the same FrameOfReferenceUID
// as the primary displaySet, if so add the representation (since it was not there)
const { id: segDisplaySetInstanceUID } = segmentation;
let segFrameOfReferenceUID = this._getFrameOfReferenceUID(segDisplaySetInstanceUID);
if (!segFrameOfReferenceUID) {
// if the segmentation displaySet does not have a FrameOfReferenceUID, we might check the
// segmentation itself maybe it has a FrameOfReferenceUID
const { FrameOfReferenceUID } = segmentation;
if (FrameOfReferenceUID) {
segFrameOfReferenceUID = FrameOfReferenceUID;
}
}
if (!segFrameOfReferenceUID) {
return;
}
let shouldDisplaySeg = false;
for (const displaySetInstanceUID of displaySetInstanceUIDs) {
const primaryFrameOfReferenceUID = this._getFrameOfReferenceUID(displaySetInstanceUID);
if (segFrameOfReferenceUID === primaryFrameOfReferenceUID) {
shouldDisplaySeg = true;
break;
}
}
if (!shouldDisplaySeg) {
return;
}
segmentationService.addSegmentationRepresentationToToolGroup(
toolGroup.id,
segmentation.id,
false, // already hydrated,
segmentation.type
);
}
}
private addOverlayRepresentationForDisplaySet(displaySet: any, viewport: any) {
const { segmentationService, toolGroupService } = this.servicesManager.services;
const { referencedVolumeId } = displaySet;
const { segmentationService } = this.servicesManager.services;
const segmentationId = displaySet.displaySetInstanceUID;
const toolGroup = toolGroupService.getToolGroupForViewport(viewport.id);
const representationType =
referencedVolumeId && cache.getVolume(referencedVolumeId) !== undefined
displaySet.Modality === 'SEG'
? csToolsEnums.SegmentationRepresentations.Labelmap
: csToolsEnums.SegmentationRepresentations.Contour;
segmentationService.addSegmentationRepresentationToToolGroup(
toolGroup.id,
segmentationService.addSegmentationRepresentation(viewport.id, {
segmentationId,
false,
representationType
);
type: representationType,
});
// store the segmentation presentation id in the viewport info
this.storePresentation({ viewportId: viewport.id });
}
// Todo: keepCamera is an interim solution until we have a better solution for
@@ -1032,7 +939,7 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
}
return this._setOtherViewport(
viewport as Types.IViewport,
viewport,
viewportData as StackViewportData,
viewportInfo,
presentations
@@ -1063,11 +970,11 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
// calculate the slab thickness based on the volume dimensions
const imageVolume = cache.getVolume(volumeId);
const { dimensions } = imageVolume;
const { dimensions, spacing } = imageVolume;
const slabThickness = Math.sqrt(
dimensions[0] * dimensions[0] +
dimensions[1] * dimensions[1] +
dimensions[2] * dimensions[2]
Math.pow(dimensions[0] * spacing[0], 2) +
Math.pow(dimensions[1] * spacing[1], 2) +
Math.pow(dimensions[2] * spacing[2], 2)
);
return slabThickness;
@@ -1128,9 +1035,9 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
const viewports = this.getRenderingEngine().getViewports();
// Store the current position presentations for each viewport.
viewports.forEach(({ id }) => {
const presentation = this.getPositionPresentation(id);
this.beforeResizePositionPresentations.set(id, presentation);
viewports.forEach(({ id: viewportId }) => {
const presentation = this._getPositionPresentation(viewportId);
this.beforeResizePositionPresentations.set(viewportId, presentation);
});
// Resize the rendering engine and render.
@@ -1159,6 +1066,65 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
this.gridResizeTimeOut = null;
}, this.gridResizeDelay);
}
private _setLutPresentation(
viewport: Types.IStackViewport | Types.IVolumeViewport,
lutPresentation: LutPresentation
): void {
if (!lutPresentation) {
return;
}
const { properties } = lutPresentation;
if (viewport instanceof BaseVolumeViewport) {
if (properties instanceof Map) {
properties.forEach((propertiesEntry, volumeId) => {
viewport.setProperties(propertiesEntry, volumeId);
});
} else {
viewport.setProperties(properties);
}
} else {
viewport.setProperties(properties);
}
}
private _setPositionPresentation(
viewport: Types.IStackViewport | Types.IVolumeViewport,
positionPresentation: PositionPresentation
): void {
const viewRef = positionPresentation?.viewReference;
if (viewRef) {
viewport.setViewReference(viewRef);
}
const viewPresentation = positionPresentation?.viewPresentation;
if (viewPresentation) {
viewport.setViewPresentation(viewPresentation);
}
}
private _setSegmentationPresentation(
viewport: Types.IStackViewport | Types.IVolumeViewport,
segmentationPresentation: SegmentationPresentation
): void {
if (!segmentationPresentation) {
return;
}
const { segmentationService } = this.servicesManager.services;
segmentationPresentation.forEach((presentation: SegmentationPresentationItem) => {
const { segmentationId, type, hydrated } = presentation;
if (hydrated) {
segmentationService.addSegmentationRepresentation(viewport.id, {
segmentationId,
type,
});
}
});
}
}
export default CornerstoneViewportService;
@@ -5,7 +5,6 @@ import {
VolumeViewport,
utilities,
} from '@cornerstonejs/core';
import { Types as CoreTypes } from '@ohif/core';
import { StackViewportData, VolumeViewportData } from '../../types/CornerstoneCacheService';
import getCornerstoneBlendMode from '../../utils/getCornerstoneBlendMode';
import getCornerstoneOrientation from '../../utils/getCornerstoneOrientation';
@@ -25,7 +24,7 @@ export type ViewportOptions = {
toolGroupId: string;
viewportId: string;
// Presentation ID to store/load presentation state from
presentationIds?: CoreTypes.PresentationIds;
presentationIds?: AppTypes.PresentationIds;
orientation?: Enums.OrientationAxis;
background?: Types.Point3;
displayArea?: Types.DisplayArea;
@@ -46,7 +45,7 @@ export type PublicViewportOptions = {
id?: string;
viewportType?: string;
toolGroupId?: string;
presentationIds?: CoreTypes.PresentationIds;
presentationIds?: string[];
viewportId?: string;
orientation?: Enums.OrientationAxis;
background?: Types.Point3;
@@ -283,7 +282,7 @@ class ViewportInfo {
return this.viewportOptions;
}
public getPresentationIds(): CoreTypes.PresentationIds {
public getPresentationIds(): AppTypes.PresentationIds | null {
const { presentationIds } = this.viewportOptions;
return presentationIds;
}
@@ -0,0 +1,4 @@
export { useLutPresentationStore } from './useLutPresentationStore';
export { usePositionPresentationStore } from './usePositionPresentationStore';
export { useSegmentationPresentationStore } from './useSegmentationPresentationStore';
export { useSynchronizersStore } from './useSynchronizersStore';
@@ -0,0 +1,42 @@
const JOIN_STR = '&';
// The default lut presentation id if none defined
const DEFAULT_STR = 'default';
// This code finds the first unique index to add to the presentation id so that
// two viewports containing the same display set in the same type of viewport
// can have different presentation information. This allows comparison of
// a single display set in two or more viewports, when the user has simply
// dragged and dropped the view in twice. For example, it allows displaying
// bone, brain and soft tissue views of a single display set, and to still
// remember the specific changes to each viewport.
const addUniqueIndex = (
arr,
key,
viewports: AppTypes.ViewportGrid.Viewports,
isUpdatingSameViewport
) => {
arr.push(0);
// If we are updating the viewport, we should not increment the index
if (isUpdatingSameViewport) {
return;
}
// The 128 is just a value that is larger than how many viewports we
// display at once, used as an upper bound on how many unique presentation
// ID's might exist for a single display set at once.
for (let displayInstance = 0; displayInstance < 128; displayInstance++) {
arr[arr.length - 1] = displayInstance;
const testId = arr.join(JOIN_STR);
if (
!Array.from(viewports.values()).find(
viewport => viewport.viewportOptions?.presentationIds?.[key] === testId
)
) {
break;
}
}
};
export { addUniqueIndex, DEFAULT_STR, JOIN_STR };
@@ -0,0 +1,166 @@
import { create } from 'zustand';
import { devtools } from 'zustand/middleware';
import { LutPresentation } from '../types/Presentation';
import { addUniqueIndex, DEFAULT_STR, JOIN_STR } from './presentationUtils';
/**
* Identifier for the LUT Presentation store type.
*/
const PRESENTATION_TYPE_ID = 'lutPresentationId';
/**
* Flag to enable or disable debug mode for the store.
* Set to `true` to enable zustand devtools.
*/
const DEBUG_STORE = false;
/**
* Represents the state and actions for managing LUT presentations.
*/
type LutPresentationState = {
/**
* Type identifier for the store.
*/
type: string;
/**
* Stores LUT presentations indexed by their presentation ID.
*/
lutPresentationStore: Record<string, LutPresentation>;
/**
* Sets the LUT presentation for a given key.
*
* @param key - The key identifying the LUT presentation.
* @param value - The `LutPresentation` to associate with the key.
*/
setLutPresentation: (key: string, value: LutPresentation) => void;
/**
* Clears all LUT presentations from the store.
*/
clearLutPresentationStore: () => void;
/**
* Retrieves the presentation ID based on the provided parameters.
*
* @param id - The presentation ID to check.
* @param options - Configuration options.
* @param options.viewport - The current viewport in grid
* @param options.viewports - All available viewports in grid
* @param options.isUpdatingSameViewport - Indicates if the same viewport is being updated.
* @returns The presentation ID or undefined.
*/
getPresentationId: (
id: string,
options: {
viewport: AppTypes.ViewportGrid.Viewport;
viewports: AppTypes.ViewportGrid.Viewports;
isUpdatingSameViewport: boolean;
}
) => string | undefined;
};
/**
* Generates a presentation ID for LUT based on the viewport configuration.
*
* @param id - The ID to check.
* @param options - Configuration options.
* @param options.viewport - The current viewport.
* @param options.viewports - All available viewports.
* @param options.isUpdatingSameViewport - Indicates if the same viewport is being updated.
* @returns The LUT presentation ID or undefined.
*/
const getLutPresentationId = (
id: string,
{
viewport,
viewports,
isUpdatingSameViewport,
}: {
viewport: AppTypes.ViewportGrid.Viewport;
viewports: AppTypes.ViewportGrid.Viewports;
isUpdatingSameViewport: boolean;
}
): string | undefined => {
if (id !== PRESENTATION_TYPE_ID) {
return;
}
const getLutId = (ds): string => {
if (!ds || !ds.options) {
return DEFAULT_STR;
}
if (ds.options.id) {
return ds.options.id;
}
const arr = Object.entries(ds.options).map(([key, val]) => `${key}=${val}`);
if (!arr.length) {
return DEFAULT_STR;
}
return arr.join(JOIN_STR);
};
if (!viewport || !viewport.viewportOptions || !viewport.displaySetInstanceUIDs?.length) {
return;
}
const { displaySetOptions, displaySetInstanceUIDs } = viewport;
const lutId = getLutId(displaySetOptions[0]);
const lutPresentationArr = [lutId];
for (const uid of displaySetInstanceUIDs) {
lutPresentationArr.push(uid);
}
addUniqueIndex(lutPresentationArr, PRESENTATION_TYPE_ID, viewports, isUpdatingSameViewport);
return lutPresentationArr.join(JOIN_STR);
};
/**
* Creates the LUT Presentation store.
*
* @param set - The zustand set function.
* @returns The LUT Presentation store state and actions.
*/
const createLutPresentationStore = (set): LutPresentationState => ({
type: PRESENTATION_TYPE_ID,
lutPresentationStore: {},
/**
* Sets the LUT presentation for a given key.
*/
setLutPresentation: (key, value) =>
set(
state => ({
lutPresentationStore: {
...state.lutPresentationStore,
[key]: value,
},
}),
false,
'setLutPresentation'
),
/**
* Clears all LUT presentations from the store.
*/
clearLutPresentationStore: () =>
set({ lutPresentationStore: {} }, false, 'clearLutPresentationStore'),
/**
* Retrieves the presentation ID based on the provided parameters.
*/
getPresentationId: getLutPresentationId,
});
/**
* Zustand store for managing LUT presentations.
* Applies devtools middleware when DEBUG_STORE is enabled.
*/
export const useLutPresentationStore = create<LutPresentationState>()(
DEBUG_STORE
? devtools(createLutPresentationStore, { name: 'LutPresentationStore' })
: createLutPresentationStore
);
@@ -0,0 +1,172 @@
import { create } from 'zustand';
import { devtools } from 'zustand/middleware';
import { PositionPresentation } from '../types/Presentation';
import { addUniqueIndex, JOIN_STR } from './presentationUtils';
const PRESENTATION_TYPE_ID = 'positionPresentationId';
const DEBUG_STORE = true;
/**
* Represents the state and actions for managing position presentations.
*/
type PositionPresentationState = {
/**
* Type identifier for the store.
*/
type: string;
/**
* Stores position presentations indexed by their presentation ID.
*/
positionPresentationStore: Record<string, PositionPresentation>;
/**
* Sets the position presentation for a given key.
*
* @param key - The key identifying the position presentation.
* @param value - The `PositionPresentation` to associate with the key.
*/
setPositionPresentation: (key: string, value: PositionPresentation) => void;
/**
* Clears all position presentations from the store.
*/
clearPositionPresentationStore: () => void;
/**
* Retrieves the presentation ID based on the provided parameters.
*
* @param id - The ID to check.
* @param options - Configuration options.
* @param options.viewport - The current viewport.
* @param options.viewports - All available viewports.
* @param options.isUpdatingSameViewport - Indicates if the same viewport is being updated.
* @returns The position presentation ID or undefined.
*/
getPresentationId: (
id: string,
options: {
viewport: any;
viewports: any;
isUpdatingSameViewport: boolean;
}
) => string | undefined;
getPositionPresentationId: (
viewport: any,
viewports?: any,
isUpdatingSameViewport?: boolean
) => string | undefined;
};
/**
* Generates a position presentation ID based on the viewport configuration.
*
* @param id - The ID to check.
* @param options - Configuration options.
* @param options.viewport - The current viewport.
* @param options.viewports - All available viewports.
* @param options.isUpdatingSameViewport - Indicates if the same viewport is being updated.
* @returns The position presentation ID or undefined.
*/
const getPresentationId = (
id: string,
{
viewport,
viewports,
isUpdatingSameViewport,
}: {
viewport: any;
viewports: any;
isUpdatingSameViewport: boolean;
}
): string | undefined => {
if (id !== PRESENTATION_TYPE_ID) {
return;
}
if (!viewport?.viewportOptions || !viewport.displaySetInstanceUIDs?.length) {
return;
}
return getPositionPresentationId(viewport, viewports, isUpdatingSameViewport);
};
function getPositionPresentationId(viewport, viewports, isUpdatingSameViewport) {
const { viewportOptions = {}, displaySetInstanceUIDs = [], displaySetOptions = [] } = viewport;
const { id: viewportOptionId, orientation } = viewportOptions;
const positionPresentationArr = [orientation || 'acquisition'];
if (viewportOptionId) {
positionPresentationArr.push(viewportOptionId);
}
if (displaySetOptions?.some(ds => ds.options?.blendMode || ds.options?.displayPreset)) {
positionPresentationArr.push(`custom`);
}
for (const uid of displaySetInstanceUIDs) {
positionPresentationArr.push(uid);
}
if (viewports && viewports.length && isUpdatingSameViewport !== undefined) {
addUniqueIndex(
positionPresentationArr,
PRESENTATION_TYPE_ID,
viewports,
isUpdatingSameViewport
);
} else {
positionPresentationArr.push(0);
}
return positionPresentationArr.join(JOIN_STR);
}
/**
* Creates the Position Presentation store.
*
* @param set - The zustand set function.
* @returns The Position Presentation store state and actions.
*/
const createPositionPresentationStore = set => ({
type: PRESENTATION_TYPE_ID,
positionPresentationStore: {},
/**
* Sets the position presentation for a given key.
*/
setPositionPresentation: (key, value) =>
set(
state => ({
positionPresentationStore: {
...state.positionPresentationStore,
[key]: value,
},
}),
false,
'setPositionPresentation'
),
/**
* Clears all position presentations from the store.
*/
clearPositionPresentationStore: () =>
set({ positionPresentationStore: {} }, false, 'clearPositionPresentationStore'),
/**
* Retrieves the presentation ID based on the provided parameters.
*/
getPresentationId,
getPositionPresentationId: getPositionPresentationId,
});
/**
* Zustand store for managing position presentations.
* Applies devtools middleware when DEBUG_STORE is enabled.
*/
export const usePositionPresentationStore = create<PositionPresentationState>()(
DEBUG_STORE
? devtools(createPositionPresentationStore, { name: 'PositionPresentationStore' })
: createPositionPresentationStore
);
@@ -0,0 +1,238 @@
import { create } from 'zustand';
import { devtools } from 'zustand/middleware';
import { SegmentationPresentation } from '../types/Presentation';
import { JOIN_STR } from './presentationUtils';
import { getViewportOrientationFromImageOrientationPatient } from '../utils/getViewportOrientationFromImageOrientationPatient';
const PRESENTATION_TYPE_ID = 'segmentationPresentationId';
const DEBUG_STORE = false;
/**
* The keys are the presentationId.
*/
type SegmentationPresentationStore = {
/**
* Type identifier for the store.
*/
type: string;
/**
* Stores segmentation presentations indexed by their presentation ID.
*/
segmentationPresentationStore: Record<string, SegmentationPresentation>;
/**
* Sets the segmentation presentation for a given segmentation ID.
*
* @param presentationId - The presentation ID.
* @param value - The `SegmentationPresentation` to associate with the ID.
*/
setSegmentationPresentation: (presentationId: string, value: SegmentationPresentation) => void;
/**
* Clears all segmentation presentations from the store.
*/
clearSegmentationPresentationStore: () => void;
/**
* Retrieves the presentation ID based on the provided parameters.
*
* @param id - The ID to check.
* @param options - Configuration options.
* @param options.viewport - The current viewport.
* @param options.viewports - All available viewports.
* @param options.isUpdatingSameViewport - Indicates if the same viewport is being updated.
* @param options.servicesManager - The services manager instance.
* @returns The segmentation presentation ID or undefined.
*/
getPresentationId: (
id: string,
options: {
viewport: AppTypes.ViewportGrid.Viewport;
viewports: AppTypes.ViewportGrid.Viewports;
isUpdatingSameViewport: boolean;
servicesManager: AppTypes.ServicesManager;
}
) => string | undefined;
/**
* Adds a new segmentation presentation state.
*
* @param presentationId - The presentation ID.
* @param segmentationPresentation - The `SegmentationPresentation` to add.
* @param servicesManager - The services manager instance.
*/
addSegmentationPresentation: (
presentationId: string,
segmentationPresentation: SegmentationPresentation,
{ servicesManager }: { servicesManager: AppTypes.ServicesManager }
) => void;
/**
* Gets the current segmentation presentation ID.
*
* @param params - Parameters for retrieving the segmentation presentation ID.
* @param params.viewport - The current viewport.
* @param params.servicesManager - The services manager instance.
* @returns The current segmentation presentation ID.
*/
getSegmentationPresentationId: ({
viewport,
servicesManager,
}: {
viewport: AppTypes.ViewportGrid.Viewport;
servicesManager: AppTypes.ServicesManager;
}) => string;
};
/**
* Generates a segmentation presentation ID based on the viewport configuration.
*
* @param id - The ID to check.
* @param options - Configuration options.
* @param options.viewport - The current viewport.
* @param options.viewports - All available viewports.
* @param options.isUpdatingSameViewport - Indicates if the same viewport is being updated.
* @param options.servicesManager - The services manager instance.
* @returns The segmentation presentation ID or undefined.
*/
const getPresentationId = (
id: string,
{
viewport,
viewports,
isUpdatingSameViewport,
servicesManager,
}: {
viewport: AppTypes.ViewportGrid.Viewport;
viewports: AppTypes.ViewportGrid.Viewports;
isUpdatingSameViewport: boolean;
servicesManager: AppTypes.ServicesManager;
}
): string | undefined => {
if (id !== PRESENTATION_TYPE_ID) {
return;
}
return _getSegmentationPresentationId({ viewport, servicesManager });
};
/**
* Helper function to generate the segmentation presentation ID.
*
* @param params - Parameters for generating the segmentation presentation ID.
* @param params.viewport - The current viewport.
* @param params.servicesManager - The services manager instance.
* @returns The segmentation presentation ID or undefined.
*/
const _getSegmentationPresentationId = ({
viewport,
servicesManager,
}: {
viewport: AppTypes.ViewportGrid.Viewport;
servicesManager: AppTypes.ServicesManager;
}) => {
if (!viewport?.viewportOptions || !viewport.displaySetInstanceUIDs?.length) {
return;
}
const { displaySetInstanceUIDs, viewportOptions } = viewport;
let orientation = viewportOptions.orientation;
if (!orientation) {
// Calculate orientation from the viewport sample image
const displaySet = servicesManager.services.displaySetService.getDisplaySetByUID(
displaySetInstanceUIDs[0]
);
const sampleImage = displaySet.images?.[0];
const imageOrientationPatient = sampleImage?.ImageOrientationPatient;
orientation = getViewportOrientationFromImageOrientationPatient(imageOrientationPatient);
}
const segmentationPresentationArr = [];
segmentationPresentationArr.push(...displaySetInstanceUIDs);
// Uncomment if unique indexing is needed
// addUniqueIndex(
// segmentationPresentationArr,
// 'segmentationPresentationId',
// viewports,
// isUpdatingSameViewport
// );
return segmentationPresentationArr.join(JOIN_STR);
};
/**
* Creates the Segmentation Presentation store.
*
* @param set - The zustand set function.
* @returns The Segmentation Presentation store state and actions.
*/
const createSegmentationPresentationStore = set => ({
type: PRESENTATION_TYPE_ID,
segmentationPresentationStore: {},
/**
* Clears all segmentation presentations from the store.
*/
clearSegmentationPresentationStore: () =>
set({ segmentationPresentationStore: {} }, false, 'clearSegmentationPresentationStore'),
/**
* Adds a new segmentation presentation to the store.
*/
addSegmentationPresentation: (
presentationId: string,
segmentationPresentation: SegmentationPresentation,
{ servicesManager }: { servicesManager: AppTypes.ServicesManager }
) =>
set(
state => ({
segmentationPresentationStore: {
...state.segmentationPresentationStore,
[presentationId]: segmentationPresentation,
},
}),
false,
'addSegmentationPresentation'
),
/**
* Sets the segmentation presentation for a given presentation ID.
*/
setSegmentationPresentation: (presentationId: string, value: SegmentationPresentation) =>
set(
state => ({
segmentationPresentationStore: {
...state.segmentationPresentationStore,
[presentationId]: value,
},
}),
false,
'setSegmentationPresentation'
),
/**
* Retrieves the presentation ID based on the provided parameters.
*/
getPresentationId,
/**
* Retrieves the current segmentation presentation ID.
*/
getSegmentationPresentationId: _getSegmentationPresentationId,
});
/**
* Zustand store for managing segmentation presentations.
* Applies devtools middleware when DEBUG_STORE is enabled.
*/
export const useSegmentationPresentationStore = create<SegmentationPresentationStore>()(
DEBUG_STORE
? devtools(createSegmentationPresentationStore, { name: 'Segmentation Presentation Store' })
: createSegmentationPresentationStore
);
@@ -0,0 +1,84 @@
import { create } from 'zustand';
import { devtools } from 'zustand/middleware';
/**
* Identifier for the synchronizers store type.
*/
const PRESENTATION_TYPE_ID = 'synchronizersStoreId';
/**
* Flag to enable or disable debug mode for the store.
* Set to `true` to enable zustand devtools.
*/
const DEBUG_STORE = false;
/**
* Information about a single synchronizer.
*/
type SynchronizerInfo = {
id: string;
type: string;
sourceViewports: Array<{ viewportId: string; renderingEngineId: string }>;
targetViewports: Array<{ viewportId: string; renderingEngineId: string }>;
};
/**
* State shape for the Synchronizers store.
*/
type SynchronizersState = {
/**
* Stores synchronizer information indexed by a unique key.
*/
synchronizersStore: Record<string, SynchronizerInfo[]>;
/**
* Sets the synchronizers for a specific viewport.
*
* @param viewportId - The ID of the viewport.
* @param synchronizers - An array of SynchronizerInfo.
*/
setSynchronizers: (viewportId: string, synchronizers: SynchronizerInfo[]) => void;
/**
* Clears the entire synchronizers store.
*/
clearSynchronizersStore: () => void;
};
/**
* Creates the Synchronizers store.
*
* @param set - The zustand set function.
* @returns The synchronizers store state and actions.
*/
const createSynchronizersStore = (set): SynchronizersState => ({
synchronizersStore: {},
type: PRESENTATION_TYPE_ID,
setSynchronizers: (viewportId: string, synchronizers: SynchronizerInfo[]) => {
set(
state => ({
synchronizersStore: {
...state.synchronizersStore,
[viewportId]: synchronizers,
},
}),
false,
'setSynchronizers'
);
},
clearSynchronizersStore: () => {
set({ synchronizersStore: {} }, false, 'clearSynchronizersStore');
},
});
/**
* Zustand store for managing synchronizers.
* Applies devtools middleware when DEBUG_STORE is enabled.
*/
export const useSynchronizersStore = create<SynchronizersState>()(
DEBUG_STORE
? devtools(createSynchronizersStore, { name: 'SynchronizersStore' })
: createSynchronizersStore
);
@@ -1,5 +1,5 @@
import { SynchronizerManager, Synchronizer, utilities } from '@cornerstonejs/tools';
import { EVENTS, getRenderingEngine, Types } from '@cornerstonejs/core';
import { SynchronizerManager, Synchronizer } from '@cornerstonejs/tools';
import { EVENTS, getRenderingEngine, type Types, utilities } from '@cornerstonejs/core';
const frameViewSyncCallback = (
synchronizerInstance: Synchronizer,
@@ -1,5 +1,5 @@
import { LengthTool, utilities } from '@cornerstonejs/tools';
import callInputDialog from '../utils/callInputDialog';
import { callInputDialog } from '@ohif/extension-default';
import getActiveViewportEnabledElement from '../utils/getActiveViewportEnabledElement';
const { calibrateImageSpacing } = utilities;
@@ -1,5 +1,5 @@
import { VolumeViewport, metaData, utilities } from '@cornerstonejs/core';
import { IStackViewport, IVolumeViewport, Point3 } from '@cornerstonejs/core/dist/esm/types';
import { IStackViewport, IVolumeViewport } from '@cornerstonejs/core/types';
import { AnnotationDisplayTool, drawing } from '@cornerstonejs/tools';
import { guid, b64toBlob } from '@ohif/core/src/utils';
import OverlayPlaneModuleProvider from './OverlayPlaneModuleProvider';
@@ -9,6 +9,13 @@ import ColorbarServiceType from '../services/ColorbarService';
import * as cornerstone from '@cornerstonejs/core';
import * as cornerstoneTools from '@cornerstonejs/tools';
import type {
SegmentRepresentation as SegmentRep,
SegmentationData as SegData,
SegmentationRepresentation as SegRep,
SegmentationInfo as SegInfo,
} from '../services/SegmentationService/SegmentationService';
declare global {
namespace AppTypes {
export type CornerstoneCacheService = CornerstoneCacheServiceType;
@@ -18,6 +25,7 @@ declare global {
export type ToolGroupService = ToolGroupServiceType;
export type ViewportActionCornersService = ViewportActionCornersServiceType;
export type ColorbarService = ColorbarServiceType;
export interface Services {
cornerstoneViewportService?: CornerstoneViewportServiceType;
toolGroupService?: ToolGroupServiceType;
@@ -28,6 +36,19 @@ declare global {
colorbarService?: ColorbarServiceType;
}
export namespace Segmentation {
export type SegmentRepresentation = SegmentRep;
export type SegmentationData = SegData;
export type SegmentationRepresentation = SegRep;
export type SegmentationInfo = SegInfo;
}
export interface PresentationIds {
lutPresentationId: string;
positionPresentationId: string;
segmentationPresentationId: string;
}
export interface Test {
services?: Services;
cornerstone?: typeof cornerstone;
@@ -1,16 +1,24 @@
import type { Types } from '@cornerstonejs/core';
import { SegmentationRepresentations } from '@cornerstonejs/tools/enums';
/**
* Represents a position presentation in a viewport. This is basically
* viewport specific camera position and zoom, and not the display set
*/
export type PositionPresentation = {
id: string;
viewportType: string;
// The view reference has the basic information as to what image orientation/slice is shown
viewReference: Types.ViewReference;
// The position information has the zoom/pan and possibly other related information, but not LUT
position: Types.ViewPresentation;
viewPresentation: Types.ViewPresentation;
/**
* Optionals
*/
initialImageIndex?: number;
// viewportId helps when hydrating SR or SEG - we can use it to filter
// presentations to get the one prior to the current viewport and reuse it
// for viewReference and viewPresentation
viewportId?: string;
};
/**
@@ -20,15 +28,40 @@ export type PositionPresentation = {
* itself
*/
export interface LutPresentation {
id: string;
viewportType: string;
presentation: Record<string, Types.ViewportProperties> | Types.ViewportProperties;
// either a single object with the properties itself or a map of properties with volumeId keys
properties: Record<string, Types.ViewportProperties> | Types.ViewportProperties;
}
/**
* Represents a LUT presentation in a viewport, and is really related
* to displaySets and not the viewport itself. So that is why it can
* be an object with volumeId keys, or a single object with the properties
* itself
*
* each presentation has a segmentationId and a type and a value for
* hydrated and config.
*
* The hydrated property can be a boolean or null. It's null if the segmentation
* representation hasn't been created yet. It's true if the representation is
* currently in the viewport. It's false if the representation was in the viewport
* but has been removed.
*
* Config is the segmentation config, Todo: add stuff here
*/
export type SegmentationPresentationItem = {
segmentationId: string;
type: SegmentationRepresentations;
hydrated: boolean | null;
config?: unknown;
};
export type SegmentationPresentation = SegmentationPresentationItem[];
/**
* Presentation can be a PositionPresentation or a LutPresentation.
*/
type Presentation = PositionPresentation | LutPresentation;
type Presentation = PositionPresentation | LutPresentation | SegmentationPresentation;
/**
* Viewport presentations object that can contain a positionPresentation
@@ -37,6 +70,7 @@ type Presentation = PositionPresentation | LutPresentation;
export type Presentations = {
positionPresentation?: PositionPresentation;
lutPresentation?: LutPresentation;
segmentationPresentation?: SegmentationPresentation;
};
export default Presentation;
@@ -0,0 +1,78 @@
import { useEffect, useState, memo } from 'react';
const MODALITIES_REQUIRING_CINE_AUTO_MOUNT = ['OT', 'US'];
const ActiveViewportBehavior = memo(
({ servicesManager, viewportId }: withAppTypes<{ viewportId: string }>) => {
const { displaySetService, cineService, viewportGridService, customizationService } =
servicesManager.services;
const [activeViewportId, setActiveViewportId] = useState(viewportId);
useEffect(() => {
const subscription = viewportGridService.subscribe(
viewportGridService.EVENTS.ACTIVE_VIEWPORT_ID_CHANGED,
({ viewportId }) => setActiveViewportId(viewportId)
);
return () => subscription.unsubscribe();
}, [viewportId, viewportGridService]);
useEffect(() => {
if (cineService.isViewportCineClosed(activeViewportId)) {
return;
}
const displaySetInstanceUIDs =
viewportGridService.getDisplaySetsUIDsForViewport(activeViewportId);
if (!displaySetInstanceUIDs) {
return;
}
const displaySets = displaySetInstanceUIDs.map(uid =>
displaySetService.getDisplaySetByUID(uid)
);
if (!displaySets.length) {
return;
}
const modalities = displaySets.map(displaySet => displaySet?.Modality);
const { modalities: sourceModalities } = customizationService.getModeCustomization(
'autoCineModalities',
{
id: 'autoCineModalities',
modalities: MODALITIES_REQUIRING_CINE_AUTO_MOUNT,
}
);
const requiresCine = modalities.some(modality => sourceModalities.includes(modality));
if (requiresCine && !cineService.getState().isCineEnabled) {
cineService.setIsCineEnabled(true);
}
}, [
activeViewportId,
cineService,
viewportGridService,
displaySetService,
customizationService,
]);
return null;
},
arePropsEqual
);
ActiveViewportBehavior.displayName = 'ActiveViewportBehavior';
function arePropsEqual(prevProps, nextProps) {
return (
prevProps.viewportId === nextProps.viewportId &&
prevProps.servicesManager === nextProps.servicesManager
);
}
export default ActiveViewportBehavior;
@@ -8,7 +8,6 @@ import {
BaseVolumeViewport,
} from '@cornerstonejs/core';
import { ToolGroupManager } from '@cornerstonejs/tools';
import PropTypes from 'prop-types';
import { ViewportDownloadForm } from '@ohif/ui';
import { getEnabledElement as OHIFgetEnabledElement } from '../state';
@@ -22,7 +21,7 @@ const CornerstoneViewportDownloadForm = ({
onClose,
activeViewportId: activeViewportIdProp,
cornerstoneViewportService,
}) => {
}: withAppTypes) => {
const enabledElement = OHIFgetEnabledElement(activeViewportIdProp);
const activeViewportElement = enabledElement?.element;
const activeViewportEnabledElement = getEnabledElement(activeViewportElement);
@@ -246,9 +245,4 @@ const CornerstoneViewportDownloadForm = ({
);
};
CornerstoneViewportDownloadForm.propTypes = {
onClose: PropTypes.func,
activeViewportId: PropTypes.string.isRequired,
};
export default CornerstoneViewportDownloadForm;
@@ -161,7 +161,7 @@ class DicomLoaderService {
getDicomDataMethod = fetchIt.bind(this, imageId);
break;
default:
throw new Error(`Unsupported image type: ${loaderType} for imageId: ${imageId}`);
return;
}
return getDicomDataMethod();
@@ -1,5 +1,5 @@
import { getEnabledElement } from '@cornerstonejs/core';
import { IEnabledElement } from '@cornerstonejs/core/dist/esm/types';
import { IEnabledElement } from '@cornerstonejs/core/types';
import { getEnabledElement as OHIFgetEnabledElement } from '../state';
@@ -22,7 +22,7 @@ export default function getCornerstoneViewportType(
return Enums.ViewportType.VIDEO;
}
if (lowerViewportType === WHOLESLIDE) {
return Enums.ViewportType.WholeSlide;
return Enums.ViewportType.WHOLE_SLIDE;
}
if (lowerViewportType === VOLUME || lowerViewportType === ORTHOGRAPHIC) {
@@ -0,0 +1,56 @@
import { CONSTANTS, utilities } from '@cornerstonejs/core';
const { MPR_CAMERA_VALUES } = CONSTANTS;
/**
* Determines the viewport orientation (axial, sagittal, or coronal) based on the image orientation patient values.
* This is done by comparing the view vectors with predefined MPR camera values.
*
* @param imageOrientationPatient - Array of 6 numbers representing the image orientation patient values.
* The first 3 numbers represent the direction cosines of the first row and the second 3 numbers
* represent the direction cosines of the first column.
*
* @returns The viewport orientation as a string ('axial', 'sagittal', 'coronal') or undefined if
* the orientation cannot be determined or if the input is invalid.
*
* @example
* ```typescript
* const orientation = getViewportOrientationFromImageOrientationPatient([1,0,0,0,1,0]);
* console.debug(orientation); // 'axial'
* ```
*/
export const getViewportOrientationFromImageOrientationPatient = (
imageOrientationPatient: number[]
): string | undefined => {
if (!imageOrientationPatient || imageOrientationPatient.length !== 6) {
return undefined;
}
const viewRight = imageOrientationPatient.slice(0, 3);
const viewDown = imageOrientationPatient.slice(3, 6);
const viewUp = [-viewDown[0], -viewDown[1], -viewDown[2]];
// Compare vectors with MPR camera values using utilities.isEqual
if (
utilities.isEqual(viewRight, MPR_CAMERA_VALUES.axial.viewRight) &&
utilities.isEqual(viewUp, MPR_CAMERA_VALUES.axial.viewUp)
) {
return 'axial';
}
if (
utilities.isEqual(viewRight, MPR_CAMERA_VALUES.sagittal.viewRight) &&
utilities.isEqual(viewUp, MPR_CAMERA_VALUES.sagittal.viewUp)
) {
return 'sagittal';
}
if (
utilities.isEqual(viewRight, MPR_CAMERA_VALUES.coronal.viewRight) &&
utilities.isEqual(viewUp, MPR_CAMERA_VALUES.coronal.viewUp)
) {
return 'coronal';
}
return undefined;
};
@@ -1,5 +1,7 @@
import SUPPORTED_TOOLS from './constants/supportedTools';
import { getDisplayUnit } from './utils';
import { getIsLocked } from './utils/getIsLocked';
import { getIsVisible } from './utils/getIsVisible';
import getSOPInstanceAttributes from './utils/getSOPInstanceAttributes';
import { utils } from '@ohif/core';
@@ -19,9 +21,11 @@ const Angle = {
getValueTypeFromToolType,
customizationService
) => {
const { annotation, viewportId } = csToolsEventDetail;
const { annotation } = csToolsEventDetail;
const { metadata, data, annotationUID } = annotation;
const isLocked = getIsLocked(annotationUID);
const isVisible = getIsVisible(annotationUID);
if (!metadata || !data) {
console.warn('Length tool: Missing metadata or data');
return null;
@@ -55,7 +59,7 @@ const Angle = {
const mappedAnnotations = getMappedAnnotations(annotation, displaySetService);
const displayText = getDisplayText(mappedAnnotations, displaySet, customizationService);
const displayText = getDisplayText(mappedAnnotations, displaySet);
const getReport = () =>
_getReport(mappedAnnotations, points, FrameOfReferenceUID, customizationService);
@@ -65,6 +69,8 @@ const Angle = {
FrameOfReferenceUID,
points,
textBox,
isLocked,
isVisible,
metadata,
referenceSeriesUID: SeriesInstanceUID,
referenceStudyUID: StudyInstanceUID,
@@ -158,12 +164,15 @@ function _getReport(mappedAnnotations, points, FrameOfReferenceUID, customizatio
};
}
function getDisplayText(mappedAnnotations, displaySet, customizationService) {
if (!mappedAnnotations || !mappedAnnotations.length) {
return '';
}
function getDisplayText(mappedAnnotations, displaySet) {
const displayText = {
primary: [],
secondary: [],
};
const displayText = [];
if (!mappedAnnotations || !mappedAnnotations.length) {
return displayText;
}
// Area is the same for all series
const { angle, unit, SeriesNumber, SOPInstanceUID, frameNumber } = mappedAnnotations[0];
@@ -181,9 +190,9 @@ function getDisplayText(mappedAnnotations, displaySet, customizationService) {
return displayText;
}
const roundedAngle = utils.roundNumber(angle, 2);
displayText.push(
`${roundedAngle} ${getDisplayUnit(unit)} (S: ${SeriesNumber}${instanceText}${frameText})`
);
displayText.primary.push(`${roundedAngle} ${getDisplayUnit(unit)}`);
displayText.secondary.push(`S: ${SeriesNumber}${instanceText}${frameText}`);
return displayText;
}
@@ -1,6 +1,7 @@
import SUPPORTED_TOOLS from './constants/supportedTools';
import { getIsLocked } from './utils/getIsLocked';
import getSOPInstanceAttributes from './utils/getSOPInstanceAttributes';
import { getIsVisible } from './utils/getIsVisible';
const Length = {
toAnnotation: measurement => {},
@@ -17,9 +18,11 @@ const Length = {
getValueTypeFromToolType,
customizationService
) => {
const { annotation, viewportId } = csToolsEventDetail;
const { annotation } = csToolsEventDetail;
const { metadata, data, annotationUID } = annotation;
const isLocked = getIsLocked(annotationUID);
const isVisible = getIsVisible(annotationUID);
if (!metadata || !data) {
console.warn('Length tool: Missing metadata or data');
return null;
@@ -53,7 +56,7 @@ const Length = {
const mappedAnnotations = getMappedAnnotations(annotation, displaySetService);
const displayText = getDisplayText(mappedAnnotations, displaySet, customizationService);
const displayText = getDisplayText(mappedAnnotations, displaySet);
return {
uid: annotationUID,
@@ -61,6 +64,8 @@ const Length = {
FrameOfReferenceUID,
points,
textBox,
isLocked,
isVisible,
metadata,
referenceSeriesUID: SeriesInstanceUID,
referenceStudyUID: StudyInstanceUID,
@@ -107,15 +112,17 @@ function getMappedAnnotations(annotation, displaySetService) {
return annotations;
}
function getDisplayText(mappedAnnotations, displaySet, customizationService) {
if (!mappedAnnotations) {
return '';
function getDisplayText(mappedAnnotations, displaySet) {
const displayText = {
primary: [],
secondary: [],
};
if (!mappedAnnotations || !mappedAnnotations.length) {
return displayText;
}
const displayText = [];
// Area is the same for all series
const { SeriesNumber, SOPInstanceUID, frameNumber } = mappedAnnotations[0];
const { SeriesNumber, SOPInstanceUID, frameNumber, text } = mappedAnnotations[0];
const instance = displaySet.instances.find(image => image.SOPInstanceUID === SOPInstanceUID);
@@ -127,7 +134,13 @@ function getDisplayText(mappedAnnotations, displaySet, customizationService) {
const instanceText = InstanceNumber ? ` I: ${InstanceNumber}` : '';
const frameText = displaySet.isMultiFrame ? ` F: ${frameNumber}` : '';
displayText.push(`(S: ${SeriesNumber}${instanceText}${frameText})`);
// Add the annotation text to the primary array
if (text) {
displayText.primary.push(text);
}
// Add the series information to the secondary array
displayText.secondary.push(`S: ${SeriesNumber}${instanceText}${frameText}`);
return displayText;
}
@@ -1,9 +1,9 @@
import { annotation } from '@cornerstonejs/tools';
import SUPPORTED_TOOLS from './constants/supportedTools';
import getSOPInstanceAttributes from './utils/getSOPInstanceAttributes';
import { utils } from '@ohif/core';
import { getDisplayUnit } from './utils';
import { getIsLocked } from './utils/getIsLocked';
import { getIsVisible } from './utils/getIsVisible';
const Bidirectional = {
toAnnotation: measurement => {},
@@ -14,9 +14,12 @@ const Bidirectional = {
getValueTypeFromToolType,
customizationService
) => {
const { annotation, viewportId } = csToolsEventDetail;
const { annotation } = csToolsEventDetail;
const { metadata, data, annotationUID } = annotation;
const isLocked = getIsLocked(annotationUID);
const isVisible = getIsVisible(annotationUID);
if (!metadata || !data) {
console.warn('Length tool: Missing metadata or data');
return null;
@@ -50,7 +53,7 @@ const Bidirectional = {
const mappedAnnotations = getMappedAnnotations(annotation, displaySetService);
const displayText = getDisplayText(mappedAnnotations, displaySet, customizationService);
const displayText = getDisplayText(mappedAnnotations, displaySet);
const getReport = () =>
_getReport(mappedAnnotations, points, FrameOfReferenceUID, customizationService);
@@ -60,6 +63,8 @@ const Bidirectional = {
FrameOfReferenceUID,
points,
textBox,
isLocked,
isVisible,
metadata,
referenceSeriesUID: SeriesInstanceUID,
referenceStudyUID: StudyInstanceUID,
@@ -153,12 +158,15 @@ function _getReport(mappedAnnotations, points, FrameOfReferenceUID, customizatio
};
}
function getDisplayText(mappedAnnotations, displaySet, customizationService) {
if (!mappedAnnotations || !mappedAnnotations.length) {
return '';
}
function getDisplayText(mappedAnnotations, displaySet) {
const displayText = {
primary: [],
secondary: [],
};
const displayText = [];
if (!mappedAnnotations || !mappedAnnotations.length) {
return displayText;
}
// Area is the same for all series
const { length, width, unit, SeriesNumber, SOPInstanceUID, frameNumber } = mappedAnnotations[0];
@@ -175,10 +183,9 @@ function getDisplayText(mappedAnnotations, displaySet, customizationService) {
const instanceText = InstanceNumber ? ` I: ${InstanceNumber}` : '';
const frameText = displaySet.isMultiFrame ? ` F: ${frameNumber}` : '';
displayText.push(
`L: ${roundedLength} ${getDisplayUnit(unit)} (S: ${SeriesNumber}${instanceText}${frameText})`
);
displayText.push(`W: ${roundedWidth} ${getDisplayUnit(unit)}`);
displayText.primary.push(`L: ${roundedLength} ${getDisplayUnit(unit)}`);
displayText.primary.push(`W: ${roundedWidth} ${getDisplayUnit(unit)}`);
displayText.secondary.push(`S: ${SeriesNumber}${instanceText}${frameText}`);
return displayText;
}
@@ -3,6 +3,8 @@ import { getDisplayUnit } from './utils';
import getSOPInstanceAttributes from './utils/getSOPInstanceAttributes';
import { utils } from '@ohif/core';
import { getStatisticDisplayString } from './utils/getValueDisplayString';
import { getIsLocked } from './utils/getIsLocked';
import { getIsVisible } from './utils/getIsVisible';
const CircleROI = {
toAnnotation: measurement => {},
@@ -13,9 +15,12 @@ const CircleROI = {
getValueTypeFromToolType,
customizationService
) => {
const { annotation, viewportId } = csToolsEventDetail;
const { annotation } = csToolsEventDetail;
const { metadata, data, annotationUID } = annotation;
const isLocked = getIsLocked(annotationUID);
const isVisible = getIsVisible(annotationUID);
if (!metadata || !data) {
console.warn('Length tool: Missing metadata or data');
return null;
@@ -49,7 +54,7 @@ const CircleROI = {
const mappedAnnotations = getMappedAnnotations(annotation, displaySetService);
const displayText = getDisplayText(mappedAnnotations, displaySet, customizationService);
const displayText = getDisplayText(mappedAnnotations, displaySet);
const getReport = () =>
_getReport(mappedAnnotations, points, FrameOfReferenceUID, customizationService);
@@ -59,6 +64,8 @@ const CircleROI = {
FrameOfReferenceUID,
points,
textBox,
isLocked,
isVisible,
metadata,
referenceSeriesUID: SeriesInstanceUID,
referenceStudyUID: StudyInstanceUID,
@@ -161,12 +168,15 @@ function _getReport(mappedAnnotations, points, FrameOfReferenceUID, customizatio
};
}
function getDisplayText(mappedAnnotations, displaySet, customizationService) {
if (!mappedAnnotations || !mappedAnnotations.length) {
return '';
}
function getDisplayText(mappedAnnotations, displaySet) {
const displayText = {
primary: [],
secondary: [],
};
const displayText = [];
if (!mappedAnnotations || !mappedAnnotations.length) {
return displayText;
}
// Area is the same for all series
const { area, SOPInstanceUID, frameNumber, areaUnit } = mappedAnnotations[0];
@@ -183,7 +193,7 @@ function getDisplayText(mappedAnnotations, displaySet, customizationService) {
// Area sometimes becomes undefined if `preventHandleOutsideImage` is off.
const roundedArea = utils.roundNumber(area || 0, 2);
displayText.push(`${roundedArea} ${getDisplayUnit(areaUnit)}`);
displayText.primary.push(`${roundedArea} ${getDisplayUnit(areaUnit)}`);
// Todo: we need a better UI for displaying all these information
mappedAnnotations.forEach(mappedAnnotation => {
@@ -191,10 +201,8 @@ function getDisplayText(mappedAnnotations, displaySet, customizationService) {
const maxStr = getStatisticDisplayString(max, unit, 'max');
const str = `${maxStr}(S:${SeriesNumber}${instanceText}${frameText})`;
if (!displayText.includes(str)) {
displayText.push(str);
}
displayText.primary.push(maxStr);
displayText.secondary.push(`S: ${SeriesNumber}${instanceText}${frameText}`);
});
return displayText;
@@ -1,5 +1,7 @@
import SUPPORTED_TOOLS from './constants/supportedTools';
import { getDisplayUnit } from './utils';
import { getIsLocked } from './utils/getIsLocked';
import { getIsVisible } from './utils/getIsVisible';
import getSOPInstanceAttributes from './utils/getSOPInstanceAttributes';
import { utils } from '@ohif/core';
@@ -19,9 +21,12 @@ const CobbAngle = {
getValueTypeFromToolType,
customizationService
) => {
const { annotation, viewportId } = csToolsEventDetail;
const { annotation } = csToolsEventDetail;
const { metadata, data, annotationUID } = annotation;
const isLocked = getIsLocked(annotationUID);
const isVisible = getIsVisible(annotationUID);
if (!metadata || !data) {
console.warn('Cobb Angle tool: Missing metadata or data');
return null;
@@ -55,7 +60,7 @@ const CobbAngle = {
const mappedAnnotations = getMappedAnnotations(annotation, displaySetService);
const displayText = getDisplayText(mappedAnnotations, displaySet, customizationService);
const displayText = getDisplayText(mappedAnnotations, displaySet);
const getReport = () =>
_getReport(mappedAnnotations, points, FrameOfReferenceUID, customizationService);
@@ -65,6 +70,8 @@ const CobbAngle = {
FrameOfReferenceUID,
points,
textBox,
isLocked,
isVisible,
metadata,
referenceSeriesUID: SeriesInstanceUID,
referenceStudyUID: StudyInstanceUID,
@@ -158,14 +165,17 @@ function _getReport(mappedAnnotations, points, FrameOfReferenceUID, customizatio
};
}
function getDisplayText(mappedAnnotations, displaySet, customizationService) {
function getDisplayText(mappedAnnotations, displaySet) {
const displayText = {
primary: [],
secondary: [],
};
if (!mappedAnnotations || !mappedAnnotations.length) {
return '';
return displayText;
}
const displayText = [];
// Area is the same for all series
// Angle is the same for all series
const { angle, unit, SeriesNumber, SOPInstanceUID, frameNumber } = mappedAnnotations[0];
const instance = displaySet.instances.find(image => image.SOPInstanceUID === SOPInstanceUID);
@@ -181,9 +191,9 @@ function getDisplayText(mappedAnnotations, displaySet, customizationService) {
return displayText;
}
const roundedAngle = utils.roundNumber(angle, 2);
displayText.push(
`${roundedAngle} ${getDisplayUnit(unit)} (S: ${SeriesNumber}${instanceText}${frameText})`
);
displayText.primary.push(`${roundedAngle} ${getDisplayUnit(unit)}`);
displayText.secondary.push(`S: ${SeriesNumber}${instanceText}${frameText}`);
return displayText;
}
Loaded 100 of 803 files, more files were not shown because too many files have changed in this diff. Show more