diff --git a/.github/workflows/deploy_ghpages.yml b/.github/workflows/deploy_ghpages.yml
index ba1cbdf78e6..d9a36ff5f31 100644
--- a/.github/workflows/deploy_ghpages.yml
+++ b/.github/workflows/deploy_ghpages.yml
@@ -2,7 +2,7 @@ name: Deploy viewer to github pages
on:
push:
- branches: [ "gradienthealth/Stack-Segmentation" ]
+ branches: [ "gradienthealth/segmentation_mode_sheet_integration" ]
#pull_request:
#branches: [ "main" ]
@@ -24,7 +24,7 @@ jobs:
uses: actions/checkout@v3
with:
repository: gradienthealth/cornerstone3D-beta
- ref: gradienthealth/stack-segmentation-support-with-zip-image-loader
+ ref: gradienthealth/segmentation_mode_sheet_integration
path: ./cornerstone3D
- name: Build cornerstone3D
@@ -37,7 +37,7 @@ jobs:
uses: actions/checkout@v3
with:
repository: gradienthealth/GradientExtensionsAndModes
- ref: gradienthealth/Segmentation-with-DicomJSON
+ ref: gradienthealth/segmentation_mode_sheet_integration
path: ./GradientExtensionsAndModes
#- name: Build GradientExtensionsAndModes
diff --git a/extensions/cornerstone-dicom-seg/src/commandsModule.ts b/extensions/cornerstone-dicom-seg/src/commandsModule.ts
index 98d8a241fd3..1d41748fcfc 100644
--- a/extensions/cornerstone-dicom-seg/src/commandsModule.ts
+++ b/extensions/cornerstone-dicom-seg/src/commandsModule.ts
@@ -19,6 +19,7 @@ import {
getTargetViewport,
} from './utils/hydrationUtils';
import generateLabelmaps2DFromImageIdMap from './utils/generateLabelmaps2DFromImageIdMap';
+import getSegmentLabel from './utils/getSegmentLabel';
const { datasetToBlob } = dcmjs.data;
@@ -100,7 +101,7 @@ const commandsModule = ({
toolGroupId,
segmentIndex: 1,
properties: {
- label: 'Segment 1',
+ label: getSegmentLabel(segmentationService.getSegmentation(segmentationId)),
},
});
@@ -318,28 +319,45 @@ const commandsModule = ({
* @returns {Object|void} Returns the naturalized report if successfully stored,
* otherwise throws an error.
*/
- storeSegmentation: async ({ segmentationId, dataSource }) => {
- const promptResult = await createReportDialogPrompt(uiDialogService, {
- extensionManager,
- });
-
- if (promptResult.action !== 1 && promptResult.value) {
- return;
- }
-
+ storeSegmentation: async ({ segmentationId, dataSource, skipLabelDialog = false }) => {
const segmentation = segmentationService.getSegmentation(segmentationId);
if (!segmentation) {
throw new Error('No segmentation found');
}
+ const { label, displaySetInstanceUID } = segmentation;
+
+ const displaySet = displaySetService.getDisplaySetByUID(displaySetInstanceUID);
+ const shouldOverWrite = displaySet && displaySet.Modality === 'SEG';
+
+ let promptResult: { action?: number; value?: string } = {};
+
+ if (!(skipLabelDialog || shouldOverWrite)) {
+ promptResult = await createReportDialogPrompt(uiDialogService, {
+ extensionManager,
+ });
+
+ if (promptResult.action !== 1 && !promptResult.value) {
+ return;
+ }
+ }
- const { label } = segmentation;
const SeriesDescription = promptResult.value || label || 'Research Derived Series';
+ segmentation.label = SeriesDescription;
const generatedData = actions.generateSegmentation({
segmentationId,
options: {
SeriesDescription,
+ // Use SeriesInstanceUID, SOPInstanceUID, SeriesNumber, Manufacturer and SeriesDate
+ // if displaySet of the segmentation already exists.
+ // Study level and patient metadata will be used automatically.
+ ...(shouldOverWrite && {
+ SeriesInstanceUID: displaySet.SeriesInstanceUID,
+ SOPInstanceUID: displaySet.instances[0].SOPInstanceUID,
+ SeriesNumber: displaySet.SeriesNumber,
+ Manufacturer: displaySet.instances[0].Manufacturer,
+ }),
},
});
@@ -358,8 +376,6 @@ const commandsModule = ({
// add the information for where we stored it to the instance as well
naturalizedReport.wadoRoot = dataSource.getConfig().wadoRoot;
- DicomMetadataStore.addInstances([naturalizedReport], true);
-
return naturalizedReport;
},
/**
diff --git a/extensions/cornerstone-dicom-seg/src/getSopClassHandlerModule.js b/extensions/cornerstone-dicom-seg/src/getSopClassHandlerModule.js
index 8a3d095e9ed..a0043fefcda 100644
--- a/extensions/cornerstone-dicom-seg/src/getSopClassHandlerModule.js
+++ b/extensions/cornerstone-dicom-seg/src/getSopClassHandlerModule.js
@@ -192,6 +192,7 @@ async function _loadSegments({ extensionManager, servicesManager, segDisplaySet,
}
});
+ /* Skip the warning message as it is annoying on auto segmentations loading.
if (!usedRecommendedDisplayCIELabValue) {
// Display a notification about the non-utilization of RecommendedDisplayCIELabValue
uiNotificationService.show({
@@ -201,7 +202,7 @@ async function _loadSegments({ extensionManager, servicesManager, segDisplaySet,
type: 'warning',
duration: 5000,
});
- }
+ }*/
Object.assign(segDisplaySet, results);
}
diff --git a/extensions/cornerstone-dicom-seg/src/panels/PanelSegmentation.tsx b/extensions/cornerstone-dicom-seg/src/panels/PanelSegmentation.tsx
index 1b4c3a2d27d..141bb8f5935 100644
--- a/extensions/cornerstone-dicom-seg/src/panels/PanelSegmentation.tsx
+++ b/extensions/cornerstone-dicom-seg/src/panels/PanelSegmentation.tsx
@@ -1,11 +1,25 @@
import { createReportAsync } from '@ohif/extension-default';
-import React, { useEffect, useState, useCallback } from 'react';
+import React, { useEffect, useState, useCallback, useReducer } from 'react';
import PropTypes from 'prop-types';
import { SegmentationGroupTable, LegacyButtonGroup, LegacyButton } from '@ohif/ui';
import callInputDialog from './callInputDialog';
import callColorPickerDialog from './colorPickerDialog';
import { useTranslation } from 'react-i18next';
+import getSegmentLabel from '../utils/getSegmentLabel';
+
+const savedStatusReducer = (state, action) => {
+ return {
+ ...state,
+ ...action.payload,
+ };
+};
+
+const SAVED_STATUS_ICON = {
+ SAVED: 'notifications-success',
+ MODIFIED: 'notifications-warning',
+ ERROR: 'notifications-error',
+};
export default function PanelSegmentation({
servicesManager,
@@ -13,7 +27,14 @@ export default function PanelSegmentation({
extensionManager,
configuration,
}) {
- const { segmentationService, viewportGridService, uiDialogService } = servicesManager.services;
+ const {
+ segmentationService,
+ viewportGridService,
+ uiDialogService,
+ displaySetService,
+ userAuthenticationService,
+ CropDisplayAreaService,
+ } = servicesManager.services;
const { t } = useTranslation('PanelSegmentation');
@@ -23,6 +44,7 @@ export default function PanelSegmentation({
);
const [segmentations, setSegmentations] = useState(() => segmentationService.getSegmentations());
+ const [savedStatusStates, dispatch] = useReducer(savedStatusReducer, {});
useEffect(() => {
// ~~ Subscription
@@ -47,7 +69,78 @@ export default function PanelSegmentation({
};
}, []);
+ useEffect(() => {
+ let changedSegmentations: any[] = [],
+ timerId;
+ const timoutInSeconds = 5;
+
+ const { unsubscribe } = segmentationService.subscribe(
+ segmentationService.EVENTS.SEGMENTATION_DATA_MODIFIED,
+ ({ segmentation }) => {
+ clearTimeout(timerId);
+ dispatch({ payload: { [segmentation.id]: SAVED_STATUS_ICON.MODIFIED } });
+
+ if (
+ !changedSegmentations.find(
+ changedSegmentation => changedSegmentation.id === segmentation.id
+ )
+ ) {
+ changedSegmentations.push(segmentation);
+ }
+
+ timerId = setTimeout(() => {
+ const datasources = extensionManager.getActiveDataSource();
+
+ const promises = changedSegmentations.map(segmentation =>
+ createReportAsync({
+ servicesManager: servicesManager,
+ getReport: () =>
+ commandsManager.runCommand('storeSegmentation', {
+ segmentationId: segmentation.id,
+ dataSource: datasources[0],
+ skipLabelDialog: true,
+ }),
+ reportType: 'Segmentation',
+ showLoadingModal: false,
+ throwErrors: true,
+ })
+ );
+
+ Promise.allSettled(promises).then(results => {
+ const payload = results.reduce((acc, result, index) => {
+ if (result.value) {
+ changedSegmentations[index].displaySetInstanceUID = result.value[0];
+ displaySetService.getDisplaySetByUID(result.value[0])?.getReferenceDisplaySet();
+ }
+
+ return {
+ ...acc,
+ [changedSegmentations[index].id]:
+ result.status === 'fulfilled' ? SAVED_STATUS_ICON.SAVED : SAVED_STATUS_ICON.ERROR,
+ };
+ }, {});
+
+ dispatch({ payload });
+
+ const savedSegmentations = Object.keys(payload).filter(
+ id => payload[id] === SAVED_STATUS_ICON.SAVED
+ );
+ changedSegmentations = changedSegmentations.filter(
+ cs => !savedSegmentations.includes(cs.id)
+ );
+ });
+ }, timoutInSeconds * 1000);
+ }
+ );
+
+ return () => {
+ unsubscribe();
+ };
+ }, []);
+
const setSegmentationActive = segmentationId => {
+ setReferencedDisplaySet(segmentationId);
+
const isSegmentationActive = segmentations.find(seg => seg.id === segmentationId)?.isActive;
if (isSegmentationActive) {
@@ -57,6 +150,31 @@ export default function PanelSegmentation({
segmentationService.setActiveSegmentationForToolGroup(segmentationId);
};
+ // Set referenced displaySet of the segmentation to the viewport
+ // if it is not displayed in any of the viewports.
+ const setReferencedDisplaySet = segmentationId => {
+ const segDisplayset = displaySetService.getDisplaySetByUID(segmentationId);
+ if (!segDisplayset) {
+ return;
+ }
+
+ const referencedDisplaySetInstanceUID = segDisplayset.referencedDisplaySetInstanceUID;
+ const { viewports, activeViewportId } = viewportGridService.getState();
+ let referencedImageLoaded = false;
+ viewports.forEach(viewport => {
+ if (viewport.displaySetInstanceUIDs.includes(referencedDisplaySetInstanceUID)) {
+ referencedImageLoaded = true;
+ }
+ });
+
+ if (!referencedImageLoaded) {
+ viewportGridService.setDisplaySetsForViewport({
+ viewportId: activeViewportId,
+ displaySetInstanceUIDs: [referencedDisplaySetInstanceUID],
+ });
+ }
+ };
+
const getToolGroupIds = segmentationId => {
const toolGroupIds = segmentationService.getToolGroupIdsWithSegmentation(segmentationId);
@@ -68,6 +186,7 @@ export default function PanelSegmentation({
};
const onSegmentationClick = (segmentationId: string) => {
+ setReferencedDisplaySet(segmentationId);
segmentationService.setActiveSegmentationForToolGroup(segmentationId);
};
@@ -78,10 +197,12 @@ export default function PanelSegmentation({
const onSegmentAdd = segmentationId => {
setSegmentationActive(segmentationId);
- segmentationService.addSegment(segmentationId);
+ const label = getSegmentLabel(segmentations.find(seg => seg.id === segmentationId));
+ segmentationService.addSegment(segmentationId, { properties: { label } });
};
const onSegmentClick = (segmentationId, segmentIndex) => {
+ setReferencedDisplaySet(segmentationId);
segmentationService.setActiveSegment(segmentationId, segmentIndex);
const toolGroupIds = getToolGroupIds(segmentationId);
@@ -210,16 +331,25 @@ export default function PanelSegmentation({
const storeSegmentation = async segmentationId => {
setSegmentationActive(segmentationId);
const datasources = extensionManager.getActiveDataSource();
+ let displaySetInstanceUIDs;
+
+ try {
+ displaySetInstanceUIDs = await createReportAsync({
+ servicesManager,
+ getReport: () =>
+ commandsManager.runCommand('storeSegmentation', {
+ segmentationId,
+ dataSource: datasources[0],
+ }),
+ reportType: 'Segmentation',
+ throwErrors: true,
+ });
- const displaySetInstanceUIDs = await createReportAsync({
- servicesManager,
- getReport: () =>
- commandsManager.runCommand('storeSegmentation', {
- segmentationId,
- dataSource: datasources[0],
- }),
- reportType: 'Segmentation',
- });
+ dispatch({ payload: { [segmentationId]: SAVED_STATUS_ICON.SAVED } });
+ } catch (error) {
+ console.warn(error.message);
+ dispatch({ payload: { [segmentationId]: SAVED_STATUS_ICON.ERROR } });
+ }
// Show the exported report in the active viewport as read only (similar to SR)
if (displaySetInstanceUIDs) {
@@ -242,13 +372,18 @@ export default function PanelSegmentation({
});
};
+ const params = new URLSearchParams(window.location.search);
+ const showAddSegmentation = params.get('disableAddSegmentation') !== 'true';
+
return (
<>
_setSegmentationConfiguration(selectedSegmentationId, 'fillAlphaInactive', value)
}
+ CropDisplayAreaService={CropDisplayAreaService}
/>
>
diff --git a/extensions/cornerstone-dicom-seg/src/panels/SegmentationToolbox.tsx b/extensions/cornerstone-dicom-seg/src/panels/SegmentationToolbox.tsx
index f2d580cff46..e25e3045e42 100644
--- a/extensions/cornerstone-dicom-seg/src/panels/SegmentationToolbox.tsx
+++ b/extensions/cornerstone-dicom-seg/src/panels/SegmentationToolbox.tsx
@@ -2,6 +2,7 @@ import React, { useCallback, useEffect, useState, useReducer } from 'react';
import { AdvancedToolbox, InputDoubleRange, useViewportGrid } from '@ohif/ui';
import { Types } from '@ohif/extension-cornerstone';
import { utilities } from '@cornerstonejs/tools';
+import { EVENTS, eventTarget } from '@cornerstonejs/core';
const { segmentation: segmentationUtils } = utilities;
@@ -24,11 +25,11 @@ const ACTIONS = {
const initialState = {
Brush: {
- brushSize: 15,
+ brushSize: 2,
mode: 'CircularBrush', // Can be 'CircularBrush' or 'SphereBrush'
},
Eraser: {
- brushSize: 15,
+ brushSize: 2,
mode: 'CircularEraser', // Can be 'CircularEraser' or 'SphereEraser'
},
Shapes: {
@@ -69,6 +70,7 @@ function SegmentationToolbox({ servicesManager, extensionManager }) {
const [toolsEnabled, setToolsEnabled] = useState(false);
const [state, dispatch] = useReducer(toolboxReducer, initialState);
+ const [brushProperties, setBrushProperties] = useState({ min: 2, max: 3, step: 0.01 });
const updateActiveTool = useCallback(() => {
if (!viewports?.size || activeViewportId === undefined) {
@@ -105,6 +107,64 @@ function SegmentationToolbox({ servicesManager, extensionManager }) {
[toolbarService, dispatch]
);
+ const setBrushSizesFromParams = () => {
+ const params = new URLSearchParams(window.location.search);
+ const toolCategories = ['Brush', 'Eraser'];
+ const defaultBrushSizeInMm = +params.get('defaultBrushSize') || 2;
+ let minBrushSizeInMm = +params.get('minBrushSize') || 2;
+ let maxBrushSizeInMm = +params.get('maxBrushSize') || 3;
+
+ const highestPixelSpacing = getPixelToMmConversionFactor(servicesManager);
+ const lowestBrushRadius = highestPixelSpacing / 2;
+
+ if (minBrushSizeInMm < lowestBrushRadius) {
+ minBrushSizeInMm = lowestBrushRadius;
+ }
+ if (maxBrushSizeInMm < lowestBrushRadius) {
+ maxBrushSizeInMm = highestPixelSpacing;
+ }
+
+ setBrushProperties({
+ min: +minBrushSizeInMm.toFixed(2),
+ max: +maxBrushSizeInMm.toFixed(2),
+ step: +((maxBrushSizeInMm - minBrushSizeInMm) / 100).toFixed(2),
+ });
+ toolCategories.forEach(toolCategory => {
+ onBrushSizeChange(defaultBrushSizeInMm, toolCategory);
+ });
+ };
+
+ useEffect(() => {
+ if (getPixelToMmConversionFactor(servicesManager)) {
+ setBrushSizesFromParams();
+ return;
+ }
+
+ const elementEnabledHandler = evt => {
+ const setDefaultBrushSize = () => {
+ setBrushSizesFromParams();
+
+ evt.detail.element.removeEventListener(
+ EVENTS.VOLUME_VIEWPORT_NEW_VOLUME,
+ setDefaultBrushSize
+ );
+ eventTarget.removeEventListener(EVENTS.STACK_VIEWPORT_NEW_STACK, setDefaultBrushSize);
+ };
+
+ evt.detail.element.addEventListener(EVENTS.VOLUME_VIEWPORT_NEW_VOLUME, setDefaultBrushSize);
+ eventTarget.addEventListener(EVENTS.STACK_VIEWPORT_NEW_STACK, setDefaultBrushSize);
+ eventTarget.removeEventListener(EVENTS.ELEMENT_ENABLED, elementEnabledHandler);
+ };
+
+ const viewportElement = getActiveViewportElement(servicesManager, extensionManager);
+ if (viewportElement) {
+ elementEnabledHandler({ detail: { element: viewportElement } });
+ return;
+ }
+
+ eventTarget.addEventListener(EVENTS.ELEMENT_ENABLED, elementEnabledHandler);
+ }, []);
+
/**
* sets the tools enabled IF there are segmentations
*/
@@ -249,10 +309,10 @@ function SegmentationToolbox({ servicesManager, extensionManager }) {
name: 'Radius (mm)',
id: 'brush-radius',
type: 'range',
- min: 0.5,
- max: 99.5,
+ min: brushProperties.min,
+ max: brushProperties.max,
value: state.Brush.brushSize,
- step: 0.5,
+ step: brushProperties.step,
onChange: value => onBrushSizeChange(value, 'Brush'),
},
{
@@ -281,10 +341,10 @@ function SegmentationToolbox({ servicesManager, extensionManager }) {
name: 'Radius (mm)',
type: 'range',
id: 'eraser-radius',
- min: 0.5,
- max: 99.5,
+ min: brushProperties.min,
+ max: brushProperties.max,
value: state.Eraser.brushSize,
- step: 0.5,
+ step: brushProperties.step,
onChange: value => onBrushSizeChange(value, 'Eraser'),
},
{
@@ -402,4 +462,30 @@ function _getToolNamesFromCategory(category) {
return toolNames;
}
+function getPixelToMmConversionFactor(servicesManager) {
+ const { viewportGridService, cornerstoneViewportService } = servicesManager.services;
+ const { activeViewportId } = viewportGridService.getState();
+ const viewport = cornerstoneViewportService.getCornerstoneViewport(activeViewportId);
+ const imageData = viewport?.getImageData();
+
+ if (!imageData) {
+ return;
+ }
+
+ const { spacing } = imageData;
+ return Math.max(spacing[0], spacing[1]);
+}
+
+function getActiveViewportElement(servicesManager, extensionManager) {
+ const utilityModule = extensionManager.getModuleEntry(
+ '@ohif/extension-cornerstone.utilityModule.common'
+ );
+ const { getEnabledElement } = utilityModule.exports;
+ const { viewportGridService } = servicesManager.services;
+
+ const { activeViewportId } = viewportGridService.getState();
+ const { element } = getEnabledElement(activeViewportId) || {};
+ return element;
+}
+
export default SegmentationToolbox;
diff --git a/extensions/cornerstone-dicom-seg/src/utils/getSegmentLabel.ts b/extensions/cornerstone-dicom-seg/src/utils/getSegmentLabel.ts
new file mode 100644
index 00000000000..7215361b598
--- /dev/null
+++ b/extensions/cornerstone-dicom-seg/src/utils/getSegmentLabel.ts
@@ -0,0 +1,8 @@
+const getSegmentLabel = (segmentation): string => {
+ const segmentationName = segmentation.label.includes('Vessel') ? 'Vessel' : 'Segment';
+ const segmentCount = segmentation.segments.filter(segment => segment).length;
+
+ return segmentationName + ' ' + (segmentCount + 1);
+};
+
+export default getSegmentLabel;
diff --git a/extensions/cornerstone/jest.config.js b/extensions/cornerstone/jest.config.js
index 2978b062ed1..9e0ecb3e4ec 100644
--- a/extensions/cornerstone/jest.config.js
+++ b/extensions/cornerstone/jest.config.js
@@ -5,6 +5,11 @@ module.exports = {
...base,
name: pkg.name,
displayName: pkg.name,
+ moduleNameMapper: {
+ ...base.moduleNameMapper,
+ '^@ohif/(.*)$': '/../../platform/$1/src',
+ '^@cornerstonejs/tools(.*)$': '/../../node_modules/@cornerstonejs/tools',
+ },
// rootDir: "../.."
// testMatch: [
// //`/platform/${pack.name}/**/*.spec.js`
diff --git a/extensions/cornerstone/src/commandsModule.ts b/extensions/cornerstone/src/commandsModule.ts
index 52645938125..44cc430a2ab 100644
--- a/extensions/cornerstone/src/commandsModule.ts
+++ b/extensions/cornerstone/src/commandsModule.ts
@@ -5,6 +5,7 @@ import {
utilities as csUtils,
Types as CoreTypes,
BaseVolumeViewport,
+ metaData,
} from '@cornerstonejs/core';
import {
ToolGroupManager,
@@ -21,6 +22,7 @@ import toggleStackImageSync from './utils/stackSync/toggleStackImageSync';
import { getFirstAnnotationSelected } from './utils/measurementServiceMappings/utils/selection';
import getActiveViewportEnabledElement from './utils/getActiveViewportEnabledElement';
import { CornerstoneServices } from './types';
+import { getImageFlips } from './utils/getImageFlips';
function commandsModule({
servicesManager,
@@ -35,6 +37,7 @@ function commandsModule({
cornerstoneViewportService,
uiNotificationService,
measurementService,
+ customizationService,
} = servicesManager.services as CornerstoneServices;
const { measurementServiceSource } = this;
@@ -359,6 +362,19 @@ function commandsModule({
],
});
},
+ recordSetToolActive: ({ toolName }) => {
+ toolbarService.recordInteraction({
+ interactionType: 'tool',
+ commands: [
+ {
+ commandName: 'setToolActive',
+ commandOptions: {
+ toolName,
+ },
+ },
+ ],
+ });
+ },
showDownloadViewportModal: () => {
const { activeViewportId } = viewportGridService.getState();
@@ -470,6 +486,16 @@ function commandsModule({
viewport.resetProperties?.();
viewport.resetCamera();
+ const { criteria: isOrientationCorrectionNeeded } = customizationService.get(
+ 'orientationCorrectionCriterion'
+ );
+ const instance = metaData.get('instance', viewport.getCurrentImageId());
+
+ if ((isOrientationCorrectionNeeded as (input) => boolean)?.(instance)) {
+ const { hFlip, vFlip } = getImageFlips(instance);
+ (hFlip || vFlip) && viewport.setCamera({ flipHorizontal: hFlip, flipVertical: vFlip });
+ }
+
viewport.render();
},
scaleViewport: ({ direction }) => {
@@ -653,6 +679,9 @@ function commandsModule({
setToolActive: {
commandFn: actions.setToolActive,
},
+ recordSetToolActive: {
+ commandFn: actions.recordSetToolActive,
+ },
rotateViewportCW: {
commandFn: actions.rotateViewport,
options: { rotation: 90 },
diff --git a/extensions/cornerstone/src/getCustomizationModule.ts b/extensions/cornerstone/src/getCustomizationModule.ts
index acb06424462..9d22282e261 100644
--- a/extensions/cornerstone/src/getCustomizationModule.ts
+++ b/extensions/cornerstone/src/getCustomizationModule.ts
@@ -1,6 +1,7 @@
import { Enums } from '@cornerstonejs/tools';
import { toolNames } from './initCornerstoneTools';
import DicomUpload from './components/DicomUpload/DicomUpload';
+import isOrientationCorrectionNeeded from './utils/isOrientationCorrectionNeeded';
const tools = {
active: [
@@ -37,6 +38,8 @@ function getCustomizationModule() {
id: 'cornerstone.overlayViewportTools',
tools,
},
+ // TODO: Move this customization to MG specific mode when introduced in OHIF.
+ { id: 'orientationCorrectionCriterion', criteria: isOrientationCorrectionNeeded },
],
},
];
diff --git a/extensions/cornerstone/src/hps/mpr.ts b/extensions/cornerstone/src/hps/mpr.ts
index 4c625c73172..a26c509e445 100644
--- a/extensions/cornerstone/src/hps/mpr.ts
+++ b/extensions/cornerstone/src/hps/mpr.ts
@@ -8,7 +8,7 @@ export const mpr: Types.HangingProtocol.Protocol = {
modifiedDate: '2023-08-15',
availableTo: {},
editableBy: {},
- // Unknown number of priors referenced - so just match any study
+ // Unknown number of priors referenced - so just match any study.
numberOfPriorsReferenced: 0,
protocolMatchingRules: [],
imageLoadStrategy: 'nth',
diff --git a/extensions/cornerstone/src/index.tsx b/extensions/cornerstone/src/index.tsx
index ac53c8f0f27..531e979f67a 100644
--- a/extensions/cornerstone/src/index.tsx
+++ b/extensions/cornerstone/src/index.tsx
@@ -30,6 +30,7 @@ import * as csWADOImageLoader from './initWADOImageLoader.js';
import { measurementMappingUtils } from './utils/measurementServiceMappings';
import type { PublicViewportOptions } from './services/ViewportService/Viewport';
import ImageOverlayViewerTool from './tools/ImageOverlayViewerTool';
+import { getImageFlips } from './utils/getImageFlips';
const Component = React.lazy(() => {
return import(/* webpackPrefetch: true */ './Viewport/OHIFCornerstoneViewport');
@@ -116,6 +117,7 @@ const cornerstoneExtension: Types.Extensions.Extension = {
},
getEnabledElement,
dicomLoaderService,
+ getImageFlips,
},
},
{
diff --git a/extensions/cornerstone/src/services/SegmentationService/SegmentationService.ts b/extensions/cornerstone/src/services/SegmentationService/SegmentationService.ts
index 2b9707c5b92..0fd41b11d69 100644
--- a/extensions/cornerstone/src/services/SegmentationService/SegmentationService.ts
+++ b/extensions/cornerstone/src/services/SegmentationService/SegmentationService.ts
@@ -471,11 +471,15 @@ class SegmentationService extends PubSubService {
// if first segmentation, we can use the default colorLUT, otherwise
// we need to generate a new one and use a new colorLUT
- const colorLUTIndex = 0;
+ let colorLUTIndex = 0;
+ const newColorLUT = this.generateNewColorLUT();
if (Object.keys(this.segmentations).length !== 0) {
- const newColorLUT = this.generateNewColorLUT();
- const colorLUTIndex = this.getNextColorLUTIndex();
+ colorLUTIndex = this.getNextColorLUTIndex();
cstSegmentation.config.color.addColorLUT(newColorLUT, colorLUTIndex);
+ } else if (!cstSegmentation.state.getColorLUT(0)) {
+ // If all loaded segmentations are removed and a new one is adding there may
+ // not be a colorLUT for index 0 as all of them should have been removed.
+ cstSegmentation.config.color.addColorLUT(newColorLUT, 0);
}
this.segmentations[segmentationId] = {
@@ -870,15 +874,9 @@ class SegmentationService extends PubSubService {
for (const [index, centroid] of centroids) {
const count = centroid.count;
const normalizedCentroid = {
- x: centroid.x / count,
- y: centroid.y / count,
- z: centroid.z / count,
+ image: [centroid.x / count, centroid.y / count, centroid.z / count],
};
- normalizedCentroid.world = imageData.indexToWorld([
- normalizedCentroid.x,
- normalizedCentroid.y,
- normalizedCentroid.z,
- ]);
+ normalizedCentroid.world = imageData.indexToWorld(normalizedCentroid.image);
result.set(index, normalizedCentroid);
}
@@ -2110,6 +2108,16 @@ class SegmentationService extends PubSubService {
if (removeFromCache && cache.getVolumeLoadObject(segmentationId)) {
cache.removeVolumeLoadObject(segmentationId);
}
+
+ const segmentation = this.getSegmentation(segmentationId);
+ const segmentationImageMap =
+ segmentation.representationData[segmentation.type].imageIdReferenceMap;
+ if (removeFromCache && segmentationImageMap) {
+ segmentationImageMap.forEach(
+ segImageId =>
+ cache.getVolumeLoadObject(segImageId) && cache.removeImageLoadObject(segImageId)
+ );
+ }
}
private _updateCornerstoneSegmentations({ segmentationId, notYetUpdatedAtSource }) {
diff --git a/extensions/cornerstone/src/services/ViewportService/CornerstoneViewportService.ts b/extensions/cornerstone/src/services/ViewportService/CornerstoneViewportService.ts
index 1180d8036f5..4882ca743cd 100644
--- a/extensions/cornerstone/src/services/ViewportService/CornerstoneViewportService.ts
+++ b/extensions/cornerstone/src/services/ViewportService/CornerstoneViewportService.ts
@@ -10,8 +10,9 @@ import {
VolumeViewport3D,
cache,
Enums as csEnums,
+ metaData,
+ eventTarget,
} from '@cornerstonejs/core';
-
import { utilities as csToolsUtils, Enums as csToolsEnums } from '@cornerstonejs/tools';
import { IViewportService } from './IViewportService';
import { RENDERING_ENGINE_ID } from './constants';
@@ -20,6 +21,7 @@ import { StackViewportData, VolumeViewportData } from '../../types/CornerstoneCa
import { Presentation, Presentations } from '../../types/Presentation';
import JumpPresets from '../../utils/JumpPresets';
+import { getImageFlips } from '../../utils/getImageFlips';
const EVENTS = {
VIEWPORT_DATA_CHANGED: 'event::cornerstoneViewportService:viewportDataChanged',
@@ -325,6 +327,8 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
viewportInfo: ViewportInfo,
presentations: Presentations
): Promise {
+ const { segmentationService, displaySetService, customizationService } =
+ this.servicesManager.services;
const displaySetOptions = viewportInfo.getDisplaySetOptions();
const { imageIds, initialImageIndex, displaySetInstanceUID } = viewportData.data;
@@ -354,24 +358,48 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
}
}
- const segmentations = this.servicesManager.services.segmentationService.getSegmentations(false);
+ const { criteria: isOrientationCorrectionNeeded } = customizationService.get(
+ 'orientationCorrectionCriterion'
+ );
+ const instance = metaData.get('instance', imageIds[initialImageIndexToUse]);
+
+ let hFlip = false,
+ vFlip = false;
+ if ((isOrientationCorrectionNeeded as (input) => boolean)?.(instance)) {
+ ({ hFlip, vFlip } = getImageFlips(instance));
+ }
+
+ const segmentations = segmentationService.getSegmentations(false);
const toolgroupId = viewportInfo.getToolGroupId();
for (const segmentation of segmentations) {
const toolGroupSegmentationRepresentations =
- this.servicesManager.services.segmentationService.getSegmentationRepresentationsForToolGroup(
- toolgroupId
- ) || [];
+ segmentationService.getSegmentationRepresentationsForToolGroup(toolgroupId) || [];
const isSegmentationInToolGroup = toolGroupSegmentationRepresentations.find(
representation => representation.segmentationId === segmentation.id
);
+ const callback = evt => {
+ if (viewport.id !== evt.detail.viewportId) {
+ return;
+ }
+
+ eventTarget.removeEventListener(csEnums.Events.STACK_VIEWPORT_IMAGES_ADDED, callback);
+
+ const camera = presentations.positionPresentation?.camera;
+ if (isSegmentationInToolGroup && camera) {
+ viewport.setCamera(camera);
+ } else {
+ (hFlip || vFlip) && viewport.setCamera({ flipHorizontal: hFlip, flipVertical: vFlip });
+ }
+ };
+
+ eventTarget.addEventListener(csEnums.Events.STACK_VIEWPORT_IMAGES_ADDED, callback);
+
if (!isSegmentationInToolGroup) {
- const segDisplaySet = this.servicesManager.services.displaySetService.getDisplaySetByUID(
- segmentation.id
- );
+ const segDisplaySet = displaySetService.getDisplaySetByUID(segmentation.id);
segDisplaySet &&
- this.servicesManager.services.segmentationService.addSegmentationRepresentationToToolGroup(
+ segmentationService.addSegmentationRepresentationToToolGroup(
toolgroupId,
segmentation.id,
segDisplaySet.isOverlayDisplaySet
@@ -382,6 +410,11 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
return viewport.setStack(imageIds, initialImageIndexToUse).then(() => {
viewport.setProperties({ ...properties });
const camera = presentations.positionPresentation?.camera;
+
+ !camera &&
+ (hFlip || vFlip) &&
+ viewport.setCamera({ flipHorizontal: hFlip, flipVertical: vFlip });
+
if (camera) {
viewport.setCamera(camera);
}
diff --git a/extensions/cornerstone/src/utils/createImageDataForStackImage.ts b/extensions/cornerstone/src/utils/createImageDataForStackImage.ts
index 74fa35d1e57..efa4150efd1 100644
--- a/extensions/cornerstone/src/utils/createImageDataForStackImage.ts
+++ b/extensions/cornerstone/src/utils/createImageDataForStackImage.ts
@@ -1,4 +1,4 @@
-import { vec3 } from 'gl-matrix';
+import { vec3, mat3 } from 'gl-matrix';
import { metaData, Types as csCoreTypes, cache } from '@cornerstonejs/core';
import vtkDataArray from '@kitware/vtk.js/Common/Core/DataArray';
import vtkImageData from '@kitware/vtk.js/Common/DataModel/ImageData';
@@ -16,11 +16,11 @@ export default function createImageDataForStackImage(imageIdReferenceMap: Map {
+ if (typeof patientOrientation === 'string') {
+ patientOrientation = patientOrientation.split('\\');
+ }
+
+ return {
+ rowDirection: patientOrientation[0],
+ columnDirection: patientOrientation[1][0],
+ };
+};
+
+const getOrientationStringLPS = vector => {
+ const sampleVectorDirectionMap = {
+ '1,0,0': 'L',
+ '-1,0,0': 'R',
+ '0,1,0': 'P',
+ '0,-1,0': 'A',
+ '0,0,1': 'H',
+ '0,0,-1': 'F',
+ };
+
+ return sampleVectorDirectionMap[vector.toString()];
+};
+
+jest.mock('@cornerstonejs/tools ', () => ({
+ utilities: { orientation: { getOrientationStringLPS } },
+}));
+jest.mock('@ohif/core', () => ({
+ defaults: { orientationDirectionVectorMap },
+ utils: { getDirectionsFromPatientOrientation },
+}));
+
+describe('getImageFlips', () => {
+ test('should return empty object if none of the parameters are provided', () => {
+ const flipsNeeded = getImageFlips({});
+ expect(flipsNeeded).toEqual({});
+ });
+
+ test('should return empty object if ImageOrientationPatient and PatientOrientation is not provided', () => {
+ const ImageLaterality = 'L';
+ const flipsNeeded = getImageFlips({
+ ImageLaterality,
+ });
+ expect(flipsNeeded).toEqual({});
+ });
+
+ test('should return empty object if ImageLaterality is not privided', () => {
+ const ImageOrientationPatient = [0, 1, 0, 0, 0, 1],
+ PatientOrientation = ['P', 'H'];
+ const flipsNeeded = getImageFlips({
+ ImageOrientationPatient,
+ PatientOrientation,
+ });
+ expect(flipsNeeded).toEqual({});
+ });
+
+ test('should return { hFlip: false, vFlip: false } if ImageOrientationPatient is [0, 1, 0, 0, 0, -1] and ImageLaterality is R', () => {
+ const ImageOrientationPatient = [0, 1, 0, 0, 0, -1],
+ PatientOrientation = ['P', 'F'],
+ ImageLaterality = 'R';
+ const flipsNeeded = getImageFlips({
+ ImageOrientationPatient,
+ PatientOrientation,
+ ImageLaterality,
+ });
+ expect(flipsNeeded).toEqual({ hFlip: false, vFlip: false });
+ });
+
+ test('should return { hFlip: false, vFlip: true } if ImageOrientationPatient is [0, -1, 0, 0, 0, 1] and ImageLaterality is L', () => {
+ const ImageOrientationPatient = [0, -1, 0, 0, 0, 1],
+ ImageLaterality = 'L';
+ const flipsNeeded = getImageFlips({
+ ImageOrientationPatient,
+ ImageLaterality,
+ });
+ expect(flipsNeeded).toEqual({ hFlip: false, vFlip: true });
+ });
+
+ test('should return { hFlip: true, vFlip: true } if ImageOrientationPatient is [0, -1, 0, -1, 0, 0] and ImageLaterality is R', () => {
+ const ImageOrientationPatient = [0, -1, 0, -1, 0, 0],
+ ImageLaterality = 'R';
+ const flipsNeeded = getImageFlips({
+ ImageOrientationPatient,
+ ImageLaterality,
+ });
+ expect(flipsNeeded).toEqual({ hFlip: true, vFlip: true });
+ });
+
+ test("should return { hFlip: true, vFlip: true } if ImageOrientationPatient is not present, PatientOrientation is ['P', 'H'] and ImageLaterality is L", () => {
+ const PatientOrientation = ['P', 'H'],
+ ImageLaterality = 'L';
+ const flipsNeeded = getImageFlips({
+ PatientOrientation,
+ ImageLaterality,
+ });
+ expect(flipsNeeded).toEqual({ hFlip: true, vFlip: true });
+ });
+
+ test("should return { hFlip: true, vFlip: false } if ImageOrientationPatient is not present, PatientOrientation is ['A', 'F'] and ImageLaterality is R", () => {
+ const PatientOrientation = ['A', 'F'],
+ ImageLaterality = 'R';
+ const flipsNeeded = getImageFlips({
+ PatientOrientation,
+ ImageLaterality,
+ });
+ expect(flipsNeeded).toEqual({ hFlip: true, vFlip: false });
+ });
+
+ test("should return { hFlip: true, vFlip: false } if ImageOrientationPatient is not present, PatientOrientation is ['A', 'FL'] and ImageLaterality is R", () => {
+ const PatientOrientation = ['A', 'FL'],
+ ImageLaterality = 'R';
+ const flipsNeeded = getImageFlips({
+ PatientOrientation,
+ ImageLaterality,
+ });
+ expect(flipsNeeded).toEqual({ hFlip: true, vFlip: false });
+ });
+
+ test("should return { hFlip: true, vFlip: false } if ImageOrientationPatient ans ImageLaterality is not present, PatientOrientation is ['P', 'FL'] and FrameLaterality is L", () => {
+ const PatientOrientation = ['P', 'FL'],
+ FrameLaterality = 'L';
+ const flipsNeeded = getImageFlips({
+ PatientOrientation,
+ FrameLaterality,
+ });
+ expect(flipsNeeded).toEqual({ hFlip: true, vFlip: false });
+ });
+
+ test("should return empty object if ImageOrientationPatient is not present, PatientOrientation is ['H', 'R'] and ImageLaterality is R since the orientation is rotated, not flipped", () => {
+ const PatientOrientation = ['H', 'R'],
+ ImageLaterality = 'R';
+ const flipsNeeded = getImageFlips({
+ PatientOrientation,
+ ImageLaterality,
+ });
+ expect(flipsNeeded).toEqual({});
+ });
+
+ test("should return empty object if ImageOrientationPatient is not present, PatientOrientation is ['F', 'L'] and ImageLaterality is L since the orientation is rotated, not flipped", () => {
+ const PatientOrientation = ['F', 'L'],
+ ImageLaterality = 'L';
+ const flipsNeeded = getImageFlips({
+ PatientOrientation,
+ ImageLaterality,
+ });
+ expect(flipsNeeded).toEqual({});
+ });
+});
diff --git a/extensions/cornerstone/src/utils/getImageFlips.ts b/extensions/cornerstone/src/utils/getImageFlips.ts
new file mode 100644
index 00000000000..bbe95a5b0e2
--- /dev/null
+++ b/extensions/cornerstone/src/utils/getImageFlips.ts
@@ -0,0 +1,123 @@
+import { defaults, utils } from '@ohif/core';
+import { utilities } from '@cornerstonejs/tools';
+import { vec3 } from 'gl-matrix';
+
+const { orientationDirectionVectorMap } = defaults;
+const { getOrientationStringLPS } = utilities.orientation;
+
+type IOP = [number, number, number, number, number, number];
+type PO = [string, string] | string;
+type IL = string;
+type FL = string;
+type Instance = {
+ ImageOrientationPatient?: IOP;
+ PatientOrientation?: PO;
+ ImageLaterality?: IL;
+ FrameLaterality?: FL;
+};
+
+/**
+ * A function to get required flipping to correct the image according to Orientation and Laterality.
+ * This function does not handle rotated images.
+ * @param instance Metadata instance of the image.
+ * @returns vertical and horizontal flipping needed to correct the image if possible.
+ */
+export function getImageFlips(instance: Instance): { vFlip?: boolean; hFlip?: boolean } {
+ const { ImageOrientationPatient, PatientOrientation, ImageLaterality, FrameLaterality } =
+ instance;
+
+ if (!(ImageOrientationPatient || PatientOrientation) || !(ImageLaterality || FrameLaterality)) {
+ console.warn(
+ 'Skipping image orientation correction due to missing ImageOrientationPatient/ PatientOrientation or/and ImageLaterality/ FrameLaterality'
+ );
+ return {};
+ }
+
+ let rowDirectionCurrent, columnDirectionCurrent, rowCosines, columnCosines;
+ if (ImageOrientationPatient) {
+ rowCosines = ImageOrientationPatient.slice(0, 3);
+ columnCosines = ImageOrientationPatient.slice(3, 6);
+ rowDirectionCurrent = getOrientationStringLPS(rowCosines);
+ columnDirectionCurrent = getOrientationStringLPS(columnCosines)[0];
+ } else {
+ ({ rowDirection: rowDirectionCurrent, columnDirection: columnDirectionCurrent } =
+ utils.getDirectionsFromPatientOrientation(PatientOrientation));
+
+ rowCosines = orientationDirectionVectorMap[rowDirectionCurrent];
+ columnCosines = orientationDirectionVectorMap[columnDirectionCurrent];
+ }
+
+ const scanAxisNormal = vec3.create();
+ vec3.cross(scanAxisNormal, rowCosines, columnCosines);
+
+ const scanAxisDirection = getOrientationStringLPS(scanAxisNormal as [number, number, number]);
+
+ if (isImageRotated(rowDirectionCurrent, columnDirectionCurrent)) {
+ // TODO: Correcting images with rotation is not implemented.
+ console.warn('Correcting images by rotating is not implemented');
+ return {};
+ }
+
+ let rowDirectionTarget, columnDirectionTarget;
+ switch (scanAxisDirection[0]) {
+ // Sagittal
+ case 'L':
+ case 'R':
+ if ((ImageLaterality || FrameLaterality) === 'L') {
+ rowDirectionTarget = 'A';
+ } else {
+ rowDirectionTarget = 'P';
+ }
+ columnDirectionTarget = 'F';
+ break;
+ // Coronal
+ case 'A':
+ case 'P':
+ if ((ImageLaterality || FrameLaterality) === 'L') {
+ rowDirectionTarget = 'R';
+ } else {
+ rowDirectionTarget = 'L';
+ }
+ columnDirectionTarget = 'F';
+ break;
+ // Axial
+ case 'H':
+ case 'F':
+ if ((ImageLaterality || FrameLaterality) === 'L') {
+ rowDirectionTarget = 'A';
+ columnDirectionTarget = 'R';
+ } else {
+ rowDirectionTarget = 'P';
+ columnDirectionTarget = 'L';
+ }
+ break;
+ }
+
+ let hFlip = false,
+ vFlip = false;
+ if (rowDirectionCurrent !== rowDirectionTarget) {
+ hFlip = true;
+ }
+ if (columnDirectionCurrent !== columnDirectionTarget) {
+ vFlip = true;
+ }
+
+ return { hFlip, vFlip };
+}
+
+function isImageRotated(rowDirection: string, columnDirection: string): boolean {
+ const possibleValues: { [key: string]: [string, string] } = {
+ xDirection: ['L', 'R'],
+ yDirection: ['P', 'A'],
+ zDirection: ['H', 'F'],
+ };
+
+ if (
+ possibleValues.yDirection.includes(columnDirection) ||
+ possibleValues.zDirection.includes(rowDirection)
+ ) {
+ return true;
+ }
+
+ return false;
+}
diff --git a/extensions/cornerstone/src/utils/isOrientationCorrectionNeeded.ts b/extensions/cornerstone/src/utils/isOrientationCorrectionNeeded.ts
new file mode 100644
index 00000000000..83e24545231
--- /dev/null
+++ b/extensions/cornerstone/src/utils/isOrientationCorrectionNeeded.ts
@@ -0,0 +1,10 @@
+const DEFAULT_AUTO_FLIP_MODALITIES: string[] = ['MG'];
+
+export default function isOrientationCorrectionNeeded(instance) {
+ const { Modality } = instance;
+
+ // Check Modality
+ const isModalityIncluded = DEFAULT_AUTO_FLIP_MODALITIES.includes(Modality);
+
+ return isModalityIncluded;
+}
diff --git a/extensions/default/src/Actions/createReportAsync.tsx b/extensions/default/src/Actions/createReportAsync.tsx
index d0f34a48a23..4006951c7d6 100644
--- a/extensions/default/src/Actions/createReportAsync.tsx
+++ b/extensions/default/src/Actions/createReportAsync.tsx
@@ -1,46 +1,82 @@
import React from 'react';
+import dcmjs from 'dcmjs';
import { DicomMetadataStore } from '@ohif/core';
+const { datasetToBlob } = dcmjs.data;
+
/**
*
* @param {*} servicesManager
*/
-async function createReportAsync({ servicesManager, getReport, reportType = 'measurement' }) {
- const { displaySetService, uiNotificationService, uiDialogService } = servicesManager.services;
- const loadingDialogId = uiDialogService.create({
- showOverlay: true,
- isDraggable: false,
- centralize: true,
- content: Loading,
- });
+async function createReportAsync({
+ servicesManager,
+ getReport,
+ reportType = 'measurement',
+ showLoadingModal = true,
+ throwErrors = false,
+}) {
+ const { displaySetService, uiNotificationService, uiDialogService, CacheAPIService } =
+ servicesManager.services;
+ const loadingDialogId =
+ showLoadingModal &&
+ uiDialogService.create({
+ showOverlay: true,
+ isDraggable: false,
+ centralize: true,
+ content: Loading,
+ });
+
+ let displaySetInstanceUID;
try {
const naturalizedReport = await getReport();
+ const { SeriesInstanceUID, SOPInstanceUID } = naturalizedReport;
+ let displaySet = displaySetService
+ .getDisplaySetsForSeries(SeriesInstanceUID)
+ ?.find(ds => ds.instances.some(instance => instance.SOPInstanceUID === SOPInstanceUID));
+
+ const shouldOverWrite = displaySet && displaySet.Modality === 'SEG';
+
// The "Mode" route listens for DicomMetadataStore changes
// When a new instance is added, it listens and
// automatically calls makeDisplaySets
DicomMetadataStore.addInstances([naturalizedReport], true);
- const displaySet = displaySetService.getMostRecentDisplaySet();
+ if (!displaySet) {
+ // If there is no displayset before adding instances, it is a new series.
+ displaySet = displaySetService.getMostRecentDisplaySet();
+ }
- const displaySetInstanceUID = displaySet.displaySetInstanceUID;
+ displaySetInstanceUID = displaySet.displaySetInstanceUID;
- uiNotificationService.show({
- title: 'Create Report',
- message: `${reportType} saved successfully`,
- type: 'success',
- });
+ showLoadingModal &&
+ uiNotificationService.show({
+ title: 'Create Report',
+ message: `${reportType} saved successfully`,
+ type: 'success',
+ });
+
+ reportType === 'Segmentation' &&
+ CacheAPIService?.updateCachedFile(datasetToBlob(naturalizedReport), displaySet);
+ if (shouldOverWrite) {
+ return;
+ }
return [displaySetInstanceUID];
} catch (error) {
- uiNotificationService.show({
- title: 'Create Report',
- message: error.message || `Failed to store ${reportType}`,
- type: 'error',
- });
+ showLoadingModal &&
+ uiNotificationService.show({
+ title: 'Create Report',
+ message: error.message || `Failed to store ${reportType}`,
+ type: 'error',
+ });
+
+ if (throwErrors) {
+ throw error;
+ }
} finally {
- uiDialogService.dismiss({ id: loadingDialogId });
+ showLoadingModal && uiDialogService.dismiss({ id: loadingDialogId });
}
}
diff --git a/modes/segmentation/src/index.tsx b/modes/segmentation/src/index.tsx
index 8b24196f161..89d8162f703 100644
--- a/modes/segmentation/src/index.tsx
+++ b/modes/segmentation/src/index.tsx
@@ -22,6 +22,12 @@ const segmentation = {
viewport: '@ohif/extension-cornerstone-dicom-seg.viewportModule.dicom-seg',
};
+const gradienthealth = {
+ form: '@gradienthealth/ohif-gradienthealth-extension.panelModule.form',
+ thumbnailList:
+ '@gradienthealth/ohif-gradienthealth-extension.panelModule.seriesList-without-tracking',
+};
+
/**
* Just two dependencies to be able to render a viewport with panels in order
* to make sure that the mode is working.
@@ -50,7 +56,14 @@ function modeFactory({ modeConfiguration }) {
* Services and other resources.
*/
onModeEnter: ({ servicesManager, extensionManager, commandsManager }) => {
- const { measurementService, toolbarService, toolGroupService } = servicesManager.services;
+ const {
+ measurementService,
+ toolbarService,
+ toolGroupService,
+ GoogleSheetsService,
+ CropDisplayAreaService,
+ CacheAPIService,
+ } = servicesManager.services;
measurementService.clearMeasurements();
@@ -86,6 +99,9 @@ function modeFactory({ modeConfiguration }) {
activateTool
));
+ GoogleSheetsService.init();
+ CropDisplayAreaService.init();
+ CacheAPIService.init();
toolbarService.init(extensionManager);
toolbarService.addButtons(toolbarButtons);
toolbarService.createButtonSection('primary', [
@@ -149,11 +165,16 @@ function modeFactory({ modeConfiguration }) {
{
path: 'template',
layoutTemplate: ({ location, servicesManager }) => {
+ const params = new URLSearchParams(location.search);
+ const rightPanels = [
+ segmentation.panelTool,
+ ...(params.get('sheetId') ? [gradienthealth.form] : []),
+ ];
return {
id: ohif.layout,
props: {
- leftPanels: [ohif.leftPanel],
- rightPanels: [segmentation.panelTool],
+ leftPanels: [gradienthealth.thumbnailList],
+ rightPanels: rightPanels,
viewports: [
{
namespace: cornerstone.viewport,
diff --git a/platform/app/public/config/gradient.js b/platform/app/public/config/gradient.js
index 0410534bf7b..a4881fc5923 100644
--- a/platform/app/public/config/gradient.js
+++ b/platform/app/public/config/gradient.js
@@ -13,7 +13,7 @@ window.config = {
// below flag is for performance reasons, but it might not work for all servers
omitQuotationForMultipartRequest: true,
- showWarningMessageForCrossOrigin: true,
+ showWarningMessageForCrossOrigin: false,
showCPUFallbackMessage: true,
showLoadingIndicator: true,
strictZSpacingForVolumeViewport: true,
diff --git a/platform/core/src/classes/MetadataProvider.ts b/platform/core/src/classes/MetadataProvider.ts
index de12ff4e33c..4c52d80198f 100644
--- a/platform/core/src/classes/MetadataProvider.ts
+++ b/platform/core/src/classes/MetadataProvider.ts
@@ -6,6 +6,7 @@ import DicomMetadataStore from '../services/DicomMetadataStore';
import fetchPaletteColorLookupTableData from '../utils/metadataProvider/fetchPaletteColorLookupTableData';
import toNumber from '../utils/toNumber';
import combineFrameInstance from '../utils/combineFrameInstance';
+import { orientationDirectionVectorMap } from '../defaults';
class MetadataProvider {
constructor() {
@@ -494,7 +495,7 @@ export default metadataProvider;
const WADO_IMAGE_LOADER = {
imagePlaneModule: instance => {
- const { ImageOrientationPatient } = instance;
+ const { ImageOrientationPatient, PatientOrientation } = instance;
// Fallback for DX images.
// TODO: We should use the rest of the results of this function
@@ -515,6 +516,13 @@ const WADO_IMAGE_LOADER = {
if (ImageOrientationPatient) {
rowCosines = ImageOrientationPatient.slice(0, 3);
columnCosines = ImageOrientationPatient.slice(3, 6);
+ } else if (PatientOrientation) {
+ let patientOrientation = PatientOrientation;
+ if (typeof patientOrientation === 'string') {
+ patientOrientation = patientOrientation.split('\\');
+ }
+ rowCosines = orientationDirectionVectorMap[patientOrientation[0]];
+ columnCosines = orientationDirectionVectorMap[patientOrientation[1][0]];
}
return {
diff --git a/platform/core/src/defaults/hotkeyBindings.js b/platform/core/src/defaults/hotkeyBindings.js
index b4c6098936c..c4b572b7049 100644
--- a/platform/core/src/defaults/hotkeyBindings.js
+++ b/platform/core/src/defaults/hotkeyBindings.js
@@ -173,6 +173,20 @@ const bindings = [
label: 'W/L Preset 5',
keys: ['5'],
},
+ {
+ commandName: 'recordSetToolActive',
+ commandOptions: { toolName: 'CircularBrush' },
+ label: 'Segmentation Brush',
+ keys: ['b'],
+ isEditable: true,
+ },
+ {
+ commandName: 'recordSetToolActive',
+ commandOptions: { toolName: 'CircularEraser' },
+ label: 'Segmentation Eraser',
+ keys: ['e'],
+ isEditable: true,
+ },
// These don't exist, so don't try applying them....
// {
// commandName: 'setWindowLevel',
diff --git a/platform/core/src/defaults/index.js b/platform/core/src/defaults/index.js
index f8c065d7b7f..ad4be530cfb 100644
--- a/platform/core/src/defaults/index.js
+++ b/platform/core/src/defaults/index.js
@@ -1,4 +1,5 @@
import hotkeyBindings from './hotkeyBindings';
import windowLevelPresets from './windowLevelPresets';
-export { hotkeyBindings, windowLevelPresets };
-export default { hotkeyBindings, windowLevelPresets };
+import orientationDirectionVectorMap from './orientationDirectionVectors';
+export { hotkeyBindings, windowLevelPresets, orientationDirectionVectorMap };
+export default { hotkeyBindings, windowLevelPresets, orientationDirectionVectorMap };
diff --git a/platform/core/src/defaults/orientationDirectionVectors.ts b/platform/core/src/defaults/orientationDirectionVectors.ts
new file mode 100644
index 00000000000..2809673b26a
--- /dev/null
+++ b/platform/core/src/defaults/orientationDirectionVectors.ts
@@ -0,0 +1,12 @@
+import { Types } from '@cornerstonejs/core';
+
+const orientationDirectionVectorMap: { [key: string]: Types.Point3 } = {
+ L: [1, 0, 0], // Left
+ R: [-1, 0, 0], // Right
+ P: [0, 1, 0], // Posterior/ Back
+ A: [0, -1, 0], // Anterior/ Front
+ H: [0, 0, 1], // Head/ Superior
+ F: [0, 0, -1], // Feet/ Inferior
+};
+
+export default orientationDirectionVectorMap;
diff --git a/platform/core/src/types/OrientationDirections.ts b/platform/core/src/types/OrientationDirections.ts
new file mode 100644
index 00000000000..d44cec4d96d
--- /dev/null
+++ b/platform/core/src/types/OrientationDirections.ts
@@ -0,0 +1 @@
+export type OrientationDirections = { rowDirection: string; columnDirection: string };
diff --git a/platform/core/src/types/index.ts b/platform/core/src/types/index.ts
index efcc8ec3ccf..088a5935f60 100644
--- a/platform/core/src/types/index.ts
+++ b/platform/core/src/types/index.ts
@@ -18,6 +18,7 @@ export type * from './StudyMetadata';
export type * from './PanelModule';
export type * from './IPubSub';
export type * from './Color';
+export type * from './OrientationDirections';
// Enum exports
export * from './TimingEnum';
diff --git a/platform/core/src/utils/getDirectionsFromPatientOrientation.ts b/platform/core/src/utils/getDirectionsFromPatientOrientation.ts
new file mode 100644
index 00000000000..33058df6f14
--- /dev/null
+++ b/platform/core/src/utils/getDirectionsFromPatientOrientation.ts
@@ -0,0 +1,16 @@
+import { OrientationDirections } from '../types';
+
+export default function getDirectionsFromPatientOrientation(
+ patientOrientation: string | [string, string]
+): OrientationDirections {
+ if (typeof patientOrientation === 'string') {
+ patientOrientation = patientOrientation.split('\\') as [string, string];
+ }
+
+ // TODO: We are only considering first direction in column orientation.
+ // ex: in ['P', 'HL'], we are only taking 'H' instead of 'HL' as column orientation.
+ return {
+ rowDirection: patientOrientation[0],
+ columnDirection: patientOrientation[1][0],
+ };
+}
diff --git a/platform/core/src/utils/index.js b/platform/core/src/utils/index.js
index d7862618e0b..860ddbf6eb5 100644
--- a/platform/core/src/utils/index.js
+++ b/platform/core/src/utils/index.js
@@ -37,6 +37,7 @@ import {
} from './sortStudy';
import { subscribeToNextViewportGridChange } from './subscribeToNextViewportGridChange';
import { splitComma, getSplitParam } from './splitComma';
+import getDirectionsFromPatientOrientation from './getDirectionsFromPatientOrientation';
// Commented out unused functionality.
// Need to implement new mechanism for derived displaySets using the displaySetManager.
@@ -80,6 +81,7 @@ const utils = {
splitComma,
getSplitParam,
generateAcceptHeader,
+ getDirectionsFromPatientOrientation,
};
export {
@@ -112,6 +114,7 @@ export {
splitComma,
getSplitParam,
generateAcceptHeader,
+ getDirectionsFromPatientOrientation,
};
export default utils;
diff --git a/platform/core/src/utils/index.test.js b/platform/core/src/utils/index.test.js
index 30d8f70ef51..044df94a749 100644
--- a/platform/core/src/utils/index.test.js
+++ b/platform/core/src/utils/index.test.js
@@ -41,6 +41,7 @@ describe('Top level exports', () => {
'subscribeToNextViewportGridChange',
'uuidv4',
'addAccessors',
+ 'getDirectionsFromPatientOrientation',
].sort();
const exports = Object.keys(utils.default).sort();
diff --git a/platform/ui/src/components/SegmentationGroupTable/SegmentationDropDownRow.tsx b/platform/ui/src/components/SegmentationGroupTable/SegmentationDropDownRow.tsx
index 25a3f88857f..401a95b3505 100644
--- a/platform/ui/src/components/SegmentationGroupTable/SegmentationDropDownRow.tsx
+++ b/platform/ui/src/components/SegmentationGroupTable/SegmentationDropDownRow.tsx
@@ -5,6 +5,7 @@ import { useTranslation } from 'react-i18next';
function SegmentationDropDownRow({
segmentation,
+ savedStatusState,
activeSegmentationId,
disableEditing,
showAddSegment,
@@ -25,18 +26,6 @@ function SegmentationDropDownRow({
return null;
}
- const segmentationClickHandler = () => {
- if (segmentation.id === activeSegmentationId) {
- onToggleShowSegments(!showSegments);
- } else {
- onSegmentationClick(segmentation.id);
-
- if (!showSegments) {
- onToggleShowSegments(true);
- }
- }
- };
-
return (
{
storeSegmentation(segmentation.id);
},
@@ -95,29 +84,38 @@ function SegmentationDropDownRow({
onSegmentationDownload(segmentation.id);
},
},
- {
+ /*{
title: t('Download DICOM RTSTRUCT'),
onClick: () => {
onSegmentationDownloadRTSS(segmentation.id);
},
- },
+ },*/
],
]}
>
-
onSegmentationClick(segmentation.id)}
>
- {segmentation.label}
+
+ {segmentation.label}
+
+
onToggleSegmentationVisibility(segmentation.id)}
>
{segmentation.isVisible ? (
@@ -132,7 +130,10 @@ function SegmentationDropDownRow({
/>
)}
-
+
onToggleShowSegments(showSegments => !showSegments)}
+ >
{showSegments ? (
{
const [showSegments, toggleShowSegments] = useState(true);
@@ -35,6 +37,7 @@ const SegmentationGroup = ({
{showSegments && (
-
+
{segmentation?.segments?.map(segment => {
if (!segment) {
return null;
@@ -82,6 +85,7 @@ const SegmentationGroup = ({
onColor={onSegmentColorClick}
onToggleVisibility={onToggleSegmentVisibility}
onToggleLocked={onToggleSegmentLock}
+ CropDisplayAreaService={CropDisplayAreaService}
/>
);
@@ -107,6 +111,7 @@ SegmentationGroup.propTypes = {
})
),
}),
+ savedStatusState: PropTypes.string,
activeSegmentationId: PropTypes.string,
disableEditing: PropTypes.bool,
showAddSegment: PropTypes.bool,
@@ -125,6 +130,7 @@ SegmentationGroup.propTypes = {
onToggleSegmentVisibility: PropTypes.func.isRequired,
onToggleSegmentLock: PropTypes.func.isRequired,
onSegmentColorClick: PropTypes.func.isRequired,
+ CropDisplayAreaService: PropTypes.any,
};
export default SegmentationGroup;
diff --git a/platform/ui/src/components/SegmentationGroupTable/SegmentationGroupSegment.tsx b/platform/ui/src/components/SegmentationGroupTable/SegmentationGroupSegment.tsx
index 677f3b1246a..c404208cb63 100644
--- a/platform/ui/src/components/SegmentationGroupTable/SegmentationGroupSegment.tsx
+++ b/platform/ui/src/components/SegmentationGroupTable/SegmentationGroupSegment.tsx
@@ -20,9 +20,16 @@ const SegmentItem = ({
onColor,
onToggleVisibility,
onToggleLocked,
+ CropDisplayAreaService,
}) => {
const [isNumberBoxHovering, setIsNumberBoxHovering] = useState(false);
+ const onFocusClick = (segmentationId, segmentIndex) => {
+ CropDisplayAreaService.focusToSegment(segmentationId, segmentIndex).then(() =>
+ onClick(segmentationId, segmentIndex)
+ );
+ };
+
const cssColor = `rgb(${color[0]},${color[1]},${color[2]})`;
return (
@@ -139,6 +146,7 @@ const SegmentItem = ({
onToggleVisibility={onToggleVisibility}
segmentationId={segmentationId}
segmentIndex={segmentIndex}
+ onFocusClick={onFocusClick}
/>
@@ -156,6 +164,7 @@ const HoveringIcons = ({
onToggleVisibility,
segmentationId,
segmentIndex,
+ onFocusClick,
}) => {
const iconClass = 'w-5 h-5 hover:cursor-pointer hover:opacity-60';
@@ -174,6 +183,7 @@ const HoveringIcons = ({
return (
+ {createIcon('tool-zoom', onFocusClick)}
{!disableEditing && createIcon('row-edit', onEdit)}
{!disableEditing &&
createIcon(
@@ -205,6 +215,7 @@ SegmentItem.propTypes = {
onDelete: PropTypes.func.isRequired,
onToggleVisibility: PropTypes.func.isRequired,
onToggleLocked: PropTypes.func,
+ CropDisplayAreaService: PropTypes.any,
};
SegmentItem.defaultProps = {
diff --git a/platform/ui/src/components/SegmentationGroupTable/SegmentationGroupTable.tsx b/platform/ui/src/components/SegmentationGroupTable/SegmentationGroupTable.tsx
index e3647081cb2..a7a22f23aeb 100644
--- a/platform/ui/src/components/SegmentationGroupTable/SegmentationGroupTable.tsx
+++ b/platform/ui/src/components/SegmentationGroupTable/SegmentationGroupTable.tsx
@@ -9,6 +9,7 @@ import SegmentationGroup from './SegmentationGroup';
const SegmentationGroupTable = ({
segmentations,
+ savedStatusStates,
// segmentation initial config
segmentationConfig,
// UI show/hide
@@ -41,6 +42,7 @@ const SegmentationGroupTable = ({
setRenderFill,
setRenderInactiveSegmentations,
setRenderOutline,
+ CropDisplayAreaService,
}) => {
const [isConfigOpen, setIsConfigOpen] = useState(false);
const [activeSegmentationId, setActiveSegmentationId] = useState(null);
@@ -73,10 +75,14 @@ const SegmentationGroupTable = ({
title={t('Segmentations')}
actionIcons={
activeSegmentation && [
- {
- name: 'row-add',
- onClick: () => onSegmentationAdd(),
- },
+ ...(showAddSegmentation && !disableEditing
+ ? [
+ {
+ name: 'row-add',
+ onClick: () => onSegmentationAdd(),
+ },
+ ]
+ : []),
{
name: 'settings-bars',
onClick: () => setIsConfigOpen(isOpen => !isOpen),
@@ -109,6 +115,7 @@ const SegmentationGroupTable = ({
key={segmentation.id}
activeSegmentationId={activeSegmentationId}
segmentation={segmentation}
+ savedStatusState={savedStatusStates[segmentation.id]}
disableEditing={disableEditing}
showAddSegment={showAddSegment}
onSegmentationClick={onSegmentationClick}
@@ -126,6 +133,7 @@ const SegmentationGroupTable = ({
onToggleSegmentLock={onToggleSegmentLock}
onSegmentColorClick={onSegmentColorClick}
showDeleteSegment={showDeleteSegment}
+ CropDisplayAreaService={CropDisplayAreaService}
/>
))
)}
@@ -151,6 +159,7 @@ SegmentationGroupTable.propTypes = {
),
})
),
+ savedStatusStates: PropTypes.object,
segmentationConfig: PropTypes.object.isRequired,
disableEditing: PropTypes.bool,
showAddSegmentation: PropTypes.bool,
@@ -178,6 +187,7 @@ SegmentationGroupTable.propTypes = {
setRenderFill: PropTypes.func.isRequired,
setRenderInactiveSegmentations: PropTypes.func.isRequired,
setRenderOutline: PropTypes.func.isRequired,
+ CropDisplayAreaService: PropTypes.any,
};
SegmentationGroupTable.defaultProps = {