Skip to content

Commit

Permalink
feat(4D-ROI): ROI Segmentation panel (4D) (#3574)
Browse files Browse the repository at this point in the history
  • Loading branch information
lscoder authored Aug 8, 2023
1 parent 7755cb9 commit 72243fa
Show file tree
Hide file tree
Showing 40 changed files with 1,663 additions and 562 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ yarn-error.log
.DS_Store
.env
*.code-workspace
.directory

# Common Example Data Directories
sampledata/
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
// import { Enums } from '@cornerstonejs/core';

const DEFAULT_COLORMAP = '2hot';
// const { BlendModes } = Enums;

function getPTOptions({
colormap,
Expand Down Expand Up @@ -48,7 +45,7 @@ function getPTViewports() {
viewportId: 'ptAxial',
viewportType: 'volume',
orientation: 'axial',
toolGroupId: 'dynamic4D-default',
toolGroupId: 'dynamic4D-pt',
initialImageOptions: {
preset: 'middle', // 'first', 'last', 'middle'
},
Expand Down Expand Up @@ -79,7 +76,7 @@ function getPTViewports() {
viewportId: 'ptSagittal',
viewportType: 'volume',
orientation: 'sagittal',
toolGroupId: 'dynamic4D-default',
toolGroupId: 'dynamic4D-pt',
initialImageOptions: {
// preset: 'middle', // 'first', 'last', 'middle'
index: 140,
Expand Down Expand Up @@ -111,7 +108,7 @@ function getPTViewports() {
viewportId: 'ptCoronal',
viewportType: 'volume',
orientation: 'coronal',
toolGroupId: 'dynamic4D-default',
toolGroupId: 'dynamic4D-pt',
initialImageOptions: {
// preset: 'middle', // 'first', 'last', 'middle'
index: 160,
Expand Down Expand Up @@ -300,29 +297,10 @@ const defaultProtocol = {
},
},
],
toolGroupIds: ['dynamic4D-default'],
// -1 would be used to indicate active only, whereas other values are
// the number of required priors referenced - so 0 means active with
// 0 or more priors.
numberOfPriorsReferenced: 0,
// Default viewport is used to define the viewport when
// additional viewports are added using the layout tool
defaultViewport: {
viewportOptions: {
viewportType: 'volume',
toolGroupId: 'dynamic4D-default',
allowUnmatchedView: true,
initialImageOptions: {
preset: 'middle', // 'first', 'last', 'middle'
},
},
displaySets: [
{
id: 'defaultDisplaySetId',
matchedDisplaySetsIndex: -1,
},
],
},
numberOfPriorsReferenced: -1,
displaySetSelectors: {
defaultDisplaySetId: {
// Unused currently
Expand Down
17 changes: 17 additions & 0 deletions extensions/cornerstone-dynamic-volume/src/getPanelModule.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React from 'react';
import { DynamicDataPanel } from './panels';
import { ROISegmentationPanel } from './panels';

function getPanelModule({
commandsManager,
Expand All @@ -16,6 +17,15 @@ function getPanelModule({
);
};

const wrappedROISegmentationPanel = () => {
return (
<ROISegmentationPanel
commandsManager={commandsManager}
servicesManager={servicesManager}
/>
);
};

return [
{
name: 'dynamic-volume',
Expand All @@ -24,6 +34,13 @@ function getPanelModule({
label: '4D Workflow',
component: wrappedDynamicDataPanel,
},
{
name: 'ROISegmentation',
iconName: 'tab-roi-threshold',
iconLabel: 'ROI Segmentation',
label: 'ROI Segmentation',
component: wrappedROISegmentationPanel,
},
];
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import React, { ReactElement } from 'react';
import PropTypes from 'prop-types';
import { Label, Select, InputRange } from '@ohif/ui';

function BrushConfiguration({
brushThresholdOptions,
brushThresholdId,
brushSize,
showThresholdSettings,
onBrushThresholdChange,
onBrushSizeChange,
}: {
brushThresholdOptions: {
value: string;
label: string;
placeHolder: string;
};
brushThresholdId: string;
brushSize: number;
showThresholdSettings: boolean;
onBrushThresholdChange: (thresholdId: string) => void;
onBrushSizeChange: (brushSize: number) => void;
}): ReactElement {
return (
<div className="flex flex-col px-4 py-2 space-y-4 bg-primary-dark text-white">
{showThresholdSettings && (
<>
<div>Threshold</div>
<div className="pb-2">
<Select
label="Brush Threshold"
closeMenuOnSelect={true}
className="mr-2 bg-black border-primary-main text-white "
options={brushThresholdOptions}
placeholder={
brushThresholdOptions.find(
option => option.value === brushThresholdId
).placeHolder
}
value={brushThresholdId}
onChange={({ value }) => onBrushThresholdChange(value)}
/>
</div>
</>
)}
<div>
<Label className="text-white">Brush Size</Label>
<InputRange
minValue={5}
maxValue={50}
value={brushSize}
step={1}
unit=""
showLabel={true}
onChange={brushSize => onBrushSizeChange(brushSize)}
inputClassName="w-full"
/>
</div>
</div>
);
}

BrushConfiguration.defaultPprops = {
showThresholdSettings: false,
};

BrushConfiguration.propTypes = {
brushThresholdOptions: PropTypes.arrayOf(
PropTypes.shape({
value: PropTypes.string.isRequired,
label: PropTypes.string.isRequired,
placeHolder: PropTypes.string.isRequired,
})
).isRequired,
brushThresholdId: PropTypes.string,
brushSize: PropTypes.number.isRequired,
showThresholdSettings: PropTypes.bool,
onBrushThresholdChange: PropTypes.func,
onBrushSizeChange: PropTypes.func.isRequired,
};

export { BrushConfiguration as default };
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
import React, { useEffect, useState, useCallback, ReactElement } from 'react';
import PropTypes from 'prop-types';
import { utilities as cstUtils } from '@cornerstonejs/tools';
import BrushConfiguration from './BrushConfiguration';
import { ServicesManager } from '@ohif/core';

const { segmentation: segmentationUtils } = cstUtils;

const DEFAULT_BRUSH_SIZE = 25;
const brushThresholds = [
{
id: 'ct-fat',
threshold: [-150, -70],
name: 'CT Fat',
},
{
id: 'ct-bone',
threshold: [200, 1000],
name: 'CT Bone',
},
{
id: 'pt',
threshold: [2.5, 100],
name: 'PT',
},
];

const getViewportIdByIndex = (servicesManager, viewportIndex) => {
const { viewportGridService } = servicesManager.services;
return viewportGridService.getState().viewports[viewportIndex]?.id;
};

const getToolGroupThresholdSettings = toolGroup => {
const currentBrushThreshold = segmentationUtils.getBrushThresholdForToolGroup(
toolGroup.id
);

const brushThreshold = brushThresholds.find(
brushThresholdItem =>
currentBrushThreshold &&
brushThresholdItem.threshold[0] === currentBrushThreshold[0] &&
brushThresholdItem.threshold[1] === currentBrushThreshold[1]
);

if (currentBrushThreshold && !brushThreshold) {
console.warn(
`No brush threshold setting found for [${currentBrushThreshold[0]}, ${currentBrushThreshold[1]}]`
);
}

return brushThreshold ?? brushThresholds[0];
};

const getViewportBrushToolSettings = (servicesManager, viewportIndex) => {
const { toolGroupService } = servicesManager.services;
const viewportId = getViewportIdByIndex(servicesManager, viewportIndex);
const toolGroup = toolGroupService.getToolGroupForViewport(viewportId);
const brushThreshold = getToolGroupThresholdSettings(toolGroup);
const brushSize =
(toolGroup && segmentationUtils.getBrushSizeForToolGroup(toolGroup.id)) ??
DEFAULT_BRUSH_SIZE;

return { brushThreshold, brushSize };
};

function BrushConfigurationWithServices({
servicesManager,
showThresholdSettings = false,
}: {
servicesManager: ServicesManager;
}): ReactElement {
const { viewportGridService, toolGroupService } = servicesManager.services;

const [activeViewportIndex, setActiveViewportIndex] = useState(
() => viewportGridService.getState().activeViewportIndex ?? 0
);

const getActiveViewportBrushToolSettings = useCallback(
() => getViewportBrushToolSettings(servicesManager, activeViewportIndex),
[servicesManager, activeViewportIndex]
);

const [selectedBrushThresholdId, setSelectedBrushThresholdId] = useState(
getActiveViewportBrushToolSettings().brushThreshold.id
);

const [brushSize, setBrushSize] = useState(
() => getActiveViewportBrushToolSettings().brushSize
);

const brushThresholdOptions = brushThresholds.map(
({ id, threshold, name }) => ({
value: id,
label: `${name} (${threshold.join(', ')})`,
placeHolder: `${name} (${threshold.join(', ')})`,
})
);

const handleBrushThresholdChange = brushThresholdId => {
const brushThreshold = brushThresholds.find(
brushThreshold => brushThreshold.id === brushThresholdId
);

const toolGroup = toolGroupService.getToolGroup();

if (!toolGroup) {
console.warn('toolGroup not found');
return;
}

segmentationUtils.setBrushThresholdForToolGroup(
toolGroup.id,
brushThreshold.threshold
);

setSelectedBrushThresholdId(brushThreshold.id);
};

const handleBrushSizeChange = brushSize => {
const toolGroup = toolGroupService.getToolGroup();

if (!toolGroup) {
console.warn('toolGroup not found');
return;
}

segmentationUtils.setBrushSizeForToolGroup(toolGroup.id, brushSize);
setBrushSize(brushSize);
};

// Updates the thresholdId for the active viewport
useEffect(() => {
const { brushThreshold, brushSize } = getActiveViewportBrushToolSettings();

setSelectedBrushThresholdId(brushThreshold.id);
setBrushSize(brushSize);
}, [activeViewportIndex, getActiveViewportBrushToolSettings]);

// Updates the active viewport index whenever it changes
useEffect(() => {
const { unsubscribe } = viewportGridService.subscribe(
viewportGridService.EVENTS.ACTIVE_VIEWPORT_INDEX_CHANGED,
({ viewportIndex, ...rest }) => {
setActiveViewportIndex(viewportIndex);
}
);

return () => {
unsubscribe();
};
}, [viewportGridService]);

return (
<BrushConfiguration
brushThresholdOptions={brushThresholdOptions}
brushThresholdId={selectedBrushThresholdId}
brushSize={brushSize}
showThresholdSettings={showThresholdSettings}
onBrushThresholdChange={handleBrushThresholdChange}
onBrushSizeChange={handleBrushSizeChange}
/>
);
}

BrushConfigurationWithServices.defaultProps = {
showThresholdSettings: false,
};

BrushConfigurationWithServices.propTypes = {
servicesManager: PropTypes.instanceOf(ServicesManager),
showThresholdSettings: PropTypes.bool.isRequired,
};

export { BrushConfigurationWithServices as default };
Loading

0 comments on commit 72243fa

Please sign in to comment.