feat(RT): add dicom RT support via volume viewports (#3310)

* feat: initial RT support

* make the segmentation service work with representation data

* feat: make segmentation service work with representations

* fix rtss vis

* fix: rt hydration

* fix the rendering of rt names

* fix imports

* refactor: Modify status and click handling for hydration of RTStructures

Modify status and click handling for hydration of RTStructures by renaming `onPillClick` to `onStatusClick` in `OHIFCornerstoneRTViewport.tsx` and `_getStatusComponent.tsx` files. Also, update initial segmentation configurations in `PanelSegmentation.tsx` and simplify configuration changes and values for segmentation service in `SegmentationService.ts`. Finally, remove console debug in `CornerstoneViewportService.ts`.

* wip for highlighting contours

* refactor rt displayset code

* review code update

* update cornerstone dependencies

* refactor: Update license year, version number, and minor code cleanup

This commit updates the license year in several files, updates the version number in package.json, and contains minor code cleanup in two files.

* add bulkdataURI retrieve for RT

* fix package version

* apply review comments

* apply review comments

* apply review comments

* feat(panels): refactor and streamline segmentation configuration and inputs

Rewrote state hooks and streamlined the configuration input for `PanelSegmentation` to be more verbose and reusable. Included several new input types, including the `InputRange` component which now shows a fixed floating value based on the step provided. The `SegmentationConfig` component now works with dynamic values controlled by `initialConfig`. These changes should improve function usability and make the code more maintainable going forward.

* fix various bugs

* fix contour delete by upgrade cs3d version

* feat(viewport, inputNumber, segmentationConfig, orthanc): Implement minimum and maximum values for input number components, and useBulkDataURI for Orthanc configuration. Compare measurement view planes with absolute viewport view planes in Cornerstone viewport.

* update yarn lock
This commit is contained in:
Alireza authored and GitHub committed 2023-04-26 12:21:08 -04:00
1 parent 69d8e6a191
commit 66863281c4
60 files changed
+2488 -678

No files matched your search

@@ -57,6 +57,7 @@ function _getDisplaySetsFromSeries(
wadoRoot,
wadoUriRoot,
wadoUri,
isOverlayDisplaySet: true,
};
const referencedSeriesSequence = instance.ReferencedSeriesSequence;
@@ -102,9 +103,12 @@ function _getDisplaySetsFromSeries(
function _load(segDisplaySet, servicesManager, extensionManager, headers) {
const { SOPInstanceUID } = segDisplaySet;
const { segmentationService } = servicesManager.services;
if (
(segDisplaySet.loading || segDisplaySet.isLoaded) &&
loadPromises[SOPInstanceUID]
loadPromises[SOPInstanceUID] &&
_segmentationExists(segDisplaySet, segmentationService)
) {
return loadPromises[SOPInstanceUID];
}
@@ -114,12 +118,6 @@ function _load(segDisplaySet, servicesManager, extensionManager, headers) {
// We don't want to fire multiple loads, so we'll wait for the first to finish
// and also return the same promise to any other callers.
loadPromises[SOPInstanceUID] = new Promise(async (resolve, reject) => {
const { segmentationService } = servicesManager.services;
if (_segmentationExistsInCache(segDisplaySet, segmentationService)) {
return;
}
if (
!segDisplaySet.segments ||
Object.keys(segDisplaySet.segments).length === 0
@@ -134,11 +132,8 @@ function _load(segDisplaySet, servicesManager, extensionManager, headers) {
}
const suppressEvents = true;
segmentationService.createSegmentationForSEGDisplaySet(
segDisplaySet,
null,
suppressEvents
)
segmentationService
.createSegmentationForSEGDisplaySet(segDisplaySet, null, suppressEvents)
.then(() => {
segDisplaySet.loading = false;
resolve();
@@ -176,12 +171,11 @@ async function _loadSegments(extensionManager, segDisplaySet, headers) {
return segments;
}
function _segmentationExistsInCache(segDisplaySet, segmentationService) {
function _segmentationExists(segDisplaySet, segmentationService) {
// This should be abstracted with the CornerstoneCacheService
const labelmapVolumeId = segDisplaySet.displaySetInstanceUID;
const segVolume = segmentationService.getLabelmapVolume(labelmapVolumeId);
return segVolume !== undefined;
return segmentationService.getSegmentation(
segDisplaySet.displaySetInstanceUID
);
}
function _getPixelData(dataset, segments) {
@@ -3,7 +3,9 @@ import React from 'react';
import { Types } from '@ohif/core';
import getSopClassHandlerModule, { protocols } from './getSopClassHandlerModule';
import getSopClassHandlerModule, {
protocols,
} from './getSopClassHandlerModule';
import PanelSegmentation from './panels/PanelSegmentation';
import getHangingProtocolModule from './getHangingProtocolModule';
@@ -31,13 +33,17 @@ const extension = {
*/
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: ({ servicesManager, commandsManager, extensionManager }: Types.Extensions.ExtensionParams): Types.Panel[] => {
getPanelModule: ({
servicesManager,
commandsManager,
extensionManager,
}): Types.Panel[] => {
const wrappedPanelSegmentation = () => {
return (
<PanelSegmentation
@@ -81,7 +87,6 @@ const extension = {
* Examples include the default sop class handler provided by the default extension
*/
getSopClassHandlerModule,
getHangingProtocolModule,
};
@@ -9,20 +9,13 @@ export default function PanelSegmentation({
servicesManager,
commandsManager,
}) {
const {
segmentationService,
uiDialogService,
viewportGridService,
toolGroupService,
cornerstoneViewportService,
} = servicesManager.services;
const { segmentationService, uiDialogService } = servicesManager.services;
const { t } = useTranslation('PanelSegmentation');
const [selectedSegmentationId, setSelectedSegmentationId] = useState(null);
const [
initialSegmentationConfigurations,
setInitialSegmentationConfigurations,
] = useState(segmentationService.getConfiguration());
const [segmentationConfiguration, setSegmentationConfiguration] = useState(
segmentationService.getConfiguration()
);
const [segmentations, setSegmentations] = useState(() =>
segmentationService.getSegmentations()
@@ -62,6 +55,7 @@ export default function PanelSegmentation({
const { unsubscribe } = segmentationService.subscribe(evt, () => {
const segmentations = segmentationService.getSegmentations();
setSegmentations(segmentations);
setSegmentationConfiguration(segmentationService.getConfiguration());
});
subscriptions.push(unsubscribe);
});
@@ -184,7 +178,7 @@ export default function PanelSegmentation({
segmentationService.toggleSegmentationVisibility(segmentationId);
};
const setSegmentationConfiguration = useCallback(
const _setSegmentationConfiguration = useCallback(
(segmentationId, key, value) => {
segmentationService.setConfiguration({
segmentationId,
@@ -214,54 +208,51 @@ export default function PanelSegmentation({
onToggleSegmentVisibility={onToggleSegmentVisibility}
onToggleSegmentationVisibility={onToggleSegmentationVisibility}
onToggleMinimizeSegmentation={onToggleMinimizeSegmentation}
segmentationConfig={{
initialConfig: initialSegmentationConfigurations,
usePercentage: true,
}}
segmentationConfig={{ initialConfig: segmentationConfiguration }}
setRenderOutline={value =>
setSegmentationConfiguration(
_setSegmentationConfiguration(
selectedSegmentationId,
'renderOutline',
value
)
}
setOutlineOpacityActive={value =>
setSegmentationConfiguration(
_setSegmentationConfiguration(
selectedSegmentationId,
'outlineOpacity',
value
)
}
setRenderFill={value =>
setSegmentationConfiguration(
_setSegmentationConfiguration(
selectedSegmentationId,
'renderFill',
value
)
}
setRenderInactiveSegmentations={value =>
setSegmentationConfiguration(
_setSegmentationConfiguration(
selectedSegmentationId,
'renderInactiveSegmentations',
value
)
}
setOutlineWidthActive={value =>
setSegmentationConfiguration(
_setSegmentationConfiguration(
selectedSegmentationId,
'outlineWidthActive',
value
)
}
setFillAlpha={value =>
setSegmentationConfiguration(
_setSegmentationConfiguration(
selectedSegmentationId,
'fillAlpha',
value
)
}
setFillAlphaInactive={value =>
setSegmentationConfiguration(
_setSegmentationConfiguration(
selectedSegmentationId,
'fillAlphaInactive',
value
@@ -1,5 +1,3 @@
import React, { useReducer } from 'react';
// Todo: use defaults in cs3d
const initialState = {
renderOutline: true,
@@ -1,34 +1,12 @@
function createSEGToolGroupAndAddTools(
toolGroupService,
toolGroupId,
extensionManager
ToolGroupService,
customizationService,
toolGroupId
) {
const utilityModule = extensionManager.getModuleEntry(
'@ohif/extension-cornerstone.utilityModule.tools'
);
const { tools } =
customizationService.get('cornerstone.overlayViewportTools') ?? {};
const { toolNames, Enums } = utilityModule.exports;
const tools = {
active: [
{
toolName: toolNames.WindowLevel,
bindings: [{ mouseButton: Enums.MouseBindings.Primary }],
},
{
toolName: toolNames.Pan,
bindings: [{ mouseButton: Enums.MouseBindings.Auxiliary }],
},
{
toolName: toolNames.Zoom,
bindings: [{ mouseButton: Enums.MouseBindings.Secondary }],
},
{ toolName: toolNames.StackScrollMouseWheel, bindings: [] },
],
enabled: [{ toolName: toolNames.SegmentationDisplay }],
};
return toolGroupService.createToolGroupAndAddTools(toolGroupId, tools, {});
return ToolGroupService.createToolGroupAndAddTools(toolGroupId, tools, {});
}
export default createSEGToolGroupAndAddTools;
@@ -3,7 +3,11 @@ import React, { useCallback, useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import OHIF, { utils } from '@ohif/core';
import {
LoadingIndicatorProgress, Notification, useViewportDialog, useViewportGrid, ViewportActionBar
LoadingIndicatorTotalPercent,
Notification,
useViewportDialog,
useViewportGrid,
ViewportActionBar,
} from '@ohif/ui';
import createSEGToolGroupAndAddTools from '../utils/initSEGToolGroup';
import promptHydrateSEG from '../utils/promptHydrateSEG';
@@ -31,6 +35,7 @@ function OHIFCornerstoneSEGViewport(props) {
toolGroupService,
segmentationService,
uiNotificationService,
customizationService,
} = servicesManager.services;
const toolGroupId = `${SEG_TOOLGROUP_BASE_NAME}-${viewportIndex}`;
@@ -58,7 +63,7 @@ function OHIFCornerstoneSEGViewport(props) {
const [segIsLoading, setSegIsLoading] = useState(!segDisplaySet.isLoaded);
const [element, setElement] = useState(null);
const [processingProgress, setProcessingProgress] = useState({
segmentIndex: 1,
percentComplete: null,
totalSegments: null,
});
@@ -129,6 +134,8 @@ function OHIFCornerstoneSEGViewport(props) {
let newSelectedSegmentIndex = selectedSegment + direction;
// Segment 0 is always background
if (newSelectedSegmentIndex > numberOfSegments - 1) {
newSelectedSegmentIndex = 1;
} else if (newSelectedSegmentIndex === 0) {
@@ -163,7 +170,7 @@ function OHIFCornerstoneSEGViewport(props) {
useEffect(() => {
const { unsubscribe } = segmentationService.subscribe(
segmentationService.EVENTS.SEGMENTATION_PIXEL_DATA_CREATED,
segmentationService.EVENTS.SEGMENTATION_LOADING_COMPLETE,
evt => {
if (
evt.segDisplaySet.displaySetInstanceUID ===
@@ -190,10 +197,10 @@ function OHIFCornerstoneSEGViewport(props) {
useEffect(() => {
const { unsubscribe } = segmentationService.subscribe(
segmentationService.EVENTS.SEGMENT_PIXEL_DATA_CREATED,
({ segmentIndex, numSegments }) => {
segmentationService.EVENTS.SEGMENT_LOADING_COMPLETE,
({ percentComplete, numSegments }) => {
setProcessingProgress({
segmentIndex,
percentComplete,
totalSegments: numSegments,
});
}
@@ -239,8 +246,8 @@ function OHIFCornerstoneSEGViewport(props) {
// only, and does NOT interfere with currently displayed segmentations.
toolGroup = createSEGToolGroupAndAddTools(
toolGroupService,
toolGroupId,
extensionManager
customizationService,
toolGroupId
);
setToolGroupCreated(true);
@@ -351,27 +358,11 @@ function OHIFCornerstoneSEGViewport(props) {
<div className="relative flex flex-row w-full h-full overflow-hidden">
{segIsLoading && (
<LoadingIndicatorProgress
<LoadingIndicatorTotalPercent
className="w-full h-full"
progress={
processingProgress.totalSegments !== null
? ((processingProgress.segmentIndex + 1) /
processingProgress.totalSegments) *
100
: null
}
textBlock={
!processingProgress.totalSegments ? (
<span className="text-white text-sm">Loading SEG ...</span>
) : (
<span className="text-white text-sm flex items-baseline space-x-2">
<div>Loading Segment</div>
<div className="w-3">{`${processingProgress.segmentIndex}`}</div>
<div>/</div>
<div>{`${processingProgress.totalSegments}`}</div>
</span>
)
}
totalNumbers={processingProgress.totalSegments}
percentComplete={processingProgress.percentComplete}
loadingText="Loading SEG..."
/>
)}
{getCornerstoneViewport()}