feat(data overlay): add foreground background control plus orientation change in the UI and add RTDOSE support (#5007)

This commit is contained in:
Alireza authored and GitHub committed 2025-05-05 16:35:05 -04:00
1 parent c1fced0573
commit cd4e6408bc
105 files changed
+3117 -788

No files matched your search

@@ -7,17 +7,14 @@ import {
Icons,
Button,
} from '@ohif/ui-next';
import { useSystem } from '@ohif/core';
/**
* The default sub-menu appearance and setup is defined here, but this can be
* replaced by
*/
const getMenuItemsDefault = ({
commandsManager,
items,
servicesManager,
...props
}: withAppTypes) => {
const getMenuItemsDefault = ({ commandsManager, items, ...props }: withAppTypes) => {
const { servicesManager } = useSystem();
const { customizationService } = servicesManager.services;
// This allows replacing the default child item for menus, whereas the entire
@@ -36,7 +33,7 @@ const getMenuItemsDefault = ({
onClick: ({ commandsManager, ...props }: withAppTypes) => () => void;
};
}) => (
<DropdownMenuItem onClick={() => item.onClick({ commandsManager, ...props })}>
<DropdownMenuItem onClick={() => item.onClick({ commandsManager, servicesManager, ...props })}>
<div className="flex items-center gap-2">
{item.iconName && <Icons.ByName name={item.iconName} />}
<span>{item.label}</span>
@@ -76,12 +73,8 @@ const getMenuItemsDefault = ({
* @returns Component bound to the bindProps
*/
export default function MoreDropdownMenu(bindProps) {
const {
menuItemsKey,
getMenuItems = getMenuItemsDefault,
commandsManager,
servicesManager,
} = bindProps;
const { menuItemsKey, getMenuItems = getMenuItemsDefault, commandsManager } = bindProps;
const { servicesManager } = useSystem();
const { customizationService } = servicesManager.services;
const items = customizationService.getCustomization(menuItemsKey);
+139
View File
@@ -3,6 +3,12 @@ import { Types, DicomMetadataStore } from '@ohif/core';
import { ContextMenuController } from './CustomizableContextMenu';
import DicomTagBrowser from './DicomTagBrowser/DicomTagBrowser';
import reuseCachedLayouts from './utils/reuseCachedLayouts';
import {
configureViewportForLayerAddition,
configureViewportForLayerRemoval,
canAddDisplaySetToViewport,
DERIVED_OVERLAY_MODALITIES,
} from './utils/layerConfigurationUtils';
import findViewportsByPosition, {
findOrCreateViewport as layoutFindOrCreate,
} from './findViewportsByPosition';
@@ -51,6 +57,137 @@ const commandsModule = ({
const contextMenuController = new ContextMenuController(servicesManager, commandsManager);
const actions = {
/**
* Adds a display set as a layer to the specified viewport
*
* @param options.viewportId - The ID of the viewport to add the layer to
* @param options.displaySetInstanceUID - The UID of the display set to add as a layer
* @param options.removeFirst - Optional flag to remove the display set first if it's already added
*/
addDisplaySetAsLayer: ({ viewportId, displaySetInstanceUID, removeFirst = false }) => {
if (!viewportId || !displaySetInstanceUID) {
console.warn('Missing required parameters for addDisplaySetAsLayer command');
return;
}
const { displaySetService, viewportGridService, hangingProtocolService } =
servicesManager.services;
// Get the display set
const displaySet = displaySetService.getDisplaySetByUID(displaySetInstanceUID);
if (!displaySet) {
return;
}
// Get current display sets for the viewport
const currentDisplaySetUIDs = viewportGridService.getDisplaySetsUIDsForViewport(viewportId);
// Check if we can add this display set to the viewport
const canAdd = canAddDisplaySetToViewport({
viewportId,
displaySetInstanceUID,
servicesManager,
});
if (!canAdd) {
return;
}
// Add the display set to the viewport
const updatedViewports = hangingProtocolService.getViewportsRequireUpdate(
viewportId,
displaySetInstanceUID
);
// Configure each viewport for layer addition
updatedViewports.forEach(viewport => {
configureViewportForLayerAddition({
viewport,
displaySetInstanceUID,
currentDisplaySetUIDs,
servicesManager,
});
});
// Update position presentation
commandsManager.runCommand('updateStoredPositionPresentation', {
viewportId,
displaySetInstanceUIDs: updatedViewports[0].displaySetInstanceUIDs,
});
// Run command to update viewports
commandsManager.run('setDisplaySetsForViewports', {
viewportsToUpdate: updatedViewports,
});
},
/**
* Removes a display set layer from the specified viewport
*
* @param options.viewportId - The ID of the viewport to remove the layer from
* @param options.displaySetInstanceUID - The UID of the display set to remove
*/
removeDisplaySetLayer: ({ viewportId, displaySetInstanceUID }) => {
if (!viewportId || !displaySetInstanceUID) {
console.warn('Missing required parameters for removeDisplaySetLayer command');
return;
}
const {
displaySetService,
viewportGridService,
hangingProtocolService,
segmentationService,
} = servicesManager.services;
// Get the display set
const displaySet = displaySetService.getDisplaySetByUID(displaySetInstanceUID);
if (!displaySet) {
return;
}
// Get current display sets for the viewport
const currentDisplaySetUIDs = viewportGridService.getDisplaySetsUIDsForViewport(viewportId);
// If the display set is not in the viewport, no need to remove it
if (!currentDisplaySetUIDs.includes(displaySetInstanceUID)) {
return;
}
// Check if it's a segmentation and handle accordingly
const isSegmentation = DERIVED_OVERLAY_MODALITIES.includes(displaySet.Modality);
if (isSegmentation) {
segmentationService.removeSegmentationRepresentations(viewportId, {
segmentationId: displaySetInstanceUID,
});
}
const updatedViewports = hangingProtocolService.getViewportsRequireUpdate(
viewportId,
displaySetInstanceUID
);
// Configure each viewport for layer removal
updatedViewports.forEach(viewport => {
configureViewportForLayerRemoval({
viewport,
displaySetInstanceUID,
currentDisplaySetUIDs,
servicesManager,
});
});
// Update position presentation
commandsManager.runCommand('updateStoredPositionPresentation', {
viewportId,
displaySetInstanceUIDs: updatedViewports[0].displaySetInstanceUIDs,
});
// Update the viewports
commandsManager.run('setDisplaySetsForViewports', {
viewportsToUpdate: updatedViewports,
});
},
/**
* Runs a command in multi-monitor mode. No-op if not multi-monitor.
*/
@@ -644,6 +781,8 @@ const commandsModule = ({
openDICOMTagViewer: actions.openDICOMTagViewer,
updateViewportDisplaySet: actions.updateViewportDisplaySet,
scrollActiveThumbnailIntoView: actions.scrollActiveThumbnailIntoView,
addDisplaySetAsLayer: actions.addDisplaySetAsLayer,
removeDisplaySetLayer: actions.removeDisplaySetLayer,
};
return {
@@ -14,6 +14,26 @@ export default {
});
},
},
{
id: 'addAsLayer',
label: 'Add as Layer',
iconName: 'ViewportViews',
onClick: ({ commandsManager, displaySetInstanceUID, servicesManager }: withAppTypes) => {
const { viewportGridService } = servicesManager.services;
// Get the active viewport
const { activeViewportId } = viewportGridService.getState();
if (!activeViewportId) {
return;
}
// Use the new command to add the display set as a layer
commandsManager.runCommand('addDisplaySetAsLayer', {
viewportId: activeViewportId,
displaySetInstanceUID,
});
},
},
],
'studyBrowser.sortFunctions': [
{
@@ -1,4 +1,5 @@
import { utils, classes } from '@ohif/core';
import i18n from '@ohif/i18n';
import { id } from './id';
import getDisplaySetMessages from './getDisplaySetMessages';
import getDisplaySetsFromUnsupportedSeries from './getDisplaySetsFromUnsupportedSeries';
@@ -99,6 +100,11 @@ const makeDisplaySet = instances => {
averageSpacingBetweenFrames: averageSpacingBetweenFrames || null,
isDynamicVolume,
dynamicVolumeInfo,
supportsWindowLevel: true,
label:
instance.SeriesDescription ||
`${i18n.t('Series')} ${instance.SeriesNumber} - ${i18n.t(instance.Modality)}`,
FrameOfReferenceUID: instance.FrameOfReferenceUID,
});
// Sort the images in this series if needed
@@ -260,6 +266,7 @@ const sopClassUids = [
sopClassDictionary.LegacyConvertedEnhancedPETImageStorage,
sopClassDictionary.RTImageStorage,
sopClassDictionary.EnhancedUSVolumeStorage,
sopClassDictionary.RTDoseStorage,
];
function getSopClassHandlerModule(appContextParam) {
+1 -10
View File
@@ -1,15 +1,6 @@
import { CommandsManager, ExtensionManager } from '@ohif/core';
import LineChartViewport from './Components/LineChartViewport/index';
const getViewportModule = ({
servicesManager,
commandsManager,
extensionManager,
}: {
servicesManager: AppTypes.ServicesManager;
commandsManager: CommandsManager;
extensionManager: ExtensionManager;
}) => {
const getViewportModule = () => {
return [
{
name: 'chartViewport',
+33 -4
View File
@@ -22,11 +22,11 @@ export default function init({
cineService.EVENTS.CINE_STATE_CHANGED,
]);
// Add
DicomMetadataStore.subscribe(DicomMetadataStore.EVENTS.INSTANCES_ADDED, handlePETImageMetadata);
DicomMetadataStore.subscribe(DicomMetadataStore.EVENTS.INSTANCES_ADDED, handleScalingModules);
// If the metadata for PET has changed by the user (e.g. manually changing the PatientWeight)
// we need to recalculate the SUV Scaling Factors
DicomMetadataStore.subscribe(DicomMetadataStore.EVENTS.SERIES_UPDATED, handlePETImageMetadata);
DicomMetadataStore.subscribe(DicomMetadataStore.EVENTS.SERIES_UPDATED, handleScalingModules);
// Adds extra custom attributes for use by hanging protocols
registerHangingProtocolAttributes({ servicesManager });
@@ -71,7 +71,7 @@ export default function init({
});
}
const handlePETImageMetadata = ({ SeriesInstanceUID, StudyInstanceUID }) => {
const handleScalingModules = ({ SeriesInstanceUID, StudyInstanceUID }) => {
const { instances } = DicomMetadataStore.getSeries(StudyInstanceUID, SeriesInstanceUID);
if (!instances?.length) {
@@ -80,12 +80,41 @@ const handlePETImageMetadata = ({ SeriesInstanceUID, StudyInstanceUID }) => {
const modality = instances[0].Modality;
if (!modality || modality !== 'PT') {
const allowedModality = ['PT', 'RTDOSE'];
if (!allowedModality.includes(modality)) {
return;
}
const imageIds = instances.map(instance => instance.imageId);
const instanceMetadataArray = [];
if (modality === 'RTDOSE') {
const DoseGridScaling = instances[0].DoseGridScaling;
const DoseSummation = instances[0].DoseSummation;
const DoseType = instances[0].DoseType;
const DoseUnit = instances[0].DoseUnit;
const NumberOfFrames = instances[0].NumberOfFrames;
const imageId = imageIds[0];
// add scaling module to the metadata
// since RTDOSE is always a multiframe we should add the scaling module to each frame
for (let i = 0; i < NumberOfFrames; i++) {
const frameIndex = i + 1;
// Todo: we should support other things like wadouri, local etc
const newImageId = `${imageId.replace(/\/frames\/\d+$/, '')}/frames/${frameIndex}`;
metadataProvider.addCustomMetadata(newImageId, 'scalingModule', {
DoseGridScaling,
DoseSummation,
DoseType,
DoseUnit,
});
}
return;
}
// try except block to prevent errors when the metadata is not correct
try {
imageIds.forEach(imageId => {
@@ -0,0 +1,157 @@
export const DERIVED_OVERLAY_MODALITIES = ['SEG', 'RTSTRUCT'];
export const DEFAULT_COLORMAP = 'hsv';
export const DEFAULT_OPACITY = 0.9;
export const DEFAULT_OPACITY_PERCENT = DEFAULT_OPACITY * 100;
/**
* Get modality-specific color and opacity settings from the customization service
*/
export function getModalityOverlayColormap(customizationService, modality) {
const modalityOverlayDefaultColorMaps = customizationService?.getCustomization(
'cornerstone.modalityOverlayDefaultColorMaps'
) || { defaultSettings: {} };
return (
modalityOverlayDefaultColorMaps.defaultSettings[modality] || {
colormap: DEFAULT_COLORMAP,
opacity: DEFAULT_OPACITY,
}
);
}
/**
* Create display set options based on modality and opacity settings
*/
export function createColormapOverlayDisplaySetOptions(displaySet, opacity, customizationService) {
if (displaySet.Modality === 'SEG') {
return {};
}
const modalitySettings = getModalityOverlayColormap(customizationService, displaySet.Modality);
return {
colormap: {
name: modalitySettings.colormap || DEFAULT_COLORMAP,
opacity: opacity / 100, // Convert from percentage to 0-1 range
},
};
}
/**
* Configure viewport for adding a display set layer
*/
export function configureViewportForLayerAddition(params: {
viewport: any;
displaySetInstanceUID: string;
currentDisplaySetUIDs: string[];
servicesManager: AppTypes.ServicesManager;
}): any {
const { viewport, displaySetInstanceUID, currentDisplaySetUIDs, servicesManager } = params;
const { cornerstoneViewportService, displaySetService, customizationService } =
servicesManager.services;
const { viewportId } = viewport;
// Set the display set UIDs for the viewport
const allDisplaySetInstanceUIDs = [...currentDisplaySetUIDs, displaySetInstanceUID];
viewport.displaySetInstanceUIDs = allDisplaySetInstanceUIDs;
if (!viewport.viewportOptions) {
viewport.viewportOptions = {};
}
const requestedLayerDisplaySet = displaySetService.getDisplaySetByUID(displaySetInstanceUID);
if (!viewport.viewportOptions.orientation) {
viewport.viewportOptions.orientation = cornerstoneViewportService.getOrientation(viewportId);
}
// Do not force volume for SEG and RTSTRUCT if there is only one display set
if (requestedLayerDisplaySet.isOverlayDisplaySet && currentDisplaySetUIDs.length === 1) {
viewport.viewportOptions.viewportType = 'stack';
} else {
viewport.viewportOptions.viewportType = 'volume';
}
// create same amount of display set options as the number of display set UIDs
const displaySetOptions = allDisplaySetInstanceUIDs.map((uid, index) => {
if (index === 0) {
// no colormap for background
return {};
}
const displaySet = displaySetService.getDisplaySetByUID(uid);
return createColormapOverlayDisplaySetOptions(displaySet, 90, customizationService);
});
viewport.displaySetOptions = displaySetOptions;
return viewport;
}
/**
* Configure viewport for removing a display set layer
*/
export function configureViewportForLayerRemoval(params: {
viewport: any;
displaySetInstanceUID: string;
currentDisplaySetUIDs: string[];
servicesManager: AppTypes.ServicesManager;
}): any {
const { viewport, displaySetInstanceUID, currentDisplaySetUIDs, servicesManager } = params;
const { cornerstoneViewportService, displaySetService } = servicesManager.services;
const { viewportId } = viewport;
// Filter out the display set to remove
viewport.displaySetInstanceUIDs = currentDisplaySetUIDs.filter(
uid => uid !== displaySetInstanceUID
);
if (!viewport.viewportOptions) {
viewport.viewportOptions = {};
}
viewport.viewportOptions.viewportType = 'volume';
// orientation
if (!viewport.viewportOptions.orientation) {
viewport.viewportOptions.orientation = cornerstoneViewportService.getOrientation(viewportId);
}
// Recreate the display set options
viewport.displaySetOptions = viewport.displaySetInstanceUIDs.map(() => {
// For simplicity, we're returning empty options for now
// In a more complete implementation, we would need to preserve existing display set options
return {};
});
return viewport;
}
/**
* Check if a display set can be added as a layer to the specified viewport
*/
export function canAddDisplaySetToViewport(params: {
viewportId: string;
displaySetInstanceUID: string;
servicesManager: AppTypes.ServicesManager;
}): boolean {
const { viewportId, displaySetInstanceUID, servicesManager } = params;
const { displaySetService, viewportGridService } = servicesManager.services;
// Check if the display set exists
const displaySet = displaySetService.getDisplaySetByUID(displaySetInstanceUID);
if (!displaySet) {
return false;
}
// Get current display sets in the viewport
const currentDisplaySetUIDs = viewportGridService.getDisplaySetsUIDsForViewport(viewportId);
// Check if the display set is already in the viewport
if (currentDisplaySetUIDs.includes(displaySetInstanceUID)) {
return false;
}
return true;
}