OHIF with Cornerstone3D 2.0 (#4347)

This commit is contained in:
Alireza authored and GitHub committed 2024-11-06 07:15:27 -05:00
1 parent 8981a61b8c
commit 7ce7127d7c
803 files changed
+32760 -15893

No files matched your search

@@ -48,6 +48,17 @@ export default class ColorbarService extends PubSubService {
super(ColorbarService.EVENTS);
}
/**
* Gets the volume ID for a given identifier by searching through the viewport's volume IDs.
* @param viewport - The viewport instance to search volumes in
* @param searchId - The identifier to search for within volume IDs
* @returns The matching volume ID if found, null otherwise
*/
private getVolumeIdForIdentifier(viewport, searchId: string): string | null {
const volumeIds = viewport.getAllVolumeIds?.() || [];
return volumeIds.length > 0 ? volumeIds.find(id => id.includes(searchId)) || null : null;
}
/**
* Adds a colorbar to a specific viewport identified by `viewportId`, using the provided `displaySetInstanceUIDs` and `options`.
* This method sets up the colorbar, associates it with the viewport, and applies initial configurations based on the provided options.
@@ -59,8 +70,18 @@ export default class ColorbarService extends PubSubService {
public addColorbar(viewportId, displaySetInstanceUIDs, options = {} as ColorbarOptions) {
const renderingEngine = getRenderingEngine(RENDERING_ENGINE_ID);
const viewport = renderingEngine.getViewport(viewportId);
if (!viewport) {
return;
}
const { element } = viewport;
const actorEntries = viewport.getActors();
if (!actorEntries || actorEntries.length === 0) {
return;
}
const { position, width: thickness, activeColormapName, colormaps } = options;
const numContainers = displaySetInstanceUIDs.length;
@@ -74,11 +95,9 @@ export default class ColorbarService extends PubSubService {
);
displaySetInstanceUIDs.forEach((displaySetInstanceUID, index) => {
const actorEntry = actorEntries.find(entry => entry.uid.includes(displaySetInstanceUID));
const volumeId = actorEntry?.uid;
const volumeId = this.getVolumeIdForIdentifier(viewport, displaySetInstanceUID);
const properties = viewport?.getProperties(volumeId);
const colormap = properties?.colormap;
// if there's an initial colormap set, and no colormap on the viewport, set it
if (activeColormapName && !colormap) {
this.setViewportColormap(
viewportId,
@@ -196,9 +215,8 @@ export default class ColorbarService extends PubSubService {
return;
}
const setViewportProperties = (viewport, uid) => {
const actorEntry = actorEntries.find(entry => entry.uid.includes(uid));
const { actor: volumeActor, uid: volumeId } = actorEntry;
viewport.setProperties({ colormap, volumeActor }, volumeId);
const volumeId = this.getVolumeIdForIdentifier(viewport, uid);
viewport.setProperties({ colormap }, volumeId);
};
if (viewport instanceof StackViewport) {
@@ -1,5 +1,5 @@
import { Types } from '@ohif/core';
import { cache as cs3DCache, Enums, volumeLoader, utilities as utils } from '@cornerstonejs/core';
import { cache as cs3DCache, Enums, volumeLoader } from '@cornerstonejs/core';
import getCornerstoneViewportType from '../../utils/getCornerstoneViewportType';
import { StackViewportData, VolumeViewportData } from '../../types/CornerstoneCacheService';
@@ -33,27 +33,11 @@ class CornerstoneCacheService {
public async createViewportData(
displaySets: Types.DisplaySet[],
viewportOptions: Record<string, unknown>,
viewportOptions: AppTypes.ViewportGrid.GridViewportOptions,
dataSource: unknown,
initialImageIndex?: number
): Promise<StackViewportData | VolumeViewportData> {
let viewportType = viewportOptions.viewportType as string;
// Todo: Since Cornerstone 3D currently doesn't support segmentation
// on stack viewport, we should check if whether the the displaySets
// that are about to be displayed are referenced in a segmentation
// as a reference volume, if so, we should hang a volume viewport
// instead of a stack viewport
if (this._shouldRenderSegmentation(displaySets)) {
// if the viewport type is volume 3D, we should let it be as it is
// Todo: in future here we should kick start the conversion of the
// segmentation to closed surface
viewportType =
viewportType === Enums.ViewportType.VOLUME_3D ? Enums.ViewportType.VOLUME_3D : 'volume';
// update viewportOptions to reflect the new viewport type
viewportOptions.viewportType = viewportType;
}
const viewportType = viewportOptions.viewportType as string;
const cs3DViewportType = getCornerstoneViewportType(viewportType, displaySets);
let viewportData: StackViewportData | VolumeViewportData;
@@ -86,7 +70,7 @@ class CornerstoneCacheService {
}
public async invalidateViewportData(
viewportData: VolumeViewportData,
viewportData: VolumeViewportData | StackViewportData,
invalidatedDisplaySetInstanceUID: string,
dataSource,
displaySetService
@@ -176,9 +160,6 @@ class CornerstoneCacheService {
): Promise<StackViewportData> {
const { uiNotificationService } = this.servicesManager.services;
const overlayDisplaySets = displaySets.filter(ds => ds.isOverlayDisplaySet);
const nonOverlayDisplaySets = displaySets.filter(ds => !ds.isOverlayDisplaySet);
// load overlays if they are not loaded
for (const overlayDisplaySet of overlayDisplaySets) {
if (overlayDisplaySet.load && overlayDisplaySet.load instanceof Function) {
const { userAuthenticationService } = this.servicesManager.services;
@@ -187,7 +168,7 @@ class CornerstoneCacheService {
await overlayDisplaySet.load({ headers });
} catch (e) {
uiNotificationService.show({
title: 'Error loading overlayDisplaySet',
title: 'Error loading displaySet',
message: e.message,
type: 'error',
});
@@ -196,43 +177,43 @@ class CornerstoneCacheService {
}
}
const displaySet = nonOverlayDisplaySets[0];
if (displaySet.load && displaySet.load instanceof Function) {
const { userAuthenticationService } = this.servicesManager.services;
const headers = userAuthenticationService.getAuthorizationHeader();
try {
await displaySet.load({ headers });
} catch (e) {
uiNotificationService.show({
title: 'Error loading displaySet',
message: e.message,
type: 'error',
});
console.error(e);
}
}
let stackImageIds = this.stackImageIds.get(displaySet.displaySetInstanceUID);
if (!stackImageIds) {
stackImageIds = this._getCornerstoneStackImageIds(displaySet, dataSource);
this.stackImageIds.set(displaySet.displaySetInstanceUID, stackImageIds);
}
// Ensuring the first non-overlay `displaySet` is always the primary one
const StackViewportData = [displaySet, ...overlayDisplaySets].map(ds => {
const { displaySetInstanceUID, StudyInstanceUID, isCompositeStack } = ds;
const StackViewportData = [];
for (const displaySet of displaySets) {
const { displaySetInstanceUID, StudyInstanceUID, isCompositeStack } = displaySet;
return {
if (displaySet.load && displaySet.load instanceof Function) {
const { userAuthenticationService } = this.servicesManager.services;
const headers = userAuthenticationService.getAuthorizationHeader();
try {
await displaySet.load({ headers });
} catch (e) {
uiNotificationService.show({
title: 'Error loading displaySet',
message: e.message,
type: 'error',
});
console.error(e);
}
}
let stackImageIds = this.stackImageIds.get(displaySet.displaySetInstanceUID);
if (!stackImageIds) {
stackImageIds = this._getCornerstoneStackImageIds(displaySet, dataSource);
// assign imageIds to the displaySet
displaySet.imageIds = stackImageIds;
this.stackImageIds.set(displaySet.displaySetInstanceUID, stackImageIds);
}
StackViewportData.push({
StudyInstanceUID,
displaySetInstanceUID,
isCompositeStack,
imageIds: stackImageIds,
initialImageIndex,
};
});
});
}
return {
viewportType,
@@ -253,6 +234,7 @@ class CornerstoneCacheService {
for (const displaySet of displaySets) {
const { Modality } = displaySet;
const isParametricMap = Modality === 'PMAP';
const isSeg = Modality === 'SEG';
// Don't create volumes for the displaySets that have custom load
// function (e.g., SEG, RT, since they rely on the reference volumes
@@ -295,7 +277,7 @@ class CornerstoneCacheService {
// Parametric maps do not have image ids but they already have volume data
// therefore a new volume should not be created.
if (!isParametricMap && (!volumeImageIds || !volume)) {
if (!isParametricMap && !isSeg && (!volumeImageIds || !volume)) {
volumeImageIds = this._getCornerstoneVolumeImageIds(displaySet, dataSource);
volume = await volumeLoader.createAndCacheVolume(volumeId, {
@@ -303,6 +285,9 @@ class CornerstoneCacheService {
});
this.volumeImageIds.set(displaySet.displaySetInstanceUID, volumeImageIds);
// Add imageIds to the displaySet for volumes
displaySet.imageIds = volumeImageIds;
}
volumeData.push({
@@ -321,39 +306,15 @@ class CornerstoneCacheService {
};
}
private _shouldRenderSegmentation(displaySets) {
const { segmentationService, displaySetService } = this.servicesManager.services;
const viewportDisplaySetInstanceUIDs = displaySets.map(
({ displaySetInstanceUID }) => displaySetInstanceUID
);
// check inside segmentations if any of them are referencing the displaySets
// that are about to be displayed
const segmentations = segmentationService.getSegmentations();
for (const segmentation of segmentations) {
const segDisplaySetInstanceUID = segmentation.displaySetInstanceUID;
const segDisplaySet = displaySetService.getDisplaySetByUID(segDisplaySetInstanceUID);
const instance = segDisplaySet.instances?.[0] || segDisplaySet.instance;
const shouldDisplaySeg = segmentationService.shouldRenderSegmentation(
viewportDisplaySetInstanceUIDs,
instance?.FrameOfReferenceUID || segDisplaySet.FrameOfReferenceUID
);
if (shouldDisplaySeg) {
return true;
}
}
}
private _getCornerstoneStackImageIds(displaySet, dataSource): string[] {
return dataSource.getImageIdsForDisplaySet(displaySet);
}
private _getCornerstoneVolumeImageIds(displaySet, dataSource): string[] {
if (displaySet.imageIds) {
return displaySet.imageIds;
}
const stackImageIds = this._getCornerstoneStackImageIds(displaySet, dataSource);
return stackImageIds;
File diff suppressed because it is too large. Load diff
@@ -1,76 +0,0 @@
import { Enums as csToolsEnums, Types as cstTypes } from '@cornerstonejs/tools';
import { Types } from '@cornerstonejs/core';
type SegmentationConfig = cstTypes.LabelmapTypes.LabelmapConfig & {
renderInactiveSegmentations: boolean;
brushSize: number;
brushThresholdGate: number;
};
type Segment = {
// the label for the segment
label: string;
// the index of the segment in the segmentation
segmentIndex: number;
// the color of the segment
color: Types.Point3;
// the opacity of the segment
opacity: number;
// whether the segment is visible
isVisible: boolean;
// whether the segment is locked
isLocked: boolean;
// display texts
displayText?: string[];
// The name of algorithm used to generate the segment. (0062,0009)
algorithmName?: string;
// Type of algorithm used to generate the segment. (0062,0008)
algorithmType?: string;
};
type Segmentation = {
// active segment index is the index of the segment that is currently being edited.
activeSegmentIndex: number;
// colorLUTIndex is the index of the color LUT that is currently being used.
colorLUTIndex: number;
// if segmentation contains any data (often calculated from labelmap)
cachedStats: Record<string, any>;
// displaySetInstanceUID
displaySetInstanceUID: string;
// displayText is the text that is displayed on the segmentation panel (often derived from the data)
displayText?: string[];
// the id of the segmentation
id: string;
// if the segmentation is the active segmentation being used in the viewer
isActive: boolean;
// if the segmentation is visible in the viewer
isVisible: boolean;
// the frame of reference UID of the segmentation
FrameOfReferenceUID: string;
// the label of the segmentation
label: string;
// the number of segments in the segmentation
segmentCount: number;
// the array of segments with their details, [null, segment1, segment2, ...]
segments: Array<Segment>;
// the set of segments that are locked
segmentsLocked: Array<number>;
// whether the segmentation is hydrated or not (non-hydrated SEG -> temporary segmentation for display in SEG Viewport
// but hydrated SEG -> segmentation that is persisted in the store)
hydrated: boolean;
// the type of the segmentation (e.g., Labelmap etc.)
type: csToolsEnums.SegmentationRepresentations;
// the segmentation representation data
representationData: SegmentationRepresentationData;
};
type LabelmapSegmentationData = {
volumeId: string;
referencedVolumeId?: string;
};
type SegmentationRepresentationData = {
LABELMAP?: LabelmapSegmentationData;
};
export type { SegmentationConfig, Segment, Segmentation };
@@ -2,6 +2,7 @@ import { synchronizers, SynchronizerManager, Synchronizer } from '@cornerstonejs
import { getRenderingEngines, utilities } from '@cornerstonejs/core';
import { pubSubServiceInterface, Types } from '@ohif/core';
import createHydrateSegmentationSynchronizer from './createHydrateSegmentationSynchronizer';
const EVENTS = {
TOOL_GROUP_CREATED: 'event::cornerstone::syncgroupservice:toolgroupcreated',
@@ -27,6 +28,7 @@ const VOI = 'voi';
const ZOOMPAN = 'zoompan';
const STACKIMAGE = 'stackimage';
const IMAGE_SLICE = 'imageslice';
const HYDRATE_SEG = 'hydrateseg';
const asSyncGroup = (syncGroup: string | SyncGroup): SyncGroup =>
typeof syncGroup === 'string' ? { type: syncGroup } : syncGroup;
@@ -51,6 +53,7 @@ export default class SyncGroupService {
// handles both stack and volume viewports
[STACKIMAGE]: synchronizers.createImageSliceSynchronizer,
[IMAGE_SLICE]: synchronizers.createImageSliceSynchronizer,
[HYDRATE_SEG]: createHydrateSegmentationSynchronizer,
};
synchronizersByType: { [key: string]: Synchronizer[] } = {};
@@ -66,18 +69,18 @@ export default class SyncGroupService {
private _createSynchronizer(type: string, id: string, options): Synchronizer | undefined {
// Initialize if not already done
this.synchronizersByType[type] = this.synchronizersByType[type] || [];
const syncCreator = this.synchronizerCreators[type.toLowerCase()];
if (syncCreator) {
const synchronizer = syncCreator(id, options);
// Pass the servicesManager along with other parameters
const synchronizer = syncCreator(id, { ...options, servicesManager: this.servicesManager });
if (synchronizer) {
this.synchronizersByType[type].push(synchronizer);
return synchronizer;
}
} else {
console.warn(`Unknown synchronizer type: ${type}, id: ${id}`);
console.debug(`Unknown synchronizer type: ${type}, id: ${id}`);
}
}
@@ -0,0 +1,79 @@
import { Types, getEnabledElementByViewportId } from '@cornerstonejs/core';
import {
SynchronizerManager,
Synchronizer,
Enums,
Types as ToolsTypes,
} from '@cornerstonejs/tools';
const { createSynchronizer } = SynchronizerManager;
const { SEGMENTATION_REPRESENTATION_ADDED } = Enums.Events;
export default function createHydrateSegmentationSynchronizer(
synchronizerName: string,
{ servicesManager, ...options }: { servicesManager: AppTypes.ServicesManager; options }
): Synchronizer {
const stackImageSynchronizer = createSynchronizer(
synchronizerName,
SEGMENTATION_REPRESENTATION_ADDED,
(synchronizerInstance, sourceViewport, targetViewport, sourceEvent) =>
segmentationRepresentationModifiedCallback(
synchronizerInstance,
sourceViewport,
targetViewport,
sourceEvent,
{ servicesManager, options }
),
{
eventSource: 'eventTarget',
}
);
return stackImageSynchronizer;
}
const segmentationRepresentationModifiedCallback = async (
synchronizerInstance: Synchronizer,
sourceViewport: Types.IViewportId,
targetViewport: Types.IViewportId,
sourceEvent: Event,
{ servicesManager, options }: { servicesManager: AppTypes.ServicesManager; options: unknown }
) => {
const event = sourceEvent as ToolsTypes.EventTypes.SegmentationRepresentationModifiedEventType;
const { segmentationId, viewportId } = event.detail;
const { segmentationService, hangingProtocolService } = servicesManager.services;
const targetViewportId = targetViewport.viewportId;
const { viewport } = getEnabledElementByViewportId(targetViewportId);
const targetFrameOfReferenceUID = viewport.getFrameOfReferenceUID();
if (!targetFrameOfReferenceUID) {
console.debug('No frame of reference UID found for the target viewport');
return;
}
const targetViewportRepresentation = segmentationService.getSegmentationRepresentations(
targetViewportId,
{ segmentationId }
);
if (targetViewportRepresentation.length > 0) {
return;
}
// whatever type the source viewport has, we need to add that to the target viewport
const sourceViewportRepresentation = segmentationService.getSegmentationRepresentations(
sourceViewport.viewportId,
{ segmentationId }
);
const type = sourceViewportRepresentation[0].type;
await segmentationService.addSegmentationRepresentation(targetViewportId, {
segmentationId,
type,
});
};
@@ -7,7 +7,7 @@ export type ActionComponentInfo = {
id: string;
component: ReactNode;
location: ViewportActionCornersLocations;
indexPriority: number;
indexPriority?: number;
};
class ViewportActionCornersService extends PubSubService {
@@ -33,21 +33,21 @@ class ViewportActionCornersService extends PubSubService {
public setServiceImplementation({
getState: getStateImplementation,
setComponent: setComponentImplementation,
setComponents: setComponentsImplementation,
clear: clearImplementation,
addComponent: addComponentImplementation,
addComponents: addComponentsImplementation,
clear: clearComponentsImplementation,
}): void {
if (getStateImplementation) {
this.serviceImplementation._getState = getStateImplementation;
}
if (setComponentImplementation) {
this.serviceImplementation._setComponent = setComponentImplementation;
if (addComponentImplementation) {
this.serviceImplementation._addComponent = addComponentImplementation;
}
if (setComponentsImplementation) {
this.serviceImplementation._setComponents = setComponentsImplementation;
if (addComponentsImplementation) {
this.serviceImplementation._addComponents = addComponentsImplementation;
}
if (clearImplementation) {
this.serviceImplementation._clear = clearImplementation;
if (clearComponentsImplementation) {
this.serviceImplementation._clear = clearComponentsImplementation;
}
}
@@ -55,12 +55,12 @@ class ViewportActionCornersService extends PubSubService {
return this.serviceImplementation._getState();
}
public setComponent(component: ActionComponentInfo) {
this.serviceImplementation._setComponent(component);
public addComponent(component: ActionComponentInfo) {
this.serviceImplementation._addComponent(component);
}
public setComponents(components: Array<ActionComponentInfo>) {
this.serviceImplementation._setComponents(components);
public addComponents(components: Array<ActionComponentInfo>) {
this.serviceImplementation._addComponents(components);
}
public clear(viewportId: string) {
@@ -18,9 +18,20 @@ import { IViewportService } from './IViewportService';
import { RENDERING_ENGINE_ID } from './constants';
import ViewportInfo, { DisplaySetOptions, PublicViewportOptions } from './Viewport';
import { StackViewportData, VolumeViewportData } from '../../types/CornerstoneCacheService';
import { LutPresentation, PositionPresentation, Presentations } from '../../types/Presentation';
import {
LutPresentation,
PositionPresentation,
Presentations,
SegmentationPresentation,
SegmentationPresentationItem,
} from '../../types/Presentation';
import JumpPresets from '../../utils/JumpPresets';
import { ViewportProperties } from '@cornerstonejs/core/types';
import { useLutPresentationStore } from '../../stores/useLutPresentationStore';
import { usePositionPresentationStore } from '../../stores/usePositionPresentationStore';
import { useSynchronizersStore } from '../../stores/useSynchronizersStore';
import { useSegmentationPresentationStore } from '../../stores/useSegmentationPresentationStore';
const EVENTS = {
VIEWPORT_DATA_CHANGED: 'event::cornerstoneViewportService:viewportDataChanged',
@@ -176,11 +187,7 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
* @param presentations - The presentations to apply to the viewport.
* @param viewportInfo - Contains a view reference for immediate application
*/
public setPresentations(
viewportId: string,
presentations?: Presentations,
viewportInfo?: ViewportInfo
): void {
public setPresentations(viewportId: string, presentations: Presentations): void {
const viewport = this.getCornerstoneViewport(viewportId) as
| Types.IStackViewport
| Types.IVolumeViewport;
@@ -189,83 +196,113 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
return;
}
const { lutPresentation, positionPresentation } = presentations;
if (lutPresentation) {
const { presentation } = lutPresentation;
if (viewport instanceof BaseVolumeViewport) {
if (presentation instanceof Map) {
presentation.forEach((properties, volumeId) => {
viewport.setProperties(properties, volumeId);
});
} else {
viewport.setProperties(presentation);
}
} else {
viewport.setProperties(presentation);
}
const { lutPresentation, positionPresentation, segmentationPresentation } = presentations;
// Always set the segmentation presentation first, since there might be some
// lutpresentation states that need to be set on the segmentation
// Todo: i think we should even await this
this._setSegmentationPresentation(viewport, segmentationPresentation);
this._setLutPresentation(viewport, lutPresentation);
this._setPositionPresentation(viewport, { ...positionPresentation, viewportId });
}
/**
* Stores the presentation state for a given viewport inside the
* each store. This is used to persist the presentation state
* across different scenarios e.g., when the viewport is changing the
* display set, or when the viewport is moving to a different layout.
*
* @param viewportId The ID of the viewport.
*/
public storePresentation({ viewportId }) {
const presentationIds = this.getPresentationIds(viewportId);
const { syncGroupService } = this.servicesManager.services;
const synchronizers = syncGroupService.getSynchronizersForViewport(viewportId);
if (!presentationIds || Object.keys(presentationIds).length === 0) {
return null;
}
const viewRef = viewportInfo?.getViewReference() || positionPresentation?.viewReference;
if (viewRef) {
viewport.setViewReference(viewRef);
const { lutPresentationId, positionPresentationId, segmentationPresentationId } =
presentationIds;
const positionPresentation = this._getPositionPresentation(viewportId);
const lutPresentation = this._getLutPresentation(viewportId);
const segmentationPresentation = this._getSegmentationPresentation(viewportId);
const { setLutPresentation } = useLutPresentationStore.getState();
const { setPositionPresentation } = usePositionPresentationStore.getState();
const { setSynchronizers } = useSynchronizersStore.getState();
const { setSegmentationPresentation } = useSegmentationPresentationStore.getState();
if (lutPresentationId) {
setLutPresentation(lutPresentationId, lutPresentation);
}
if (positionPresentation?.position) {
viewport.setViewPresentation(positionPresentation.position);
if (positionPresentationId) {
setPositionPresentation(positionPresentationId, positionPresentation);
}
if (segmentationPresentationId) {
setSegmentationPresentation(segmentationPresentationId, segmentationPresentation);
}
if (synchronizers?.length) {
setSynchronizers(
viewportId,
synchronizers.map(synchronizer => ({
id: synchronizer.id,
sourceViewports: [...synchronizer.getSourceViewports()],
targetViewports: [...synchronizer.getTargetViewports()],
}))
);
}
}
/**
* Retrieves the position presentation information for a given viewport.
* @param viewportId The ID of the viewport.
* @returns The position presentation object containing various properties
* such as ID, viewport type, initial image index, view plane normal, view up, zoom, and pan.
* Retrieves the presentations for a given viewport.
* @param viewportId - The ID of the viewport.
* @returns The presentations for the viewport.
*/
public getPositionPresentation(viewportId: string): PositionPresentation {
public getPresentations(viewportId: string): Presentations {
const positionPresentation = this._getPositionPresentation(viewportId);
const lutPresentation = this._getLutPresentation(viewportId);
const segmentationPresentation = this._getSegmentationPresentation(viewportId);
return {
positionPresentation,
lutPresentation,
segmentationPresentation,
};
}
private getPresentationIds(viewportId: string): AppTypes.PresentationIds | null {
const viewportInfo = this.viewportsById.get(viewportId);
if (!viewportInfo) {
return;
return null;
}
const presentationIds = viewportInfo.getPresentationIds();
if (!presentationIds) {
return;
}
const { positionPresentationId } = presentationIds;
return viewportInfo.getPresentationIds();
}
private _getPositionPresentation(viewportId: string): PositionPresentation {
const csViewport = this.getCornerstoneViewport(viewportId);
if (!csViewport) {
return;
}
const viewportInfo = this.viewportsById.get(viewportId);
return {
id: positionPresentationId,
viewportType: viewportInfo.getViewportType(),
viewReference: csViewport instanceof VolumeViewport3D ? null : csViewport.getViewReference(),
position: csViewport.getViewPresentation({ pan: true, zoom: true }),
viewPresentation: csViewport.getViewPresentation({ pan: true, zoom: true }),
viewportId,
};
}
/**
* Retrieves the LUT (Lookup Table) presentation for a given viewport.
* @param viewportId The ID of the viewport.
* @returns The LUT presentation object, or undefined if the viewport does not exist.
*/
public getLutPresentation(viewportId: string): LutPresentation {
const viewportInfo = this.viewportsById.get(viewportId);
if (!viewportInfo) {
return;
}
const presentationIds = viewportInfo.getPresentationIds();
if (!presentationIds) {
return;
}
const { lutPresentationId } = presentationIds;
private _getLutPresentation(viewportId: string): LutPresentation {
const csViewport = this.getCornerstoneViewport(viewportId) as
| Types.IStackViewport
| Types.IVolumeViewport;
@@ -275,116 +312,39 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
}
const cleanProperties = properties => {
if (properties.isComputedVOI) {
delete properties.voiRange;
delete properties.VOILUTFunction;
if (properties?.isComputedVOI) {
delete properties?.voiRange;
delete properties?.VOILUTFunction;
}
return properties;
};
const presentation =
const properties =
csViewport instanceof BaseVolumeViewport
? new Map()
: cleanProperties(csViewport.getProperties());
if (presentation instanceof Map) {
csViewport.getActors().forEach(({ uid: volumeId }) => {
const properties = cleanProperties(csViewport.getProperties(volumeId));
presentation.set(volumeId, properties);
if (properties instanceof Map) {
const volumeIds = (csViewport as Types.IBaseVolumeViewport).getAllVolumeIds();
volumeIds?.forEach(volumeId => {
const csProps = cleanProperties(csViewport.getProperties(volumeId));
properties.set(volumeId, csProps);
});
}
return {
id: lutPresentationId,
viewportType: viewportInfo.getViewportType(),
presentation,
};
}
/**
* Retrieves the presentations for a given viewport.
* @param viewportId - The ID of the viewport.
* @returns The presentations for the viewport.
*/
public getPresentations(viewportId: string): Presentations {
const viewportInfo = this.viewportsById.get(viewportId);
if (!viewportInfo) {
return;
}
const positionPresentation = this.getPositionPresentation(viewportId);
const lutPresentation = this.getLutPresentation(viewportId);
return {
positionPresentation,
lutPresentation,
viewportType: viewportInfo.getViewportType(),
properties,
};
}
/**
* Stores the presentation state for a given viewport inside the
* stateSyncService. This is used to persist the presentation state
* across different scenarios e.g., when the viewport is changing the
* display set, or when the viewport is moving to a different layout.
*
* @param viewportId The ID of the viewport.
*/
public storePresentation({ viewportId }) {
let presentations = null as Presentations;
try {
presentations = this.getPresentations(viewportId);
if (!presentations?.positionPresentation && !presentations?.lutPresentation) {
return;
}
} catch (error) {
console.warn(error);
return;
}
private _getSegmentationPresentation(viewportId: string): SegmentationPresentation {
const { segmentationService } = this.servicesManager.services;
const { stateSyncService, syncGroupService } = this.servicesManager.services;
const synchronizers = syncGroupService.getSynchronizersForViewport(viewportId);
const { positionPresentationStore, synchronizersStore, lutPresentationStore } =
stateSyncService.getState();
const { lutPresentation, positionPresentation } = presentations;
const { id: positionPresentationId } = positionPresentation;
const { id: lutPresentationId } = lutPresentation;
const updateStore = (store, id, value) => ({ ...store, [id]: value });
const newState = {} as { [key: string]: any };
if (lutPresentationId) {
newState.lutPresentationStore = updateStore(
lutPresentationStore,
lutPresentationId,
lutPresentation
);
}
if (positionPresentationId) {
newState.positionPresentationStore = updateStore(
positionPresentationStore,
positionPresentationId,
positionPresentation
);
}
if (synchronizers?.length) {
newState.synchronizersStore = updateStore(
synchronizersStore,
viewportId,
synchronizers.map(synchronizer => ({
id: synchronizer.id,
sourceViewports: [...synchronizer.getSourceViewports()],
targetViewports: [...synchronizer.getTargetViewports()],
}))
);
}
stateSyncService.store(newState);
const presentation = segmentationService.getPresentation(viewportId);
return presentation;
}
/**
@@ -423,6 +383,11 @@ 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');
}
@@ -431,8 +396,7 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
// since those are the newly set ones, we set them here so that it handles defaults
const displaySetOptions = viewportInfo.setPublicDisplaySetOptions(publicDisplaySetOptions);
// Specify an over-ride for the viewport type, even though it is in the public
// viewport options, because the one in the public viewport options is a suggestion
// for initial view, whereas the one in viewportData is a requirement based on the
// viewport options, because the one in the viewportData is a requirement based on the
// type of data being displayed.
const viewportOptions = viewportInfo.setPublicViewportOptions(
publicViewportOptions,
@@ -647,13 +611,21 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
properties.colormap = colormap ?? properties.colormap;
}
this._handleOverlays(viewport);
viewport.element.addEventListener(csEnums.Events.VIEWPORT_NEW_IMAGE_SET, evt => {
const { element } = evt.detail;
if (!imageIds?.length) {
return;
}
if (element !== viewport.element) {
return;
}
return viewport.setStack(imageIds, initialImageIndexToUse).then(() => {
csToolsUtils.stackContextPrefetch.enable(element);
});
let imageIdsToSet = imageIds;
const res = this._processExtraDisplaySetsForViewport(viewport);
imageIdsToSet = res?.imageIds ?? imageIdsToSet;
return viewport.setStack(imageIdsToSet, initialImageIndexToUse).then(() => {
viewport.setProperties({ ...properties });
this.setPresentations(viewport.id, presentations, viewportInfo);
if (displayArea) {
@@ -702,7 +674,7 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
const lastSliceIndex = numberOfSlices - 1;
if (imageIndex !== undefined) {
return csToolsUtils.clip(imageIndex, 0, lastSliceIndex);
return csUtils.clip(imageIndex, 0, lastSliceIndex);
}
if (preset === JumpPresets.First) {
@@ -764,7 +736,6 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
const displaySetOptions = displaySetOptionsArray[index];
const { volumeId } = volume;
volumeInputArray.push({
imageIds,
volumeId,
@@ -775,8 +746,7 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
this.viewportsDisplaySets.set(viewport.id, displaySetInstanceUIDs);
const volumesNotLoaded = volumeToLoad.filter(volume => !volume.loadStatus.loaded);
const volumesNotLoaded = volumeToLoad.filter(volume => !volume.loadStatus?.loaded);
if (volumesNotLoaded.length) {
if (hangingProtocolService.getShouldPerformCustomImageLoad()) {
// delegate the volume loading to the hanging protocol service if it has a custom image load strategy
@@ -787,7 +757,7 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
}
volumesNotLoaded.forEach(volume => {
if (!volume.loadStatus.loading) {
if (!volume.loadStatus?.loading && volume.load instanceof Function) {
volume.load();
}
});
@@ -798,8 +768,7 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
}
public async setVolumesForViewport(viewport, volumeInputArray, presentations) {
const { displaySetService, toolGroupService, viewportGridService } =
this.servicesManager.services;
const { displaySetService, viewportGridService } = this.servicesManager.services;
const viewportInfo = this.getViewportInfo(viewport.id);
const displaySetOptions = viewportInfo.getDisplaySetOptions();
@@ -811,7 +780,7 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
const { volumeId } = volumeInput;
const displaySetOption = displaySetOptions[index];
const { voi, voiInverted, colormap, displayPreset } = displaySetOption;
const properties = {};
const properties = {} as ViewportProperties;
if (voi && (voi.windowWidth || voi.windowCenter)) {
const { lower, upper } = csUtils.windowLevel.toLowHighRange(
@@ -836,22 +805,21 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
return { properties, volumeId };
});
// For SEG and RT viewports
this._processExtraDisplaySetsForViewport(viewport);
await viewport.setVolumes(volumeInputArray);
volumesProperties.forEach(({ properties, volumeId }) => {
viewport.setProperties(properties, volumeId);
});
this.setPresentations(viewport.id, presentations, viewportInfo);
this._handleOverlays(viewport);
const toolGroup = toolGroupService.getToolGroupForViewport(viewport.id);
csToolsUtils.segmentation.triggerSegmentationRender(toolGroup.id);
const imageIndex = this._getInitialImageIndexForViewport(viewportInfo);
if (imageIndex !== undefined) {
csToolsUtils.jumpToSlice(viewport.element, {
csUtils.jumpToSlice(viewport.element, {
imageIndex,
});
}
@@ -863,7 +831,9 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
});
}
private _handleOverlays(viewport: Types.IStackViewport | Types.IVolumeViewport) {
private _processExtraDisplaySetsForViewport(
viewport: Types.IStackViewport | Types.IVolumeViewport
) {
const { displaySetService } = this.servicesManager.services;
// load any secondary displaySets
@@ -877,104 +847,41 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
displaySet?.isOverlayDisplaySet && ['SEG', 'RTSTRUCT'].includes(displaySet.Modality)
);
if (segOrRTSOverlayDisplaySet) {
this.addOverlayRepresentationForDisplaySet(segOrRTSOverlayDisplaySet, viewport);
} else {
// If the displaySet is not a SEG displaySet we assume it is a primary displaySet
// and we can look into hydrated segmentations to check if any of them are
// associated with the primary displaySet
// get segmentations only returns the hydrated segmentations
this._addSegmentationRepresentationToToolGroupIfNecessary(displaySetInstanceUIDs, viewport);
// 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) {
return;
}
const referenceDisplaySet = displaySetService.getDisplaySetByUID(
segOrRTSOverlayDisplaySet.referencedDisplaySetInstanceUID
);
const imageIds = referenceDisplaySet.images.map(image => image.imageId);
this.addOverlayRepresentationForDisplaySet(segOrRTSOverlayDisplaySet, viewport);
return { imageIds };
}
private _addSegmentationRepresentationToToolGroupIfNecessary(
displaySetInstanceUIDs: string[],
viewport: any
private addOverlayRepresentationForDisplaySet(
displaySet: OhifTypes.DisplaySet,
viewport: Types.IViewport
) {
const { segmentationService, toolGroupService } = this.servicesManager.services;
const toolGroup = toolGroupService.getToolGroupForViewport(viewport.id);
// this only returns hydrated segmentations
const segmentations = segmentationService.getSegmentations();
for (const segmentation of segmentations) {
const toolGroupSegmentationRepresentations =
segmentationService.getSegmentationRepresentationsForToolGroup(toolGroup.id) || [];
// if there is already a segmentation representation for this segmentation
// for this toolGroup, don't bother at all
const isSegmentationInToolGroup = toolGroupSegmentationRepresentations.find(
representation => representation.segmentationId === segmentation.id
);
if (isSegmentationInToolGroup) {
continue;
}
// otherwise, check if the hydrated segmentations are in the same FrameOfReferenceUID
// as the primary displaySet, if so add the representation (since it was not there)
const { id: segDisplaySetInstanceUID } = segmentation;
let segFrameOfReferenceUID = this._getFrameOfReferenceUID(segDisplaySetInstanceUID);
if (!segFrameOfReferenceUID) {
// if the segmentation displaySet does not have a FrameOfReferenceUID, we might check the
// segmentation itself maybe it has a FrameOfReferenceUID
const { FrameOfReferenceUID } = segmentation;
if (FrameOfReferenceUID) {
segFrameOfReferenceUID = FrameOfReferenceUID;
}
}
if (!segFrameOfReferenceUID) {
return;
}
let shouldDisplaySeg = false;
for (const displaySetInstanceUID of displaySetInstanceUIDs) {
const primaryFrameOfReferenceUID = this._getFrameOfReferenceUID(displaySetInstanceUID);
if (segFrameOfReferenceUID === primaryFrameOfReferenceUID) {
shouldDisplaySeg = true;
break;
}
}
if (!shouldDisplaySeg) {
return;
}
segmentationService.addSegmentationRepresentationToToolGroup(
toolGroup.id,
segmentation.id,
false, // already hydrated,
segmentation.type
);
}
}
private addOverlayRepresentationForDisplaySet(displaySet: any, viewport: any) {
const { segmentationService, toolGroupService } = this.servicesManager.services;
const { referencedVolumeId } = displaySet;
const { segmentationService } = this.servicesManager.services;
const segmentationId = displaySet.displaySetInstanceUID;
const toolGroup = toolGroupService.getToolGroupForViewport(viewport.id);
const representationType =
referencedVolumeId && cache.getVolume(referencedVolumeId) !== undefined
displaySet.Modality === 'SEG'
? csToolsEnums.SegmentationRepresentations.Labelmap
: csToolsEnums.SegmentationRepresentations.Contour;
segmentationService.addSegmentationRepresentationToToolGroup(
toolGroup.id,
segmentationService.addSegmentationRepresentation(viewport.id, {
segmentationId,
false,
representationType
);
type: representationType,
});
// store the segmentation presentation id in the viewport info
this.storePresentation({ viewportId: viewport.id });
}
// Todo: keepCamera is an interim solution until we have a better solution for
@@ -1032,7 +939,7 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
}
return this._setOtherViewport(
viewport as Types.IViewport,
viewport,
viewportData as StackViewportData,
viewportInfo,
presentations
@@ -1063,11 +970,11 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
// calculate the slab thickness based on the volume dimensions
const imageVolume = cache.getVolume(volumeId);
const { dimensions } = imageVolume;
const { dimensions, spacing } = imageVolume;
const slabThickness = Math.sqrt(
dimensions[0] * dimensions[0] +
dimensions[1] * dimensions[1] +
dimensions[2] * dimensions[2]
Math.pow(dimensions[0] * spacing[0], 2) +
Math.pow(dimensions[1] * spacing[1], 2) +
Math.pow(dimensions[2] * spacing[2], 2)
);
return slabThickness;
@@ -1128,9 +1035,9 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
const viewports = this.getRenderingEngine().getViewports();
// Store the current position presentations for each viewport.
viewports.forEach(({ id }) => {
const presentation = this.getPositionPresentation(id);
this.beforeResizePositionPresentations.set(id, presentation);
viewports.forEach(({ id: viewportId }) => {
const presentation = this._getPositionPresentation(viewportId);
this.beforeResizePositionPresentations.set(viewportId, presentation);
});
// Resize the rendering engine and render.
@@ -1159,6 +1066,65 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
this.gridResizeTimeOut = null;
}, this.gridResizeDelay);
}
private _setLutPresentation(
viewport: Types.IStackViewport | Types.IVolumeViewport,
lutPresentation: LutPresentation
): void {
if (!lutPresentation) {
return;
}
const { properties } = lutPresentation;
if (viewport instanceof BaseVolumeViewport) {
if (properties instanceof Map) {
properties.forEach((propertiesEntry, volumeId) => {
viewport.setProperties(propertiesEntry, volumeId);
});
} else {
viewport.setProperties(properties);
}
} else {
viewport.setProperties(properties);
}
}
private _setPositionPresentation(
viewport: Types.IStackViewport | Types.IVolumeViewport,
positionPresentation: PositionPresentation
): void {
const viewRef = positionPresentation?.viewReference;
if (viewRef) {
viewport.setViewReference(viewRef);
}
const viewPresentation = positionPresentation?.viewPresentation;
if (viewPresentation) {
viewport.setViewPresentation(viewPresentation);
}
}
private _setSegmentationPresentation(
viewport: Types.IStackViewport | Types.IVolumeViewport,
segmentationPresentation: SegmentationPresentation
): void {
if (!segmentationPresentation) {
return;
}
const { segmentationService } = this.servicesManager.services;
segmentationPresentation.forEach((presentation: SegmentationPresentationItem) => {
const { segmentationId, type, hydrated } = presentation;
if (hydrated) {
segmentationService.addSegmentationRepresentation(viewport.id, {
segmentationId,
type,
});
}
});
}
}
export default CornerstoneViewportService;
@@ -5,7 +5,6 @@ import {
VolumeViewport,
utilities,
} from '@cornerstonejs/core';
import { Types as CoreTypes } from '@ohif/core';
import { StackViewportData, VolumeViewportData } from '../../types/CornerstoneCacheService';
import getCornerstoneBlendMode from '../../utils/getCornerstoneBlendMode';
import getCornerstoneOrientation from '../../utils/getCornerstoneOrientation';
@@ -25,7 +24,7 @@ export type ViewportOptions = {
toolGroupId: string;
viewportId: string;
// Presentation ID to store/load presentation state from
presentationIds?: CoreTypes.PresentationIds;
presentationIds?: AppTypes.PresentationIds;
orientation?: Enums.OrientationAxis;
background?: Types.Point3;
displayArea?: Types.DisplayArea;
@@ -46,7 +45,7 @@ export type PublicViewportOptions = {
id?: string;
viewportType?: string;
toolGroupId?: string;
presentationIds?: CoreTypes.PresentationIds;
presentationIds?: string[];
viewportId?: string;
orientation?: Enums.OrientationAxis;
background?: Types.Point3;
@@ -283,7 +282,7 @@ class ViewportInfo {
return this.viewportOptions;
}
public getPresentationIds(): CoreTypes.PresentationIds {
public getPresentationIds(): AppTypes.PresentationIds | null {
const { presentationIds } = this.viewportOptions;
return presentationIds;
}