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

@@ -38,14 +38,16 @@ export default class ColorbarService extends PubSubService {
public static REGISTRATION = {
name: 'colorbarService',
create: () => {
return new ColorbarService();
create: ({ servicesManager }: OhifTypes.Extensions.ExtensionParams) => {
return new ColorbarService(servicesManager);
},
};
colorbars = {};
servicesManager: AppTypes.ServicesManager;
constructor() {
constructor(servicesManager: AppTypes.ServicesManager) {
super(ColorbarService.EVENTS);
this.servicesManager = servicesManager;
}
/**
@@ -68,6 +70,7 @@ export default class ColorbarService extends PubSubService {
* @param options Configuration options for the colorbar, including position, colormaps, active colormap name, ticks, and width.
*/
public addColorbar(viewportId, displaySetInstanceUIDs, options = {} as ColorbarOptions) {
const { displaySetService } = this.servicesManager.services;
const renderingEngine = getRenderingEngine(RENDERING_ENGINE_ID);
const viewport = renderingEngine.getViewport(viewportId);
@@ -95,6 +98,12 @@ export default class ColorbarService extends PubSubService {
);
displaySetInstanceUIDs.forEach((displaySetInstanceUID, index) => {
// don't show colorbar for overlay display sets (e.g. segmentation)
const displaySet = displaySetService.getDisplaySetByUID(displaySetInstanceUID);
if (displaySet.isOverlayDisplaySet) {
return;
}
const volumeId = this.getVolumeIdForIdentifier(viewport, displaySetInstanceUID);
const properties = viewport?.getProperties(volumeId);
const colormap = properties?.colormap;
@@ -123,9 +132,19 @@ export default class ColorbarService extends PubSubService {
volumeId: viewport instanceof VolumeViewport ? volumeId : undefined,
});
if (this.colorbars[viewportId]) {
this.colorbars[viewportId].push({ colorbar, container: colorbarContainer });
this.colorbars[viewportId].push({
colorbar,
container: colorbarContainer,
displaySetInstanceUID,
});
} else {
this.colorbars[viewportId] = [{ colorbar, container: colorbarContainer }];
this.colorbars[viewportId] = [
{
colorbar,
container: colorbarContainer,
displaySetInstanceUID,
},
];
}
});
@@ -136,24 +155,50 @@ export default class ColorbarService extends PubSubService {
}
/**
* Removes the colorbar associated with a given viewport ID. This involves cleaning up any created DOM elements and internal references.
* Removes a colorbar from a specific viewport. If displaySetInstanceUID is provided,
* only the colorbar associated with that specific displaySetInstanceUID will be removed.
* Otherwise, all colorbars for the given viewport will be removed.
*
* @param viewportId The identifier for the viewport from which the colorbar will be removed.
* @param displaySetInstanceUID Optional. The specific display set instance UID associated with the colorbar to remove.
*/
public removeColorbar(viewportId) {
public removeColorbar(viewportId, displaySetInstanceUID?: string) {
const colorbarInfo = this.colorbars[viewportId];
if (!colorbarInfo) {
return;
}
colorbarInfo.forEach(({ colorbar, container }) => {
container.parentNode.removeChild(container);
});
if (displaySetInstanceUID) {
// Find the index of the colorbar with the matching displaySetInstanceUID
const index = colorbarInfo.findIndex(
info => info.displaySetInstanceUID === displaySetInstanceUID
);
delete this.colorbars[viewportId];
if (index !== -1) {
// Remove only the specific colorbar container
const { container } = colorbarInfo[index];
container.parentNode.removeChild(container);
// Remove the colorbar from the array
colorbarInfo.splice(index, 1);
// If there are no more colorbars for this viewport, remove the entry
if (colorbarInfo.length === 0) {
delete this.colorbars[viewportId];
}
}
} else {
// Remove all colorbars for the viewport (original behavior)
colorbarInfo.forEach(({ container }) => {
container.parentNode.removeChild(container);
});
delete this.colorbars[viewportId];
}
this._broadcastEvent(ColorbarService.EVENTS.STATE_CHANGED, {
viewportId,
displaySetInstanceUID,
changeType: ChangeTypes.Removed,
});
}
@@ -3,8 +3,7 @@ import { cache as cs3DCache, Enums, volumeLoader } from '@cornerstonejs/core';
import getCornerstoneViewportType from '../../utils/getCornerstoneViewportType';
import { StackViewportData, VolumeViewportData } from '../../types/CornerstoneCacheService';
const VOLUME_LOADER_SCHEME = 'cornerstoneStreamingImageVolume';
import { VOLUME_LOADER_SCHEME } from '../../constants';
class CornerstoneCacheService {
static REGISTRATION = {
@@ -8,26 +8,29 @@
* and geometry Id
*/
export function mapROIContoursToRTStructData(structureSet: unknown, rtDisplaySetUID: unknown) {
return structureSet.ROIContours.map(({ contourPoints, ROINumber, ROIName, colorArray }) => {
const data = contourPoints.map(({ points, ...rest }) => {
const newPoints = points.map(({ x, y, z }) => {
return [x, y, z];
return structureSet.ROIContours.map(
({ contourPoints, ROINumber, ROIName, colorArray, ROIGroup }) => {
const data = contourPoints.map(({ points, ...rest }) => {
const newPoints = points.map(({ x, y, z }) => {
return [x, y, z];
});
return {
...rest,
points: newPoints,
};
});
const id = ROIName || ROINumber;
return {
...rest,
points: newPoints,
data,
id,
segmentIndex: ROINumber,
color: colorArray,
group: ROIGroup,
geometryId: `${rtDisplaySetUID}:${id}:segmentIndex-${ROINumber}`,
};
});
const id = ROIName || ROINumber;
return {
data,
id,
segmentIndex: ROINumber,
color: colorArray,
geometryId: `${rtDisplaySetUID}:${id}:segmentIndex-${ROINumber}`,
};
});
}
);
}
@@ -29,6 +29,7 @@ import { updateLabelmapSegmentationImageReferences } from '@cornerstonejs/tools/
import { triggerSegmentationRepresentationModified } from '@cornerstonejs/tools/segmentation/triggerSegmentationEvents';
import { convertStackToVolumeLabelmap } from '@cornerstonejs/tools/segmentation/helpers/convertStackToVolumeLabelmap';
import { getLabelmapImageIds } from '@cornerstonejs/tools/segmentation';
import { VOLUME_LOADER_SCHEME } from '../../constants';
const LABELMAP = csToolsEnums.SegmentationRepresentations.Labelmap;
const CONTOUR = csToolsEnums.SegmentationRepresentations.Contour;
@@ -80,8 +81,6 @@ const EVENTS = {
const VALUE_TYPES = {};
const VOLUME_LOADER_SCHEME = 'cornerstoneStreamingImageVolume';
class SegmentationService extends PubSubService {
static REGISTRATION = {
name: 'segmentationService',
@@ -615,7 +614,7 @@ class SegmentationService extends PubSubService {
// Process each segment similarly to the SEG function
for (const rtStructData of allRTStructData) {
const { data, id, color, segmentIndex, geometryId } = rtStructData;
const { data, id, color, segmentIndex, geometryId, group } = rtStructData;
// Add the color to the colorLUT array
colorLUT.push(color);
@@ -646,6 +645,7 @@ class SegmentationService extends PubSubService {
cachedStats: segmentsCachedStats,
locked: false,
active: false,
group,
};
// Broadcast segment loading progress
@@ -10,6 +10,11 @@ export type ActionComponentInfo = {
indexPriority?: number;
};
export type AlignAndSide = {
align: 'start' | 'end' | 'center';
side: 'top' | 'bottom' | 'left' | 'right';
};
class ViewportActionCornersService extends PubSubService {
public static readonly EVENTS = {};
public static readonly LOCATIONS = ViewportActionCornersLocations;
@@ -66,6 +71,22 @@ class ViewportActionCornersService extends PubSubService {
public clear(viewportId: string) {
this.serviceImplementation._clear(viewportId);
}
public getAlignAndSide(location: ViewportActionCornersLocations): AlignAndSide {
switch (location) {
case ViewportActionCornersLocations.topLeft:
return { align: 'start', side: 'bottom' };
case ViewportActionCornersLocations.topRight:
return { align: 'end', side: 'bottom' };
case ViewportActionCornersLocations.bottomLeft:
return { align: 'start', side: 'top' };
case ViewportActionCornersLocations.bottomRight:
return { align: 'end', side: 'top' };
default:
console.debug('Unknown location, defaulting to bottom-start');
return { align: 'start', side: 'bottom' };
}
}
}
export default ViewportActionCornersService;
@@ -32,6 +32,7 @@ import { useLutPresentationStore } from '../../stores/useLutPresentationStore';
import { usePositionPresentationStore } from '../../stores/usePositionPresentationStore';
import { useSynchronizersStore } from '../../stores/useSynchronizersStore';
import { useSegmentationPresentationStore } from '../../stores/useSegmentationPresentationStore';
import { VOLUME_LOADER_SCHEME } from '../../constants';
const EVENTS = {
VIEWPORT_DATA_CHANGED: 'event::cornerstoneViewportService:viewportDataChanged',
@@ -407,11 +408,6 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
// and we would lose the presentation.
this.storePresentation({ viewportId: viewportInfo.getViewportId() });
// Todo: i don't like this here, move it
this.servicesManager.services.segmentationService.clearSegmentationRepresentations(
viewportInfo.getViewportId()
);
if (!viewportInfo) {
throw new Error('element is not enabled for the given viewportId');
}
@@ -513,6 +509,11 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
return this.viewportsById.get(viewportId);
}
public getOrientation(viewportId: string): string {
const viewportInfo = this.getViewportInfo(viewportId);
return viewportInfo.getOrientation();
}
/**
* Looks through the viewports to see if the specified measurement can be
* displayed in one of the viewports. This function tries to get a "best fit"
@@ -761,8 +762,8 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
const { imageIds, displaySetInstanceUID } = data;
let volume = data.volume;
if (!volume) {
const displaySet = displaySetService.getDisplaySetByUID(displaySetInstanceUID);
const displaySet = displaySetService.getDisplaySetByUID(displaySetInstanceUID);
if (!volume && displaySet.images) {
volume = csToolsUtils.getOrCreateImageVolume(displaySet.images.map(image => image.imageId));
}
@@ -780,6 +781,8 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
volumeInputArray.push({
imageIds,
volumeId,
modality: displaySet.Modality,
displaySetInstanceUID,
blendMode: displaySetOptions.blendMode,
slabThickness: this._getSlabThickness(displaySetOptions, volumeId),
});
@@ -822,8 +825,15 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
const displaySetUIDs = viewportGridService.getDisplaySetsUIDsForViewport(viewport.id);
const displaySet = displaySetService.getDisplaySetByUID(displaySetUIDs[0]);
const displaySetModality = displaySet?.Modality;
// filter overlay display sets (e.g. segmentation) since they will get handled below via the segmentation service
const filteredVolumeInputArray = volumeInputArray.filter(volumeInput => {
const displaySet = displaySetService.getDisplaySetByUID(volumeInput.displaySetInstanceUID);
return !displaySet?.isOverlayDisplaySet;
});
// Todo: use presentations states
const volumesProperties = volumeInputArray.map((volumeInput, index) => {
const volumesProperties = filteredVolumeInputArray.map((volumeInput, index) => {
const { volumeId } = volumeInput;
const displaySetOption = displaySetOptions[index];
const { voi, voiInverted, colormap, displayPreset } = displaySetOption;
@@ -853,9 +863,29 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
});
// For SEG and RT viewports
const { addOverlayFn } = this._processExtraDisplaySetsForViewport(viewport) || {};
const { addOverlayFn, imageIds } = this._processExtraDisplaySetsForViewport(viewport) || {};
await viewport.setVolumes(volumeInputArray);
if (!filteredVolumeInputArray.length && addOverlayFn) {
// if there is no volume input array, and there is an addOverlayFn, means we need to take
// care of the background overlay display set first then the addOverlayFn will add the
// SEG displaySet
const sampleImageId = imageIds[0];
const backgroundDisplaySet = displaySetService.getDisplaySetsBy(
displaySet =>
!displaySet.isOverlayDisplaySet &&
displaySet.images.some(image => image.imageId === sampleImageId)
);
if (backgroundDisplaySet.length !== 1) {
throw new Error('Background display set not found');
}
await viewport.setVolumes([
{ volumeId: `${VOLUME_LOADER_SCHEME}:${backgroundDisplaySet[0].displaySetInstanceUID}` },
]);
} else {
await viewport.setVolumes(filteredVolumeInputArray);
}
if (addOverlayFn) {
addOverlayFn();
@@ -896,31 +926,30 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
// load any secondary displaySets
const displaySetInstanceUIDs = this.viewportsDisplaySets.get(viewport.id);
// Can be SEG or RTSTRUCT for now but not PMAP
const segOrRTSOverlayDisplaySet = displaySetInstanceUIDs
// Find overlay display sets (e.g. SEG, RTSTRUCT)
const overlayDisplaySet = displaySetInstanceUIDs
.map(displaySetService.getDisplaySetByUID)
.find(
displaySet =>
displaySet?.isOverlayDisplaySet && ['SEG', 'RTSTRUCT'].includes(displaySet.Modality)
);
.find(displaySet => displaySet?.isOverlayDisplaySet);
// if it is only the overlay displaySet, then we need to get the reference
// displaySet imageIds and set them as the imageIds for the viewport,
// here we can do some logic if the reference is missing
// then find the most similar match of displaySet instead
if (!segOrRTSOverlayDisplaySet) {
if (!overlayDisplaySet) {
return;
}
const referenceDisplaySet = displaySetService.getDisplaySetByUID(
segOrRTSOverlayDisplaySet.referencedDisplaySetInstanceUID
);
const imageIds = referenceDisplaySet.images.map(image => image.imageId);
let imageIds;
if (overlayDisplaySet.referencedDisplaySetInstanceUID) {
const referenceDisplaySet = displaySetService.getDisplaySetByUID(
overlayDisplaySet.referencedDisplaySetInstanceUID
);
imageIds = referenceDisplaySet.images.map(image => image.imageId);
}
return {
imageIds,
addOverlayFn: () =>
this.addOverlayRepresentationForDisplaySet(segOrRTSOverlayDisplaySet, viewport),
addOverlayFn: () => this.addOverlayRepresentationForDisplaySet(overlayDisplaySet, viewport),
};
}
@@ -316,6 +316,10 @@ class ViewportInfo {
return this.viewportOptions.orientation;
}
public setOrientation(orientation: Enums.OrientationAxis): void {
this.viewportOptions.orientation = orientation;
}
public getDisplayArea(): Types.DisplayArea {
return this.viewportOptions.displayArea;
}