OHIF with Cornerstone3D 2.0 (#4347)

This commit is contained in:
Alireza 2024-11-06 07:15:27 -05:00 committed by GitHub
parent 8981a61b8c
commit 7ce7127d7c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
803 changed files with 32760 additions and 15893 deletions

View File

@ -180,10 +180,6 @@ jobs:
- run: - run:
name: Authenticate with NPM registry name: Authenticate with NPM registry
command: echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" > ~/repo/.npmrc command: echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" > ~/repo/.npmrc
- run:
name: Increase the event emitter limit
command: |
node ./increaseEventEmitterLimit.mjs
- run: - run:
name: build half of the packages (to avoid out of memory in circleci) name: build half of the packages (to avoid out of memory in circleci)
command: | command: |
@ -230,10 +226,6 @@ jobs:
- run: - run:
name: Authenticate with NPM registry name: Authenticate with NPM registry
command: echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" > ~/repo/.npmrc command: echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" > ~/repo/.npmrc
- run:
name: Increase the event emitter limit
command: |
node ./increaseEventEmitterLimit.mjs
- run: - run:
name: build half of the packages (to avoid out of memory in circleci) name: build half of the packages (to avoid out of memory in circleci)
command: | command: |

View File

@ -144,11 +144,6 @@ module.exports = (env, argv, { SRC_DIR, ENTRY }) => {
}, },
], ],
}, },
{
test: /\.js$/,
enforce: 'pre',
use: 'source-map-loader',
},
transpileJavaScriptRule(mode), transpileJavaScriptRule(mode),
loadWebWorkersRule, loadWebWorkersRule,
// loadShadersRule, // loadShadersRule,
@ -188,8 +183,8 @@ module.exports = (env, argv, { SRC_DIR, ENTRY }) => {
'@hooks': path.resolve(__dirname, '../platform/app/src/hooks'), '@hooks': path.resolve(__dirname, '../platform/app/src/hooks'),
'@routes': path.resolve(__dirname, '../platform/app/src/routes'), '@routes': path.resolve(__dirname, '../platform/app/src/routes'),
'@state': path.resolve(__dirname, '../platform/app/src/state'), '@state': path.resolve(__dirname, '../platform/app/src/state'),
'@cornerstonejs/dicom-image-loader': 'dicom-microscopy-viewer':
'@cornerstonejs/dicom-image-loader/dist/dynamic-import/cornerstoneDICOMImageLoader.min.js', 'dicom-microscopy-viewer/dist/dynamic-import/dicomMicroscopyViewer.min.js',
}, },
// Which directories to search when resolving modules // Which directories to search when resolving modules
modules: [ modules: [
@ -217,7 +212,7 @@ module.exports = (env, argv, { SRC_DIR, ENTRY }) => {
new webpack.ProvidePlugin({ new webpack.ProvidePlugin({
Buffer: ['buffer', 'Buffer'], Buffer: ['buffer', 'Buffer'],
}), }),
...(isProdBuild ? [] : [new ReactRefreshWebpackPlugin()]), ...(isProdBuild ? [] : [new ReactRefreshWebpackPlugin({ overlay: false })]),
// Uncomment to generate bundle analyzer // Uncomment to generate bundle analyzer
// new BundleAnalyzerPlugin(), // new BundleAnalyzerPlugin(),
], ],

View File

@ -288,3 +288,10 @@ Bloch, B. Nicolas, Jain, Ashali, & Jaffe, C. Carl. (2015). BREAST-DIAGNOSIS [Dat
Comstock, C. E., Gatsonis, C., Newstead, G. M., Snyder, B. S., Gareen, I. F., Bergin, J. T., Rahbar, H., Sung, J. S., Jacobs, C., Harvey, J. A., Nicholson, M. H., Ward, R. C., Holt, J., Prather, A., Miller, K. D., Schnall, M. D., & Kuhl, C. K. (2023). Abbreviated Breast MRI and Digital Tomosynthesis Mammography in Screening Women With Dense Breasts (EA1141) (Version 1) [dataset]. The Cancer Imaging Archive. https://doi.org/10.7937/2BAS-HR33 Comstock, C. E., Gatsonis, C., Newstead, G. M., Snyder, B. S., Gareen, I. F., Bergin, J. T., Rahbar, H., Sung, J. S., Jacobs, C., Harvey, J. A., Nicholson, M. H., Ward, R. C., Holt, J., Prather, A., Miller, K. D., Schnall, M. D., & Kuhl, C. K. (2023). Abbreviated Breast MRI and Digital Tomosynthesis Mammography in Screening Women With Dense Breasts (EA1141) (Version 1) [dataset]. The Cancer Imaging Archive. https://doi.org/10.7937/2BAS-HR33
https://www.cancerimagingarchive.net/collection/ea1141/ https://www.cancerimagingarchive.net/collection/ea1141/
### 1.2.276.0.7230010.3.1.2.2155604110.4180.1021041295.21
From OFFIS DICOM-Team
https://www.offis.de/
OFFIS DICOM-Team

View File

@ -10,7 +10,7 @@
}, },
"dependencies": { "dependencies": {
"@babel/runtime": "^7.20.13", "@babel/runtime": "^7.20.13",
"@kitware/vtk.js": "30.4.1", "@kitware/vtk.js": "32.1.0",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"core-js": "^3.2.1", "core-js": "^3.2.1",
"moment": "^2.9.4" "moment": "^2.9.4"
@ -77,7 +77,7 @@
"stylus": "^0.59.0", "stylus": "^0.59.0",
"stylus-loader": "^7.1.3", "stylus-loader": "^7.1.3",
"terser-webpack-plugin": "^5.1.4", "terser-webpack-plugin": "^5.1.4",
"typescript": "4.6.4", "typescript": "5.5.4",
"unused-webpack-plugin": "2.4.0", "unused-webpack-plugin": "2.4.0",
"webpack": "5.94.0", "webpack": "5.94.0",
"webpack-bundle-analyzer": "^4.8.0", "webpack-bundle-analyzer": "^4.8.0",

View File

@ -45,7 +45,7 @@
"glob-parent": "^6.0.2", "glob-parent": "^6.0.2",
"trim": "^1.0.0", "trim": "^1.0.0",
"package-json": "^8.1.0", "package-json": "^8.1.0",
"typescript": "4.6.4", "typescript": "5.5.4",
"sharp": "^0.32.6" "sharp": "^0.32.6"
} }
} }

View File

@ -46,9 +46,9 @@
}, },
"dependencies": { "dependencies": {
"@babel/runtime": "^7.20.13", "@babel/runtime": "^7.20.13",
"@cornerstonejs/adapters": "^1.86.0", "@cornerstonejs/adapters": "^2.1.13",
"@cornerstonejs/core": "^1.86.0", "@cornerstonejs/core": "^2.1.13",
"@kitware/vtk.js": "30.4.1", "@kitware/vtk.js": "32.1.0",
"react-color": "^2.19.3" "react-color": "^2.19.3"
} }
} }

View File

@ -97,7 +97,7 @@ function _getDisplaySetsFromSeries(
}; };
// Does not get the referenced volumeId during parametric displaySet creation because the // 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 = () => { displaySet.getReferencedVolumeId = () => {
if (displaySet.referencedVolumeId) { if (displaySet.referencedVolumeId) {
return displaySet.referencedVolumeId; return displaySet.referencedVolumeId;
@ -119,6 +119,22 @@ function _getDisplaySetsFromSeries(
return [displaySet]; 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( async function _load(
displaySet, displaySet,
servicesManager: AppTypes.ServicesManager, servicesManager: AppTypes.ServicesManager,
@ -132,11 +148,11 @@ async function _load(
return volumeLoadObject.promise; 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 // 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. // and also return the same promise to any other callers.
// loadPromises[SOPInstanceUID] = new Promise(async (resolve, reject) => {
const promise = _loadParametricMap({ const promise = _loadParametricMap({
extensionManager, extensionManager,
displaySet, displaySet,
@ -149,18 +165,26 @@ async function _load(
promise promise
.then(() => { .then(() => {
displaySet.loadStatus.loading = false; displaySet.loading = false;
displaySet.loadStatus.loaded = true; displaySet.isLoaded = true;
// Broadcast that loading is complete
servicesManager.services.segmentationService._broadcastEvent(
servicesManager.services.segmentationService.EVENTS.SEGMENTATION_LOADING_COMPLETE,
{
pmapDisplaySet: displaySet,
}
);
}) })
.catch(err => { .catch(err => {
displaySet.loadStatus.loading = false; displaySet.loading = false;
displaySet.isLoaded = false;
throw err; throw err;
}); });
return promise; return promise;
} }
async function _loadParametricMap({ extensionManager, displaySet, headers }: withAppTypes) { async function _loadParametricMap({ displaySet, headers }: withAppTypes) {
const arrayBuffer = await dicomLoaderService.findDicomDataPromise(displaySet, null, headers); const arrayBuffer = await dicomLoaderService.findDicomDataPromise(displaySet, null, headers);
const referencedVolumeId = displaySet.getReferencedVolumeId(); const referencedVolumeId = displaySet.getReferencedVolumeId();
const cachedReferencedVolume = cache.getVolume(referencedVolumeId); const cachedReferencedVolume = cache.getVolume(referencedVolumeId);
@ -189,9 +213,12 @@ async function _loadParametricMap({ extensionManager, displaySet, headers }: wit
}, },
}); });
derivedVolume.getScalarData().set(pixelData); const newPixelData = new TypedArrayConstructor(pixelData.length);
for (let i = 0; i < pixelData.length; i++) {
const range = derivedVolume.imageData.getPointData().getScalars().getRange(); newPixelData[i] = pixelData[i] * 100;
}
derivedVolume.voxelManager.setCompleteScalarDataArray(newPixelData);
const range = getRangeFromPixelData(newPixelData);
const windowLevel = csUtils.windowLevel.toWindowLevel(range[0], range[1]); const windowLevel = csUtils.windowLevel.toWindowLevel(range[0], range[1]);
derivedVolume.metadata.voiLut = [windowLevel]; derivedVolume.metadata.voiLut = [windowLevel];

View File

@ -1,6 +0,0 @@
function createPMAPToolGroupAndAddTools(ToolGroupService, customizationService, toolGroupId) {
const { tools } = customizationService.get('cornerstone.overlayViewportTools') ?? {};
return ToolGroupService.createToolGroupAndAddTools(toolGroupId, tools);
}
export default createPMAPToolGroupAndAddTools;

View File

@ -1,16 +1,19 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React, { useCallback, useEffect, useRef } from 'react'; import React, { useCallback, useEffect, useRef, useState } from 'react';
import { useViewportGrid } from '@ohif/ui'; import { useViewportGrid, LoadingIndicatorTotalPercent } from '@ohif/ui';
import createPMAPToolGroupAndAddTools from '../utils/initPMAPToolGroup';
const PMAP_TOOLGROUP_BASE_NAME = 'PMAPToolGroup';
function OHIFCornerstonePMAPViewport(props: withAppTypes) { function OHIFCornerstonePMAPViewport(props: withAppTypes) {
const { displaySets, viewportOptions, displaySetOptions, servicesManager, extensionManager } = const {
props; displaySets,
children,
viewportOptions,
displaySetOptions,
servicesManager,
extensionManager,
} = props;
const viewportId = viewportOptions.viewportId; const viewportId = viewportOptions.viewportId;
const { displaySetService, toolGroupService, customizationService } = servicesManager.services; const { displaySetService, segmentationService, uiNotificationService } =
const toolGroupId = `${PMAP_TOOLGROUP_BASE_NAME}-${viewportId}`; servicesManager.services;
// PMAP viewport will always have a single display set // PMAP viewport will always have a single display set
if (displaySets.length !== 1) { if (displaySets.length !== 1) {
@ -32,6 +35,24 @@ function OHIFCornerstonePMAPViewport(props: withAppTypes) {
metadata: referencedDisplaySetMetadata, 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 getCornerstoneViewport = useCallback(() => {
const { displaySet: referencedDisplaySet } = referencedDisplaySetRef.current; const { displaySet: referencedDisplaySet } = referencedDisplaySetRef.current;
const { component: Component } = extensionManager.getModuleEntry( const { component: Component } = extensionManager.getModuleEntry(
@ -46,12 +67,25 @@ function OHIFCornerstonePMAPViewport(props: withAppTypes) {
Object.assign(pmapDisplaySetOptions.options, { Object.assign(pmapDisplaySetOptions.options, {
colormap: { colormap: {
name: 'rainbow', name: 'rainbow_2',
opacity: [ opacity: [
{ value: 0, opacity: 0.5 }, { value: 0, opacity: 0 },
{ value: 1, opacity: 1 }, { 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 ( return (
@ -61,7 +95,6 @@ function OHIFCornerstonePMAPViewport(props: withAppTypes) {
displaySets={[referencedDisplaySet, pmapDisplaySet]} displaySets={[referencedDisplaySet, pmapDisplaySet]}
viewportOptions={{ viewportOptions={{
viewportType: 'volume', viewportType: 'volume',
toolGroupId: toolGroupId,
orientation: viewportOptions.orientation, orientation: viewportOptions.orientation,
viewportId: viewportOptions.viewportId, viewportId: viewportOptions.viewportId,
}} }}
@ -73,7 +106,6 @@ function OHIFCornerstonePMAPViewport(props: withAppTypes) {
displaySetOptions, displaySetOptions,
props, props,
pmapDisplaySet, pmapDisplaySet,
toolGroupId,
viewportOptions.orientation, viewportOptions.orientation,
viewportOptions.viewportId, viewportOptions.viewportId,
]); ]);
@ -98,23 +130,33 @@ function OHIFCornerstonePMAPViewport(props: withAppTypes) {
}; };
}, [activeViewportId, displaySetService, viewportGridService, viewports]); }, [activeViewportId, displaySetService, viewportGridService, viewports]);
useEffect(() => { let childrenWithProps = null;
let toolGroup = toolGroupService.getToolGroup(toolGroupId);
if (toolGroup) { if (children && children.length) {
return; childrenWithProps = children.map((child, index) => {
} return (
child &&
// This creates a custom tool group which has the lifetime of this view only React.cloneElement(child, {
toolGroup = createPMAPToolGroupAndAddTools(toolGroupService, customizationService, toolGroupId); viewportId,
key: index,
return () => toolGroupService.destroyToolGroup(toolGroupId); })
}, [customizationService, toolGroupId, toolGroupService]); );
});
}
return ( return (
<> <>
<div className="relative flex h-full w-full flex-row overflow-hidden"> <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()} {getCornerstoneViewport()}
{childrenWithProps}
</div> </div>
</> </>
); );

View File

@ -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;

View File

@ -72,22 +72,27 @@ function _getDisplaySetsFromSeries(
displaySet.referencedImages = instance.ReferencedSeriesSequence.ReferencedInstanceSequence; displaySet.referencedImages = instance.ReferencedSeriesSequence.ReferencedInstanceSequence;
displaySet.referencedSeriesInstanceUID = referencedSeries.SeriesInstanceUID; displaySet.referencedSeriesInstanceUID = referencedSeries.SeriesInstanceUID;
displaySet.getReferenceDisplaySet = () => { const { displaySetService } = servicesManager.services;
const { displaySetService } = servicesManager.services; const referencedDisplaySets = displaySetService.getDisplaySetsForSeries(
const referencedDisplaySets = displaySetService.getDisplaySetsForSeries( displaySet.referencedSeriesInstanceUID
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();
}
}
); );
} else {
if (!referencedDisplaySets || referencedDisplaySets.length === 0) {
throw new Error('Referenced DisplaySet is missing for the RT');
}
const referencedDisplaySet = referencedDisplaySets[0]; const referencedDisplaySet = referencedDisplaySets[0];
displaySet.referencedDisplaySetInstanceUID = referencedDisplaySet.displaySetInstanceUID; displaySet.referencedDisplaySetInstanceUID = referencedDisplaySet.displaySetInstanceUID;
}
return referencedDisplaySet;
};
displaySet.load = ({ headers }) => _load(displaySet, servicesManager, extensionManager, headers); 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. // and also return the same promise to any other callers.
loadPromises[SOPInstanceUID] = new Promise(async (resolve, reject) => { loadPromises[SOPInstanceUID] = new Promise(async (resolve, reject) => {
if (!rtDisplaySet.structureSet) { if (!rtDisplaySet.structureSet) {
const structureSet = await loadRTStruct( const structureSet = await loadRTStruct(extensionManager, rtDisplaySet, headers);
extensionManager,
rtDisplaySet,
rtDisplaySet.getReferenceDisplaySet(),
headers
);
rtDisplaySet.structureSet = structureSet; rtDisplaySet.structureSet = structureSet;
} }
const suppressEvents = true;
segmentationService segmentationService
.createSegmentationForRTDisplaySet(rtDisplaySet, null, suppressEvents) .createSegmentationForRTDisplaySet(rtDisplaySet)
.then(() => { .then(() => {
rtDisplaySet.loading = false; rtDisplaySet.loading = false;
resolve(); resolve();

View File

@ -2,6 +2,7 @@ import { id } from './id';
import React from 'react'; import React from 'react';
import { Types } from '@ohif/core'; import { Types } from '@ohif/core';
import getSopClassHandlerModule from './getSopClassHandlerModule'; import getSopClassHandlerModule from './getSopClassHandlerModule';
import getCommandsModule from './getCommandsModule';
const Component = React.lazy(() => { const Component = React.lazy(() => {
return import(/* webpackPrefetch: true */ './viewports/OHIFCornerstoneRTViewport'); 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. * You ID can be anything you want, but it should be unique.
*/ */
id, id,
getCommandsModule,
/** /**
* PanelModule should provide a list of panels that will be available in OHIF * PanelModule should provide a list of panels that will be available in OHIF

View File

@ -89,12 +89,7 @@ async function checkAndLoadContourData(instance, datasource) {
}); });
} }
export default async function loadRTStruct( export default async function loadRTStruct(extensionManager, rtStructDisplaySet, headers) {
extensionManager,
rtStructDisplaySet,
referencedDisplaySet,
headers
) {
const utilityModule = extensionManager.getModuleEntry( const utilityModule = extensionManager.getModuleEntry(
'@ohif/extension-cornerstone.utilityModule.common' '@ohif/extension-cornerstone.utilityModule.common'
); );
@ -102,8 +97,6 @@ export default async function loadRTStruct(
const { bulkDataURI } = dataSource.getConfig?.() || {}; const { bulkDataURI } = dataSource.getConfig?.() || {};
const { dicomLoaderService } = utilityModule.exports; const { dicomLoaderService } = utilityModule.exports;
const imageIdSopInstanceUidPairs =
_getImageIdSopInstanceUidPairsForDisplaySet(referencedDisplaySet);
// Set here is loading is asynchronous. // Set here is loading is asynchronous.
// If this function throws its set back to false. // If this function throws its set back to false.
@ -149,8 +142,7 @@ export default async function loadRTStruct(
const contourPoints = []; const contourPoints = [];
for (let c = 0; c < ContourSequenceArray.length; c++) { for (let c = 0; c < ContourSequenceArray.length; c++) {
const { ContourImageSequence, ContourData, NumberOfContourPoints, ContourGeometricType } = const { ContourData, NumberOfContourPoints, ContourGeometricType } = ContourSequenceArray[c];
ContourSequenceArray[c];
let isSupported = false; let isSupported = false;
@ -194,24 +186,6 @@ export default async function loadRTStruct(
return structureSet; 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( function _setROIContourMetadata(
structureSet, structureSet,
StructureSetROISequence, StructureSetROISequence,

View File

@ -10,7 +10,6 @@ function promptHydrateRT({
servicesManager, servicesManager,
rtDisplaySet, rtDisplaySet,
viewportId, viewportId,
toolGroupId = 'default',
preHydrateCallbacks, preHydrateCallbacks,
hydrateRTDisplaySet, hydrateRTDisplaySet,
}: withAppTypes) { }: withAppTypes) {
@ -27,16 +26,13 @@ function promptHydrateRT({
callback(); callback();
}); });
window.setTimeout(async () => { const isHydrated = await hydrateRTDisplaySet({
const isHydrated = await hydrateRTDisplaySet({ rtDisplaySet,
rtDisplaySet, viewportId,
viewportId, servicesManager,
toolGroupId, });
servicesManager,
});
resolve(isHydrated); resolve(isHydrated);
}, 0);
} }
}); });
} }

View File

@ -4,7 +4,9 @@ import { useViewportGrid, LoadingIndicatorTotalPercent, ViewportActionArrows } f
import promptHydrateRT from '../utils/promptHydrateRT'; import promptHydrateRT from '../utils/promptHydrateRT';
import _getStatusComponent from './_getStatusComponent'; import _getStatusComponent from './_getStatusComponent';
import createRTToolGroupAndAddTools from '../utils/initRTToolGroup'; import createRTToolGroupAndAddTools from '../utils/initRTToolGroup';
import { SegmentationRepresentations } from '@cornerstonejs/tools/enums';
const RT_TOOLGROUP_BASE_NAME = 'RTToolGroup'; const RT_TOOLGROUP_BASE_NAME = 'RTToolGroup';
@ -62,7 +64,10 @@ function OHIFCornerstoneRTViewport(props: withAppTypes) {
const { viewports, activeViewportId } = viewportGrid; const { viewports, activeViewportId } = viewportGrid;
const referencedDisplaySet = rtDisplaySet.getReferenceDisplaySet(); const referencedDisplaySetInstanceUID = rtDisplaySet.referencedDisplaySetInstanceUID;
const referencedDisplaySet = displaySetService.getDisplaySetByUID(
referencedDisplaySetInstanceUID
);
const referencedDisplaySetMetadata = _getReferencedDisplaySetMetadata(referencedDisplaySet); const referencedDisplaySetMetadata = _getReferencedDisplaySetMetadata(referencedDisplaySet);
referencedDisplaySetRef.current = { referencedDisplaySetRef.current = {
@ -91,12 +96,15 @@ function OHIFCornerstoneRTViewport(props: withAppTypes) {
}); });
}, [viewportGrid]); }, [viewportGrid]);
const hydrateRTDisplaySet = ({ rtDisplaySet, viewportId }) => { const hydrateRTDisplaySet = useCallback(
commandsManager.runCommand('loadSegmentationDisplaySetsForViewport', { ({ rtDisplaySet, viewportId }) => {
displaySets: [rtDisplaySet], commandsManager.runCommand('hydrateRTSDisplaySet', {
viewportId, displaySet: rtDisplaySet,
}); viewportId,
}; });
},
[commandsManager]
);
const getCornerstoneViewport = useCallback(() => { const getCornerstoneViewport = useCallback(() => {
const { component: Component } = extensionManager.getModuleEntry( const { component: Component } = extensionManager.getModuleEntry(
@ -143,10 +151,10 @@ function OHIFCornerstoneRTViewport(props: withAppTypes) {
newSelectedSegmentIndex = numberOfSegments - 1; newSelectedSegmentIndex = numberOfSegments - 1;
} }
segmentationService.jumpToSegmentCenter(segmentationId, newSelectedSegmentIndex, toolGroupId); segmentationService.jumpToSegmentCenter(segmentationId, newSelectedSegmentIndex, viewportId);
setSelectedSegment(newSelectedSegmentIndex); setSelectedSegment(newSelectedSegmentIndex);
}, },
[selectedSegment] [selectedSegment, segmentationService]
); );
useEffect(() => { useEffect(() => {
@ -243,7 +251,7 @@ function OHIFCornerstoneRTViewport(props: withAppTypes) {
return () => { return () => {
// remove the segmentation representations if seg displayset changed // remove the segmentation representations if seg displayset changed
segmentationService.removeSegmentationRepresentationFromToolGroup(toolGroupId); segmentationService.removeSegmentationRepresentations(viewportId);
toolGroupService.destroyToolGroup(toolGroupId); toolGroupService.destroyToolGroup(toolGroupId);
}; };
@ -254,11 +262,27 @@ function OHIFCornerstoneRTViewport(props: withAppTypes) {
return () => { return () => {
// remove the segmentation representations if seg displayset changed // remove the segmentation representations if seg displayset changed
segmentationService.removeSegmentationRepresentationFromToolGroup(toolGroupId); segmentationService.removeSegmentationRepresentations(viewportId);
referencedDisplaySetRef.current = null; referencedDisplaySetRef.current = null;
}; };
}, [rtDisplaySet]); }, [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; 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(() => { useEffect(() => {
viewportActionCornersService.setComponents([ viewportActionCornersService.addComponents([
{ {
viewportId, viewportId,
id: 'viewportStatusComponent', id: 'viewportStatusComponent',
@ -318,7 +326,7 @@ function OHIFCornerstoneRTViewport(props: withAppTypes) {
key="actionArrows" key="actionArrows"
onArrowsClick={onSegmentChange} onArrowsClick={onSegmentChange}
className={ className={
viewportId === activeViewportId ? 'visible' : 'invisible group-hover:visible' viewportId === activeViewportId ? 'visible' : 'invisible group-hover/pane:visible'
} }
></ViewportActionArrows> ></ViewportActionArrows>
), ),

View File

@ -46,9 +46,9 @@
}, },
"dependencies": { "dependencies": {
"@babel/runtime": "^7.20.13", "@babel/runtime": "^7.20.13",
"@cornerstonejs/adapters": "^1.86.0", "@cornerstonejs/adapters": "^2.1.13",
"@cornerstonejs/core": "^1.86.0", "@cornerstonejs/core": "^2.1.13",
"@kitware/vtk.js": "30.4.1", "@kitware/vtk.js": "32.1.0",
"react-color": "^2.19.3" "react-color": "^2.19.3"
} }
} }

View File

@ -14,18 +14,22 @@ import vtkImageMarchingSquares from '@kitware/vtk.js/Filters/General/ImageMarchi
import vtkDataArray from '@kitware/vtk.js/Common/Core/DataArray'; import vtkDataArray from '@kitware/vtk.js/Common/Core/DataArray';
import vtkImageData from '@kitware/vtk.js/Common/DataModel/ImageData'; import vtkImageData from '@kitware/vtk.js/Common/DataModel/ImageData';
import {
updateViewportsForSegmentationRendering,
getUpdatedViewportsForSegmentation,
getTargetViewport,
} from './utils/hydrationUtils';
const { segmentation: segmentationUtils } = utilities; const { segmentation: segmentationUtils } = utilities;
const { datasetToBlob } = dcmjs.data; 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 { const {
Cornerstone3D: { Cornerstone3D: {
Segmentation: { generateLabelMaps2DFrom3D, generateSegmentation }, Segmentation: { generateSegmentation },
}, },
} = adaptersSEG; } = adaptersSEG;
@ -42,87 +46,14 @@ const commandsModule = ({
extensionManager, extensionManager,
}: Types.Extensions.ExtensionParams): Types.Extensions.CommandsModule => { }: Types.Extensions.ExtensionParams): Types.Extensions.CommandsModule => {
const { const {
uiNotificationService,
segmentationService, segmentationService,
uiDialogService, uiDialogService,
displaySetService, displaySetService,
viewportGridService, viewportGridService,
toolGroupService, toolGroupService,
cornerstoneViewportService, } = servicesManager.services as AppTypes.Services;
} = servicesManager.services;
const actions = { 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. * Loads segmentations for a specified viewport.
* The function prepares the viewport for rendering, then loads the segmentation details. * The function prepares the viewport for rendering, then loads the segmentation details.
@ -134,106 +65,30 @@ const commandsModule = ({
* *
*/ */
loadSegmentationsForViewport: async ({ segmentations, viewportId }) => { 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 // Todo: handle adding more than one segmentation
const displaySet = displaySets[0]; const viewport = getTargetViewport({ viewportId, viewportGridService });
const referencedDisplaySet = displaySetService.getDisplaySetByUID( const displaySetInstanceUID = viewport.displaySetInstanceUIDs[0];
displaySet.referencedDisplaySetInstanceUID
);
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
const initialSliceIndex = viewport.getSliceIndex();
updateViewportsForSegmentationRendering({ const segmentation = segmentations[0];
viewportId, const segmentationId = segmentation.segmentationId;
servicesManager, const label = segmentation.config.label;
displaySet, const segments = segmentation.config.segments;
loadFn: async () => {
const segDisplaySet = displaySet;
const suppressEvents = false;
const serviceFunction =
segDisplaySet.Modality === 'SEG'
? 'createSegmentationForSEGDisplaySet'
: 'createSegmentationForRTDisplaySet';
const boundFn = segmentationService[serviceFunction].bind(segmentationService); const displaySet = displaySetService.getDisplaySetByUID(displaySetInstanceUID);
const segmentationId = await boundFn(segDisplaySet, null, suppressEvents);
const segmentation = segmentationService.getSegmentation(segmentationId); await segmentationService.createLabelmapForDisplaySet(displaySet, {
segmentation.description = `S${referencedDisplaySet.SeriesNumber}: ${referencedDisplaySet.SeriesDescription}`; segmentationId,
return segmentationId; segments,
}, label,
initialSliceIndex,
}); });
segmentationService.addOrUpdateSegmentation(segmentation);
await segmentationService.addSegmentationRepresentation(viewport.viewportId, {
segmentationId,
});
return segmentationId;
}, },
/** /**
* Generates a segmentation from a given segmentation ID. * Generates a segmentation from a given segmentation ID.
@ -251,25 +106,61 @@ const commandsModule = ({
generateSegmentation: ({ segmentationId, options = {} }) => { generateSegmentation: ({ segmentationId, options = {} }) => {
const segmentation = cornerstoneToolsSegmentation.state.getSegmentation(segmentationId); const segmentation = cornerstoneToolsSegmentation.state.getSegmentation(segmentationId);
const { referencedVolumeId } = segmentation.representationData.LABELMAP; const { imageIds } = segmentation.representationData.Labelmap;
const segmentationVolume = cache.getVolume(segmentationId); const segImages = imageIds.map(imageId => cache.getImage(imageId));
const referencedVolume = cache.getVolume(referencedVolumeId); const referencedImages = segImages.map(image => cache.getImage(image.referencedImageId));
const referencedImages = referencedVolume.getCornerstoneImages();
const labelmapObj = generateLabelMaps2DFrom3D(segmentationVolume); const labelmaps2D = [];
// Generate fake metadata as an example let z = 0;
labelmapObj.metadata = [];
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); 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 // segmentation service already has a color for each segment
if (!segment) { if (!segment) {
return; 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( const RecommendedDisplayCIELabValue = dcmjs.data.Colors.rgb2DICOMLAB(
color.slice(0, 3).map(value => value / 255) color.slice(0, 3).map(value => value / 255)
@ -292,12 +183,12 @@ const commandsModule = ({
CodeMeaning: 'Tissue', CodeMeaning: 'Tissue',
}, },
}; };
labelmapObj.metadata[segmentIndex] = segmentMetadata; labelmap3D.metadata[segmentIndex] = segmentMetadata;
}); });
const generatedSegmentation = generateSegmentation( const generatedSegmentation = generateSegmentation(
referencedImages, referencedImages,
labelmapObj, labelmap3D,
metaData, metaData,
options options
); );
@ -445,18 +336,19 @@ const commandsModule = ({
}; };
const definitions = { const definitions = {
getUpdatedViewportsForSegmentation: { /**
commandFn: actions.getUpdatedViewportsForSegmentation, * Obsolete?
}, */
loadSegmentationDisplaySetsForViewport: { loadSegmentationDisplaySetsForViewport: {
commandFn: actions.loadSegmentationDisplaySetsForViewport, commandFn: actions.loadSegmentationDisplaySetsForViewport,
}, },
/**
* Obsolete?
*/
loadSegmentationsForViewport: { loadSegmentationsForViewport: {
commandFn: actions.loadSegmentationsForViewport, commandFn: actions.loadSegmentationsForViewport,
}, },
createEmptySegmentationForViewport: {
commandFn: actions.createEmptySegmentationForViewport,
},
generateSegmentation: { generateSegmentation: {
commandFn: actions.generateSegmentation, commandFn: actions.generateSegmentation,
}, },

View File

@ -20,6 +20,17 @@ const segProtocol: Types.HangingProtocol.Protocol = {
viewportType: 'stack', viewportType: 'stack',
toolGroupId: 'default', toolGroupId: 'default',
allowUnmatchedView: true, allowUnmatchedView: true,
syncGroups: [
{
type: 'hydrateseg',
id: 'sameFORId',
source: true,
target: true,
// options: {
// matchingRules: ['sameFOR'],
// },
},
],
}, },
displaySets: [ displaySets: [
{ {
@ -52,7 +63,20 @@ const segProtocol: Types.HangingProtocol.Protocol = {
}, },
viewports: [ viewports: [
{ {
viewportOptions: { allowUnmatchedView: true }, viewportOptions: {
allowUnmatchedView: true,
syncGroups: [
{
type: 'hydrateseg',
id: 'sameFORId',
source: true,
target: true,
// options: {
// matchingRules: ['sameFOR'],
// },
},
],
},
displaySets: [ displaySets: [
{ {
id: 'segDisplaySetId', id: 'segDisplaySetId',

View File

@ -1,6 +1,6 @@
import { utils } from '@ohif/core'; import { utils } from '@ohif/core';
import { metaData, cache, triggerEvent, eventTarget } from '@cornerstonejs/core'; import { metaData, triggerEvent, eventTarget } from '@cornerstonejs/core';
import { CONSTANTS } from '@cornerstonejs/tools'; import { CONSTANTS, segmentation as cstSegmentation } from '@cornerstonejs/tools';
import { adaptersSEG, Enums } from '@cornerstonejs/adapters'; import { adaptersSEG, Enums } from '@cornerstonejs/adapters';
import { SOPClassHandlerId } from './id'; import { SOPClassHandlerId } from './id';
@ -70,28 +70,32 @@ function _getDisplaySetsFromSeries(
displaySet.referencedImages = instance.ReferencedSeriesSequence.ReferencedInstanceSequence; displaySet.referencedImages = instance.ReferencedSeriesSequence.ReferencedInstanceSequence;
displaySet.referencedSeriesInstanceUID = referencedSeries.SeriesInstanceUID; displaySet.referencedSeriesInstanceUID = referencedSeries.SeriesInstanceUID;
const { displaySetService } = servicesManager.services;
const referencedDisplaySets = displaySetService.getDisplaySetsForSeries(
displaySet.referencedSeriesInstanceUID
);
displaySet.getReferenceDisplaySet = () => { const referencedDisplaySet = referencedDisplaySets[0];
const { displaySetService } = servicesManager.services;
const referencedDisplaySets = displaySetService.getDisplaySetsForSeries( if (!referencedDisplaySet) {
displaySet.referencedSeriesInstanceUID // 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();
}
}
); );
} else {
if (!referencedDisplaySets || referencedDisplaySets.length === 0) {
throw new Error('Referenced DisplaySet is missing for the SEG');
}
const referencedDisplaySet = referencedDisplaySets[0];
displaySet.referencedDisplaySetInstanceUID = referencedDisplaySet.displaySetInstanceUID; 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 }) => displaySet.load = async ({ headers }) =>
await _load(displaySet, servicesManager, extensionManager, headers); await _load(displaySet, servicesManager, extensionManager, headers);
@ -111,7 +115,7 @@ function _load(
if ( if (
(segDisplaySet.loading || segDisplaySet.isLoaded) && (segDisplaySet.loading || segDisplaySet.isLoaded) &&
loadPromises[SOPInstanceUID] && loadPromises[SOPInstanceUID] &&
_segmentationExists(segDisplaySet, segmentationService) _segmentationExists(segDisplaySet)
) { ) {
return loadPromises[SOPInstanceUID]; return loadPromises[SOPInstanceUID];
} }
@ -135,9 +139,8 @@ function _load(
} }
} }
const suppressEvents = true;
segmentationService segmentationService
.createSegmentationForSEGDisplaySet(segDisplaySet, null, suppressEvents) .createSegmentationForSEGDisplaySet(segDisplaySet)
.then(() => { .then(() => {
segDisplaySet.loading = false; segDisplaySet.loading = false;
resolve(); resolve();
@ -166,20 +169,20 @@ async function _loadSegments({
const { dicomLoaderService } = utilityModule.exports; const { dicomLoaderService } = utilityModule.exports;
const arrayBuffer = await dicomLoaderService.findDicomDataPromise(segDisplaySet, null, headers); const arrayBuffer = await dicomLoaderService.findDicomDataPromise(segDisplaySet, null, headers);
const cachedReferencedVolume = cache.getVolume(segDisplaySet.referencedVolumeId); const referencedDisplaySet = servicesManager.services.displaySetService.getDisplaySetByUID(
segDisplaySet.referencedDisplaySetInstanceUID
);
if (!cachedReferencedVolume) { if (!referencedDisplaySet) {
throw new Error( throw new Error('referencedDisplaySet is missing for SEG');
'Referenced Volume is missing for the SEG, and stack viewport SEG is not supported yet'
);
} }
const { imageIds } = cachedReferencedVolume; const { instances: images } = referencedDisplaySet;
const imageIds = images.map(({ imageId }) => imageId);
// Todo: what should be defaults here // Todo: what should be defaults here
const tolerance = 0.001; const tolerance = 0.001;
const skipOverlapping = true; const skipOverlapping = true;
eventTarget.addEventListener(Enums.Events.SEGMENTATION_LOAD_PROGRESS, evt => { eventTarget.addEventListener(Enums.Events.SEGMENTATION_LOAD_PROGRESS, evt => {
const { percentComplete } = evt.detail; const { percentComplete } = evt.detail;
segmentationService._broadcastEvent(segmentationService.EVENTS.SEGMENT_LOADING_COMPLETE, { segmentationService._broadcastEvent(segmentationService.EVENTS.SEGMENT_LOADING_COMPLETE, {
@ -231,9 +234,8 @@ async function _loadSegments({
Object.assign(segDisplaySet, results); Object.assign(segDisplaySet, results);
} }
function _segmentationExists(segDisplaySet, segmentationService: AppTypes.SegmentationService) { function _segmentationExists(segDisplaySet) {
// This should be abstracted with the CornerstoneCacheService return cstSegmentation.state.getSegmentation(segDisplaySet.displaySetInstanceUID);
return segmentationService.getSegmentation(segDisplaySet.displaySetInstanceUID);
} }
function getSopClassHandlerModule({ servicesManager, extensionManager }) { function getSopClassHandlerModule({ servicesManager, extensionManager }) {

View File

@ -8,7 +8,7 @@ export function getToolbarModule({ servicesManager }: withAppTypes) {
// forcing the button to have black background since initially // forcing the button to have black background since initially
// it is designed for the toolbox not the toolbar on top // it is designed for the toolbox not the toolbar on top
// we should then branch the buttonSectionId to have different styles // we should then branch the buttonSectionId to have different styles
const segmentations = segmentationService.getSegmentations(); const segmentations = segmentationService.getSegmentationRepresentations(viewportId);
if (!segmentations?.length) { if (!segmentations?.length) {
return { return {
disabled: true, disabled: true,

View File

@ -3,7 +3,6 @@ import React from 'react';
import getSopClassHandlerModule from './getSopClassHandlerModule'; import getSopClassHandlerModule from './getSopClassHandlerModule';
import getHangingProtocolModule from './getHangingProtocolModule'; import getHangingProtocolModule from './getHangingProtocolModule';
import getPanelModule from './getPanelModule';
import getCommandsModule from './commandsModule'; import getCommandsModule from './commandsModule';
import { getToolbarModule } from './getToolbarModule'; import { getToolbarModule } from './getToolbarModule';
@ -28,13 +27,6 @@ const extension = {
* You ID can be anything you want, but it should be unique. * You ID can be anything you want, but it should be unique.
*/ */
id, 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, getCommandsModule,
getToolbarModule, getToolbarModule,
getViewportModule({ servicesManager, extensionManager, commandsManager }) { getViewportModule({ servicesManager, extensionManager, commandsManager }) {

View File

@ -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,
};

View File

@ -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;

View File

@ -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 };

View File

@ -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,
};

View File

@ -11,7 +11,7 @@ function promptHydrateSEG({
segDisplaySet, segDisplaySet,
viewportId, viewportId,
preHydrateCallbacks, preHydrateCallbacks,
hydrateSEGDisplaySet, hydrateCallback,
}: withAppTypes) { }: withAppTypes) {
const { uiViewportDialogService } = servicesManager.services; const { uiViewportDialogService } = servicesManager.services;
const extensionManager = servicesManager._extensionManager; const extensionManager = servicesManager._extensionManager;
@ -28,7 +28,7 @@ function promptHydrateSEG({
}); });
window.setTimeout(async () => { window.setTimeout(async () => {
const isHydrated = await hydrateSEGDisplaySet({ const isHydrated = await hydrateCallback({
segDisplaySet, segDisplaySet,
viewportId, viewportId,
}); });

View File

@ -1,10 +1,11 @@
import PropTypes from 'prop-types';
import React, { useCallback, useEffect, useRef, useState } from 'react'; import React, { useCallback, useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { LoadingIndicatorTotalPercent, useViewportGrid, ViewportActionArrows } from '@ohif/ui'; import { LoadingIndicatorTotalPercent, useViewportGrid, ViewportActionArrows } from '@ohif/ui';
import createSEGToolGroupAndAddTools from '../utils/initSEGToolGroup'; import createSEGToolGroupAndAddTools from '../utils/initSEGToolGroup';
import promptHydrateSEG from '../utils/promptHydrateSEG'; import promptHydrateSEG from '../utils/promptHydrateSEG';
import _getStatusComponent from './_getStatusComponent'; import _getStatusComponent from './_getStatusComponent';
import { useSegmentationPresentationStore } from '@ohif/extension-cornerstone';
import { SegmentationRepresentations } from '@cornerstonejs/tools/enums';
const SEG_TOOLGROUP_BASE_NAME = 'SEGToolGroup'; const SEG_TOOLGROUP_BASE_NAME = 'SEGToolGroup';
@ -37,7 +38,6 @@ function OHIFCornerstoneSEGViewport(props: withAppTypes) {
} }
const segDisplaySet = displaySets[0]; const segDisplaySet = displaySets[0];
const [viewportGrid, viewportGridService] = useViewportGrid(); const [viewportGrid, viewportGridService] = useViewportGrid();
// States // States
@ -61,7 +61,11 @@ function OHIFCornerstoneSEGViewport(props: withAppTypes) {
const { viewports, activeViewportId } = viewportGrid; const { viewports, activeViewportId } = viewportGrid;
const referencedDisplaySet = segDisplaySet.getReferenceDisplaySet(); const referencedDisplaySetInstanceUID = segDisplaySet.referencedDisplaySetInstanceUID;
const referencedDisplaySet = displaySetService.getDisplaySetByUID(
referencedDisplaySetInstanceUID
);
const referencedDisplaySetMetadata = _getReferencedDisplaySetMetadata( const referencedDisplaySetMetadata = _getReferencedDisplaySetMetadata(
referencedDisplaySet, referencedDisplaySet,
segDisplaySet segDisplaySet
@ -98,18 +102,17 @@ function OHIFCornerstoneSEGViewport(props: withAppTypes) {
'@ohif/extension-cornerstone.viewportModule.cornerstone' '@ohif/extension-cornerstone.viewportModule.cornerstone'
); );
const { displaySet: referencedDisplaySet } = referencedDisplaySetRef.current;
// Todo: jump to the center of the first segment // Todo: jump to the center of the first segment
return ( return (
<Component <Component
{...props} {...props}
displaySets={[referencedDisplaySet, segDisplaySet]} displaySets={[segDisplaySet]}
viewportOptions={{ viewportOptions={{
viewportType: 'volume', viewportType: viewportOptions.viewportType,
toolGroupId: toolGroupId, toolGroupId: toolGroupId,
orientation: viewportOptions.orientation, orientation: viewportOptions.orientation,
viewportId: viewportOptions.viewportId, viewportId: viewportOptions.viewportId,
presentationIds: viewportOptions.presentationIds,
}} }}
onElementEnabled={evt => { onElementEnabled={evt => {
props.onElementEnabled?.(evt); props.onElementEnabled?.(evt);
@ -139,12 +142,35 @@ function OHIFCornerstoneSEGViewport(props: withAppTypes) {
newSelectedSegmentIndex = numberOfSegments - 1; newSelectedSegmentIndex = numberOfSegments - 1;
} }
segmentationService.jumpToSegmentCenter(segmentationId, newSelectedSegmentIndex, toolGroupId); segmentationService.jumpToSegmentCenter(segmentationId, newSelectedSegmentIndex, viewportId);
setSelectedSegment(newSelectedSegmentIndex); setSelectedSegment(newSelectedSegmentIndex);
}, },
[selectedSegment] [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(() => { useEffect(() => {
if (segIsLoading) { if (segIsLoading) {
return; return;
@ -155,15 +181,18 @@ function OHIFCornerstoneSEGViewport(props: withAppTypes) {
viewportId, viewportId,
segDisplaySet, segDisplaySet,
preHydrateCallbacks: [storePresentationState], preHydrateCallbacks: [storePresentationState],
hydrateSEGDisplaySet, hydrateCallback: hydrateSEG,
}).then(isHydrated => { }).then(isHydrated => {
if (isHydrated) { if (isHydrated) {
setIsHydrated(true); setIsHydrated(true);
} }
}); });
}, [servicesManager, viewportId, segDisplaySet, segIsLoading]); }, [servicesManager, viewportId, segDisplaySet, segIsLoading, hydrateSEG]);
useEffect(() => { useEffect(() => {
// on new seg display set, remove all segmentations from all viewports
segmentationService.clearSegmentationRepresentations(viewportId);
const { unsubscribe } = segmentationService.subscribe( const { unsubscribe } = segmentationService.subscribe(
segmentationService.EVENTS.SEGMENTATION_LOADING_COMPLETE, segmentationService.EVENTS.SEGMENTATION_LOADING_COMPLETE,
evt => { evt => {
@ -222,6 +251,9 @@ function OHIFCornerstoneSEGViewport(props: withAppTypes) {
if (toolGroup) { if (toolGroup) {
return; 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 // This creates a custom tool group which has the lifetime of this view
// only, and does NOT interfere with currently displayed segmentations. // only, and does NOT interfere with currently displayed segmentations.
@ -229,33 +261,14 @@ function OHIFCornerstoneSEGViewport(props: withAppTypes) {
return () => { return () => {
// remove the segmentation representations if seg displayset changed // 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 // Only destroy the viewport specific implementation
toolGroupService.destroyToolGroup(toolGroupId); 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 () => { const onStatusClick = useCallback(async () => {
// Before hydrating a SEG and make it added to all viewports in the grid // 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 // 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 // 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 // the presentation state, the viewport will be reset to the default presentation
storePresentationState(); storePresentationState();
const isHydrated = await hydrateSEGDisplaySet({ hydrateSEG();
segDisplaySet, }, [storePresentationState, hydrateSEG]);
viewportId,
});
setIsHydrated(isHydrated);
}, [hydrateSEGDisplaySet, segDisplaySet, storePresentationState, viewportId]);
useEffect(() => { useEffect(() => {
viewportActionCornersService.setComponents([ viewportActionCornersService.addComponents([
{ {
viewportId, viewportId,
id: 'viewportStatusComponent', id: 'viewportStatusComponent',
@ -292,7 +300,7 @@ function OHIFCornerstoneSEGViewport(props: withAppTypes) {
key="actionArrows" key="actionArrows"
onArrowsClick={onSegmentChange} onArrowsClick={onSegmentChange}
className={ className={
viewportId === activeViewportId ? 'visible' : 'invisible group-hover:visible' viewportId === activeViewportId ? 'visible' : 'invisible group-hover/pane:visible'
} }
></ViewportActionArrows> ></ViewportActionArrows>
), ),
@ -332,18 +340,6 @@ function OHIFCornerstoneSEGViewport(props: withAppTypes) {
}); });
} }
const {
PatientID,
PatientName,
PatientSex,
PatientAge,
SliceThickness,
ManufacturerModelName,
StudyDate,
SeriesDescription,
SpacingBetweenSlices,
} = referencedDisplaySetRef.current.metadata;
return ( return (
<> <>
<div className="relative flex h-full w-full flex-row overflow-hidden"> <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) { function _getReferencedDisplaySetMetadata(referencedDisplaySet, segDisplaySet) {
const { SharedFunctionalGroupsSequence } = segDisplaySet.instance; const { SharedFunctionalGroupsSequence } = segDisplaySet.instance;

View File

@ -46,9 +46,9 @@
}, },
"dependencies": { "dependencies": {
"@babel/runtime": "^7.20.13", "@babel/runtime": "^7.20.13",
"@cornerstonejs/adapters": "^1.86.0", "@cornerstonejs/adapters": "^2.1.13",
"@cornerstonejs/core": "^1.86.0", "@cornerstonejs/core": "^2.1.13",
"@cornerstonejs/tools": "^1.86.0", "@cornerstonejs/tools": "^2.1.13",
"classnames": "^2.3.2" "classnames": "^2.3.2"
} }
} }

View File

@ -15,8 +15,15 @@ const MEASUREMENT_TRACKING_EXTENSION_ID = '@ohif/extension-measurement-tracking'
const SR_TOOLGROUP_BASE_NAME = 'SRToolGroup'; const SR_TOOLGROUP_BASE_NAME = 'SRToolGroup';
function OHIFCornerstoneSRMeasurementViewport(props: withAppTypes) { function OHIFCornerstoneSRMeasurementViewport(props: withAppTypes) {
const { children, dataSource, displaySets, viewportOptions, servicesManager, extensionManager } = const {
props; commandsManager,
children,
dataSource,
displaySets,
viewportOptions,
servicesManager,
extensionManager,
} = props;
const [appConfig] = useAppConfig(); const [appConfig] = useAppConfig();
@ -63,6 +70,7 @@ function OHIFCornerstoneSRMeasurementViewport(props: withAppTypes) {
trackedMeasurements = tracked?.[0]; trackedMeasurements = tracked?.[0];
sendTrackedMeasurementsEvent = tracked?.[1]; sendTrackedMeasurementsEvent = tracked?.[1];
} }
if (!sendTrackedMeasurementsEvent) { if (!sendTrackedMeasurementsEvent) {
// if no panels from measurement-tracking extension is used, this code will run // if no panels from measurement-tracking extension is used, this code will run
trackedMeasurements = null; trackedMeasurements = null;
@ -72,6 +80,7 @@ function OHIFCornerstoneSRMeasurementViewport(props: withAppTypes) {
{ servicesManager, extensionManager, appConfig }, { servicesManager, extensionManager, appConfig },
displaySetInstanceUID displaySetInstanceUID
); );
const displaySets = displaySetService.getDisplaySetsForSeries(SeriesInstanceUIDs[0]); const displaySets = displaySetService.getDisplaySetsForSeries(SeriesInstanceUIDs[0]);
if (displaySets.length) { if (displaySets.length) {
viewportGridService.setDisplaySetsForViewports([ 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 * Store the tracking identifiers per viewport in order to be able to
* show the SR measurements on the referenced image on the correct viewport, * show the SR measurements on the referenced image on the correct viewport,
@ -297,17 +311,12 @@ function OHIFCornerstoneSRMeasurementViewport(props: withAppTypes) {
updateSR(); updateSR();
}, [measurementSelected, element, setTrackingIdentifiers, srDisplaySet]); }, [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(() => { useEffect(() => {
setIsLocked(trackedMeasurements?.context?.trackedSeries?.length > 0); setIsLocked(trackedMeasurements?.context?.trackedSeries?.length > 0);
}, [trackedMeasurements]); }, [trackedMeasurements]);
useEffect(() => { useEffect(() => {
viewportActionCornersService.setComponents([ viewportActionCornersService.addComponents([
{ {
viewportId, viewportId,
id: 'viewportStatusComponent', id: 'viewportStatusComponent',

View File

@ -12,7 +12,7 @@ import {
utilities as csToolsUtils, utilities as csToolsUtils,
} from '@cornerstonejs/tools'; } from '@cornerstonejs/tools';
import { Types, MeasurementService } from '@ohif/core'; 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 { Enums as CSExtensionEnums } from '@ohif/extension-cornerstone';
import DICOMSRDisplayTool from './tools/DICOMSRDisplayTool'; import DICOMSRDisplayTool from './tools/DICOMSRDisplayTool';
import SCOORD3DPointTool from './tools/SCOORD3DPointTool'; import SCOORD3DPointTool from './tools/SCOORD3DPointTool';
@ -95,7 +95,7 @@ export default function init({
const imageIndex = (currentViewport as StackViewport) const imageIndex = (currentViewport as StackViewport)
.getImageIds() .getImageIds()
.indexOf(referencedImageId); .indexOf(referencedImageId);
csToolsUtils.jumpToSlice(currentViewport.element, { imageIndex }); csUtils.jumpToSlice(currentViewport.element, { imageIndex });
} catch (error) { } catch (error) {
console.warn('Unable to jump to image based on measurement coordinate', error); console.warn('Unable to jump to image based on measurement coordinate', error);
} }

View File

@ -45,11 +45,19 @@ export default function hydrateStructuredReport(
displaySetInstanceUID displaySetInstanceUID
) { ) {
const annotationManager = CsAnnotation.state.getAnnotationManager(); const annotationManager = CsAnnotation.state.getAnnotationManager();
const disableEditing = appConfig?.disableEditing;
const dataSource = extensionManager.getActiveDataSource()[0]; const dataSource = extensionManager.getActiveDataSource()[0];
const { measurementService, displaySetService, customizationService } = servicesManager.services; const { measurementService, displaySetService, customizationService } = servicesManager.services;
const codingValues = customizationService.getCustomization('codingValues', {}); const codingValues = customizationService.getCustomization('codingValues', {});
const { disableEditing } = customizationService.getCustomization(
'PanelMeasurement.disableEditing',
{
id: 'default.disableEditing',
disableEditing: false,
}
);
const displaySet = displaySetService.getDisplaySetByUID(displaySetInstanceUID); const displaySet = displaySetService.getDisplaySetByUID(displaySetInstanceUID);
// TODO -> We should define a strict versioning somewhere. // TODO -> We should define a strict versioning somewhere.

View File

@ -42,9 +42,8 @@
}, },
"dependencies": { "dependencies": {
"@babel/runtime": "^7.20.13", "@babel/runtime": "^7.20.13",
"@cornerstonejs/core": "^1.86.0", "@cornerstonejs/core": "^2.1.13",
"@cornerstonejs/streaming-image-volume-loader": "^1.82.4", "@cornerstonejs/tools": "^2.1.13",
"@cornerstonejs/tools": "^1.86.0",
"classnames": "^2.3.2" "classnames": "^2.3.2"
} }
} }

View File

@ -110,7 +110,12 @@ function _getTimePointsData(volume) {
return { timePoints, timePointsUnit }; 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 displaySets = displaySetService.getActiveDisplaySets();
const dynamic4DDisplaySet = displaySets.find(displaySet => { const dynamic4DDisplaySet = displaySets.find(displaySet => {
@ -145,8 +150,11 @@ function _getSegmentationData(segmentation, volumesTimePointsCache, displaySetSe
referencedDynamicVolume.imageIds[0] referencedDynamicVolume.imageIds[0]
); );
const segmentationVolume = segmentationService.getLabelmapVolume(segmentation.segmentationId);
const maskVolumeId = segmentationVolume?.volumeId;
const [timeData, _] = csToolsUtils.dynamicVolume.getDataInTime(referencedDynamicVolume, { const [timeData, _] = csToolsUtils.dynamicVolume.getDataInTime(referencedDynamicVolume, {
maskVolumeId: segmentation.id, maskVolumeId,
}) as number[][]; }) as number[][];
const pixelCount = timeData.length; const pixelCount = timeData.length;
@ -155,27 +163,15 @@ function _getSegmentationData(segmentation, volumesTimePointsCache, displaySetSe
return []; 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 // Todo: this is useless we should be able to grab color with just segRepUID and segmentIndex
const color = csTools.segmentation.config.color.getColorForSegmentIndex( // const color = csTools.segmentation.config.color.getSegmentIndexColor(
toolGroupId, // segmentationRepresentationUID,
segmentationRepresentationUID, // 1 // segmentIndex
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); let timePointsData = volumesTimePointsCache.get(referencedDynamicVolume);
if (!timePointsData) { if (!timePointsData) {
@ -219,14 +215,14 @@ function _getSegmentationData(segmentation, volumesTimePointsCache, displaySetSe
}; };
} }
function _getInstanceFromSegmentations(segmentations, displaySetService) { function _getInstanceFromSegmentations(segmentations, { servicesManager }) {
if (!segmentations.length) { if (!segmentations.length) {
return; return;
} }
const volumesTimePointsCache = new WeakMap(); const volumesTimePointsCache = new WeakMap();
const segmentationsData = segmentations.map(segmentation => const segmentationsData = segmentations.map(segmentation =>
_getSegmentationData(segmentation, volumesTimePointsCache, displaySetService) _getSegmentationData(segmentation, volumesTimePointsCache, { servicesManager })
); );
const { date: seriesDate, time: seriesTime } = _getDateTimeStr(); const { date: seriesDate, time: seriesTime } = _getDateTimeStr();
@ -266,10 +262,11 @@ function _getInstanceFromSegmentations(segmentations, displaySetService) {
} }
function updateSegmentationsChartDisplaySet({ servicesManager }: withAppTypes): void { function updateSegmentationsChartDisplaySet({ servicesManager }: withAppTypes): void {
const { segmentationService, displaySetService } = servicesManager.services; debugger;
const { segmentationService } = servicesManager.services;
const segmentations = segmentationService.getSegmentations(); const segmentations = segmentationService.getSegmentations();
const { seriesMetadata, instance } = const { seriesMetadata, instance } =
_getInstanceFromSegmentations(segmentations, displaySetService) ?? {}; _getInstanceFromSegmentations(segmentations, { servicesManager }) ?? {};
if (seriesMetadata && instance) { if (seriesMetadata && instance) {
// An event is triggered after adding the instance and the displaySet is created // An event is triggered after adding the instance and the displaySet is created

View File

@ -84,11 +84,11 @@ const commandsModule = ({ commandsManager, servicesManager }: withAppTypes) => {
const stdDeviation = Math.sqrt(variance); const stdDeviation = Math.sqrt(variance);
return stdDeviation; return stdDeviation;
} }
// Iterate through each segmentation to get the timeData and ijkCoords // 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, { const [timeData, ijkCoords] = utilities.dynamicVolume.getDataInTime(dynamicVolume, {
maskVolumeId: segmentation.id, maskVolumeId: volume.volumeId,
}) as number[][]; }) as number[][];
if (summaryStats) { if (summaryStats) {
@ -113,17 +113,33 @@ const commandsModule = ({ commandsManager, servicesManager }: withAppTypes) => {
for (let timeIndex = 0; timeIndex < maxLength; timeIndex++) { for (let timeIndex = 0; timeIndex < maxLength; timeIndex++) {
// for each voxel in the ROI, get the value at the current time point // for each voxel in the ROI, get the value at the current time point
const voxelValues = []; 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++) { 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); mean.push(sum / numVoxels);
const minimum = Math.min(...voxelValues); min.push(minValue);
min.push(minimum); minIJK.push(ijkCoords[minIndex]);
minIJK.push(ijkCoords[voxelValues.indexOf(minimum)]); max.push(maxValue);
const maximum = Math.max(...voxelValues); maxIJK.push(ijkCoords[maxIndex]);
max.push(maximum);
maxIJK.push(ijkCoords[voxelValues.indexOf(maximum)]);
std.push(calculateStandardDeviation(voxelValues)); 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'); throw new Error('No reference display set found based on the dynamic data');
} }
const segmentationId = await segmentationService.createSegmentationForDisplaySet( const displaySet = displaySetService.getDisplaySetByUID(
referenceDisplaySet.displaySetInstanceUID, referenceDisplaySet.displaySetInstanceUID
{ label }
); );
// Add Segmentation to all toolGroupIds in the viewer const segmentationId = await segmentationService.createLabelmapForDisplaySet(displaySet, {
const toolGroupIds = Array.from( label,
viewports.values(), });
viewport => viewport.viewportOptions.toolGroupId
);
const representationType = LABELMAP; const firstViewport = viewports.values().next().value;
for (const toolGroupId of toolGroupIds) { await segmentationService.addSegmentationRepresentation(firstViewport.viewportId, {
const hydrateSegmentation = true; segmentationId,
await segmentationService.addSegmentationRepresentationToToolGroup( });
toolGroupId,
segmentationId,
hydrateSegmentation,
representationType
);
segmentationService.setActiveSegmentationForToolGroup(segmentationId, toolGroupId);
}
return segmentationId; return segmentationId;
}, },

View File

@ -147,8 +147,8 @@ function getFusionViewports() {
name: DEFAULT_COLORMAP, name: DEFAULT_COLORMAP,
opacity: [ opacity: [
{ value: 0, opacity: 0 }, { value: 0, opacity: 0 },
{ value: 0.1, opacity: 0.3 }, { value: 0.1, opacity: 0.8 },
{ value: 1, opacity: 0.3 }, { value: 1, opacity: 0.8 },
], ],
}, },
}; };
@ -191,6 +191,15 @@ function getFusionViewports() {
syncInvertState: false, syncInvertState: false,
}, },
}, },
{
type: 'hydrateseg',
id: 'sameFORId',
source: true,
target: true,
options: {
matchingRules: ['sameFOR'],
},
},
], ],
}, },
displaySets: [ displaySets: [
@ -240,6 +249,15 @@ function getFusionViewports() {
syncInvertState: false, syncInvertState: false,
}, },
}, },
{
type: 'hydrateseg',
id: 'sameFORId',
source: true,
target: true,
options: {
matchingRules: ['sameFOR'],
},
},
], ],
}, },
displaySets: [ displaySets: [
@ -289,6 +307,15 @@ function getFusionViewports() {
syncInvertState: false, syncInvertState: false,
}, },
}, },
{
type: 'hydrateseg',
id: 'sameFORId',
source: true,
target: true,
options: {
matchingRules: ['sameFOR'],
},
},
], ],
}, },
displaySets: [ displaySets: [

View File

@ -1,10 +1,11 @@
import React from 'react'; import React from 'react';
import { DynamicDataPanel } from './panels'; import { DynamicDataPanel } from './panels';
import { Toolbox } from '@ohif/ui-next'; import { Toolbox } from '@ohif/ui-next';
import { PanelSegmentation } from '@ohif/extension-cornerstone';
import DynamicExport from './panels/DynamicExport'; import DynamicExport from './panels/DynamicExport';
function getPanelModule({ commandsManager, extensionManager, servicesManager }) { function getPanelModule({ commandsManager, extensionManager, servicesManager, configuration }) {
const wrappedDynamicDataPanel = ({}) => { const wrappedDynamicDataPanel = () => {
return ( return (
<DynamicDataPanel <DynamicDataPanel
commandsManager={commandsManager} commandsManager={commandsManager}
@ -14,7 +15,7 @@ function getPanelModule({ commandsManager, extensionManager, servicesManager })
); );
}; };
const wrappedDynamicToolbox = ({}) => { const wrappedDynamicSegmentation = () => {
return ( return (
<> <>
<Toolbox <Toolbox
@ -24,18 +25,17 @@ function getPanelModule({ commandsManager, extensionManager, servicesManager })
buttonSectionId="dynamic-toolbox" buttonSectionId="dynamic-toolbox"
title="Threshold Tools" title="Threshold Tools"
/> />
</> <PanelSegmentation
);
};
const wrappedDynamicExport = () => {
return (
<>
<DynamicExport
commandsManager={commandsManager}
servicesManager={servicesManager} servicesManager={servicesManager}
commandsManager={commandsManager}
extensionManager={extensionManager} extensionManager={extensionManager}
/> configuration={configuration}
>
<DynamicExport
servicesManager={servicesManager}
commandsManager={commandsManager}
/>
</PanelSegmentation>
</> </>
); );
}; };
@ -49,18 +49,11 @@ function getPanelModule({ commandsManager, extensionManager, servicesManager })
component: wrappedDynamicDataPanel, component: wrappedDynamicDataPanel,
}, },
{ {
name: 'dynamic-toolbox', name: 'dynamic-segmentation',
iconName: 'tab-4d', iconName: 'tab-segmentation',
iconLabel: '4D Workflow', iconLabel: 'Segmentation',
label: 'Dynamic Toolbox', label: 'Segmentation',
component: wrappedDynamicToolbox, component: wrappedDynamicSegmentation,
},
{
name: 'dynamic-export',
iconName: 'tab-4d',
iconLabel: '4D Workflow',
label: '4D Workflow',
component: wrappedDynamicExport,
}, },
]; ];
} }

View File

@ -1,6 +1,5 @@
import React from 'react'; import React from 'react';
import PanelGenerateImage from './PanelGenerateImage'; import PanelGenerateImage from './PanelGenerateImage';
import { Separator } from '@ohif/ui-next';
function DynamicDataPanel({ servicesManager, commandsManager, tab }: withAppTypes) { function DynamicDataPanel({ servicesManager, commandsManager, tab }: withAppTypes) {
return ( return (

View File

@ -1,73 +1,52 @@
import React, { useState, useEffect } from 'react'; import React from 'react';
import { ActionButtons } from '@ohif/ui'; import { Button, Icons } from '@ohif/ui-next';
import { useTranslation } from 'react-i18next'; import { useSegmentations } from '@ohif/extension-cornerstone';
function DynamicExport({ commandsManager, servicesManager, extensionManager }: withAppTypes) { function DynamicExport({ commandsManager, servicesManager }: withAppTypes) {
const { segmentationService } = servicesManager.services; const segmentations = useSegmentations({ servicesManager });
const { t } = useTranslation('dynamicExport');
const [segmentations, setSegmentations] = useState(() => segmentationService.getSegmentations()); if (!segmentations?.length) {
return null;
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();
});
};
}, []);
return ( return (
<div> <div className="flex gap-2">
<div className="mt-3 flex justify-center px-2"> <div className="flex h-8 w-full items-center rounded pr-0.5">
<ActionButtons <Button
actions={actions} size="sm"
t={t} 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>
</div> </div>
); );

View File

@ -1,6 +1,5 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { import {
InputDoubleRange,
Button, Button,
PanelSection, PanelSection,
ButtonGroup, ButtonGroup,
@ -10,6 +9,8 @@ import {
Tooltip, Tooltip,
} from '@ohif/ui'; } from '@ohif/ui';
import { DoubleSlider } from '@ohif/ui-next';
import { Enums } from '@cornerstonejs/core'; import { Enums } from '@cornerstonejs/core';
const controlClassNames = { const controlClassNames = {
@ -55,21 +56,15 @@ const DynamicVolumeControls = ({
const [computeViewMode, setComputeViewMode] = useState(Enums.DynamicOperatorType.SUM); const [computeViewMode, setComputeViewMode] = useState(Enums.DynamicOperatorType.SUM);
const [sliderRangeValues, setSliderRangeValues] = useState([framesLength / 4, framesLength / 2]); const [sliderRangeValues, setSliderRangeValues] = useState([0, framesLength - 1]);
useEffect(() => {
setSliderRangeValues([framesLength / 4, framesLength / 2]);
}, [framesLength]);
const handleSliderChange = newValues => { const handleSliderChange = newValues => {
onDoubleRangeChange(newValues); onDoubleRangeChange(newValues);
if (newValues[0] === sliderRangeValues[0] && newValues[1] === sliderRangeValues[1]) {
return;
}
setSliderRangeValues(newValues); setSliderRangeValues(newValues);
}; };
const formatLabel = value => Math.round(value);
return ( return (
<div className="flex select-none flex-col"> <div className="flex select-none flex-col">
<PanelSection <PanelSection
@ -134,7 +129,7 @@ const DynamicVolumeControls = ({
} }
/> />
<ButtonGroup <ButtonGroup
className={`mt-2 w-full `} className={`mt-2 w-full`}
separated={true} separated={true}
> >
<button <button
@ -156,15 +151,15 @@ const DynamicVolumeControls = ({
{Enums.DynamicOperatorType.SUBTRACT.toString().toUpperCase()} {Enums.DynamicOperatorType.SUBTRACT.toString().toUpperCase()}
</button> </button>
</ButtonGroup> </ButtonGroup>
<div className="w-ful mt-2"> <div className="mt-2 w-full">
<InputDoubleRange <DoubleSlider
values={sliderRangeValues} min={0}
onChange={handleSliderChange} max={framesLength - 1}
minValue={0}
showLabel={true}
allowNumberEdit={true}
maxValue={framesLength}
step={1} step={1}
defaultValue={sliderRangeValues}
onValueChange={handleSliderChange}
formatLabel={formatLabel}
className="w-full"
/> />
</div> </div>
<Button <Button
@ -216,14 +211,14 @@ function FrameControls({
> >
<Icon <Icon
name={getPlayPauseIconName()} 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> </IconButton>
<InputNumber <InputNumber
value={currentFrameIndex} value={currentFrameIndex}
onChange={onFrameChange} onChange={onFrameChange}
minValue={0} minValue={0}
maxValue={framesLength} maxValue={framesLength - 1}
label="Frame" label="Frame"
{...controlClassNames} {...controlClassNames}
/> />

View File

@ -1,8 +1,6 @@
import React, { useEffect, useState, useRef } from 'react'; import React, { useEffect, useState, useRef } from 'react';
import PropTypes from 'prop-types';
import { useCine, useViewportGrid } from '@ohif/ui'; import { useCine, useViewportGrid } from '@ohif/ui';
import { cache, utilities as csUtils, volumeLoader, eventTarget } from '@cornerstonejs/core'; import { utilities as csUtils, volumeLoader, eventTarget, Enums, cache } from '@cornerstonejs/core';
import { Enums } from '@cornerstonejs/streaming-image-volume-loader';
import { utilities as cstUtils } from '@cornerstonejs/tools'; import { utilities as cstUtils } from '@cornerstonejs/tools';
import DynamicVolumeControls from './DynamicVolumeControls'; import DynamicVolumeControls from './DynamicVolumeControls';
@ -16,8 +14,8 @@ export default function PanelGenerateImage({ servicesManager, commandsManager }:
const [{ activeViewportId }] = useViewportGrid(); const [{ activeViewportId }] = useViewportGrid();
// //
const [timePointsRange, setTimePointsRange] = useState([]); const [timePointsRange, setTimePointsRange] = useState([0, 0]);
const [timePointsRangeToUseForGenerate, setTimePointsRangeToUseForGenerate] = useState([]); const [timePointsRangeToUseForGenerate, setTimePointsRangeToUseForGenerate] = useState([0, 0]);
const [computedDisplaySet, setComputedDisplaySet] = useState(null); const [computedDisplaySet, setComputedDisplaySet] = useState(null);
const [dynamicVolume, setDynamicVolume] = useState(null); const [dynamicVolume, setDynamicVolume] = useState(null);
const [frameRate, setFrameRate] = useState(20); const [frameRate, setFrameRate] = useState(20);
@ -31,47 +29,62 @@ export default function PanelGenerateImage({ servicesManager, commandsManager }:
const computedVolumeId = `cornerstoneStreamingImageVolume:${uuidComputedVolume.current}`; const computedVolumeId = `cornerstoneStreamingImageVolume:${uuidComputedVolume.current}`;
useEffect(() => { 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 => { evtDetails.viewportData.data.forEach(volumeData => {
if (volumeData.volume.isDynamicVolume()) { if (volumeData.volume?.isDynamicVolume()) {
setDynamicVolume(volumeData.volume); setDynamicVolume(volumeData.volume);
uuidDynamicVolume.current = volumeData.displaySetInstanceUID; 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 () => { 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]); }, [cornerstoneViewportService]);
useEffect(() => {
const { unsubscribe } = servicesManager.services.cineService.subscribe(
servicesManager.services.cineService.EVENTS.CINE_STATE_CHANGED,
evt => {
setIsPlaying(evt.isPlaying);
}
);
return () => {
unsubscribe();
};
}, [cineService]);
useEffect(() => { useEffect(() => {
const displaySetUIDs = viewportGridService.getDisplaySetsUIDsForViewport(activeViewportId); const displaySetUIDs = viewportGridService.getDisplaySetsUIDsForViewport(activeViewportId);
if (!displaySetUIDs || displaySetUIDs.length === 0) { if (!displaySetUIDs?.length) {
return; return;
} }
const displaySets = displaySetUIDs.map(displaySetUID => const displaySets = displaySetUIDs.map(displaySetService.getDisplaySetByUID);
displaySetService.getDisplaySetByUID(displaySetUID)
);
const dynamicVolumeDisplaySet = displaySets.find(displaySet => displaySet.isDynamicVolume); const dynamicVolumeDisplaySet = displaySets.find(displaySet => displaySet.isDynamicVolume);
if (!dynamicVolumeDisplaySet) { if (!dynamicVolumeDisplaySet) {
@ -88,23 +101,16 @@ export default function PanelGenerateImage({ servicesManager, commandsManager }:
setDynamicVolume(dynamicVolume); setDynamicVolume(dynamicVolume);
uuidDynamicVolume.current = dynamicVolumeDisplaySet.displaySetInstanceUID; uuidDynamicVolume.current = dynamicVolumeDisplaySet.displaySetInstanceUID;
setTimePointsRange([1, dynamicVolume.numTimePoints]); const newRange = [1, dynamicVolume.numTimePoints];
}, [activeViewportId, cornerstoneViewportService]); setTimePointsRange(newRange);
setTimePointsRangeToUseForGenerate(newRange);
useEffect(() => { }, [
// ~~ Subscription activeViewportId,
const evt = Enums.Events.DYNAMIC_VOLUME_TIME_POINT_INDEX_CHANGED; viewportGridService,
displaySetService,
const callback = evt => { cornerstoneViewportService,
setTimePointRendered(evt.detail.timePointIndex); cineService,
}; ]);
eventTarget.addEventListener(evt, callback);
return () => {
eventTarget.removeEventListener(evt, callback);
};
}, [cornerstoneViewportService]);
function renderGeneratedImage(displaySet) { function renderGeneratedImage(displaySet) {
commandsManager.runCommand('swapDynamicWithComputedDisplaySet', { commandsManager.runCommand('swapDynamicWithComputedDisplaySet', {
@ -130,26 +136,19 @@ export default function PanelGenerateImage({ servicesManager, commandsManager }:
let computedVolume = cache.getVolume(computedVolumeId); let computedVolume = cache.getVolume(computedVolumeId);
if (!computedVolume) { if (!computedVolume) {
await createComputedVolume(dynamicVolumeId, computedVolumeId); computedVolume = await volumeLoader.createAndCacheDerivedVolume(dynamicVolumeId, {
computedVolume = cache.getVolume(computedVolumeId); 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]); cstUtils.dynamicVolume.updateVolumeFromTimeData(dynamicVolume, operationName, options);
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);
// If computed display set does not exist, create an object to be used as // 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 // the displaySet. If it does exist, update the image data and vtkTexture
@ -166,6 +165,7 @@ export default function PanelGenerateImage({ servicesManager, commandsManager }:
madeInClient: true, madeInClient: true,
FrameOfReferenceUID: dynamicVolume.metadata.FrameOfReferenceUID, FrameOfReferenceUID: dynamicVolume.metadata.FrameOfReferenceUID,
isDerived: true, isDerived: true,
imageIds: computedVolume.imageIds,
}; };
setComputedDisplaySet(displaySet); setComputedDisplaySet(displaySet);
renderGeneratedImage(displaySet); renderGeneratedImage(displaySet);
@ -173,19 +173,8 @@ export default function PanelGenerateImage({ servicesManager, commandsManager }:
commandsManager.runCommand('updateVolumeData', { commandsManager.runCommand('updateVolumeData', {
volume: computedVolume, 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(); cornerstoneViewportService.getRenderingEngine().render();
renderGeneratedImage(computedDisplaySet); renderGeneratedImage(computedDisplaySet);
// }
} }
} }
@ -248,28 +237,7 @@ export default function PanelGenerateImage({ servicesManager, commandsManager }:
onGenerate={onGenerateImage} onGenerate={onGenerateImage}
onDynamicClick={displayingComputed ? () => renderDynamicImage(computedDisplaySet) : null} onDynamicClick={displayingComputed ? () => renderDynamicImage(computedDisplaySet) : null}
onDoubleRangeChange={handleSliderChange} 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,
};

View File

@ -38,7 +38,7 @@
"@cornerstonejs/codec-libjpeg-turbo-8bit": "^1.2.2", "@cornerstonejs/codec-libjpeg-turbo-8bit": "^1.2.2",
"@cornerstonejs/codec-openjpeg": "^1.2.4", "@cornerstonejs/codec-openjpeg": "^1.2.4",
"@cornerstonejs/codec-openjph": "^2.4.5", "@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", "@icr/polyseg-wasm": "^0.4.0",
"@ohif/core": "3.9.0-beta.105", "@ohif/core": "3.9.0-beta.105",
"@ohif/ui": "3.9.0-beta.105", "@ohif/ui": "3.9.0-beta.105",
@ -55,12 +55,11 @@
}, },
"dependencies": { "dependencies": {
"@babel/runtime": "^7.20.13", "@babel/runtime": "^7.20.13",
"@cornerstonejs/adapters": "^1.86.0", "@cornerstonejs/adapters": "^2.1.13",
"@cornerstonejs/core": "^1.86.0", "@cornerstonejs/core": "^2.1.13",
"@cornerstonejs/streaming-image-volume-loader": "^1.82.4", "@cornerstonejs/tools": "^2.1.13",
"@cornerstonejs/tools": "^1.86.0",
"@icr/polyseg-wasm": "^0.4.0", "@icr/polyseg-wasm": "^0.4.0",
"@kitware/vtk.js": "30.4.1", "@kitware/vtk.js": "32.1.0",
"html2canvas": "^1.4.1", "html2canvas": "^1.4.1",
"lodash.debounce": "^4.0.8", "lodash.debounce": "^4.0.8",
"lodash.merge": "^4.6.2", "lodash.merge": "^4.6.2",

View File

@ -1,10 +1,9 @@
import React, { useEffect, useRef, useCallback, useState } from 'react'; import React, { useEffect, useRef, useCallback, useState } from 'react';
import { useResizeDetector } from 'react-resize-detector'; import { useResizeDetector } from 'react-resize-detector';
import PropTypes from 'prop-types';
import * as cs3DTools from '@cornerstonejs/tools'; import * as cs3DTools from '@cornerstonejs/tools';
import { Enums, eventTarget, getEnabledElement } from '@cornerstonejs/core'; import { Enums, eventTarget, getEnabledElement } from '@cornerstonejs/core';
import { MeasurementService } from '@ohif/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 type { Types as csTypes } from '@cornerstonejs/core';
import { setEnabledElement } from '../state'; import { setEnabledElement } from '../state';
@ -17,8 +16,10 @@ import type { Types } from '@ohif/core';
import OHIFViewportActionCorners from '../components/OHIFViewportActionCorners'; import OHIFViewportActionCorners from '../components/OHIFViewportActionCorners';
import { getWindowLevelActionMenu } from '../components/WindowLevelActionMenu/getWindowLevelActionMenu'; import { getWindowLevelActionMenu } from '../components/WindowLevelActionMenu/getWindowLevelActionMenu';
import { useAppConfig } from '@state'; import { useAppConfig } from '@state';
import { getViewportDataOverlaySettingsMenu } from '../components/ViewportDataOverlaySettingMenu';
import { LutPresentation, PositionPresentation } from '../types/Presentation'; import { getViewportPresentations } from '../utils/presentations/getViewportPresentations';
import { useSynchronizersStore } from '../stores/useSynchronizersStore';
import ActiveViewportBehavior from '../utils/ActiveViewportBehavior';
const STACK = 'stack'; const STACK = 'stack';
@ -28,428 +29,401 @@ const STACK = 'stack';
*/ */
let cacheJumpToMeasurementEvent; 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 // 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. // Then we don't need to worry about the re-renders if the props change.
const OHIFCornerstoneViewport = React.memo((props: withAppTypes) => { const OHIFCornerstoneViewport = React.memo(
const { (
displaySets, props: withAppTypes<{
dataSource, viewportId: string;
viewportOptions, displaySets: AppTypes.DisplaySet[];
displaySetOptions, viewportOptions: AppTypes.ViewportGrid.GridViewportOptions;
servicesManager, initialImageIndex: number;
commandsManager, }>
onElementEnabled, ) => {
// eslint-disable-next-line react/prop-types const {
onElementDisabled, displaySets,
isJumpToMeasurementDisabled = false, dataSource,
// Note: you SHOULD NOT use the initialImageIdOrIndex for manipulation viewportOptions,
// of the imageData in the OHIFCornerstoneViewport. This prop is used displaySetOptions,
// to set the initial state of the viewport's first image to render servicesManager,
// eslint-disable-next-line react/prop-types commandsManager,
initialImageIndex, onElementEnabled,
// if the viewport is part of a hanging protocol layout // eslint-disable-next-line react/prop-types
// we should not really rely on the old synchronizers and onElementDisabled,
// you see below we only rehydrate the synchronizers if the viewport isJumpToMeasurementDisabled = false,
// is not part of the hanging protocol layout. HPs should // Note: you SHOULD NOT use the initialImageIdOrIndex for manipulation
// define their own synchronizers. Since the synchronizers are // of the imageData in the OHIFCornerstoneViewport. This prop is used
// viewportId dependent and // to set the initial state of the viewport's first image to render
// eslint-disable-next-line react/prop-types // eslint-disable-next-line react/prop-types
isHangingProtocolLayout, initialImageIndex,
} = props; // 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');
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();
} }
}, [elementRef]);
const cleanUpServices = useCallback( // Make sure displaySetOptions has one object per displaySet
viewportInfo => { while (displaySetOptions.length < displaySets.length) {
const renderingEngineId = viewportInfo.getRenderingEngineId(); displaySetOptions.push({});
const syncGroups = viewportInfo.getSyncGroups(); }
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); const {
}, displaySetService,
[viewportId] toolbarService,
); toolGroupService,
syncGroupService,
cornerstoneViewportService,
segmentationService,
cornerstoneCacheService,
viewportActionCornersService,
} = servicesManager.services;
const elementEnabledHandler = useCallback( const [viewportDialogState] = useViewportDialog();
evt => { // useCallback for scroll bar height calculation
// check this is this element reference and return early if doesn't match const setImageScrollBarHeight = useCallback(() => {
if (evt.detail.element !== elementRef.current) { const scrollbarHeight = `${elementRef.current.clientHeight - 40}px`;
return; setScrollbarHeight(scrollbarHeight);
}, [elementRef]);
// useCallback for onResize
const onResize = useCallback(() => {
if (elementRef.current) {
cornerstoneViewportService.resize();
setImageScrollBarHeight();
} }
}, [elementRef]);
const { viewportId, element } = evt.detail; const cleanUpServices = useCallback(
const viewportInfo = cornerstoneViewportService.getViewportInfo(viewportId); viewportInfo => {
setEnabledElement(viewportId, element); const renderingEngineId = viewportInfo.getRenderingEngineId();
setEnabledVPElement(element); const syncGroups = viewportInfo.getSyncGroups();
const renderingEngineId = viewportInfo.getRenderingEngineId(); toolGroupService.removeViewportFromToolGroup(viewportId, renderingEngineId);
const toolGroupId = viewportInfo.getToolGroupId(); syncGroupService.removeViewportFromSyncGroup(viewportId, renderingEngineId, syncGroups);
const syncGroups = viewportInfo.getSyncGroups();
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; const elementEnabledHandler = useCallback(
evt => {
if (synchronizersStore?.[viewportId]?.length && !isHangingProtocolLayout) { // check this is this element reference and return early if doesn't match
// If the viewport used to have a synchronizer, re apply it again if (evt.detail.element !== elementRef.current) {
_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) {
return; 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); const viewportInfo = cornerstoneViewportService.getViewportInfo(viewportId);
if (viewportInfo.hasDisplaySet(invalidatedDisplaySetInstanceUID)) { if (!viewportInfo) {
const viewportData = viewportInfo.getViewportData(); return;
const newViewportData = await cornerstoneCacheService.invalidateViewportData(
viewportData,
invalidatedDisplaySetInstanceUID,
dataSource,
displaySetService
);
const keepCamera = true;
cornerstoneViewportService.updateViewport(viewportId, newViewportData, keepCamera);
} }
}
);
return () => {
unsubscribe();
};
}, [viewportId]);
useEffect(() => { cornerstoneViewportService.storePresentation({ viewportId });
// handle the default viewportType to be stack
if (!viewportOptions.viewportType) {
viewportOptions.viewportType = STACK;
}
const loadViewportData = async () => { // This should be done after the store presentation since synchronizers
const viewportData = await cornerstoneCacheService.createViewportData( // will get cleaned up and they need the viewportInfo to be present
displaySets, cleanUpServices(viewportInfo);
viewportOptions,
dataSource,
initialImageIndex
);
// The presentation state will have been stored previously by closing if (onElementDisabled && typeof onElementDisabled === 'function') {
// a viewport. Otherwise, this viewport will be unchanged and the onElementDisabled(viewportInfo);
// presentation information will be directly carried over. }
const state = stateSyncService.getState();
const lutPresentationStore = state.lutPresentationStore as LutPresentation;
const positionPresentationStore = state.positionPresentationStore as PositionPresentation;
const { presentationIds } = viewportOptions; cornerstoneViewportService.disableElement(viewportId);
const presentations = {
positionPresentation: positionPresentationStore[presentationIds?.positionPresentationId], eventTarget.removeEventListener(Enums.Events.ELEMENT_ENABLED, elementEnabledHandler);
lutPresentation: lutPresentationStore[presentationIds?.lutPresentationId],
}; };
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 // subscribe to displaySet metadata invalidation (updates)
// Used for segmentation hydration right now, since the logic to decide whether // Currently, if the metadata changes we need to re-render the display set
// a viewport needs to render a segmentation lives inside the CornerstoneViewportService // for it to take effect in the viewport. As we deal with scaling in the loading,
// so we need to re-render (force update via change of the needsRerendering) so that React // we need to remove the old volume from the cache, and let the
// does the diffing and decides we should render this again (although the id and element has not changed) // viewport to re-add it which will use the new metadata. Otherwise, the
// so that the CornerstoneViewportService can decide whether to render the segmentation or not. Not that we reached here we can turn it off. // viewport will use the cached volume and the new metadata will not be used.
if (viewportOptions.needsRerendering) { // Note: this approach does not actually end of sending network requests
viewportOptions.needsRerendering = false; // 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( const viewportInfo = cornerstoneViewportService.getViewportInfo(viewportId);
viewportId,
viewportData, if (viewportInfo.hasDisplaySet(invalidatedDisplaySetInstanceUID)) {
viewportOptions, const viewportData = viewportInfo.getViewportData();
displaySetOptions, const newViewportData = await cornerstoneCacheService.invalidateViewportData(
presentations viewportData,
invalidatedDisplaySetInstanceUID,
dataSource,
displaySetService
);
const keepCamera = true;
cornerstoneViewportService.updateViewport(viewportId, newViewportData, keepCamera);
}
}
); );
if (measurement) { return () => {
cs3DTools.annotation.selection.setAnnotationSelected(measurement.uid); unsubscribe();
};
}, [viewportId]);
useEffect(() => {
// handle the default viewportType to be stack
if (!viewportOptions.viewportType) {
viewportOptions.viewportType = STACK;
} }
};
loadViewportData(); const loadViewportData = async () => {
}, [viewportOptions, displaySets, dataSource]); const viewportData = await cornerstoneCacheService.createViewportData(
displaySets,
viewportOptions,
dataSource,
initialImageIndex
);
/** const presentations = getViewportPresentations(viewportId, viewportOptions);
* 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( let measurement;
elementRef, if (cacheJumpToMeasurementEvent?.viewportId === viewportId) {
viewportId, measurement = cacheJumpToMeasurementEvent.measurement;
servicesManager // 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 () => { cornerstoneViewportService.setViewportData(
unsubscribeFromJumpToMeasurementEvents(); viewportId,
}; viewportData,
}, [displaySets, elementRef, viewportId]); viewportOptions,
displaySetOptions,
presentations
);
// Set up the window level action menu in the viewport action corners. if (measurement) {
useEffect(() => { cs3DTools.annotation.selection.setAnnotationSelected(measurement.uid);
// Doing an === check here because the default config value when not set is true }
if (appConfig.addWindowLevelActionMenu === false) { };
return;
}
// TODO: In the future we should consider using the customization service loadViewportData();
// to determine if and in which corner various action components should go. }, [viewportOptions, displaySets, dataSource]);
const wlActionMenu = getWindowLevelActionMenu({
viewportId, /**
element: elementRef.current, * 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, displaySets,
viewportId,
viewportActionCornersService,
servicesManager, servicesManager,
commandsManager, commandsManager,
verticalDirection: AllInOneMenu.VerticalDirection.TopToBottom, appConfig,
horizontalDirection: AllInOneMenu.HorizontalDirection.RightToLeft, ]);
const { ref: resizeRef } = useResizeDetector({
onResize,
}); });
viewportActionCornersService.setComponent({ return (
viewportId, <React.Fragment>
id: 'windowLevelActionMenu', <div className="viewport-wrapper">
component: wlActionMenu, <div
location: viewportActionCornersService.LOCATIONS.topRight, className="cornerstone-viewport-element"
indexPriority: -100, style={{ height: '100%', width: '100%' }}
}); onContextMenu={e => e.preventDefault()}
}, [displaySets, viewportId, viewportActionCornersService, servicesManager, commandsManager]); onMouseDown={e => e.preventDefault()}
ref={el => {
const { ref: resizeRef } = useResizeDetector({ resizeRef.current = el;
onResize, elementRef.current = el;
}); }}
return ( ></div>
<React.Fragment> <CornerstoneOverlays
<div className="viewport-wrapper"> viewportId={viewportId}
<div toolBarService={toolbarService}
className="cornerstone-viewport-element" element={elementRef.current}
style={{ height: '100%', width: '100%' }} scrollbarHeight={scrollbarHeight}
onContextMenu={e => e.preventDefault()} servicesManager={servicesManager}
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}
/> />
)} <CinePlayer
</div> enabledVPElement={enabledVPElement}
{/* The OHIFViewportActionCorners follows the viewport in the DOM so that it is naturally at a higher z-index.*/} viewportId={viewportId}
<OHIFViewportActionCorners viewportId={viewportId} /> servicesManager={servicesManager}
</React.Fragment> />
); <ActiveViewportBehavior
}, areEqual); 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) { function _subscribeToJumpToMeasurementEvents(elementRef, viewportId, servicesManager) {
const { measurementService, cornerstoneViewportService } = servicesManager.services; const { measurementService, cornerstoneViewportService } = servicesManager.services;
@ -542,12 +516,15 @@ function _jumpToMeasurement(measurement, targetElementRef, viewportId, servicesM
} }
} }
function _rehydrateSynchronizers( function _rehydrateSynchronizers(viewportId: string, syncGroupService: any) {
synchronizersStore: { [key: string]: unknown }, const { synchronizersStore } = useSynchronizersStore.getState();
viewportId: string, const synchronizers = synchronizersStore[viewportId];
syncGroupService: any
) { if (!synchronizers) {
synchronizersStore[viewportId].forEach(synchronizerObj => { return;
}
synchronizers.forEach(synchronizerObj => {
if (!synchronizerObj.id) { if (!synchronizerObj.id) {
return; return;
} }
@ -597,17 +574,91 @@ function _rehydrateSynchronizers(
// Component displayName // Component displayName
OHIFCornerstoneViewport.displayName = 'OHIFCornerstoneViewport'; OHIFCornerstoneViewport.displayName = 'OHIFCornerstoneViewport';
OHIFCornerstoneViewport.propTypes = { function areEqual(prevProps, nextProps) {
displaySets: PropTypes.array.isRequired, if (nextProps.needsRerendering) {
dataSource: PropTypes.object.isRequired, console.debug('OHIFCornerstoneViewport: Rerender caused by: needsRerendering');
viewportOptions: PropTypes.object, return false;
displaySetOptions: PropTypes.arrayOf(PropTypes.any), }
servicesManager: PropTypes.object.isRequired,
onElementEnabled: PropTypes.func, if (prevProps.displaySets.length !== nextProps.displaySets.length) {
isJumpToMeasurementDisabled: PropTypes.bool, console.debug('OHIFCornerstoneViewport: Rerender caused by: displaySets length change');
// Note: you SHOULD NOT use the initialImageIdOrIndex for manipulation return false;
// 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]), 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; export default OHIFCornerstoneViewport;

View File

@ -2,9 +2,9 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { vec3 } from 'gl-matrix'; import { vec3 } from 'gl-matrix';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { metaData, Enums, utilities } from '@cornerstonejs/core'; 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 { 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 { formatPN, formatDICOMDate, formatDICOMTime, formatNumberPrecision } from './utils';
import { StackViewportData, VolumeViewportData } from '../../types/CornerstoneCacheService'; import { StackViewportData, VolumeViewportData } from '../../types/CornerstoneCacheService';
@ -45,14 +45,14 @@ const OverlayItemComponents = {
'ohif.overlayItem.instanceNumber': InstanceNumberOverlayItem, 'ohif.overlayItem.instanceNumber': InstanceNumberOverlayItem,
}; };
const studyDateItem = { const studyDateItem = {
id: 'StudyDate', id: 'StudyDate',
customizationType: 'ohif.overlayItem', customizationType: 'ohif.overlayItem',
label: '', label: '',
title: 'Study date', title: 'Study date',
condition: ({ referenceInstance }) => referenceInstance?.StudyDate, condition: ({ referenceInstance }) => referenceInstance?.StudyDate,
contentF: ({ referenceInstance, formatters: { formatDate } }) => formatDate(referenceInstance.StudyDate), contentF: ({ referenceInstance, formatters: { formatDate } }) =>
formatDate(referenceInstance.StudyDate),
}; };
const seriesDescriptionItem = { const seriesDescriptionItem = {
@ -63,15 +63,19 @@ const seriesDescriptionItem = {
condition: ({ referenceInstance }) => { condition: ({ referenceInstance }) => {
return referenceInstance && referenceInstance.SeriesDescription; 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 topRightItems = { id: 'cornerstoneOverlayTopRight', items: [] };
const bottomLeftItems = { const bottomLeftItems = {
id: 'cornerstoneOverlayBottomLeft', items: [ id: 'cornerstoneOverlayBottomLeft',
items: [
{ {
id: 'WindowLevel', id: 'WindowLevel',
customizationType: 'ohif.overlayItem.windowLevel', customizationType: 'ohif.overlayItem.windowLevel',
@ -79,12 +83,12 @@ const bottomLeftItems = {
{ {
id: 'ZoomLevel', id: 'ZoomLevel',
customizationType: 'ohif.overlayItem.zoomLevel', customizationType: 'ohif.overlayItem.zoomLevel',
condition: (props) => { condition: props => {
const activeToolName = props.toolGroupService.getActiveToolForViewport(props.viewportId); const activeToolName = props.toolGroupService.getActiveToolForViewport(props.viewportId);
return activeToolName === 'Zoom'; return activeToolName === 'Zoom';
}, },
}, },
] ],
}; };
const bottomRightItems = { const bottomRightItems = {
@ -94,7 +98,7 @@ const bottomRightItems = {
id: 'InstanceNumber', id: 'InstanceNumber',
customizationType: 'ohif.overlayItem.instanceNumber', customizationType: 'ohif.overlayItem.instanceNumber',
}, },
] ],
}; };
/** /**
@ -145,19 +149,18 @@ function CustomizableViewportOverlay({
// append functionality. This code enables the historical usage, but // append functionality. This code enables the historical usage, but
// the recommended functionality is to append to the default values in // the recommended functionality is to append to the default values in
// cornerstoneOverlay rather than defining individual items. // cornerstoneOverlay rather than defining individual items.
const topLeftCustomization = customizationService.getCustomization( const topLeftCustomization =
'cornerstoneOverlayTopLeft' customizationService.getCustomization('cornerstoneOverlayTopLeft') ||
) || cornerstoneOverlay?.topLeftItems; cornerstoneOverlay?.topLeftItems;
const topRightCustomization = customizationService.getCustomization( const topRightCustomization =
'cornerstoneOverlayTopRight' customizationService.getCustomization('cornerstoneOverlayTopRight') ||
) || cornerstoneOverlay?.topRightItems; cornerstoneOverlay?.topRightItems;
const bottomLeftCustomization = customizationService.getCustomization( const bottomLeftCustomization =
'cornerstoneOverlayBottomLeft' customizationService.getCustomization('cornerstoneOverlayBottomLeft') ||
) || cornerstoneOverlay?.bottomLeftItems; cornerstoneOverlay?.bottomLeftItems;
const bottomRightCustomization = customizationService.getCustomization( const bottomRightCustomization =
'cornerstoneOverlayBottomRight' customizationService.getCustomization('cornerstoneOverlayBottomRight') ||
) || cornerstoneOverlay?.bottomRightItems; cornerstoneOverlay?.bottomRightItems;
const instanceNumber = useMemo( const instanceNumber = useMemo(
() => () =>
@ -177,13 +180,12 @@ function CustomizableViewportOverlay({
return { return {
displaySets, displaySets,
displaySet, displaySet,
instance: instances[imageIndex], instance: instances?.[imageIndex],
instances, instances,
referenceInstance, referenceInstance,
}; };
}, [viewportData, viewportId, instanceNumber, cornerstoneViewportService]); }, [viewportData, viewportId, instanceNumber, cornerstoneViewportService]);
/** /**
* Updating the VOI when the viewport changes its voi * Updating the VOI when the viewport changes its voi
*/ */
@ -307,7 +309,8 @@ function CustomizableViewportOverlay({
<> <>
{items.map((item, index) => ( {items.map((item, index) => (
<div key={`${keyPrefix}_${index}`}> <div key={`${keyPrefix}_${index}`}>
{(!item?.condition || item.condition(props)) && _renderOverlayItem(item, props) || null} {((!item?.condition || item.condition(props)) && _renderOverlayItem(item, props)) ||
null}
</div> </div>
))} ))}
</> </>
@ -316,7 +319,6 @@ function CustomizableViewportOverlay({
[_renderOverlayItem] [_renderOverlayItem]
); );
return ( return (
<ViewportOverlay <ViewportOverlay
topLeft={getContent(topLeftCustomization, 'topLeftOverlayItem')} topLeft={getContent(topLeftCustomization, 'topLeftOverlayItem')}
@ -335,7 +337,9 @@ function getDisplaySets(viewportData, displaySetService) {
if (!viewportData?.data?.length) { if (!viewportData?.data?.length) {
return null; 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) { if (!displaySets.length) {
return null; return null;
} }
@ -382,7 +386,6 @@ function _getInstanceNumberFromStack(viewportData, imageIndex) {
return parseInt(instanceNumber); return parseInt(instanceNumber);
} }
// Since volume viewports can be in any view direction, they can render // 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 // 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 // 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 // Todo: support fusion of acquisition plane which has instanceNumber
const { volume } = volumes[0]; const { volume } = volumes[0];
if (!volume) {
return;
}
const { direction, imageIds } = volume; const { direction, imageIds } = volume;
const cornerstoneViewport = cornerstoneViewportService.getCornerstoneViewport(viewportId); const cornerstoneViewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
@ -443,9 +451,10 @@ function OverlayItem(props) {
style={{ color, background }} style={{ color, background }}
title={title} 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> <span className="ml-1 mr-2 shrink-0">{value}</span>
</div>); </div>
);
} }
/** /**

View File

@ -1,7 +1,6 @@
import React, { useEffect } from 'react'; import React, { useEffect } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { Enums, VolumeViewport3D } from '@cornerstonejs/core'; import { Enums, VolumeViewport3D, utilities as csUtils } from '@cornerstonejs/core';
import { utilities as csToolsUtils } from '@cornerstonejs/tools';
import { ImageScrollbar } from '@ohif/ui'; import { ImageScrollbar } from '@ohif/ui';
function CornerstoneImageScrollbar({ function CornerstoneImageScrollbar({
@ -12,7 +11,9 @@ function CornerstoneImageScrollbar({
setImageSliceData, setImageSliceData,
scrollbarHeight, scrollbarHeight,
servicesManager, servicesManager,
}: withAppTypes) { }: withAppTypes<{
element: HTMLElement;
}>) {
const { cineService, cornerstoneViewportService } = servicesManager.services; const { cineService, cornerstoneViewportService } = servicesManager.services;
const onImageScrollbarChange = (imageIndex, viewportId) => { const onImageScrollbarChange = (imageIndex, viewportId) => {
@ -26,7 +27,7 @@ function CornerstoneImageScrollbar({
cineService.setCine({ id: viewportId, isPlaying: false }); cineService.setCine({ id: viewportId, isPlaying: false });
} }
csToolsUtils.jumpToSlice(viewport.element, { csUtils.jumpToSlice(viewport.element, {
imageIndex, imageIndex,
debounceLoading: true, debounceLoading: true,
}); });
@ -64,7 +65,9 @@ function CornerstoneImageScrollbar({
const updateIndex = event => { const updateIndex = event => {
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId); const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
if(!viewport || viewport instanceof VolumeViewport3D) return if (!viewport || viewport instanceof VolumeViewport3D) {
return;
}
const { imageIndex, newImageIdIndex = imageIndex } = event.detail; const { imageIndex, newImageIdIndex = imageIndex } = event.detail;
const numberOfSlices = viewport.getNumberOfSlices(); const numberOfSlices = viewport.getNumberOfSlices();
// find the index of imageId in the imageIds // find the index of imageId in the imageIds

View File

@ -1,14 +1,7 @@
import React, { useEffect, useState, useMemo } from 'react'; import React, { useEffect, useState, useMemo } from 'react';
import classNames from 'classnames'; import classNames from 'classnames';
import { import { metaData, Enums, Types, getEnabledElement } from '@cornerstonejs/core';
metaData,
Enums,
Types,
getEnabledElement,
utilities as csUtils,
} from '@cornerstonejs/core';
import { utilities } from '@cornerstonejs/tools'; import { utilities } from '@cornerstonejs/tools';
import PropTypes from 'prop-types';
import { vec3 } from 'gl-matrix'; import { vec3 } from 'gl-matrix';
import './ViewportOrientationMarkers.css'; import './ViewportOrientationMarkers.css';
@ -31,8 +24,9 @@ function ViewportOrientationMarkers({
useEffect(() => { useEffect(() => {
const cameraModifiedListener = (evt: Types.EventTypes.CameraModifiedEvent) => { const cameraModifiedListener = (evt: Types.EventTypes.CameraModifiedEvent) => {
const { rotation, previousCamera, camera } = evt.detail; const { previousCamera, camera } = evt.detail;
const { rotation } = camera;
if (rotation !== undefined) { if (rotation !== undefined) {
setRotation(rotation); setRotation(rotation);
} }
@ -74,7 +68,12 @@ function ViewportOrientationMarkers({
return false; return false;
} }
({ rowCosines, columnCosines, isDefaultValueSetForColumnCosine, isDefaultValueSetForColumnCosine } = metaData.get('imagePlaneModule', imageId) || {}); ({
rowCosines,
columnCosines,
isDefaultValueSetForColumnCosine,
isDefaultValueSetForColumnCosine,
} = metaData.get('imagePlaneModule', imageId) || {});
} else { } else {
if (!element || !getEnabledElement(element)) { if (!element || !getEnabledElement(element)) {
return ''; return '';
@ -90,7 +89,13 @@ function ViewportOrientationMarkers({
rowCosines = viewRight; rowCosines = viewRight;
} }
if (!rowCosines || !columnCosines || rotation === undefined || isDefaultValueSetForRowCosine || isDefaultValueSetForColumnCosine) { if (
!rowCosines ||
!columnCosines ||
rotation === undefined ||
isDefaultValueSetForRowCosine ||
isDefaultValueSetForColumnCosine
) {
return ''; return '';
} }

View File

@ -12,15 +12,23 @@ import {
utilities as cstUtils, utilities as cstUtils,
ReferenceLinesTool, ReferenceLinesTool,
} from '@cornerstonejs/tools'; } from '@cornerstonejs/tools';
import { Types as OhifTypes } from '@ohif/core'; import { Types as OhifTypes } from '@ohif/core';
import {
callLabelAutocompleteDialog,
showLabelAnnotationPopup,
createReportAsync,
callInputDialog,
colorPickerDialog,
} from '@ohif/extension-default';
import { vec3, mat4 } from 'gl-matrix'; import { vec3, mat4 } from 'gl-matrix';
import CornerstoneViewportDownloadForm from './utils/CornerstoneViewportDownloadForm'; import CornerstoneViewportDownloadForm from './utils/CornerstoneViewportDownloadForm';
import { callLabelAutocompleteDialog, showLabelAnnotationPopup } from './utils/callInputDialog';
import toggleImageSliceSync from './utils/imageSliceSync/toggleImageSliceSync'; import toggleImageSliceSync from './utils/imageSliceSync/toggleImageSliceSync';
import { getFirstAnnotationSelected } from './utils/measurementServiceMappings/utils/selection'; import { getFirstAnnotationSelected } from './utils/measurementServiceMappings/utils/selection';
import getActiveViewportEnabledElement from './utils/getActiveViewportEnabledElement'; import getActiveViewportEnabledElement from './utils/getActiveViewportEnabledElement';
import toggleVOISliceSync from './utils/toggleVOISliceSync'; import toggleVOISliceSync from './utils/toggleVOISliceSync';
import { usePositionPresentationStore, useSegmentationPresentationStore } from './stores';
const toggleSyncFunctions = { const toggleSyncFunctions = {
imageSlice: toggleImageSliceSync, imageSlice: toggleImageSliceSync,
@ -29,6 +37,7 @@ const toggleSyncFunctions = {
function commandsModule({ function commandsModule({
servicesManager, servicesManager,
extensionManager,
commandsManager, commandsManager,
}: OhifTypes.Extensions.ExtensionParams): OhifTypes.Extensions.CommandsModule { }: OhifTypes.Extensions.ExtensionParams): OhifTypes.Extensions.CommandsModule {
const { const {
@ -101,7 +110,49 @@ function commandsModule({
commandsManager.run(options, optionsToUse); 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 }) { getNearbyToolData({ nearbyToolData, element, canvasCoordinates }) {
return nearbyToolData ?? cstUtils.getAnnotationNearPoint(element, canvasCoordinates); return nearbyToolData ?? cstUtils.getAnnotationNearPoint(element, canvasCoordinates);
}, },
@ -435,9 +486,10 @@ function commandsModule({
viewport.setCamera({ viewUp: rotatedViewUp as CoreTypes.Point3 }); viewport.setCamera({ viewUp: rotatedViewUp as CoreTypes.Point3 });
viewport.render(); viewport.render();
} else if (viewport.getRotation !== undefined) { } else if (viewport.getRotation !== undefined) {
const currentRotation = viewport.getRotation(); const presentation = viewport.getViewPresentation();
const { rotation: currentRotation } = presentation;
const newRotation = (currentRotation + rotation) % 360; const newRotation = (currentRotation + rotation) % 360;
viewport.setProperties({ rotation: newRotation }); viewport.setViewPresentation({ rotation: newRotation });
viewport.render(); viewport.render();
} }
}, },
@ -554,7 +606,7 @@ function commandsModule({
// Set slice to last slice // Set slice to last slice
const options = { imageIndex: jumpIndex }; const options = { imageIndex: jumpIndex };
cstUtils.jumpToSlice(viewport.element, options); csUtils.jumpToSlice(viewport.element, options);
}, },
scroll: ({ direction }) => { scroll: ({ direction }) => {
const enabledElement = _getActiveViewportEnabledElement(); const enabledElement = _getActiveViewportEnabledElement();
@ -566,7 +618,7 @@ function commandsModule({
const { viewport } = enabledElement; const { viewport } = enabledElement;
const options = { delta: direction }; const options = { delta: direction };
cstUtils.scroll(viewport, options); csUtils.scroll(viewport, options);
}, },
setViewportColormap: ({ setViewportColormap: ({
viewportId, viewportId,
@ -577,7 +629,6 @@ function commandsModule({
}) => { }) => {
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId); const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
const actorEntries = viewport.getActors();
let hpOpacity; let hpOpacity;
// Retrieve active protocol's viewport match details // Retrieve active protocol's viewport match details
const { viewportMatchDetails } = hangingProtocolService.getActiveProtocol(); const { viewportMatchDetails } = hangingProtocolService.getActiveProtocol();
@ -596,14 +647,8 @@ function commandsModule({
// HP takes priority over the default opacity // HP takes priority over the default opacity
colormap = { ...colormap, opacity: hpOpacity || 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) { if (viewport instanceof StackViewport) {
setViewportProperties(viewport, viewportId); viewport.setProperties({ colormap });
} }
if (viewport instanceof VolumeViewport) { if (viewport instanceof VolumeViewport) {
@ -611,7 +656,9 @@ function commandsModule({
const { viewports } = viewportGridService.getState(); const { viewports } = viewportGridService.getState();
displaySetInstanceUID = viewports.get(viewportId)?.displaySetInstanceUIDs[0]; displaySetInstanceUID = viewports.get(viewportId)?.displaySetInstanceUIDs[0];
} }
setViewportProperties(viewport, displaySetInstanceUID);
const volumeId = viewport.getVolumeId();
viewport.setProperties({ colormap }, volumeId);
} }
if (immediate) { if (immediate) {
@ -827,6 +874,339 @@ function commandsModule({
ins?.resetCrosshairs(); 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 = { const definitions = {
@ -988,6 +1368,84 @@ function commandsModule({
toggleActiveDisabledToolbar: { toggleActiveDisabledToolbar: {
commandFn: actions.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 { return {

View File

@ -1,7 +1,6 @@
import React, { useCallback, useEffect, useState, useRef } from 'react'; import React, { useCallback, useEffect, useState, useRef } from 'react';
import { CinePlayer, useCine } from '@ohif/ui'; import { CinePlayer, useCine } from '@ohif/ui';
import { Enums, eventTarget, cache } from '@cornerstonejs/core'; import { Enums, eventTarget, cache } from '@cornerstonejs/core';
import { Enums as StreamingEnums } from '@cornerstonejs/streaming-image-volume-loader';
import { useAppConfig } from '@state'; import { useAppConfig } from '@state';
function WrappedCinePlayer({ function WrappedCinePlayer({
@ -101,7 +100,7 @@ function WrappedCinePlayer({
return; 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 // this doesn't makes sense that we are listening to this event on viewport element
enabledVPElement.addEventListener( enabledVPElement.addEventListener(
Enums.Events.VOLUME_VIEWPORT_NEW_VOLUME, Enums.Events.VOLUME_VIEWPORT_NEW_VOLUME,
@ -111,7 +110,10 @@ function WrappedCinePlayer({
return () => { return () => {
cineService.setCine({ id: viewportId, isPlaying: false }); 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( enabledVPElement.removeEventListener(
Enums.Events.VOLUME_VIEWPORT_NEW_VOLUME, Enums.Events.VOLUME_VIEWPORT_NEW_VOLUME,
newDisplaySetHandler newDisplaySetHandler
@ -181,13 +183,13 @@ function RenderCinePlayer({
}; };
eventTarget.addEventListener( eventTarget.addEventListener(
StreamingEnums.Events.DYNAMIC_VOLUME_TIME_POINT_INDEX_CHANGED, Enums.Events.DYNAMIC_VOLUME_TIME_POINT_INDEX_CHANGED,
handleTimePointIndexChange handleTimePointIndexChange
); );
return () => { return () => {
eventTarget.removeEventListener( eventTarget.removeEventListener(
StreamingEnums.Events.DYNAMIC_VOLUME_TIME_POINT_INDEX_CHANGED, Enums.Events.DYNAMIC_VOLUME_TIME_POINT_INDEX_CHANGED,
handleTimePointIndexChange handleTimePointIndexChange
); );
}; };
@ -199,7 +201,7 @@ function RenderCinePlayer({
} }
const { volumeId, timePointIndex, numTimePoints, splittingTag } = dynamicInfo || {}; const { volumeId, timePointIndex, numTimePoints, splittingTag } = dynamicInfo || {};
const volume = cache.getVolume(volumeId); const volume = cache.getVolume(volumeId, true);
volume.timePointIndex = timePointIndex; volume.timePointIndex = timePointIndex;
setDynamicInfo({ volumeId, timePointIndex, numTimePoints, label: splittingTag }); setDynamicInfo({ volumeId, timePointIndex, numTimePoints, label: splittingTag });
@ -207,7 +209,7 @@ function RenderCinePlayer({
const updateDynamicInfo = useCallback(props => { const updateDynamicInfo = useCallback(props => {
const { volumeId, timePointIndex } = props; const { volumeId, timePointIndex } = props;
const volume = cache.getVolume(volumeId); const volume = cache.getVolume(volumeId, true);
volume.timePointIndex = timePointIndex; volume.timePointIndex = timePointIndex;
}, []); }, []);
@ -223,6 +225,7 @@ function RenderCinePlayer({
isPlaying: false, isPlaying: false,
}); });
cineService.setIsCineEnabled(false); cineService.setIsCineEnabled(false);
cineService.setViewportCineClosed(viewportId);
}} }}
onPlayPauseChange={isPlaying => { onPlayPauseChange={isPlaying => {
cineService.setCine({ cineService.setCine({

View File

@ -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;

View File

@ -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' };
}
};

View File

@ -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} />;
}

View File

@ -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 PropTypes from 'prop-types';
import debounce from 'lodash.debounce'; import debounce from 'lodash.debounce';
import { PanelSection, WindowLevel } from '@ohif/ui'; import { PanelSection, WindowLevel } from '@ohif/ui';
import vtkColorMaps from '@kitware/vtk.js/Rendering/Core/ColorTransferFunction/ColorMaps'; import { Enums, eventTarget } from '@cornerstonejs/core';
import { Enums, eventTarget, cache as cs3DCache, utilities as csUtils } from '@cornerstonejs/core'; import { useActiveViewportDisplaySets } from '@ohif/core';
import { getViewportVolumeHistogram } from './getViewportVolumeHistogram'; import {
getNodeOpacity,
isPetVolumeWithDefaultOpacity,
isVolumeWithConstantOpacity,
getWindowLevelsData,
} from './utils';
const { Events } = Enums; const { Events } = Enums;
@ -16,19 +21,16 @@ const ViewportWindowLevel = ({
}>): ReactElement => { }>): ReactElement => {
const { cornerstoneViewportService } = servicesManager.services; const { cornerstoneViewportService } = servicesManager.services;
const [windowLevels, setWindowLevels] = useState([]); 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( const getViewportsWithVolumeIds = useCallback(
(volumeIds: string[]) => { (volumeIds: string[]) => {
const renderingEngine = cornerstoneViewportService.getRenderingEngine(); const renderingEngine = cornerstoneViewportService.getRenderingEngine();
const viewports = renderingEngine.getVolumeViewports(); const viewports = renderingEngine.getVolumeViewports();
return viewports.filter(vp => { return viewports.filter(vp => {
const viewportVolumeIds = vp.getActors().map(actor => actor.uid); const viewportVolumeIds = vp.getActors().map(actor => actor.referencedId);
return ( return (
volumeIds.length === viewportVolumeIds.length && volumeIds.length === viewportVolumeIds.length &&
volumeIds.every(volumeId => viewportVolumeIds.includes(volumeId)) volumeIds.every(volumeId => viewportVolumeIds.includes(volumeId))
@ -38,97 +40,10 @@ const ViewportWindowLevel = ({
[cornerstoneViewportService] [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 getVolumeOpacity = useCallback((viewport, volumeId) => {
const volumeActor = viewport.getActor(volumeId).actor; const volumeActor = viewport.getActors().find(actor => actor.referencedId === volumeId)?.actor;
if (isPetVolumeWithDefaultOpacity(volumeId, volumeActor)) { if (isPetVolumeWithDefaultOpacity(volumeId, volumeActor)) {
// Get the opacity from the second node at 0.1
return getNodeOpacity(volumeActor, 1); return getNodeOpacity(volumeActor, 1);
} else if (isVolumeWithConstantOpacity(volumeActor)) { } else if (isVolumeWithConstantOpacity(volumeActor)) {
return getNodeOpacity(volumeActor, 0); return getNodeOpacity(volumeActor, 0);
@ -137,85 +52,14 @@ const ViewportWindowLevel = ({
return undefined; 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(() => { const updateViewportHistograms = useCallback(() => {
getWindowLevelsData(viewportId).then(windowLevels => { const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
setWindowLevels(windowLevels); const viewportInfo = cornerstoneViewportService.getViewportInfo(viewportId);
getWindowLevelsData(viewport, viewportInfo, getVolumeOpacity).then(data => {
setWindowLevels(data);
}); });
}, [viewportId, getWindowLevelsData]); }, [viewportId, cornerstoneViewportService, getVolumeOpacity]);
const handleCornerstoneVOIModified = useCallback( const handleCornerstoneVOIModified = useCallback(
e => { e => {
@ -252,8 +96,8 @@ const ViewportWindowLevel = ({
[windowLevels] [windowLevels]
); );
const debouncedHandleCornerstoneVOIModified = useCallback( const debouncedHandleCornerstoneVOIModified = useMemo(
debounce(handleCornerstoneVOIModified, 100), () => debounce(handleCornerstoneVOIModified, 100),
[handleCornerstoneVOIModified] [handleCornerstoneVOIModified]
); );
@ -280,7 +124,7 @@ const ViewportWindowLevel = ({
return; return;
} }
const viewportVolumeIds = viewport.getActors().map(actor => actor.uid); const viewportVolumeIds = viewport.getActors().map(actor => actor.referencedId);
const viewports = getViewportsWithVolumeIds(viewportVolumeIds); const viewports = getViewportsWithVolumeIds(viewportVolumeIds);
viewports.forEach(vp => { viewports.forEach(vp => {
@ -291,49 +135,50 @@ const ViewportWindowLevel = ({
[getViewportsWithVolumeIds, cornerstoneViewportService] [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(() => { 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); document.addEventListener(Events.VOI_MODIFIED, debouncedHandleCornerstoneVOIModified, true);
eventTarget.addEventListener(
Events.IMAGE_VOLUME_LOADING_COMPLETED,
handleImageVolumeLoadingCompleted
);
const intervalId = setInterval(() => {
if (isLoading) {
updateViewportHistograms();
}
}, 1000);
return () => { return () => {
eventTarget.removeEventListener(
Events.IMAGE_VOLUME_LOADING_COMPLETED,
updateViewportHistograms
);
document.removeEventListener( document.removeEventListener(
Events.VOI_MODIFIED, Events.VOI_MODIFIED,
debouncedHandleCornerstoneVOIModified, debouncedHandleCornerstoneVOIModified,
true 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(() => { useEffect(() => {
const { unsubscribe } = cornerstoneViewportService.subscribe( const { unsubscribe } = cornerstoneViewportService.subscribe(
cornerstoneViewportService.EVENTS.VIEWPORT_VOLUMES_CHANGED, 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 () => { return () => {
unsubscribe(); unsubscribe();
}; };
}, [viewportId, cornerstoneViewportService, updateViewportHistograms]); }, [viewportId, cornerstoneViewportService, updateViewportHistograms, displaySetIds, isLoading]);
return ( return (
<PanelSection title="Window Level"> <PanelSection title="Window Level">

View File

@ -20,10 +20,6 @@ const workerFn = () => {
const getViewportVolumeHistogram = async (viewport, volume, options?) => { const getViewportVolumeHistogram = async (viewport, volume, options?) => {
workerManager.registerWorker('histogram-worker', workerFn, WorkerOptions); workerManager.registerWorker('histogram-worker', workerFn, WorkerOptions);
if (!volume?.loadStatus.loaded) {
return undefined;
}
const volumeImageData = viewport.getImageData(volume.volumeId); const volumeImageData = viewport.getImageData(volume.volumeId);
if (!volumeImageData) { if (!volumeImageData) {
@ -32,12 +28,15 @@ const getViewportVolumeHistogram = async (viewport, volume, options?) => {
let scalarData = volume.scalarData; let scalarData = volume.scalarData;
let prevTimePoint;
if (volume.numTimePoints > 1) { if (volume.numTimePoints > 1) {
prevTimePoint = volume.timePointIndex; const targetTimePoint = volume.numTimePoints - 1; // or any other time point you need
const middleTimePoint = Math.round(volume.numTimePoints / 2); scalarData = volume.voxelManager.getTimePointScalarData(targetTimePoint);
volume.timePointIndex = middleTimePoint; } else {
scalarData = volume.getScalarData(middleTimePoint); scalarData = volume.voxelManager.getCompleteScalarDataArray();
}
if (!scalarData?.length) {
return undefined;
} }
const { dimensions, origin, direction, spacing } = volume; const { dimensions, origin, direction, spacing } = volume;
@ -50,11 +49,12 @@ const getViewportVolumeHistogram = async (viewport, volume, options?) => {
scalarData, scalarData,
}); });
// after we calculate the range let's reset the timePointIndex
if (volume.numTimePoints > 1) {
volume.timePointIndex = prevTimePoint;
}
const { minimum: min, maximum: max } = range; const { minimum: min, maximum: max } = range;
if (min === Infinity || max === -Infinity) {
return undefined;
}
const calcHistOptions = { const calcHistOptions = {
numBins: 256, numBins: 256,
min: Math.max(min, options?.min ?? min), min: Math.max(min, options?.min ?? min),

View File

@ -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);
};

View File

@ -1,6 +1,6 @@
import React, { ReactElement, useCallback, useEffect, useRef, useState, useMemo } from 'react'; import React, { ReactElement, useCallback, useEffect, useRef, useState, useMemo } from 'react';
import { AllInOneMenu, ButtonGroup, SwitchButton } from '@ohif/ui'; import { AllInOneMenu, ButtonGroup, SwitchButton } from '@ohif/ui';
import { StackViewport } from '@cornerstonejs/core'; import { StackViewport, Types } from '@cornerstonejs/core';
import { ColormapProps } from '../../types/Colormap'; import { ColormapProps } from '../../types/Colormap';
export function Colormap({ export function Colormap({
@ -54,8 +54,10 @@ export function Colormap({
return colormap; return colormap;
} }
const actorEntries = viewport.getActors(); const actorEntries = viewport.getActors();
const actorEntry = actorEntries.find(entry => entry.uid.includes(displaySetInstanceUID)); const actorEntry = actorEntries?.find(entry =>
const { colormap } = viewport.getProperties(actorEntry.uid); entry.referencedId.includes(displaySetInstanceUID)
);
const { colormap } = (viewport as Types.IVolumeViewport).getProperties(actorEntry.referencedId);
if (!colormap) { if (!colormap) {
return colormaps.find(c => c.Name === 'Grayscale') || colormaps[0]; return colormaps.find(c => c.Name === 'Grayscale') || colormaps[0];
} }

View File

@ -15,12 +15,12 @@ import { ViewportPreset } from '../../types/ViewportPresets';
import { VolumeViewport3D } from '@cornerstonejs/core'; import { VolumeViewport3D } from '@cornerstonejs/core';
import { utilities } from '@cornerstonejs/core'; import { utilities } from '@cornerstonejs/core';
export const nonWLModalities = ['SR', 'SEG', 'SM', 'RTSTRUCT', 'RTPLAN', 'RTDOSE'];
export type WindowLevelActionMenuProps = { export type WindowLevelActionMenuProps = {
viewportId: string; viewportId: string;
element: HTMLElement; element: HTMLElement;
presets: Array<Record<string, Array<WindowLevelPreset>>>; presets: Array<Record<string, Array<WindowLevelPreset>>>;
verticalDirection: AllInOneMenu.VerticalDirection;
horizontalDirection: AllInOneMenu.HorizontalDirection;
colorbarProperties: ColorbarProperties; colorbarProperties: ColorbarProperties;
displaySets: Array<any>; displaySets: Array<any>;
volumeRenderingPresets: Array<ViewportPreset>; volumeRenderingPresets: Array<ViewportPreset>;
@ -53,8 +53,6 @@ export function WindowLevelActionMenu({
const backgroundColor = viewportInfo.getViewportOptions().background; const backgroundColor = viewportInfo.getViewportOptions().background;
const isLight = backgroundColor ? utilities.isEqual(backgroundColor, [1, 1, 1]) : false; const isLight = backgroundColor ? utilities.isEqual(backgroundColor, [1, 1, 1]) : false;
const nonImageModalities = ['SR', 'SEG', 'SM', 'RTSTRUCT', 'RTPLAN', 'RTDOSE'];
const { t } = useTranslation('WindowLevelActionMenu'); const { t } = useTranslation('WindowLevelActionMenu');
const [viewportGrid] = useViewportGrid(); const [viewportGrid] = useViewportGrid();
@ -119,7 +117,7 @@ export function WindowLevelActionMenu({
horizontalDirection={horizontalDirection} horizontalDirection={horizontalDirection}
iconClassName={classNames( iconClassName={classNames(
// Visible on hover and for the active viewport // 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', 'flex shrink-0 cursor-pointer rounded active:text-white text-primary-light',
isLight ? ' hover:bg-secondary-dark' : 'hover:bg-secondary-light/60' isLight ? ' hover:bg-secondary-dark' : 'hover:bg-secondary-light/60'
)} )}
@ -133,7 +131,7 @@ export function WindowLevelActionMenu({
{!is3DVolume && ( {!is3DVolume && (
<Colorbar <Colorbar
viewportId={viewportId} viewportId={viewportId}
displaySets={displaySets.filter(ds => !nonImageModalities.includes(ds.Modality))} displaySets={displaySets.filter(ds => !nonWLModalities.includes(ds.Modality))}
commandsManager={commandsManager} commandsManager={commandsManager}
servicesManager={servicesManager} servicesManager={servicesManager}
colorbarProperties={colorbarProperties} colorbarProperties={colorbarProperties}
@ -149,7 +147,7 @@ export function WindowLevelActionMenu({
<Colormap <Colormap
colormaps={colormaps} colormaps={colormaps}
viewportId={viewportId} viewportId={viewportId}
displaySets={displaySets.filter(ds => !nonImageModalities.includes(ds.Modality))} displaySets={displaySets.filter(ds => !nonWLModalities.includes(ds.Modality))}
commandsManager={commandsManager} commandsManager={commandsManager}
servicesManager={servicesManager} servicesManager={servicesManager}
/> />

View File

@ -1,5 +1,5 @@
import React, { ReactNode } from 'react'; import React, { ReactNode } from 'react';
import { WindowLevelActionMenu } from './WindowLevelActionMenu'; import { nonWLModalities, WindowLevelActionMenu } from './WindowLevelActionMenu';
export function getWindowLevelActionMenu({ export function getWindowLevelActionMenu({
viewportId, viewportId,
@ -9,7 +9,11 @@ export function getWindowLevelActionMenu({
commandsManager, commandsManager,
verticalDirection, verticalDirection,
horizontalDirection, horizontalDirection,
}: withAppTypes): ReactNode { }: withAppTypes<{
viewportId: string;
element: HTMLElement;
displaySets: AppTypes.DisplaySet[];
}>): ReactNode {
const { customizationService } = servicesManager.services; const { customizationService } = servicesManager.services;
const { presets } = customizationService.get('cornerstone.windowLevelPresets'); const { presets } = customizationService.get('cornerstone.windowLevelPresets');
@ -24,6 +28,14 @@ export function getWindowLevelActionMenu({
return { [displaySet.Modality]: presets[displaySet.Modality] }; 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 ( return (
<WindowLevelActionMenu <WindowLevelActionMenu
viewportId={viewportId} viewportId={viewportId}

View File

@ -35,8 +35,8 @@ export const ViewportActionCornersContext = createContext(DEFAULT_STATE);
export function ViewportActionCornersProvider({ children, service }) { export function ViewportActionCornersProvider({ children, service }) {
const viewportActionCornersReducer = (state, action) => { const viewportActionCornersReducer = (state, action) => {
switch (action.type) { switch (action.type) {
case 'SET_ACTION_COMPONENT': { case 'ADD_ACTION_COMPONENT': {
const { viewportId, id, component, location, indexPriority = 0 } = action.payload; const { viewportId, id, component, location, indexPriority } = action.payload;
// Get the components at the specified location of the specified viewport. // Get the components at the specified location of the specified viewport.
let locationComponents = state?.[viewportId]?.[location] let locationComponents = state?.[viewportId]?.[location]
? [...state[viewportId][location]] ? [...state[viewportId][location]]
@ -56,27 +56,48 @@ export function ViewportActionCornersProvider({ children, service }) {
// Insert the component from the payload but // Insert the component from the payload but
// do not insert an undefined or null component. // do not insert an undefined or null component.
if (component) { if (component) {
const insertionIndex = locationComponents.findIndex( let insertionIndex;
component => indexPriority <= component.indexPriority 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 = [
...locationComponents.slice(0, insertionIndex), ...locationComponents.slice(0, insertionIndex),
{ {
id, id,
component, component,
indexPriority, indexPriority: indexPriority ?? defaultPriority,
}, },
...locationComponents.slice(insertionIndex + 1), ...locationComponents.slice(insertionIndex),
]; ];
} }
return { return {
...state, ...state,
...{ [viewportId]: {
[viewportId]: { ...state[viewportId],
...state[viewportId], [location]: locationComponents,
[location]: locationComponents,
},
}, },
}; };
} }
@ -100,17 +121,17 @@ export function ViewportActionCornersProvider({ children, service }) {
return viewportActionCornersState; return viewportActionCornersState;
}, [viewportActionCornersState]); }, [viewportActionCornersState]);
const setComponent = useCallback( const addComponent = useCallback(
(actionComponentInfo: ActionComponentInfo) => { (actionComponentInfo: ActionComponentInfo) => {
dispatch({ type: 'SET_ACTION_COMPONENT', payload: actionComponentInfo }); dispatch({ type: 'ADD_ACTION_COMPONENT', payload: actionComponentInfo });
}, },
[dispatch] [dispatch]
); );
const setComponents = useCallback( const addComponents = useCallback(
(actionComponentInfos: Array<ActionComponentInfo>) => { (actionComponentInfos: Array<ActionComponentInfo>) => {
actionComponentInfos.forEach(actionComponentInfo => actionComponentInfos.forEach(actionComponentInfo =>
dispatch({ type: 'SET_ACTION_COMPONENT', payload: actionComponentInfo }) dispatch({ type: 'ADD_ACTION_COMPONENT', payload: actionComponentInfo })
); );
}, },
[dispatch] [dispatch]
@ -120,28 +141,28 @@ export function ViewportActionCornersProvider({ children, service }) {
(viewportId: string) => dispatch({ type: 'CLEAR_ACTION_COMPONENTS', payload: viewportId }), (viewportId: string) => dispatch({ type: 'CLEAR_ACTION_COMPONENTS', payload: viewportId }),
[dispatch] [dispatch]
); );
useEffect(() => { useEffect(() => {
if (service) { if (service) {
service.setServiceImplementation({ service.setServiceImplementation({
getState, getState,
setComponent, addComponent,
setComponents, addComponents,
clear, 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 viewportCornerActions = {
const api = {
getState, getState,
setComponent: props => service.setComponent(props), addComponent: props => service.addComponent(props),
setComponents: props => service.setComponents(props), addComponents: props => service.addComponents(props),
clear: props => service.clear(props), clear: props => service.clear(props),
}; };
const contextValue = useMemo( const contextValue = useMemo(
() => [viewportActionCornersState, api], () => [viewportActionCornersState, viewportCornerActions],
[viewportActionCornersState, api] [viewportActionCornersState, viewportCornerActions]
); );
return ( return (

View File

@ -1,6 +1,5 @@
import { Enums } from '@cornerstonejs/tools'; import { Enums } from '@cornerstonejs/tools';
import { toolNames } from './initCornerstoneTools'; import { toolNames } from './initCornerstoneTools';
import DicomUpload from './components/DicomUpload/DicomUpload';
import defaultWindowLevelPresets from './components/WindowLevelActionMenu/defaultWindowLevelPresets'; import defaultWindowLevelPresets from './components/WindowLevelActionMenu/defaultWindowLevelPresets';
import { colormaps } from './utils/colormaps'; import { colormaps } from './utils/colormaps';
import { CONSTANTS } from '@cornerstonejs/core'; import { CONSTANTS } from '@cornerstonejs/core';
@ -23,10 +22,12 @@ const tools = {
toolName: toolNames.Zoom, toolName: toolNames.Zoom,
bindings: [{ mouseButton: Enums.MouseBindings.Secondary }], bindings: [{ mouseButton: Enums.MouseBindings.Secondary }],
}, },
{ toolName: toolNames.StackScrollMouseWheel, bindings: [] }, {
toolName: toolNames.StackScroll,
bindings: [{ mouseButton: Enums.MouseBindings.Wheel }],
},
], ],
enabled: [ enabled: [
{ toolName: toolNames.SegmentationDisplay },
{ {
toolName: toolNames.PlanarFreehandContourSegmentation, toolName: toolNames.PlanarFreehandContourSegmentation,
configuration: { configuration: {
@ -38,13 +39,6 @@ const tools = {
function getCustomizationModule() { function getCustomizationModule() {
return [ return [
{
name: 'cornerstoneDicomUploadComponent',
value: {
id: 'dicomUploadComponent',
component: DicomUpload,
},
},
{ {
name: 'default', name: 'default',
value: [ value: [
@ -120,7 +114,7 @@ function getCustomizationModule() {
type: 'value', type: 'value',
}, },
{ {
value: 'areaUnit', value: 'areaUnits',
for: ['area'], for: ['area'],
type: 'unit', type: 'unit',
}, },
@ -140,7 +134,7 @@ function getCustomizationModule() {
}, },
{ {
displayName: 'Unit', displayName: 'Unit',
value: 'areaUnit', value: 'areaUnits',
type: 'value', type: 'value',
}, },
], ],
@ -170,12 +164,12 @@ function getCustomizationModule() {
type: 'value', type: 'value',
}, },
{ {
value: 'modalityUnit', value: 'pixelValueUnits',
for: ['mean', 'max' /** 'stdDev **/], for: ['mean', 'max' /** 'stdDev **/],
type: 'unit', type: 'unit',
}, },
{ {
value: 'areaUnit', value: 'areaUnits',
for: ['area'], for: ['area'],
type: 'unit', type: 'unit',
}, },

View File

@ -1,15 +1,11 @@
import React from 'react'; import React from 'react';
import { useAppConfig } from '@state';
import { Toolbox } from '@ohif/ui-next'; import { Toolbox } from '@ohif/ui-next';
import PanelSegmentation from './panels/PanelSegmentation'; import PanelSegmentation from './panels/PanelSegmentation';
import ActiveViewportWindowLevel from './components/ActiveViewportWindowLevel';
const getPanelModule = ({ commandsManager, servicesManager, extensionManager }: withAppTypes) => { const getPanelModule = ({ commandsManager, servicesManager, extensionManager }: withAppTypes) => {
const { customizationService } = servicesManager.services;
const wrappedPanelSegmentation = ({ configuration }) => { const wrappedPanelSegmentation = ({ configuration }) => {
const [appConfig] = useAppConfig();
return ( return (
<PanelSegmentation <PanelSegmentation
commandsManager={commandsManager} commandsManager={commandsManager}
@ -17,16 +13,25 @@ const getPanelModule = ({ commandsManager, servicesManager, extensionManager }:
extensionManager={extensionManager} extensionManager={extensionManager}
configuration={{ configuration={{
...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 wrappedPanelSegmentationWithTools = ({ configuration }) => {
const [appConfig] = useAppConfig();
return ( return (
<> <>
<Toolbox <Toolbox
@ -45,8 +50,6 @@ const getPanelModule = ({ commandsManager, servicesManager, extensionManager }:
extensionManager={extensionManager} extensionManager={extensionManager}
configuration={{ configuration={{
...configuration, ...configuration,
disableEditing: appConfig.disableEditing,
...customizationService.get('segmentation.panel'),
}} }}
/> />
</> </>
@ -54,6 +57,12 @@ const getPanelModule = ({ commandsManager, servicesManager, extensionManager }:
}; };
return [ return [
{
name: 'activeViewportWindowLevel',
component: () => {
return <ActiveViewportWindowLevel servicesManager={servicesManager} />;
},
},
{ {
name: 'panelSegmentation', name: 'panelSegmentation',
iconName: 'tab-segmentation', iconName: 'tab-segmentation',
@ -61,6 +70,13 @@ const getPanelModule = ({ commandsManager, servicesManager, extensionManager }:
label: 'Segmentation', label: 'Segmentation',
component: wrappedPanelSegmentation, component: wrappedPanelSegmentation,
}, },
{
name: 'panelSegmentationNoHeader',
iconName: 'tab-segmentation',
iconLabel: 'Segmentation',
label: 'Segmentation',
component: wrappedPanelSegmentationNoHeader,
},
{ {
name: 'panelSegmentationWithTools', name: 'panelSegmentationWithTools',
iconName: 'tab-segmentation', iconName: 'tab-segmentation',

View File

@ -89,7 +89,7 @@ function _getDisplaySetsFromSeries(instances, servicesManager, extensionManager)
const displaySet = { const displaySet = {
plugin: 'microscopy', plugin: 'microscopy',
Modality: 'SM', Modality: 'SM',
viewportType: csEnums.ViewportType.WholeSlide, viewportType: csEnums.ViewportType.WHOLE_SLIDE,
altImageText: 'Microscopy', altImageText: 'Microscopy',
displaySetInstanceUID: utils.guid(), displaySetInstanceUID: utils.guid(),
SOPInstanceUID, SOPInstanceUID,

View File

@ -6,6 +6,12 @@ const getToggledClassName = (isToggled: boolean) => {
: '!text-common-bright hover:!bg-primary-dark hover:text-primary-light'; : '!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) { export default function getToolbarModule({ commandsManager, servicesManager }: withAppTypes) {
const { const {
toolGroupService, toolGroupService,
@ -20,6 +26,52 @@ export default function getToolbarModule({ commandsManager, servicesManager }: w
return [ return [
// functions/helpers to be used by the toolbar buttons to decide if they should // functions/helpers to be used by the toolbar buttons to decide if they should
// enabled or not // 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', name: 'evaluate.cornerstoneTool',
evaluate: ({ viewportId, button, toolNames, disabledText }) => { evaluate: ({ viewportId, button, toolNames, disabledText }) => {
@ -32,11 +84,7 @@ export default function getToolbarModule({ commandsManager, servicesManager }: w
const toolName = toolbarService.getToolNameForButton(button); const toolName = toolbarService.getToolNameForButton(button);
if (!toolGroup || (!toolGroup.hasTool(toolName) && !toolNames)) { if (!toolGroup || (!toolGroup.hasTool(toolName) && !toolNames)) {
return { return getDisabledState(disabledText);
disabled: true,
className: '!text-common-bright ohif-disabled',
disabledText: disabledText ?? 'Not available on the current viewport',
};
} }
const isPrimaryActive = toolNames 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', name: 'evaluate.viewportProperties.toggle',
evaluate: ({ viewportId, button }) => { evaluate: ({ viewportId, button }) => {
@ -254,11 +268,7 @@ export default function getToolbarModule({ commandsManager, servicesManager }: w
}); });
if (!areReconstructable) { if (!areReconstructable) {
return { return getDisabledState(disabledText);
disabled: true,
className: '!text-common-bright ohif-disabled',
disabledText: disabledText ?? 'Not available on the current viewport',
};
} }
const isMpr = protocol?.id === 'mpr'; const isMpr = protocol?.id === 'mpr';
@ -288,11 +298,7 @@ function _evaluateToggle({
const toolName = toolbarService.getToolNameForButton(button); const toolName = toolbarService.getToolNameForButton(button);
if (!toolGroup.hasTool(toolName)) { if (!toolGroup.hasTool(toolName)) {
return { return getDisabledState(disabledText);
disabled: true,
className: '!text-common-bright ohif-disabled',
disabledText: disabledText ?? 'Not available on the current viewport',
};
} }
const isOff = offModes.includes(toolGroup.getToolOptions(toolName).mode); const isOff = offModes.includes(toolGroup.getToolOptions(toolName).mode);

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -1,5 +1,25 @@
import { Types } from '@ohif/core'; 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 = { export const mpr: Types.HangingProtocol.Protocol = {
id: 'mpr', id: 'mpr',
name: 'MPR', name: 'MPR',
@ -10,7 +30,6 @@ export const mpr: Types.HangingProtocol.Protocol = {
modifiedDate: '2023-08-15', modifiedDate: '2023-08-15',
availableTo: {}, availableTo: {},
editableBy: {}, editableBy: {},
// Unknown number of priors referenced - so just match any study
numberOfPriorsReferenced: 0, numberOfPriorsReferenced: 0,
protocolMatchingRules: [], protocolMatchingRules: [],
imageLoadStrategy: 'nth', imageLoadStrategy: 'nth',
@ -71,17 +90,7 @@ export const mpr: Types.HangingProtocol.Protocol = {
initialImageOptions: { initialImageOptions: {
preset: 'middle', preset: 'middle',
}, },
syncGroups: [ syncGroups: [VOI_SYNC_GROUP, HYDRATE_SEG_SYNC_GROUP],
{
type: 'voi',
id: 'mpr',
source: true,
target: true,
options: {
syncColormap: true,
},
},
],
}, },
displaySets: [ displaySets: [
{ {
@ -98,17 +107,7 @@ export const mpr: Types.HangingProtocol.Protocol = {
initialImageOptions: { initialImageOptions: {
preset: 'middle', preset: 'middle',
}, },
syncGroups: [ syncGroups: [VOI_SYNC_GROUP, HYDRATE_SEG_SYNC_GROUP],
{
type: 'voi',
id: 'mpr',
source: true,
target: true,
options: {
syncColormap: true,
},
},
],
}, },
displaySets: [ displaySets: [
{ {
@ -125,17 +124,7 @@ export const mpr: Types.HangingProtocol.Protocol = {
initialImageOptions: { initialImageOptions: {
preset: 'middle', preset: 'middle',
}, },
syncGroups: [ syncGroups: [VOI_SYNC_GROUP, HYDRATE_SEG_SYNC_GROUP],
{
type: 'voi',
id: 'mpr',
source: true,
target: true,
options: {
syncColormap: true,
},
},
],
}, },
displaySets: [ displaySets: [
{ {

View File

@ -6,7 +6,6 @@ import {
imageLoadPoolManager, imageLoadPoolManager,
imageRetrievalPoolManager, imageRetrievalPoolManager,
} from '@cornerstonejs/core'; } from '@cornerstonejs/core';
import * as csStreamingImageVolumeLoader from '@cornerstonejs/streaming-image-volume-loader';
import { Enums as cs3DToolsEnums } from '@cornerstonejs/tools'; import { Enums as cs3DToolsEnums } from '@cornerstonejs/tools';
import { Types } from '@ohif/core'; import { Types } from '@ohif/core';
import Enums from './enums'; import Enums from './enums';
@ -33,17 +32,28 @@ import { id } from './id';
import { measurementMappingUtils } from './utils/measurementServiceMappings'; import { measurementMappingUtils } from './utils/measurementServiceMappings';
import type { PublicViewportOptions } from './services/ViewportService/Viewport'; import type { PublicViewportOptions } from './services/ViewportService/Viewport';
import ImageOverlayViewerTool from './tools/ImageOverlayViewerTool'; import ImageOverlayViewerTool from './tools/ImageOverlayViewerTool';
import { showLabelAnnotationPopup } from './utils/callInputDialog';
import ViewportActionCornersService from './services/ViewportActionCornersService/ViewportActionCornersService'; import ViewportActionCornersService from './services/ViewportActionCornersService/ViewportActionCornersService';
import { ViewportActionCornersProvider } from './contextProviders/ViewportActionCornersProvider'; import { ViewportActionCornersProvider } from './contextProviders/ViewportActionCornersProvider';
import ActiveViewportWindowLevel from './components/ActiveViewportWindowLevel';
import getSOPInstanceAttributes from './utils/measurementServiceMappings/utils/getSOPInstanceAttributes'; import getSOPInstanceAttributes from './utils/measurementServiceMappings/utils/getSOPInstanceAttributes';
import { findNearbyToolData } from './utils/findNearbyToolData'; import { findNearbyToolData } from './utils/findNearbyToolData';
import { createFrameViewSynchronizer } from './synchronizers/frameViewSynchronizer'; import { createFrameViewSynchronizer } from './synchronizers/frameViewSynchronizer';
import { getSopClassHandlerModule } from './getSopClassHandlerModule'; 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 { imageRetrieveMetadataProvider } = cornerstone.utilities;
const Component = React.lazy(() => { const Component = React.lazy(() => {
@ -84,9 +94,8 @@ const cornerstoneExtension: Types.Extensions.Extension = {
]); ]);
toolbarService.registerEventForToolbarUpdate(segmentationService, [ toolbarService.registerEventForToolbarUpdate(segmentationService, [
segmentationService.EVENTS.SEGMENTATION_ADDED,
segmentationService.EVENTS.SEGMENTATION_REMOVED, segmentationService.EVENTS.SEGMENTATION_REMOVED,
segmentationService.EVENTS.SEGMENTATION_UPDATED, segmentationService.EVENTS.SEGMENTATION_MODIFIED,
]); ]);
toolbarService.registerEventForToolbarUpdate(cornerstone.eventTarget, [ toolbarService.registerEventForToolbarUpdate(cornerstone.eventTarget, [
@ -108,9 +117,9 @@ const cornerstoneExtension: Types.Extensions.Extension = {
// how to define them. // how to define them.
imageRetrieveMetadataProvider.add('stack', stackRetrieveOptions); imageRetrieveMetadataProvider.add('stack', stackRetrieveOptions);
}, },
getPanelModule,
onModeExit: ({ servicesManager }: withAppTypes): void => { 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 // Empty out the image load and retrieval pools to prevent memory leaks
// on the mode exits // on the mode exits
Object.values(cs3DEnums.RequestType).forEach(type => { Object.values(cs3DEnums.RequestType).forEach(type => {
@ -121,6 +130,13 @@ const cornerstoneExtension: Types.Extensions.Extension = {
cineService.setIsCineEnabled(false); cineService.setIsCineEnabled(false);
enabledElementReset(); 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; const { syncGroupService } = servicesManager.services;
syncGroupService.registerCustomSynchronizer('frameview', createFrameViewSynchronizer); syncGroupService.registerCustomSynchronizer('frameview', createFrameViewSynchronizer);
servicesManager.services.customizationService.setGlobalCustomization('dicomUploadComponent', {
component: props => <DicomUpload {...props} />,
});
return init.call(this, props); return init.call(this, props);
}, },
getToolbarModule, getToolbarModule,
getPanelModule({ servicesManager }) {
return [
{
name: 'activeViewportWindowLevel',
component: () => {
return <ActiveViewportWindowLevel servicesManager={servicesManager} />;
},
},
];
},
getHangingProtocolModule, getHangingProtocolModule,
getViewportModule({ servicesManager, commandsManager }) { getViewportModule({ servicesManager, commandsManager }) {
const ExtendedOHIFCornerstoneViewport = props => { const ExtendedOHIFCornerstoneViewport = props => {
@ -196,7 +204,6 @@ const cornerstoneExtension: Types.Extensions.Extension = {
}, },
getEnabledElement, getEnabledElement,
dicomLoaderService, dicomLoaderService,
showLabelAnnotationPopup,
}, },
}, },
{ {
@ -235,6 +242,17 @@ export {
ImageOverlayViewerTool, ImageOverlayViewerTool,
getSOPInstanceAttributes, getSOPInstanceAttributes,
dicomLoaderService, dicomLoaderService,
// Export all stores
useLutPresentationStore,
usePositionPresentationStore,
useSegmentationPresentationStore,
useSynchronizersStore,
Enums, Enums,
useMeasurements,
useActiveViewportSegmentationRepresentations,
useSegmentations,
PanelSegmentation,
PanelMeasurement,
DicomUpload,
}; };
export default cornerstoneExtension; export default cornerstoneExtension;

View File

@ -1,4 +1,4 @@
import OHIF, { Types, errorHandler } from '@ohif/core'; import OHIF, { errorHandler } from '@ohif/core';
import React from 'react'; import React from 'react';
import * as cornerstone from '@cornerstonejs/core'; import * as cornerstone from '@cornerstonejs/core';
@ -13,12 +13,13 @@ import {
getEnabledElement, getEnabledElement,
Settings, Settings,
utilities as csUtilities, utilities as csUtilities,
Enums as csEnums,
} from '@cornerstonejs/core'; } from '@cornerstonejs/core';
import { import {
cornerstoneStreamingImageVolumeLoader, cornerstoneStreamingImageVolumeLoader,
cornerstoneStreamingDynamicImageVolumeLoader, cornerstoneStreamingDynamicImageVolumeLoader,
} from '@cornerstonejs/streaming-image-volume-loader'; } from '@cornerstonejs/core/loaders';
import RequestTypes from '@cornerstonejs/core/enums/RequestType';
import initWADOImageLoader from './initWADOImageLoader'; import initWADOImageLoader from './initWADOImageLoader';
import initCornerstoneTools from './initCornerstoneTools'; import initCornerstoneTools from './initCornerstoneTools';
@ -33,6 +34,10 @@ import initContextMenu from './initContextMenu';
import initDoubleClick from './initDoubleClick'; import initDoubleClick from './initDoubleClick';
import initViewTiming from './utils/initViewTiming'; import initViewTiming from './utils/initViewTiming';
import { colormaps } from './utils/colormaps'; 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; const { registerColormap } = csUtilities.colormap;
@ -46,27 +51,12 @@ export default async function init({
servicesManager, servicesManager,
commandsManager, commandsManager,
extensionManager, extensionManager,
appConfig appConfig,
}: Types.Extensions.ExtensionParams): Promise<void> { }: withAppTypes): Promise<void> {
// Note: this should run first before initializing the cornerstone // Note: this should run first before initializing the cornerstone
// DO NOT CHANGE THE ORDER // 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({ await cs3DInit({
rendering: {
preferSizeOverAccuracy: Boolean(appConfig.preferSizeOverAccuracy),
useNorm16Texture: Boolean(appConfig.useNorm16Texture),
},
peerImport: appConfig.peerImport, peerImport: appConfig.peerImport,
}); });
@ -99,63 +89,39 @@ export default async function init({
cornerstoneViewportService, cornerstoneViewportService,
hangingProtocolService, hangingProtocolService,
viewportGridService, viewportGridService,
stateSyncService,
studyPrefetcherService,
} = servicesManager.services; } = servicesManager.services;
window.services = servicesManager.services; window.services = servicesManager.services;
window.extensionManager = extensionManager; window.extensionManager = extensionManager;
window.commandsManager = commandsManager; 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()) { if (appConfig.showCPUFallbackMessage && cornerstone.getShouldUseCPURendering()) {
_showCPURenderingModal(uiModalService, hangingProtocolService); _showCPURenderingModal(uiModalService, hangingProtocolService);
} }
const { getPresentationId: getLutPresentationId } = useLutPresentationStore.getState();
// Stores a map from `lutPresentationId` to a Presentation object so that const { getPresentationId: getSegmentationPresentationId } =
// an OHIFCornerstoneViewport can be redisplayed with the same LUT useSegmentationPresentationStore.getState();
stateSyncService.register('lutPresentationStore', { clearOnModeExit: true });
// Stores synchronizers state to be restored const { getPresentationId: getPositionPresentationId } = usePositionPresentationStore.getState();
stateSyncService.register('synchronizersStore', { clearOnModeExit: true });
// Stores a map from `positionPresentationId` to a Presentation object so that // register presentation id providers
// an OHIFCornerstoneViewport can be redisplayed with the same position viewportGridService.addPresentationIdProvider(
stateSyncService.register('positionPresentationStore', { 'positionPresentationId',
clearOnModeExit: true, getPositionPresentationId
}); );
viewportGridService.addPresentationIdProvider('lutPresentationId', getLutPresentationId);
viewportGridService.addPresentationIdProvider(
'segmentationPresentationId',
getSegmentationPresentationId
);
// Stores the entire ViewportGridService getState when toggling to one up cornerstoneTools.segmentation.config.style.setStyle(
// (e.g. via a double click) so that it can be restored when toggling back. { type: SegmentationRepresentations.Contour },
stateSyncService.register('toggleOneUpViewportGridStore', { {
clearOnModeExit: true, renderFill: false,
}); }
);
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,
});
const metadataProvider = OHIF.classes.MetadataProvider; 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 // These are set reasonably low to allow for interleaved retrieves and slower
// connections. // connections.
imageLoadPoolManager.maxNumRequests = { imageLoadPoolManager.maxNumRequests = {
interaction: appConfig?.maxNumRequests?.interaction || 10, [RequestTypes.Interaction]: appConfig?.maxNumRequests?.interaction || 10,
thumbnail: appConfig?.maxNumRequests?.thumbnail || 5, [RequestTypes.Thumbnail]: appConfig?.maxNumRequests?.thumbnail || 5,
prefetch: appConfig?.maxNumRequests?.prefetch || 5, [RequestTypes.Prefetch]: appConfig?.maxNumRequests?.prefetch || 5,
[RequestTypes.Compute]: appConfig?.maxNumRequests?.compute || 10,
}; };
initWADOImageLoader(userAuthenticationService, appConfig, extensionManager); initWADOImageLoader(userAuthenticationService, appConfig, extensionManager);
@ -201,17 +168,23 @@ export default async function init({
hangingProtocolService.subscribe( hangingProtocolService.subscribe(
hangingProtocolService.EVENTS.CUSTOM_IMAGE_LOAD_PERFORMED, hangingProtocolService.EVENTS.CUSTOM_IMAGE_LOAD_PERFORMED,
volumeInputArrayMap => { volumeInputArrayMap => {
const { lutPresentationStore } = useLutPresentationStore.getState();
const { segmentationPresentationStore } = useSegmentationPresentationStore.getState();
const { positionPresentationStore } = usePositionPresentationStore.getState();
for (const entry of volumeInputArrayMap.entries()) { for (const entry of volumeInputArrayMap.entries()) {
const [viewportId, volumeInputArray] = entry; const [viewportId, volumeInputArray] = entry;
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId); const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
const ohifViewport = cornerstoneViewportService.getViewportInfo(viewportId); const ohifViewport = cornerstoneViewportService.getViewportInfo(viewportId);
const { lutPresentationStore, positionPresentationStore } = stateSyncService.getState();
const { presentationIds } = ohifViewport.getViewportOptions(); const { presentationIds } = ohifViewport.getViewportOptions();
const presentations = { const presentations = {
positionPresentation: positionPresentationStore[presentationIds?.positionPresentationId], positionPresentation: positionPresentationStore[presentationIds?.positionPresentationId],
lutPresentation: lutPresentationStore[presentationIds?.lutPresentationId], lutPresentation: lutPresentationStore[presentationIds?.lutPresentationId],
segmentationPresentation:
segmentationPresentationStore[presentationIds?.segmentationPresentationId],
}; };
cornerstoneViewportService.setVolumesForViewport(viewport, volumeInputArray, presentations); cornerstoneViewportService.setVolumesForViewport(viewport, volumeInputArray, presentations);
@ -249,10 +222,6 @@ export default async function init({
handler(detail.error); 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_FAILED, imageLoadFailedHandler);
eventTarget.addEventListener(EVENTS.IMAGE_LOAD_ERROR, imageLoadFailedHandler); eventTarget.addEventListener(EVENTS.IMAGE_LOAD_ERROR, imageLoadFailedHandler);
@ -269,26 +238,11 @@ export default async function init({
commandsManager.runCommand('resetCrosshairs', { viewportId }); commandsManager.runCommand('resetCrosshairs', { viewportId });
}); });
// eventTarget.addEventListener(EVENTS.STACK_VIEWPORT_NEW_STACK, toolbarEventListener);
initViewTiming({ element }); 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_ENABLED, elementEnabledHandler.bind(null));
eventTarget.addEventListener(EVENTS.ELEMENT_DISABLED, elementDisabledHandler.bind(null));
colormaps.forEach(registerColormap); colormaps.forEach(registerColormap);
// Event listener // Event listener
@ -301,8 +255,45 @@ export default async function init({
type: 'error', 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() { function CPUModal() {

View File

@ -1,12 +1,9 @@
import { cache } from '@cornerstonejs/core'; import { cache, Types } from '@cornerstonejs/core';
import { utilities } from '@cornerstonejs/tools'; import { utilities } from '@cornerstonejs/tools';
function _getVolumesFromViewport(viewport) { function _getVolumeFromViewport(viewport: Types.IBaseVolumeViewport) {
return viewport ? viewport.getActors().map(actor => cache.getVolume(actor.uid)) : []; const volumeIds = viewport.getAllVolumeIds();
} const volumes = volumeIds.map(id => cache.getVolume(id));
function _getVolumeFromViewport(viewport) {
const volumes = _getVolumesFromViewport(viewport).filter(volume => volume);
const dynamicVolume = volumes.find(volume => volume.isDynamicVolume()); const dynamicVolume = volumes.find(volume => volume.isDynamicVolume());
return dynamicVolume ?? volumes[0]; return dynamicVolume ?? volumes[0];

View File

@ -2,9 +2,8 @@ import {
PanTool, PanTool,
WindowLevelTool, WindowLevelTool,
StackScrollTool, StackScrollTool,
StackScrollMouseWheelTool, VolumeRotateTool,
ZoomTool, ZoomTool,
VolumeRotateMouseWheelTool,
MIPJumpToClickTool, MIPJumpToClickTool,
LengthTool, LengthTool,
RectangleROITool, RectangleROITool,
@ -19,7 +18,6 @@ import {
CobbAngleTool, CobbAngleTool,
MagnifyTool, MagnifyTool,
CrosshairsTool, CrosshairsTool,
SegmentationDisplayTool,
RectangleScissorsTool, RectangleScissorsTool,
SphereScissorsTool, SphereScissorsTool,
CircleScissorsTool, CircleScissorsTool,
@ -47,15 +45,15 @@ export default function initCornerstoneTools(configuration = {}) {
CrosshairsTool.isAnnotation = false; CrosshairsTool.isAnnotation = false;
ReferenceLinesTool.isAnnotation = false; ReferenceLinesTool.isAnnotation = false;
AdvancedMagnifyTool.isAnnotation = false; AdvancedMagnifyTool.isAnnotation = false;
PlanarFreehandContourSegmentationTool.isAnnotation = false;
init(configuration); init(configuration);
addTool(PanTool); addTool(PanTool);
addTool(WindowLevelTool); addTool(WindowLevelTool);
addTool(StackScrollMouseWheelTool);
addTool(StackScrollTool); addTool(StackScrollTool);
addTool(VolumeRotateTool);
addTool(ZoomTool); addTool(ZoomTool);
addTool(ProbeTool); addTool(ProbeTool);
addTool(VolumeRotateMouseWheelTool);
addTool(MIPJumpToClickTool); addTool(MIPJumpToClickTool);
addTool(LengthTool); addTool(LengthTool);
addTool(RectangleROITool); addTool(RectangleROITool);
@ -69,7 +67,6 @@ export default function initCornerstoneTools(configuration = {}) {
addTool(CobbAngleTool); addTool(CobbAngleTool);
addTool(MagnifyTool); addTool(MagnifyTool);
addTool(CrosshairsTool); addTool(CrosshairsTool);
addTool(SegmentationDisplayTool);
addTool(RectangleScissorsTool); addTool(RectangleScissorsTool);
addTool(SphereScissorsTool); addTool(SphereScissorsTool);
addTool(CircleScissorsTool); addTool(CircleScissorsTool);
@ -108,9 +105,8 @@ const toolNames = {
ArrowAnnotate: ArrowAnnotateTool.toolName, ArrowAnnotate: ArrowAnnotateTool.toolName,
WindowLevel: WindowLevelTool.toolName, WindowLevel: WindowLevelTool.toolName,
StackScroll: StackScrollTool.toolName, StackScroll: StackScrollTool.toolName,
StackScrollMouseWheel: StackScrollMouseWheelTool.toolName,
Zoom: ZoomTool.toolName, Zoom: ZoomTool.toolName,
VolumeRotateMouseWheel: VolumeRotateMouseWheelTool.toolName, VolumeRotate: VolumeRotateTool.toolName,
MipJumpToClick: MIPJumpToClickTool.toolName, MipJumpToClick: MIPJumpToClickTool.toolName,
Length: LengthTool.toolName, Length: LengthTool.toolName,
DragProbe: DragProbeTool.toolName, DragProbe: DragProbeTool.toolName,
@ -124,7 +120,6 @@ const toolNames = {
CobbAngle: CobbAngleTool.toolName, CobbAngle: CobbAngleTool.toolName,
Magnify: MagnifyTool.toolName, Magnify: MagnifyTool.toolName,
Crosshairs: CrosshairsTool.toolName, Crosshairs: CrosshairsTool.toolName,
SegmentationDisplay: SegmentationDisplayTool.toolName,
Brush: BrushTool.toolName, Brush: BrushTool.toolName,
PaintFill: PaintFillTool.toolName, PaintFill: PaintFillTool.toolName,
ReferenceLines: ReferenceLinesTool.toolName, ReferenceLines: ReferenceLinesTool.toolName,

View File

@ -1,4 +1,4 @@
import { eventTarget } from '@cornerstonejs/core'; import { eventTarget, Types } from '@cornerstonejs/core';
import { Enums, annotation } from '@cornerstonejs/tools'; import { Enums, annotation } from '@cornerstonejs/tools';
import { DicomMetadataStore } from '@ohif/core'; import { DicomMetadataStore } from '@ohif/core';
@ -7,6 +7,7 @@ import { toolNames } from './initCornerstoneTools';
import { onCompletedCalibrationLine } from './tools/CalibrationLineTool'; import { onCompletedCalibrationLine } from './tools/CalibrationLineTool';
import measurementServiceMappingsFactory from './utils/measurementServiceMappings/measurementServiceMappingsFactory'; import measurementServiceMappingsFactory from './utils/measurementServiceMappings/measurementServiceMappingsFactory';
import getSOPInstanceAttributes from './utils/measurementServiceMappings/utils/getSOPInstanceAttributes'; 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 { CORNERSTONE_3D_TOOLS_SOURCE_NAME, CORNERSTONE_3D_TOOLS_SOURCE_VERSION } = CSExtensionEnums;
const { removeAnnotation } = annotation.state; const { removeAnnotation } = annotation.state;
@ -344,8 +345,7 @@ const connectMeasurementServiceToTools = (measurementService, cornerstoneViewpor
return; return;
} }
const { uid, label } = measurement; const { uid, label, isLocked, isVisible } = measurement;
const sourceAnnotation = annotation.state.getAnnotation(uid); const sourceAnnotation = annotation.state.getAnnotation(uid);
const { data, metadata } = sourceAnnotation; const { data, metadata } = sourceAnnotation;
@ -361,7 +361,23 @@ const connectMeasurementServiceToTools = (measurementService, cornerstoneViewpor
data.text = label; 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);
} }
); );

View File

@ -1,47 +1,18 @@
import * as cornerstone from '@cornerstonejs/core';
import { volumeLoader } from '@cornerstonejs/core'; import { volumeLoader } from '@cornerstonejs/core';
import { import {
cornerstoneStreamingImageVolumeLoader, cornerstoneStreamingImageVolumeLoader,
cornerstoneStreamingDynamicImageVolumeLoader, cornerstoneStreamingDynamicImageVolumeLoader,
} from '@cornerstonejs/streaming-image-volume-loader'; } from '@cornerstonejs/core/loaders';
import dicomImageLoader, { webWorkerManager } from '@cornerstonejs/dicom-image-loader'; import dicomImageLoader from '@cornerstonejs/dicom-image-loader';
import dicomParser from 'dicom-parser';
import { errorHandler, utils } from '@ohif/core'; import { errorHandler, utils } from '@ohif/core';
const { registerVolumeLoader } = volumeLoader; 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( export default function initWADOImageLoader(
userAuthenticationService, userAuthenticationService,
appConfig, appConfig,
extensionManager extensionManager
) { ) {
dicomImageLoader.external.cornerstone = cornerstone;
dicomImageLoader.external.dicomParser = dicomParser;
registerVolumeLoader('cornerstoneStreamingImageVolume', cornerstoneStreamingImageVolumeLoader); registerVolumeLoader('cornerstoneStreamingImageVolume', cornerstoneStreamingImageVolumeLoader);
registerVolumeLoader( registerVolumeLoader(
@ -49,17 +20,11 @@ export default function initWADOImageLoader(
cornerstoneStreamingDynamicImageVolumeLoader cornerstoneStreamingDynamicImageVolumeLoader
); );
dicomImageLoader.configure({ dicomImageLoader.init({
decodeConfig: { maxWebWorkers: Math.min(
// !! IMPORTANT !! Math.max(navigator.hardwareConcurrency - 1, 1),
// We should set this flag to false, since, by default @cornerstonejs/dicom-image-loader appConfig.maxNumberOfWebWorkers
// 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),
},
beforeSend: function (xhr) { beforeSend: function (xhr) {
//TODO should be removed in the future and request emitted by DicomWebDataSource //TODO should be removed in the future and request emitted by DicomWebDataSource
const sourceConfig = extensionManager.getActiveDataSource()?.[0].getConfig() ?? {}; const sourceConfig = extensionManager.getActiveDataSource()?.[0].getConfig() ?? {};
@ -84,16 +49,8 @@ export default function initWADOImageLoader(
errorHandler.getHTTPErrorHandler(error); errorHandler.getHTTPErrorHandler(error);
}, },
}); });
initWebWorkers(appConfig);
} }
export function destroy() { export function destroy() {
// Note: we don't want to call .terminate on the webWorkerManager since console.debug('Destroying WADO Image Loader');
// that resets the config
const webWorkers = webWorkerManager.webWorkers;
for (let i = 0; i < webWorkers.length; i++) {
webWorkers[i].worker.terminate();
}
webWorkers.length = 0;
} }

View File

@ -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>
</>
);
}

View File

@ -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>
</>
);
}

View File

@ -48,6 +48,17 @@ export default class ColorbarService extends PubSubService {
super(ColorbarService.EVENTS); 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`. * 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. * 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) { public addColorbar(viewportId, displaySetInstanceUIDs, options = {} as ColorbarOptions) {
const renderingEngine = getRenderingEngine(RENDERING_ENGINE_ID); const renderingEngine = getRenderingEngine(RENDERING_ENGINE_ID);
const viewport = renderingEngine.getViewport(viewportId); const viewport = renderingEngine.getViewport(viewportId);
if (!viewport) {
return;
}
const { element } = viewport; const { element } = viewport;
const actorEntries = viewport.getActors(); const actorEntries = viewport.getActors();
if (!actorEntries || actorEntries.length === 0) {
return;
}
const { position, width: thickness, activeColormapName, colormaps } = options; const { position, width: thickness, activeColormapName, colormaps } = options;
const numContainers = displaySetInstanceUIDs.length; const numContainers = displaySetInstanceUIDs.length;
@ -74,11 +95,9 @@ export default class ColorbarService extends PubSubService {
); );
displaySetInstanceUIDs.forEach((displaySetInstanceUID, index) => { displaySetInstanceUIDs.forEach((displaySetInstanceUID, index) => {
const actorEntry = actorEntries.find(entry => entry.uid.includes(displaySetInstanceUID)); const volumeId = this.getVolumeIdForIdentifier(viewport, displaySetInstanceUID);
const volumeId = actorEntry?.uid;
const properties = viewport?.getProperties(volumeId); const properties = viewport?.getProperties(volumeId);
const colormap = properties?.colormap; const colormap = properties?.colormap;
// if there's an initial colormap set, and no colormap on the viewport, set it
if (activeColormapName && !colormap) { if (activeColormapName && !colormap) {
this.setViewportColormap( this.setViewportColormap(
viewportId, viewportId,
@ -196,9 +215,8 @@ export default class ColorbarService extends PubSubService {
return; return;
} }
const setViewportProperties = (viewport, uid) => { const setViewportProperties = (viewport, uid) => {
const actorEntry = actorEntries.find(entry => entry.uid.includes(uid)); const volumeId = this.getVolumeIdForIdentifier(viewport, uid);
const { actor: volumeActor, uid: volumeId } = actorEntry; viewport.setProperties({ colormap }, volumeId);
viewport.setProperties({ colormap, volumeActor }, volumeId);
}; };
if (viewport instanceof StackViewport) { if (viewport instanceof StackViewport) {

View File

@ -1,5 +1,5 @@
import { Types } from '@ohif/core'; 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 getCornerstoneViewportType from '../../utils/getCornerstoneViewportType';
import { StackViewportData, VolumeViewportData } from '../../types/CornerstoneCacheService'; import { StackViewportData, VolumeViewportData } from '../../types/CornerstoneCacheService';
@ -33,27 +33,11 @@ class CornerstoneCacheService {
public async createViewportData( public async createViewportData(
displaySets: Types.DisplaySet[], displaySets: Types.DisplaySet[],
viewportOptions: Record<string, unknown>, viewportOptions: AppTypes.ViewportGrid.GridViewportOptions,
dataSource: unknown, dataSource: unknown,
initialImageIndex?: number initialImageIndex?: number
): Promise<StackViewportData | VolumeViewportData> { ): Promise<StackViewportData | VolumeViewportData> {
let viewportType = viewportOptions.viewportType as string; const 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 cs3DViewportType = getCornerstoneViewportType(viewportType, displaySets); const cs3DViewportType = getCornerstoneViewportType(viewportType, displaySets);
let viewportData: StackViewportData | VolumeViewportData; let viewportData: StackViewportData | VolumeViewportData;
@ -86,7 +70,7 @@ class CornerstoneCacheService {
} }
public async invalidateViewportData( public async invalidateViewportData(
viewportData: VolumeViewportData, viewportData: VolumeViewportData | StackViewportData,
invalidatedDisplaySetInstanceUID: string, invalidatedDisplaySetInstanceUID: string,
dataSource, dataSource,
displaySetService displaySetService
@ -176,9 +160,6 @@ class CornerstoneCacheService {
): Promise<StackViewportData> { ): Promise<StackViewportData> {
const { uiNotificationService } = this.servicesManager.services; const { uiNotificationService } = this.servicesManager.services;
const overlayDisplaySets = displaySets.filter(ds => ds.isOverlayDisplaySet); 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) { for (const overlayDisplaySet of overlayDisplaySets) {
if (overlayDisplaySet.load && overlayDisplaySet.load instanceof Function) { if (overlayDisplaySet.load && overlayDisplaySet.load instanceof Function) {
const { userAuthenticationService } = this.servicesManager.services; const { userAuthenticationService } = this.servicesManager.services;
@ -187,7 +168,7 @@ class CornerstoneCacheService {
await overlayDisplaySet.load({ headers }); await overlayDisplaySet.load({ headers });
} catch (e) { } catch (e) {
uiNotificationService.show({ uiNotificationService.show({
title: 'Error loading overlayDisplaySet', title: 'Error loading displaySet',
message: e.message, message: e.message,
type: 'error', 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 // Ensuring the first non-overlay `displaySet` is always the primary one
const StackViewportData = [displaySet, ...overlayDisplaySets].map(ds => { const StackViewportData = [];
const { displaySetInstanceUID, StudyInstanceUID, isCompositeStack } = ds; 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, StudyInstanceUID,
displaySetInstanceUID, displaySetInstanceUID,
isCompositeStack, isCompositeStack,
imageIds: stackImageIds, imageIds: stackImageIds,
initialImageIndex, initialImageIndex,
}; });
}); }
return { return {
viewportType, viewportType,
@ -253,6 +234,7 @@ class CornerstoneCacheService {
for (const displaySet of displaySets) { for (const displaySet of displaySets) {
const { Modality } = displaySet; const { Modality } = displaySet;
const isParametricMap = Modality === 'PMAP'; const isParametricMap = Modality === 'PMAP';
const isSeg = Modality === 'SEG';
// Don't create volumes for the displaySets that have custom load // Don't create volumes for the displaySets that have custom load
// function (e.g., SEG, RT, since they rely on the reference volumes // 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 // Parametric maps do not have image ids but they already have volume data
// therefore a new volume should not be created. // therefore a new volume should not be created.
if (!isParametricMap && (!volumeImageIds || !volume)) { if (!isParametricMap && !isSeg && (!volumeImageIds || !volume)) {
volumeImageIds = this._getCornerstoneVolumeImageIds(displaySet, dataSource); volumeImageIds = this._getCornerstoneVolumeImageIds(displaySet, dataSource);
volume = await volumeLoader.createAndCacheVolume(volumeId, { volume = await volumeLoader.createAndCacheVolume(volumeId, {
@ -303,6 +285,9 @@ class CornerstoneCacheService {
}); });
this.volumeImageIds.set(displaySet.displaySetInstanceUID, volumeImageIds); this.volumeImageIds.set(displaySet.displaySetInstanceUID, volumeImageIds);
// Add imageIds to the displaySet for volumes
displaySet.imageIds = volumeImageIds;
} }
volumeData.push({ 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[] { private _getCornerstoneStackImageIds(displaySet, dataSource): string[] {
return dataSource.getImageIdsForDisplaySet(displaySet); return dataSource.getImageIdsForDisplaySet(displaySet);
} }
private _getCornerstoneVolumeImageIds(displaySet, dataSource): string[] { private _getCornerstoneVolumeImageIds(displaySet, dataSource): string[] {
if (displaySet.imageIds) {
return displaySet.imageIds;
}
const stackImageIds = this._getCornerstoneStackImageIds(displaySet, dataSource); const stackImageIds = this._getCornerstoneStackImageIds(displaySet, dataSource);
return stackImageIds; return stackImageIds;

View File

@ -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 };

View File

@ -2,6 +2,7 @@ import { synchronizers, SynchronizerManager, Synchronizer } from '@cornerstonejs
import { getRenderingEngines, utilities } from '@cornerstonejs/core'; import { getRenderingEngines, utilities } from '@cornerstonejs/core';
import { pubSubServiceInterface, Types } from '@ohif/core'; import { pubSubServiceInterface, Types } from '@ohif/core';
import createHydrateSegmentationSynchronizer from './createHydrateSegmentationSynchronizer';
const EVENTS = { const EVENTS = {
TOOL_GROUP_CREATED: 'event::cornerstone::syncgroupservice:toolgroupcreated', TOOL_GROUP_CREATED: 'event::cornerstone::syncgroupservice:toolgroupcreated',
@ -27,6 +28,7 @@ const VOI = 'voi';
const ZOOMPAN = 'zoompan'; const ZOOMPAN = 'zoompan';
const STACKIMAGE = 'stackimage'; const STACKIMAGE = 'stackimage';
const IMAGE_SLICE = 'imageslice'; const IMAGE_SLICE = 'imageslice';
const HYDRATE_SEG = 'hydrateseg';
const asSyncGroup = (syncGroup: string | SyncGroup): SyncGroup => const asSyncGroup = (syncGroup: string | SyncGroup): SyncGroup =>
typeof syncGroup === 'string' ? { type: syncGroup } : syncGroup; typeof syncGroup === 'string' ? { type: syncGroup } : syncGroup;
@ -51,6 +53,7 @@ export default class SyncGroupService {
// handles both stack and volume viewports // handles both stack and volume viewports
[STACKIMAGE]: synchronizers.createImageSliceSynchronizer, [STACKIMAGE]: synchronizers.createImageSliceSynchronizer,
[IMAGE_SLICE]: synchronizers.createImageSliceSynchronizer, [IMAGE_SLICE]: synchronizers.createImageSliceSynchronizer,
[HYDRATE_SEG]: createHydrateSegmentationSynchronizer,
}; };
synchronizersByType: { [key: string]: Synchronizer[] } = {}; synchronizersByType: { [key: string]: Synchronizer[] } = {};
@ -66,18 +69,18 @@ export default class SyncGroupService {
private _createSynchronizer(type: string, id: string, options): Synchronizer | undefined { private _createSynchronizer(type: string, id: string, options): Synchronizer | undefined {
// Initialize if not already done // Initialize if not already done
this.synchronizersByType[type] = this.synchronizersByType[type] || []; this.synchronizersByType[type] = this.synchronizersByType[type] || [];
const syncCreator = this.synchronizerCreators[type.toLowerCase()]; const syncCreator = this.synchronizerCreators[type.toLowerCase()];
if (syncCreator) { if (syncCreator) {
const synchronizer = syncCreator(id, options); // Pass the servicesManager along with other parameters
const synchronizer = syncCreator(id, { ...options, servicesManager: this.servicesManager });
if (synchronizer) { if (synchronizer) {
this.synchronizersByType[type].push(synchronizer); this.synchronizersByType[type].push(synchronizer);
return synchronizer; return synchronizer;
} }
} else { } else {
console.warn(`Unknown synchronizer type: ${type}, id: ${id}`); console.debug(`Unknown synchronizer type: ${type}, id: ${id}`);
} }
} }

View File

@ -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,
});
};

View File

@ -7,7 +7,7 @@ export type ActionComponentInfo = {
id: string; id: string;
component: ReactNode; component: ReactNode;
location: ViewportActionCornersLocations; location: ViewportActionCornersLocations;
indexPriority: number; indexPriority?: number;
}; };
class ViewportActionCornersService extends PubSubService { class ViewportActionCornersService extends PubSubService {
@ -33,21 +33,21 @@ class ViewportActionCornersService extends PubSubService {
public setServiceImplementation({ public setServiceImplementation({
getState: getStateImplementation, getState: getStateImplementation,
setComponent: setComponentImplementation, addComponent: addComponentImplementation,
setComponents: setComponentsImplementation, addComponents: addComponentsImplementation,
clear: clearImplementation, clear: clearComponentsImplementation,
}): void { }): void {
if (getStateImplementation) { if (getStateImplementation) {
this.serviceImplementation._getState = getStateImplementation; this.serviceImplementation._getState = getStateImplementation;
} }
if (setComponentImplementation) { if (addComponentImplementation) {
this.serviceImplementation._setComponent = setComponentImplementation; this.serviceImplementation._addComponent = addComponentImplementation;
} }
if (setComponentsImplementation) { if (addComponentsImplementation) {
this.serviceImplementation._setComponents = setComponentsImplementation; this.serviceImplementation._addComponents = addComponentsImplementation;
} }
if (clearImplementation) { if (clearComponentsImplementation) {
this.serviceImplementation._clear = clearImplementation; this.serviceImplementation._clear = clearComponentsImplementation;
} }
} }
@ -55,12 +55,12 @@ class ViewportActionCornersService extends PubSubService {
return this.serviceImplementation._getState(); return this.serviceImplementation._getState();
} }
public setComponent(component: ActionComponentInfo) { public addComponent(component: ActionComponentInfo) {
this.serviceImplementation._setComponent(component); this.serviceImplementation._addComponent(component);
} }
public setComponents(components: Array<ActionComponentInfo>) { public addComponents(components: Array<ActionComponentInfo>) {
this.serviceImplementation._setComponents(components); this.serviceImplementation._addComponents(components);
} }
public clear(viewportId: string) { public clear(viewportId: string) {

View File

@ -18,9 +18,20 @@ import { IViewportService } from './IViewportService';
import { RENDERING_ENGINE_ID } from './constants'; import { RENDERING_ENGINE_ID } from './constants';
import ViewportInfo, { DisplaySetOptions, PublicViewportOptions } from './Viewport'; import ViewportInfo, { DisplaySetOptions, PublicViewportOptions } from './Viewport';
import { StackViewportData, VolumeViewportData } from '../../types/CornerstoneCacheService'; 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 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 = { const EVENTS = {
VIEWPORT_DATA_CHANGED: 'event::cornerstoneViewportService:viewportDataChanged', 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 presentations - The presentations to apply to the viewport.
* @param viewportInfo - Contains a view reference for immediate application * @param viewportInfo - Contains a view reference for immediate application
*/ */
public setPresentations( public setPresentations(viewportId: string, presentations: Presentations): void {
viewportId: string,
presentations?: Presentations,
viewportInfo?: ViewportInfo
): void {
const viewport = this.getCornerstoneViewport(viewportId) as const viewport = this.getCornerstoneViewport(viewportId) as
| Types.IStackViewport | Types.IStackViewport
| Types.IVolumeViewport; | Types.IVolumeViewport;
@ -189,83 +196,113 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
return; return;
} }
const { lutPresentation, positionPresentation } = presentations; const { lutPresentation, positionPresentation, segmentationPresentation } = presentations;
if (lutPresentation) {
const { presentation } = lutPresentation; // Always set the segmentation presentation first, since there might be some
if (viewport instanceof BaseVolumeViewport) { // lutpresentation states that need to be set on the segmentation
if (presentation instanceof Map) { // Todo: i think we should even await this
presentation.forEach((properties, volumeId) => { this._setSegmentationPresentation(viewport, segmentationPresentation);
viewport.setProperties(properties, volumeId);
}); this._setLutPresentation(viewport, lutPresentation);
} else { this._setPositionPresentation(viewport, { ...positionPresentation, viewportId });
viewport.setProperties(presentation); }
}
} else { /**
viewport.setProperties(presentation); * 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; const { lutPresentationId, positionPresentationId, segmentationPresentationId } =
if (viewRef) { presentationIds;
viewport.setViewReference(viewRef);
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. * Retrieves the presentations for a given viewport.
* @param viewportId The ID of the viewport. * @param viewportId - The ID of the viewport.
* @returns The position presentation object containing various properties * @returns The presentations for the viewport.
* such as ID, viewport type, initial image index, view plane normal, view up, zoom, and pan.
*/ */
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); const viewportInfo = this.viewportsById.get(viewportId);
if (!viewportInfo) { if (!viewportInfo) {
return; return null;
} }
const presentationIds = viewportInfo.getPresentationIds(); return viewportInfo.getPresentationIds();
}
if (!presentationIds) {
return;
}
const { positionPresentationId } = presentationIds;
private _getPositionPresentation(viewportId: string): PositionPresentation {
const csViewport = this.getCornerstoneViewport(viewportId); const csViewport = this.getCornerstoneViewport(viewportId);
if (!csViewport) { if (!csViewport) {
return; return;
} }
const viewportInfo = this.viewportsById.get(viewportId);
return { return {
id: positionPresentationId,
viewportType: viewportInfo.getViewportType(), viewportType: viewportInfo.getViewportType(),
viewReference: csViewport instanceof VolumeViewport3D ? null : csViewport.getViewReference(), viewReference: csViewport instanceof VolumeViewport3D ? null : csViewport.getViewReference(),
position: csViewport.getViewPresentation({ pan: true, zoom: true }), viewPresentation: csViewport.getViewPresentation({ pan: true, zoom: true }),
viewportId,
}; };
} }
/** private _getLutPresentation(viewportId: string): LutPresentation {
* 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;
const csViewport = this.getCornerstoneViewport(viewportId) as const csViewport = this.getCornerstoneViewport(viewportId) as
| Types.IStackViewport | Types.IStackViewport
| Types.IVolumeViewport; | Types.IVolumeViewport;
@ -275,116 +312,39 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
} }
const cleanProperties = properties => { const cleanProperties = properties => {
if (properties.isComputedVOI) { if (properties?.isComputedVOI) {
delete properties.voiRange; delete properties?.voiRange;
delete properties.VOILUTFunction; delete properties?.VOILUTFunction;
} }
return properties; return properties;
}; };
const presentation = const properties =
csViewport instanceof BaseVolumeViewport csViewport instanceof BaseVolumeViewport
? new Map() ? new Map()
: cleanProperties(csViewport.getProperties()); : cleanProperties(csViewport.getProperties());
if (presentation instanceof Map) { if (properties instanceof Map) {
csViewport.getActors().forEach(({ uid: volumeId }) => { const volumeIds = (csViewport as Types.IBaseVolumeViewport).getAllVolumeIds();
const properties = cleanProperties(csViewport.getProperties(volumeId)); volumeIds?.forEach(volumeId => {
presentation.set(volumeId, properties); 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); const viewportInfo = this.viewportsById.get(viewportId);
if (!viewportInfo) {
return;
}
const positionPresentation = this.getPositionPresentation(viewportId);
const lutPresentation = this.getLutPresentation(viewportId);
return { return {
positionPresentation, viewportType: viewportInfo.getViewportType(),
lutPresentation, properties,
}; };
} }
/** private _getSegmentationPresentation(viewportId: string): SegmentationPresentation {
* Stores the presentation state for a given viewport inside the const { segmentationService } = this.servicesManager.services;
* 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;
}
const { stateSyncService, syncGroupService } = this.servicesManager.services; const presentation = segmentationService.getPresentation(viewportId);
return presentation;
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);
} }
/** /**
@ -423,6 +383,11 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
// and we would lose the presentation. // and we would lose the presentation.
this.storePresentation({ viewportId: viewportInfo.getViewportId() }); this.storePresentation({ viewportId: viewportInfo.getViewportId() });
// Todo: i don't like this here, move it
this.servicesManager.services.segmentationService.clearSegmentationRepresentations(
viewportInfo.getViewportId()
);
if (!viewportInfo) { if (!viewportInfo) {
throw new Error('element is not enabled for the given viewportId'); 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 // since those are the newly set ones, we set them here so that it handles defaults
const displaySetOptions = viewportInfo.setPublicDisplaySetOptions(publicDisplaySetOptions); const displaySetOptions = viewportInfo.setPublicDisplaySetOptions(publicDisplaySetOptions);
// Specify an over-ride for the viewport type, even though it is in the public // 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 // viewport options, because the one in the viewportData is a requirement based on the
// for initial view, whereas the one in viewportData is a requirement based on the
// type of data being displayed. // type of data being displayed.
const viewportOptions = viewportInfo.setPublicViewportOptions( const viewportOptions = viewportInfo.setPublicViewportOptions(
publicViewportOptions, publicViewportOptions,
@ -647,13 +611,21 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
properties.colormap = colormap ?? properties.colormap; 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) { if (element !== viewport.element) {
return; 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 }); viewport.setProperties({ ...properties });
this.setPresentations(viewport.id, presentations, viewportInfo); this.setPresentations(viewport.id, presentations, viewportInfo);
if (displayArea) { if (displayArea) {
@ -702,7 +674,7 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
const lastSliceIndex = numberOfSlices - 1; const lastSliceIndex = numberOfSlices - 1;
if (imageIndex !== undefined) { if (imageIndex !== undefined) {
return csToolsUtils.clip(imageIndex, 0, lastSliceIndex); return csUtils.clip(imageIndex, 0, lastSliceIndex);
} }
if (preset === JumpPresets.First) { if (preset === JumpPresets.First) {
@ -764,7 +736,6 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
const displaySetOptions = displaySetOptionsArray[index]; const displaySetOptions = displaySetOptionsArray[index];
const { volumeId } = volume; const { volumeId } = volume;
volumeInputArray.push({ volumeInputArray.push({
imageIds, imageIds,
volumeId, volumeId,
@ -775,8 +746,7 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
this.viewportsDisplaySets.set(viewport.id, displaySetInstanceUIDs); 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 (volumesNotLoaded.length) {
if (hangingProtocolService.getShouldPerformCustomImageLoad()) { if (hangingProtocolService.getShouldPerformCustomImageLoad()) {
// delegate the volume loading to the hanging protocol service if it has a custom image load strategy // 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 => { volumesNotLoaded.forEach(volume => {
if (!volume.loadStatus.loading) { if (!volume.loadStatus?.loading && volume.load instanceof Function) {
volume.load(); volume.load();
} }
}); });
@ -798,8 +768,7 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
} }
public async setVolumesForViewport(viewport, volumeInputArray, presentations) { public async setVolumesForViewport(viewport, volumeInputArray, presentations) {
const { displaySetService, toolGroupService, viewportGridService } = const { displaySetService, viewportGridService } = this.servicesManager.services;
this.servicesManager.services;
const viewportInfo = this.getViewportInfo(viewport.id); const viewportInfo = this.getViewportInfo(viewport.id);
const displaySetOptions = viewportInfo.getDisplaySetOptions(); const displaySetOptions = viewportInfo.getDisplaySetOptions();
@ -811,7 +780,7 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
const { volumeId } = volumeInput; const { volumeId } = volumeInput;
const displaySetOption = displaySetOptions[index]; const displaySetOption = displaySetOptions[index];
const { voi, voiInverted, colormap, displayPreset } = displaySetOption; const { voi, voiInverted, colormap, displayPreset } = displaySetOption;
const properties = {}; const properties = {} as ViewportProperties;
if (voi && (voi.windowWidth || voi.windowCenter)) { if (voi && (voi.windowWidth || voi.windowCenter)) {
const { lower, upper } = csUtils.windowLevel.toLowHighRange( const { lower, upper } = csUtils.windowLevel.toLowHighRange(
@ -836,22 +805,21 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
return { properties, volumeId }; return { properties, volumeId };
}); });
// For SEG and RT viewports
this._processExtraDisplaySetsForViewport(viewport);
await viewport.setVolumes(volumeInputArray); await viewport.setVolumes(volumeInputArray);
volumesProperties.forEach(({ properties, volumeId }) => { volumesProperties.forEach(({ properties, volumeId }) => {
viewport.setProperties(properties, volumeId); viewport.setProperties(properties, volumeId);
}); });
this.setPresentations(viewport.id, presentations, viewportInfo); 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); const imageIndex = this._getInitialImageIndexForViewport(viewportInfo);
if (imageIndex !== undefined) { if (imageIndex !== undefined) {
csToolsUtils.jumpToSlice(viewport.element, { csUtils.jumpToSlice(viewport.element, {
imageIndex, 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; const { displaySetService } = this.servicesManager.services;
// load any secondary displaySets // load any secondary displaySets
@ -877,104 +847,41 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
displaySet?.isOverlayDisplaySet && ['SEG', 'RTSTRUCT'].includes(displaySet.Modality) displaySet?.isOverlayDisplaySet && ['SEG', 'RTSTRUCT'].includes(displaySet.Modality)
); );
if (segOrRTSOverlayDisplaySet) { // if it is only the overlay displaySet, then we need to get the reference
this.addOverlayRepresentationForDisplaySet(segOrRTSOverlayDisplaySet, viewport); // displaySet imageIds and set them as the imageIds for the viewport,
} else { // here we can do some logic if the reference is missing
// If the displaySet is not a SEG displaySet we assume it is a primary displaySet // then find the most similar match of displaySet instead
// and we can look into hydrated segmentations to check if any of them are if (!segOrRTSOverlayDisplaySet) {
// associated with the primary displaySet return;
// get segmentations only returns the hydrated segmentations
this._addSegmentationRepresentationToToolGroupIfNecessary(displaySetInstanceUIDs, viewport);
} }
const referenceDisplaySet = displaySetService.getDisplaySetByUID(
segOrRTSOverlayDisplaySet.referencedDisplaySetInstanceUID
);
const imageIds = referenceDisplaySet.images.map(image => image.imageId);
this.addOverlayRepresentationForDisplaySet(segOrRTSOverlayDisplaySet, viewport);
return { imageIds };
} }
private _addSegmentationRepresentationToToolGroupIfNecessary( private addOverlayRepresentationForDisplaySet(
displaySetInstanceUIDs: string[], displaySet: OhifTypes.DisplaySet,
viewport: any viewport: Types.IViewport
) { ) {
const { segmentationService, toolGroupService } = this.servicesManager.services; const { segmentationService } = 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 segmentationId = displaySet.displaySetInstanceUID; const segmentationId = displaySet.displaySetInstanceUID;
const toolGroup = toolGroupService.getToolGroupForViewport(viewport.id);
const representationType = const representationType =
referencedVolumeId && cache.getVolume(referencedVolumeId) !== undefined displaySet.Modality === 'SEG'
? csToolsEnums.SegmentationRepresentations.Labelmap ? csToolsEnums.SegmentationRepresentations.Labelmap
: csToolsEnums.SegmentationRepresentations.Contour; : csToolsEnums.SegmentationRepresentations.Contour;
segmentationService.addSegmentationRepresentationToToolGroup( segmentationService.addSegmentationRepresentation(viewport.id, {
toolGroup.id,
segmentationId, segmentationId,
false, type: representationType,
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 // 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( return this._setOtherViewport(
viewport as Types.IViewport, viewport,
viewportData as StackViewportData, viewportData as StackViewportData,
viewportInfo, viewportInfo,
presentations presentations
@ -1063,11 +970,11 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
// calculate the slab thickness based on the volume dimensions // calculate the slab thickness based on the volume dimensions
const imageVolume = cache.getVolume(volumeId); const imageVolume = cache.getVolume(volumeId);
const { dimensions } = imageVolume; const { dimensions, spacing } = imageVolume;
const slabThickness = Math.sqrt( const slabThickness = Math.sqrt(
dimensions[0] * dimensions[0] + Math.pow(dimensions[0] * spacing[0], 2) +
dimensions[1] * dimensions[1] + Math.pow(dimensions[1] * spacing[1], 2) +
dimensions[2] * dimensions[2] Math.pow(dimensions[2] * spacing[2], 2)
); );
return slabThickness; return slabThickness;
@ -1128,9 +1035,9 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
const viewports = this.getRenderingEngine().getViewports(); const viewports = this.getRenderingEngine().getViewports();
// Store the current position presentations for each viewport. // Store the current position presentations for each viewport.
viewports.forEach(({ id }) => { viewports.forEach(({ id: viewportId }) => {
const presentation = this.getPositionPresentation(id); const presentation = this._getPositionPresentation(viewportId);
this.beforeResizePositionPresentations.set(id, presentation); this.beforeResizePositionPresentations.set(viewportId, presentation);
}); });
// Resize the rendering engine and render. // Resize the rendering engine and render.
@ -1159,6 +1066,65 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
this.gridResizeTimeOut = null; this.gridResizeTimeOut = null;
}, this.gridResizeDelay); }, 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; export default CornerstoneViewportService;

View File

@ -5,7 +5,6 @@ import {
VolumeViewport, VolumeViewport,
utilities, utilities,
} from '@cornerstonejs/core'; } from '@cornerstonejs/core';
import { Types as CoreTypes } from '@ohif/core';
import { StackViewportData, VolumeViewportData } from '../../types/CornerstoneCacheService'; import { StackViewportData, VolumeViewportData } from '../../types/CornerstoneCacheService';
import getCornerstoneBlendMode from '../../utils/getCornerstoneBlendMode'; import getCornerstoneBlendMode from '../../utils/getCornerstoneBlendMode';
import getCornerstoneOrientation from '../../utils/getCornerstoneOrientation'; import getCornerstoneOrientation from '../../utils/getCornerstoneOrientation';
@ -25,7 +24,7 @@ export type ViewportOptions = {
toolGroupId: string; toolGroupId: string;
viewportId: string; viewportId: string;
// Presentation ID to store/load presentation state from // Presentation ID to store/load presentation state from
presentationIds?: CoreTypes.PresentationIds; presentationIds?: AppTypes.PresentationIds;
orientation?: Enums.OrientationAxis; orientation?: Enums.OrientationAxis;
background?: Types.Point3; background?: Types.Point3;
displayArea?: Types.DisplayArea; displayArea?: Types.DisplayArea;
@ -46,7 +45,7 @@ export type PublicViewportOptions = {
id?: string; id?: string;
viewportType?: string; viewportType?: string;
toolGroupId?: string; toolGroupId?: string;
presentationIds?: CoreTypes.PresentationIds; presentationIds?: string[];
viewportId?: string; viewportId?: string;
orientation?: Enums.OrientationAxis; orientation?: Enums.OrientationAxis;
background?: Types.Point3; background?: Types.Point3;
@ -283,7 +282,7 @@ class ViewportInfo {
return this.viewportOptions; return this.viewportOptions;
} }
public getPresentationIds(): CoreTypes.PresentationIds { public getPresentationIds(): AppTypes.PresentationIds | null {
const { presentationIds } = this.viewportOptions; const { presentationIds } = this.viewportOptions;
return presentationIds; return presentationIds;
} }

View File

@ -0,0 +1,4 @@
export { useLutPresentationStore } from './useLutPresentationStore';
export { usePositionPresentationStore } from './usePositionPresentationStore';
export { useSegmentationPresentationStore } from './useSegmentationPresentationStore';
export { useSynchronizersStore } from './useSynchronizersStore';

View File

@ -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 };

View File

@ -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
);

View File

@ -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
);

View File

@ -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
);

View File

@ -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
);

View File

@ -1,5 +1,5 @@
import { SynchronizerManager, Synchronizer, utilities } from '@cornerstonejs/tools'; import { SynchronizerManager, Synchronizer } from '@cornerstonejs/tools';
import { EVENTS, getRenderingEngine, Types } from '@cornerstonejs/core'; import { EVENTS, getRenderingEngine, type Types, utilities } from '@cornerstonejs/core';
const frameViewSyncCallback = ( const frameViewSyncCallback = (
synchronizerInstance: Synchronizer, synchronizerInstance: Synchronizer,

View File

@ -1,5 +1,5 @@
import { LengthTool, utilities } from '@cornerstonejs/tools'; import { LengthTool, utilities } from '@cornerstonejs/tools';
import callInputDialog from '../utils/callInputDialog'; import { callInputDialog } from '@ohif/extension-default';
import getActiveViewportEnabledElement from '../utils/getActiveViewportEnabledElement'; import getActiveViewportEnabledElement from '../utils/getActiveViewportEnabledElement';
const { calibrateImageSpacing } = utilities; const { calibrateImageSpacing } = utilities;

View File

@ -1,5 +1,5 @@
import { VolumeViewport, metaData, utilities } from '@cornerstonejs/core'; 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 { AnnotationDisplayTool, drawing } from '@cornerstonejs/tools';
import { guid, b64toBlob } from '@ohif/core/src/utils'; import { guid, b64toBlob } from '@ohif/core/src/utils';
import OverlayPlaneModuleProvider from './OverlayPlaneModuleProvider'; import OverlayPlaneModuleProvider from './OverlayPlaneModuleProvider';

View File

@ -9,6 +9,13 @@ import ColorbarServiceType from '../services/ColorbarService';
import * as cornerstone from '@cornerstonejs/core'; import * as cornerstone from '@cornerstonejs/core';
import * as cornerstoneTools from '@cornerstonejs/tools'; 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 { declare global {
namespace AppTypes { namespace AppTypes {
export type CornerstoneCacheService = CornerstoneCacheServiceType; export type CornerstoneCacheService = CornerstoneCacheServiceType;
@ -18,6 +25,7 @@ declare global {
export type ToolGroupService = ToolGroupServiceType; export type ToolGroupService = ToolGroupServiceType;
export type ViewportActionCornersService = ViewportActionCornersServiceType; export type ViewportActionCornersService = ViewportActionCornersServiceType;
export type ColorbarService = ColorbarServiceType; export type ColorbarService = ColorbarServiceType;
export interface Services { export interface Services {
cornerstoneViewportService?: CornerstoneViewportServiceType; cornerstoneViewportService?: CornerstoneViewportServiceType;
toolGroupService?: ToolGroupServiceType; toolGroupService?: ToolGroupServiceType;
@ -28,6 +36,19 @@ declare global {
colorbarService?: ColorbarServiceType; 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 { export interface Test {
services?: Services; services?: Services;
cornerstone?: typeof cornerstone; cornerstone?: typeof cornerstone;

View File

@ -1,16 +1,24 @@
import type { Types } from '@cornerstonejs/core'; import type { Types } from '@cornerstonejs/core';
import { SegmentationRepresentations } from '@cornerstonejs/tools/enums';
/** /**
* Represents a position presentation in a viewport. This is basically * Represents a position presentation in a viewport. This is basically
* viewport specific camera position and zoom, and not the display set * viewport specific camera position and zoom, and not the display set
*/ */
export type PositionPresentation = { export type PositionPresentation = {
id: string;
viewportType: string; viewportType: string;
// The view reference has the basic information as to what image orientation/slice is shown // The view reference has the basic information as to what image orientation/slice is shown
viewReference: Types.ViewReference; viewReference: Types.ViewReference;
// The position information has the zoom/pan and possibly other related information, but not LUT // 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 * itself
*/ */
export interface LutPresentation { export interface LutPresentation {
id: string;
viewportType: 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. * 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 * Viewport presentations object that can contain a positionPresentation
@ -37,6 +70,7 @@ type Presentation = PositionPresentation | LutPresentation;
export type Presentations = { export type Presentations = {
positionPresentation?: PositionPresentation; positionPresentation?: PositionPresentation;
lutPresentation?: LutPresentation; lutPresentation?: LutPresentation;
segmentationPresentation?: SegmentationPresentation;
}; };
export default Presentation; export default Presentation;

View File

@ -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;

View File

@ -8,7 +8,6 @@ import {
BaseVolumeViewport, BaseVolumeViewport,
} from '@cornerstonejs/core'; } from '@cornerstonejs/core';
import { ToolGroupManager } from '@cornerstonejs/tools'; import { ToolGroupManager } from '@cornerstonejs/tools';
import PropTypes from 'prop-types';
import { ViewportDownloadForm } from '@ohif/ui'; import { ViewportDownloadForm } from '@ohif/ui';
import { getEnabledElement as OHIFgetEnabledElement } from '../state'; import { getEnabledElement as OHIFgetEnabledElement } from '../state';
@ -22,7 +21,7 @@ const CornerstoneViewportDownloadForm = ({
onClose, onClose,
activeViewportId: activeViewportIdProp, activeViewportId: activeViewportIdProp,
cornerstoneViewportService, cornerstoneViewportService,
}) => { }: withAppTypes) => {
const enabledElement = OHIFgetEnabledElement(activeViewportIdProp); const enabledElement = OHIFgetEnabledElement(activeViewportIdProp);
const activeViewportElement = enabledElement?.element; const activeViewportElement = enabledElement?.element;
const activeViewportEnabledElement = getEnabledElement(activeViewportElement); const activeViewportEnabledElement = getEnabledElement(activeViewportElement);
@ -246,9 +245,4 @@ const CornerstoneViewportDownloadForm = ({
); );
}; };
CornerstoneViewportDownloadForm.propTypes = {
onClose: PropTypes.func,
activeViewportId: PropTypes.string.isRequired,
};
export default CornerstoneViewportDownloadForm; export default CornerstoneViewportDownloadForm;

View File

@ -161,7 +161,7 @@ class DicomLoaderService {
getDicomDataMethod = fetchIt.bind(this, imageId); getDicomDataMethod = fetchIt.bind(this, imageId);
break; break;
default: default:
throw new Error(`Unsupported image type: ${loaderType} for imageId: ${imageId}`); return;
} }
return getDicomDataMethod(); return getDicomDataMethod();

View File

@ -1,5 +1,5 @@
import { getEnabledElement } from '@cornerstonejs/core'; 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'; import { getEnabledElement as OHIFgetEnabledElement } from '../state';

View File

@ -22,7 +22,7 @@ export default function getCornerstoneViewportType(
return Enums.ViewportType.VIDEO; return Enums.ViewportType.VIDEO;
} }
if (lowerViewportType === WHOLESLIDE) { if (lowerViewportType === WHOLESLIDE) {
return Enums.ViewportType.WholeSlide; return Enums.ViewportType.WHOLE_SLIDE;
} }
if (lowerViewportType === VOLUME || lowerViewportType === ORTHOGRAPHIC) { if (lowerViewportType === VOLUME || lowerViewportType === ORTHOGRAPHIC) {

View File

@ -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;
};

Some files were not shown because too many files have changed in this diff Show More