feat(segmentation): Enhance Segmentation Tools with Preview and Selection Features (#4870)
This commit is contained in:
1 parent
20042f9109
commit
5f112337b6
24 files changed
+392
-236
No files matched your search
@@ -46,8 +46,8 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.20.13",
|
||||
"@cornerstonejs/adapters": "^3.7.1",
|
||||
"@cornerstonejs/core": "^3.7.1",
|
||||
"@cornerstonejs/adapters": "^3.7.5",
|
||||
"@cornerstonejs/core": "^3.7.5",
|
||||
"@kitware/vtk.js": "32.12.0",
|
||||
"react-color": "^2.19.3"
|
||||
}
|
||||
|
||||
@@ -46,8 +46,8 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.20.13",
|
||||
"@cornerstonejs/adapters": "^3.7.1",
|
||||
"@cornerstonejs/core": "^3.7.1",
|
||||
"@cornerstonejs/adapters": "^3.7.5",
|
||||
"@cornerstonejs/core": "^3.7.5",
|
||||
"@kitware/vtk.js": "32.12.0",
|
||||
"react-color": "^2.19.3"
|
||||
}
|
||||
|
||||
@@ -46,9 +46,9 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.20.13",
|
||||
"@cornerstonejs/adapters": "^3.7.1",
|
||||
"@cornerstonejs/core": "^3.7.1",
|
||||
"@cornerstonejs/tools": "^3.7.1",
|
||||
"@cornerstonejs/adapters": "^3.7.5",
|
||||
"@cornerstonejs/core": "^3.7.5",
|
||||
"@cornerstonejs/tools": "^3.7.5",
|
||||
"classnames": "^2.3.2"
|
||||
}
|
||||
}
|
||||
@@ -42,8 +42,8 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.20.13",
|
||||
"@cornerstonejs/core": "^3.7.1",
|
||||
"@cornerstonejs/tools": "^3.7.1",
|
||||
"@cornerstonejs/core": "^3.7.5",
|
||||
"@cornerstonejs/tools": "^3.7.5",
|
||||
"classnames": "^2.3.2"
|
||||
}
|
||||
}
|
||||
@@ -38,7 +38,7 @@
|
||||
"@cornerstonejs/codec-libjpeg-turbo-8bit": "^1.2.2",
|
||||
"@cornerstonejs/codec-openjpeg": "^1.2.4",
|
||||
"@cornerstonejs/codec-openjph": "^2.4.5",
|
||||
"@cornerstonejs/dicom-image-loader": "^3.7.1",
|
||||
"@cornerstonejs/dicom-image-loader": "^3.7.5",
|
||||
"@ohif/core": "3.10.0-beta.130",
|
||||
"@ohif/ui": "3.10.0-beta.130",
|
||||
"dcmjs": "*",
|
||||
@@ -51,11 +51,11 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.20.13",
|
||||
"@cornerstonejs/adapters": "^3.7.1",
|
||||
"@cornerstonejs/core": "^3.7.1",
|
||||
"@cornerstonejs/labelmap-interpolation": "^3.7.1",
|
||||
"@cornerstonejs/polymorphic-segmentation": "^3.7.1",
|
||||
"@cornerstonejs/tools": "^3.7.1",
|
||||
"@cornerstonejs/adapters": "^3.7.5",
|
||||
"@cornerstonejs/core": "^3.7.5",
|
||||
"@cornerstonejs/labelmap-interpolation": "^3.7.5",
|
||||
"@cornerstonejs/polymorphic-segmentation": "^3.7.5",
|
||||
"@cornerstonejs/tools": "^3.7.5",
|
||||
"@itk-wasm/morphological-contour-interpolation": "1.1.0",
|
||||
"@kitware/vtk.js": "32.12.0",
|
||||
"html2canvas": "^1.4.1",
|
||||
|
||||
@@ -41,6 +41,23 @@ const toggleSyncFunctions = {
|
||||
voi: toggleVOISliceSync,
|
||||
};
|
||||
|
||||
const getLabelmapTools = ({ toolGroupService }) => {
|
||||
const labelmapTools = [];
|
||||
const toolGroupIds = toolGroupService.getToolGroupIds();
|
||||
toolGroupIds.forEach(toolGroupId => {
|
||||
const toolGroup = cornerstoneTools.ToolGroupManager.getToolGroup(toolGroupId);
|
||||
const tools = toolGroup.getToolInstances();
|
||||
// tools is an object with toolName as the key and tool as the value
|
||||
Object.keys(tools).forEach(toolName => {
|
||||
const tool = tools[toolName];
|
||||
if (tool instanceof cornerstoneTools.LabelmapBaseTool) {
|
||||
labelmapTools.push(tool);
|
||||
}
|
||||
});
|
||||
});
|
||||
return labelmapTools;
|
||||
};
|
||||
|
||||
function commandsModule({
|
||||
servicesManager,
|
||||
commandsManager,
|
||||
@@ -57,6 +74,7 @@ function commandsModule({
|
||||
colorbarService,
|
||||
hangingProtocolService,
|
||||
syncGroupService,
|
||||
toolbarService,
|
||||
segmentationService,
|
||||
displaySetService,
|
||||
} = servicesManager.services as AppTypes.Services;
|
||||
@@ -1484,6 +1502,41 @@ function commandsModule({
|
||||
redo: () => {
|
||||
DefaultHistoryMemo.redo();
|
||||
},
|
||||
toggleSegmentPreviewEdit: ({ toggle }) => {
|
||||
const labelmapTools = getLabelmapTools({ toolGroupService });
|
||||
labelmapTools.forEach(tool => {
|
||||
tool.configuration = {
|
||||
...tool.configuration,
|
||||
preview: {
|
||||
...tool.configuration.preview,
|
||||
enabled: toggle,
|
||||
},
|
||||
};
|
||||
});
|
||||
},
|
||||
toggleSegmentSelect: ({ toggle }) => {
|
||||
const toolGroupIds = toolGroupService.getToolGroupIds();
|
||||
toolGroupIds.forEach(toolGroupId => {
|
||||
const toolGroup = cornerstoneTools.ToolGroupManager.getToolGroup(toolGroupId);
|
||||
if (toggle) {
|
||||
toolGroup.setToolActive(cornerstoneTools.SegmentSelectTool.toolName);
|
||||
} else {
|
||||
toolGroup.setToolDisabled(cornerstoneTools.SegmentSelectTool.toolName);
|
||||
}
|
||||
});
|
||||
},
|
||||
acceptPreview: () => {
|
||||
const labelmapTools = getLabelmapTools({ toolGroupService });
|
||||
labelmapTools.forEach(tool => {
|
||||
tool.acceptPreview();
|
||||
});
|
||||
},
|
||||
rejectPreview: () => {
|
||||
const labelmapTools = getLabelmapTools({ toolGroupService });
|
||||
labelmapTools.forEach(tool => {
|
||||
tool.rejectPreview();
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
const definitions = {
|
||||
@@ -1755,6 +1808,10 @@ function commandsModule({
|
||||
interpolateLabelmap: actions.interpolateLabelmap,
|
||||
runSegmentBidirectional: actions.runSegmentBidirectional,
|
||||
downloadCSVSegmentationReport: actions.downloadCSVSegmentationReport,
|
||||
toggleSegmentPreviewEdit: actions.toggleSegmentPreviewEdit,
|
||||
toggleSegmentSelect: actions.toggleSegmentSelect,
|
||||
acceptPreview: actions.acceptPreview,
|
||||
rejectPreview: actions.rejectPreview,
|
||||
};
|
||||
|
||||
return {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { CustomDropdownMenuContent } from './CustomDropdownMenuContent';
|
||||
import { CustomSegmentStatisticsHeader } from './CustomSegmentStatisticsHeader';
|
||||
import React, { useState } from 'react';
|
||||
import { Switch } from '@ohif/ui-next';
|
||||
|
||||
export default function getSegmentationPanelCustomization({ commandsManager, servicesManager }) {
|
||||
return {
|
||||
@@ -30,5 +32,40 @@ export default function getSegmentationPanelCustomization({ commandsManager, ser
|
||||
volume: 'Volume',
|
||||
lesionGlycolysis: 'Lesion Glycolysis',
|
||||
},
|
||||
'segmentationToolbox.config': () => {
|
||||
// Get initial states based on current configuration
|
||||
const [previewEdits, setPreviewEdits] = useState(false);
|
||||
const [toggleSegmentEnabled, setToggleSegmentEnabled] = useState(false);
|
||||
|
||||
const handlePreviewEditsChange = checked => {
|
||||
setPreviewEdits(checked);
|
||||
commandsManager.run('toggleSegmentPreviewEdit', { toggle: checked });
|
||||
};
|
||||
|
||||
const handleToggleSegmentEnabledChange = checked => {
|
||||
setToggleSegmentEnabled(checked);
|
||||
commandsManager.run('toggleSegmentSelect', { toggle: checked });
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-muted flex flex-col gap-4 border-b border-b-[2px] border-black px-2 py-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<Switch
|
||||
checked={previewEdits}
|
||||
onCheckedChange={handlePreviewEditsChange}
|
||||
/>
|
||||
<span className="text-base text-white">Preview edits before creating</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<Switch
|
||||
checked={toggleSegmentEnabled}
|
||||
onCheckedChange={handleToggleSegmentEnabledChange}
|
||||
/>
|
||||
<span className="text-base text-white">Highlight segments to select</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -37,6 +37,7 @@ import {
|
||||
LivewireContourTool,
|
||||
OrientationMarkerTool,
|
||||
WindowLevelRegionTool,
|
||||
SegmentSelectTool,
|
||||
} from '@cornerstonejs/tools';
|
||||
import * as polySeg from '@cornerstonejs/polymorphic-segmentation';
|
||||
|
||||
@@ -96,7 +97,7 @@ export default function initCornerstoneTools(configuration = {}) {
|
||||
addTool(OrientationMarkerTool);
|
||||
addTool(WindowLevelRegionTool);
|
||||
addTool(PlanarFreehandContourSegmentationTool);
|
||||
|
||||
addTool(SegmentSelectTool);
|
||||
// Modify annotation tools to use dashed lines on SR
|
||||
const annotationStyle = {
|
||||
textBoxFontSize: '15px',
|
||||
@@ -150,6 +151,7 @@ const toolNames = {
|
||||
WindowLevelRegion: WindowLevelRegionTool.toolName,
|
||||
PlanarFreehandContourSegmentation: PlanarFreehandContourSegmentationTool.toolName,
|
||||
SegmentBidirectional: SegmentBidirectionalTool.toolName,
|
||||
SegmentSelect: SegmentSelectTool.toolName,
|
||||
};
|
||||
|
||||
export { toolNames };
|
||||
@@ -264,10 +264,14 @@ class SegmentationService extends PubSubService {
|
||||
{
|
||||
segmentationId,
|
||||
type,
|
||||
config,
|
||||
suppressEvents = false,
|
||||
}: {
|
||||
segmentationId: string;
|
||||
type?: csToolsEnums.SegmentationRepresentations;
|
||||
config?: {
|
||||
blendMode?: csEnums.BlendModes;
|
||||
};
|
||||
suppressEvents?: boolean;
|
||||
}
|
||||
): Promise<void> {
|
||||
@@ -301,7 +305,8 @@ class SegmentationService extends PubSubService {
|
||||
segmentationId,
|
||||
representationTypeToUse,
|
||||
colorLUTIndex,
|
||||
isConverted
|
||||
isConverted,
|
||||
config
|
||||
);
|
||||
|
||||
if (!suppressEvents) {
|
||||
@@ -1349,12 +1354,15 @@ class SegmentationService extends PubSubService {
|
||||
segmentationId: string,
|
||||
representationType: csToolsEnums.SegmentationRepresentations,
|
||||
colorLUTIndex: number,
|
||||
isConverted: boolean
|
||||
isConverted: boolean,
|
||||
config?: {
|
||||
blendMode?: csEnums.BlendModes;
|
||||
}
|
||||
): Promise<void> {
|
||||
const representation = {
|
||||
type: representationType,
|
||||
segmentationId,
|
||||
config: { colorLUTOrIndex: colorLUTIndex },
|
||||
config: { colorLUTOrIndex: colorLUTIndex, ...config },
|
||||
};
|
||||
|
||||
const addRepresentation = () =>
|
||||
|
||||
+14
-8
@@ -1,4 +1,4 @@
|
||||
import { Types, getEnabledElementByViewportId } from '@cornerstonejs/core';
|
||||
import { Enums as CoreEnums, Types, getEnabledElementByViewportId } from '@cornerstonejs/core';
|
||||
import {
|
||||
SynchronizerManager,
|
||||
Synchronizer,
|
||||
@@ -7,7 +7,8 @@ import {
|
||||
} from '@cornerstonejs/tools';
|
||||
|
||||
const { createSynchronizer } = SynchronizerManager;
|
||||
const { SEGMENTATION_REPRESENTATION_ADDED } = Enums.Events;
|
||||
const { SEGMENTATION_REPRESENTATION_MODIFIED } = Enums.Events;
|
||||
const { BlendModes } = CoreEnums;
|
||||
|
||||
export default function createHydrateSegmentationSynchronizer(
|
||||
synchronizerName: string,
|
||||
@@ -15,15 +16,16 @@ export default function createHydrateSegmentationSynchronizer(
|
||||
): Synchronizer {
|
||||
const stackImageSynchronizer = createSynchronizer(
|
||||
synchronizerName,
|
||||
SEGMENTATION_REPRESENTATION_ADDED,
|
||||
(synchronizerInstance, sourceViewport, targetViewport, sourceEvent) =>
|
||||
segmentationRepresentationModifiedCallback(
|
||||
SEGMENTATION_REPRESENTATION_MODIFIED,
|
||||
(synchronizerInstance, sourceViewport, targetViewport, sourceEvent) => {
|
||||
return segmentationRepresentationModifiedCallback(
|
||||
synchronizerInstance,
|
||||
sourceViewport,
|
||||
targetViewport,
|
||||
sourceEvent,
|
||||
{ servicesManager, options }
|
||||
),
|
||||
);
|
||||
},
|
||||
{
|
||||
eventSource: 'eventTarget',
|
||||
}
|
||||
@@ -41,8 +43,8 @@ const segmentationRepresentationModifiedCallback = async (
|
||||
) => {
|
||||
const event = sourceEvent as ToolsTypes.EventTypes.SegmentationRepresentationModifiedEventType;
|
||||
|
||||
const { segmentationId, viewportId } = event.detail;
|
||||
const { segmentationService, hangingProtocolService } = servicesManager.services;
|
||||
const { segmentationId } = event.detail;
|
||||
const { segmentationService } = servicesManager.services;
|
||||
|
||||
const targetViewportId = targetViewport.viewportId;
|
||||
|
||||
@@ -75,5 +77,9 @@ const segmentationRepresentationModifiedCallback = async (
|
||||
await segmentationService.addSegmentationRepresentation(targetViewportId, {
|
||||
segmentationId,
|
||||
type,
|
||||
config: {
|
||||
blendMode:
|
||||
viewport.getBlendMode() === 1 ? BlendModes.LABELMAP_EDGE_PROJECTION_BLEND : undefined,
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -2,7 +2,7 @@ import React from 'react';
|
||||
import { useNavigate, useLocation } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { Header, useModal } from '@ohif/ui-next';
|
||||
import { Button, Header, Icons, useModal } from '@ohif/ui-next';
|
||||
import { useSystem } from '@ohif/core';
|
||||
import { Toolbar } from '../Toolbar/Toolbar';
|
||||
import HeaderPatientInfo from './HeaderPatientInfo';
|
||||
@@ -10,7 +10,7 @@ import { PatientInfoVisibility } from './HeaderPatientInfo/HeaderPatientInfo';
|
||||
import { preserveQueryParameters, publicUrl } from '@ohif/app';
|
||||
|
||||
function ViewerHeader({ appConfig }: withAppTypes<{ appConfig: AppTypes.Config }>) {
|
||||
const { servicesManager, extensionManager } = useSystem();
|
||||
const { servicesManager, extensionManager, commandsManager } = useSystem();
|
||||
const { customizationService } = servicesManager.services;
|
||||
|
||||
const navigate = useNavigate();
|
||||
@@ -94,6 +94,28 @@ function ViewerHeader({ appConfig }: withAppTypes<{ appConfig: AppTypes.Config }
|
||||
/>
|
||||
)
|
||||
}
|
||||
UndoRedo={
|
||||
<div className="text-primary-active flex cursor-pointer items-center">
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="hover:bg-primary-dark"
|
||||
onClick={() => {
|
||||
commandsManager.run('undo');
|
||||
}}
|
||||
>
|
||||
<Icons.Undo className="" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="hover:bg-primary-dark"
|
||||
onClick={() => {
|
||||
commandsManager.run('redo');
|
||||
}}
|
||||
>
|
||||
<Icons.Redo className="" />
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className="relative flex justify-center gap-[4px]">
|
||||
<Toolbar servicesManager={servicesManager} />
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import React from 'react';
|
||||
import { PanelSection, ToolSettings } from '@ohif/ui-next';
|
||||
import React, { useState } from 'react';
|
||||
import { Icons, PanelSection, ToolSettings } from '@ohif/ui-next';
|
||||
import { useSystem, useToolbar } from '@ohif/core';
|
||||
import classnames from 'classnames';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface ButtonProps {
|
||||
isActive?: boolean;
|
||||
@@ -20,7 +21,10 @@ interface ButtonProps {
|
||||
*/
|
||||
export function Toolbox({ buttonSectionId, title }: { buttonSectionId: string; title: string }) {
|
||||
const { servicesManager } = useSystem();
|
||||
const { toolbarService } = servicesManager.services;
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { toolbarService, customizationService } = servicesManager.services;
|
||||
const [showConfig, setShowConfig] = useState(false);
|
||||
|
||||
const { toolbarButtons: toolboxSections, onInteraction } = useToolbar({
|
||||
servicesManager,
|
||||
@@ -72,12 +76,27 @@ export function Toolbox({ buttonSectionId, title }: { buttonSectionId: string; t
|
||||
onInteraction?.({ itemId });
|
||||
};
|
||||
|
||||
const CustomConfigComponent = customizationService.getCustomization(`${buttonSectionId}.config`);
|
||||
|
||||
return (
|
||||
<PanelSection>
|
||||
<PanelSection.Header>
|
||||
<span>{title}</span>
|
||||
<PanelSection.Header className="flex items-center justify-between">
|
||||
<span>{t(title)}</span>
|
||||
{CustomConfigComponent && (
|
||||
<div className="ml-auto mr-2">
|
||||
<Icons.Settings
|
||||
className="text-primary-active h-4 w-4"
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
setShowConfig(!showConfig);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</PanelSection.Header>
|
||||
|
||||
<PanelSection.Content className="flex-shrink-0 border-none">
|
||||
{showConfig && <CustomConfigComponent />}
|
||||
{toolboxSections.map(section => {
|
||||
const sectionId = section.componentProps.buttonSection;
|
||||
const buttons = toolbarService.getButtonSection(sectionId) as any[];
|
||||
|
||||
@@ -32,8 +32,8 @@
|
||||
"start": "yarn run dev"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@cornerstonejs/core": "^3.7.1",
|
||||
"@cornerstonejs/tools": "^3.7.1",
|
||||
"@cornerstonejs/core": "^3.7.5",
|
||||
"@cornerstonejs/tools": "^3.7.5",
|
||||
"@ohif/core": "3.10.0-beta.130",
|
||||
"@ohif/extension-cornerstone-dicom-sr": "3.10.0-beta.130",
|
||||
"@ohif/extension-default": "3.10.0-beta.130",
|
||||
|
||||
@@ -54,7 +54,6 @@ export default function PanelRoiThresholdSegmentation({
|
||||
// Find the first segmentation with a TMTV value since all of them have the same value
|
||||
const stats = segmentationService.getSegmentationGroupStats(segmentationIds);
|
||||
const tmtvValue = stats?.tmtv;
|
||||
const tlgValue = stats?.tlg;
|
||||
|
||||
return (
|
||||
<div className="mt-2 mb-10 flex flex-col">
|
||||
@@ -65,7 +64,6 @@ export default function PanelRoiThresholdSegmentation({
|
||||
{'TMTV:'}
|
||||
</span>
|
||||
<div className="text-white">{`${tmtvValue?.toFixed(3)} mL`}</div>
|
||||
<div className="text-white">{`${tlgValue?.toFixed(3)} mL`}</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
Reference in new issue
Block a user