ohif-viewer/extensions/cornerstone/src/services/SegmentationService/SegmentationService.ts
2025-03-27 15:52:22 -04:00

1834 lines
60 KiB
TypeScript

import {
cache,
Enums as csEnums,
eventTarget,
geometryLoader,
getEnabledElementByViewportId,
imageLoader,
Types as csTypes,
utilities as csUtils,
metaData,
} from '@cornerstonejs/core';
import {
Enums as csToolsEnums,
segmentation as cstSegmentation,
Types as cstTypes,
} from '@cornerstonejs/tools';
import { PubSubService, Types as OHIFTypes } from '@ohif/core';
import i18n from '@ohif/i18n';
import { easeInOutBell, easeInOutBellRelative } from '../../utils/transitions';
import { mapROIContoursToRTStructData } from './RTSTRUCT/mapROIContoursToRTStructData';
import { SegmentationRepresentations } from '@cornerstonejs/tools/enums';
import { addColorLUT } from '@cornerstonejs/tools/segmentation/addColorLUT';
import { getNextColorLUTIndex } from '@cornerstonejs/tools/segmentation/getNextColorLUTIndex';
import { Segment } from '@cornerstonejs/tools/types/SegmentationStateTypes';
import { ContourStyle, LabelmapStyle, SurfaceStyle } from '@cornerstonejs/tools/types';
import { ViewportType } from '@cornerstonejs/core/enums';
import { SegmentationPresentation, SegmentationPresentationItem } from '../../types/Presentation';
import { updateLabelmapSegmentationImageReferences } from '@cornerstonejs/tools/segmentation/updateLabelmapSegmentationImageReferences';
import { triggerSegmentationRepresentationModified } from '@cornerstonejs/tools/segmentation/triggerSegmentationEvents';
import { convertStackToVolumeLabelmap } from '@cornerstonejs/tools/segmentation/helpers/convertStackToVolumeLabelmap';
import { getLabelmapImageIds } from '@cornerstonejs/tools/segmentation';
const LABELMAP = csToolsEnums.SegmentationRepresentations.Labelmap;
const CONTOUR = csToolsEnums.SegmentationRepresentations.Contour;
export type SegmentRepresentation = {
segmentIndex: number;
color: csTypes.Color;
opacity: number;
visible: boolean;
};
export type SegmentationData = cstTypes.Segmentation;
export type SegmentationRepresentation = cstTypes.SegmentationRepresentation & {
viewportId: string;
id: string;
label: string;
styles: cstTypes.RepresentationStyle;
segments: {
[key: number]: SegmentRepresentation;
};
};
export type SegmentationInfo = {
segmentation: SegmentationData;
representation?: SegmentationRepresentation;
};
const EVENTS = {
SEGMENTATION_MODIFIED: 'event::segmentation_modified',
// fired when the segmentation is added
SEGMENTATION_ADDED: 'event::segmentation_added',
//
SEGMENTATION_DATA_MODIFIED: 'event::segmentation_data_modified',
// fired when the segmentation is removed
SEGMENTATION_REMOVED: 'event::segmentation_removed',
//
// fired when segmentation representation is added
SEGMENTATION_REPRESENTATION_MODIFIED: 'event::segmentation_representation_modified',
// fired when segmentation representation is removed
SEGMENTATION_REPRESENTATION_REMOVED: 'event::segmentation_representation_removed',
//
// LOADING EVENTS
// fired when the active segment is loaded in SEG or RTSTRUCT
SEGMENT_LOADING_COMPLETE: 'event::segment_loading_complete',
// loading completed for all segments
SEGMENTATION_LOADING_COMPLETE: 'event::segmentation_loading_complete',
};
const VALUE_TYPES = {};
const VOLUME_LOADER_SCHEME = 'cornerstoneStreamingImageVolume';
class SegmentationService extends PubSubService {
static REGISTRATION = {
name: 'segmentationService',
altName: 'SegmentationService',
create: ({ servicesManager }: OHIFTypes.Extensions.ExtensionParams): SegmentationService => {
return new SegmentationService({ servicesManager });
},
};
private _segmentationIdToColorLUTIndexMap: Map<string, number>;
private _segmentationGroupStatsMap: Map<string, any>;
readonly servicesManager: AppTypes.ServicesManager;
highlightIntervalId = null;
readonly EVENTS = EVENTS;
constructor({ servicesManager }) {
super(EVENTS);
this._segmentationIdToColorLUTIndexMap = new Map();
this.servicesManager = servicesManager;
this._segmentationGroupStatsMap = new Map();
}
public onModeEnter(): void {
this._initSegmentationService();
}
public onModeExit(): void {
this.destroy();
}
/**
* Retrieves a segmentation by its ID.
*
* @param segmentationId - The unique identifier of the segmentation to retrieve.
* @returns The segmentation object if found, or undefined if not found.
*
* @remarks
* This method directly accesses the cornerstone tools segmentation state to fetch
* the segmentation data. It's useful when you need to access specific properties
* or perform operations on a particular segmentation.
*/
public getSegmentation(segmentationId: string): cstTypes.Segmentation | undefined {
return cstSegmentation.state.getSegmentation(segmentationId);
}
/**
* Retrieves all segmentations from the cornerstone tools segmentation state.
*
* @returns An array of all segmentations currently stored in the state
*
* @remarks
* This is a convenience method that directly accesses the cornerstone tools
* segmentation state to get all available segmentations. It returns the raw
* segmentation objects without any additional processing or filtering.
*/
public getSegmentations(): cstTypes.Segmentation[] | [] {
return cstSegmentation.state.getSegmentations();
}
public getPresentation(viewportId: string): SegmentationPresentation {
const segmentationPresentations: SegmentationPresentation = [];
const segmentationsMap = new Map<string, SegmentationPresentationItem>();
const representations = this.getSegmentationRepresentations(viewportId);
for (const representation of representations) {
const { segmentationId } = representation;
if (!representation) {
continue;
}
const { type } = representation;
segmentationsMap.set(segmentationId, {
segmentationId,
type,
hydrated: true,
config: representation.config || {},
});
}
// Check inside the removedDisplaySetAndRepresentationMaps to see if any of the representations are not hydrated
// const hydrationMap = this._segmentationRepresentationHydrationMaps.get(presentationId);
// if (hydrationMap) {
// hydrationMap.forEach(rep => {
// segmentationsMap.set(rep.segmentationId, {
// segmentationId: rep.segmentationId,
// type: rep.type,
// hydrated: rep.hydrated,
// config: rep.config || {},
// });
// });
// }
// // Convert the Map to an array
segmentationPresentations.push(...segmentationsMap.values());
return segmentationPresentations;
}
public getRepresentationsForSegmentation(
segmentationId: string
): { viewportId: string; representations: any[] }[] {
const representations =
cstSegmentation.state.getSegmentationRepresentationsBySegmentationId(segmentationId);
return representations;
}
/**
* Retrieves segmentation representations (labelmap, contour, surface) based on specified criteria.
*
* @param viewportId - The ID of the viewport.
* @param specifier - An object containing optional `segmentationId` and `type` to filter the representations.
* @returns An array of `SegmentationRepresentation` matching the criteria, or an empty array if none are found.
*
* @remarks
* This method filters the segmentation representations according to the provided `specifier`:
* - **No `segmentationId` or `type` provided**: Returns all representations associated with the given `viewportId`.
* - **Only `segmentationId` provided**: Returns all representations with that `segmentationId`, regardless of `viewportId`.
* - **Only `type` provided**: Returns all representations of that `type` associated with the given `viewportId`.
* - **Both `segmentationId` and `type` provided**: Returns representations matching both criteria, regardless of `viewportId`.
*/
public getSegmentationRepresentations(
viewportId: string,
specifier: {
segmentationId?: string;
type?: SegmentationRepresentations;
} = {}
): SegmentationRepresentation[] {
// Get all representations for the viewportId
const representations = cstSegmentation.state.getSegmentationRepresentations(
viewportId,
specifier
);
// Map to our SegmentationRepresentation type
const ohifRepresentations = representations.map(repr =>
this._toOHIFSegmentationRepresentation(viewportId, repr)
);
return ohifRepresentations;
}
public destroy = () => {
eventTarget.removeEventListener(
csToolsEnums.Events.SEGMENTATION_MODIFIED,
this._onSegmentationModifiedFromSource
);
eventTarget.removeEventListener(
csToolsEnums.Events.SEGMENTATION_REMOVED,
this._onSegmentationModifiedFromSource
);
eventTarget.removeEventListener(
csToolsEnums.Events.SEGMENTATION_DATA_MODIFIED,
this._onSegmentationDataModifiedFromSource
);
eventTarget.removeEventListener(
csToolsEnums.Events.SEGMENTATION_REPRESENTATION_ADDED,
this._onSegmentationModifiedFromSource
);
eventTarget.removeEventListener(
csToolsEnums.Events.SEGMENTATION_ADDED,
this._onSegmentationAddedFromSource
);
this.listeners = {};
};
public async addSegmentationRepresentation(
viewportId: string,
{
segmentationId,
type,
config,
suppressEvents = false,
}: {
segmentationId: string;
type?: csToolsEnums.SegmentationRepresentations;
config?: {
blendMode?: csEnums.BlendModes;
};
suppressEvents?: boolean;
}
): Promise<void> {
const segmentation = this.getSegmentation(segmentationId);
const csViewport = this.getAndValidateViewport(viewportId);
const colorLUTIndex = this._segmentationIdToColorLUTIndexMap.get(segmentationId);
const defaultRepresentationType = csToolsEnums.SegmentationRepresentations.Labelmap;
let representationTypeToUse = type || defaultRepresentationType;
let isConverted = false;
if (type === csToolsEnums.SegmentationRepresentations.Labelmap) {
const { isVolumeViewport, isVolumeSegmentation } = this.determineViewportAndSegmentationType(
csViewport,
segmentation
);
({ representationTypeToUse, isConverted } = await this.handleViewportConversion(
isVolumeViewport,
isVolumeSegmentation,
csViewport,
segmentation,
viewportId,
segmentationId,
representationTypeToUse
));
}
await this._addSegmentationRepresentation(
viewportId,
segmentationId,
representationTypeToUse,
colorLUTIndex,
isConverted,
config
);
if (!suppressEvents) {
this._broadcastEvent(this.EVENTS.SEGMENTATION_REPRESENTATION_MODIFIED, { segmentationId });
}
}
/**
* Creates an labelmap segmentation for a given display set
*
* @param displaySet - The display set to create the segmentation for.
* @param options - Optional parameters for creating the segmentation.
* @param options.segmentationId - Custom segmentation ID. If not provided, a UUID will be generated.
* @param options.FrameOfReferenceUID - Frame of reference UID for the segmentation.
* @param options.label - Label for the segmentation.
* @returns A promise that resolves to the created segmentation ID.
*/
public async createLabelmapForDisplaySet(
displaySet: AppTypes.DisplaySet,
options?: {
segmentationId?: string;
segments?: { [segmentIndex: number]: Partial<cstTypes.Segment> };
FrameOfReferenceUID?: string;
label?: string;
}
): Promise<string> {
// Todo: random does not makes sense, make this better, like
// labelmap 1, 2, 3 etc
const segmentationId = options?.segmentationId ?? `${csUtils.uuidv4()}`;
const isDynamicVolume = displaySet.isDynamicVolume;
let referenceImageIds = displaySet.imageIds;
if (isDynamicVolume) {
// get the middle timepoint for referenceImageIds
const timePoints = displaySet.dynamicVolumeInfo.timePoints;
const middleTimePoint = timePoints[Math.floor(timePoints.length / 2)];
referenceImageIds = middleTimePoint;
}
const derivedImages = await imageLoader.createAndCacheDerivedLabelmapImages(referenceImageIds);
const segs = this.getSegmentations();
const label = options.label || `Segmentation ${segs.length + 1}`;
const segImageIds = derivedImages.map(image => image.imageId);
const segmentationPublicInput: cstTypes.SegmentationPublicInput = {
segmentationId,
representation: {
type: LABELMAP,
data: {
imageIds: segImageIds,
referencedVolumeId: this._getVolumeIdForDisplaySet(displaySet),
referencedImageIds: referenceImageIds,
},
},
config: {
label,
segments:
options.segments && Object.keys(options.segments).length > 0
? options.segments
: {
1: {
label: `${i18n.t('Segment')} 1`,
active: true,
},
},
cachedStats: {
info: `S${displaySet.SeriesNumber}: ${displaySet.SeriesDescription}`,
},
},
};
this.addOrUpdateSegmentation(segmentationPublicInput);
return segmentationId;
}
public async createSegmentationForSEGDisplaySet(
segDisplaySet,
options: {
segmentationId?: string;
type: SegmentationRepresentations;
} = {
type: LABELMAP,
}
): Promise<string> {
const { type } = options;
let { segmentationId } = options;
const { labelMapImages } = segDisplaySet;
if (type !== LABELMAP) {
throw new Error('Only labelmap type is supported for SEG display sets right now');
}
if (!labelMapImages || !labelMapImages.length) {
throw new Error('SEG reading failed');
}
segmentationId = segmentationId ?? segDisplaySet.displaySetInstanceUID;
const referencedDisplaySetInstanceUID = segDisplaySet.referencedDisplaySetInstanceUID;
const referencedDisplaySet = this.servicesManager.services.displaySetService.getDisplaySetByUID(
referencedDisplaySetInstanceUID
);
const images = referencedDisplaySet.instances;
if (!images.length) {
throw new Error('No instances were provided for the referenced display set of the SEG');
}
const imageIds = images.map(image => image.imageId);
const derivedImages = labelMapImages?.flat();
const derivedImageIds = derivedImages.map(image => image.imageId);
segDisplaySet.images = derivedImages.map(image => ({
...image,
...metaData.get('instance', image.referencedImageId),
}));
// We should parse the segmentation as separate slices to support overlapping segments.
// This parsing should occur in the CornerstoneJS library adapters.
// For now, we use the volume returned from the library and chop it here.
let firstSegmentedSliceImageId = null;
for (let i = 0; i < derivedImages.length; i++) {
const voxelManager = derivedImages[i].voxelManager as csTypes.IVoxelManager<number>;
const scalarData = voxelManager.getScalarData();
voxelManager.setScalarData(scalarData);
// Check if this slice has any non-zero voxels and we haven't found one yet
if (!firstSegmentedSliceImageId && scalarData.some(value => value !== 0)) {
firstSegmentedSliceImageId = derivedImages[i].referencedImageId;
}
}
// assign the first non zero voxel image id to the segDisplaySet
segDisplaySet.firstSegmentedSliceImageId = firstSegmentedSliceImageId;
const segmentsInfo = segDisplaySet.segMetadata.data;
const segments: { [segmentIndex: string]: cstTypes.Segment } = {};
const colorLUT = [];
segmentsInfo.forEach((segmentInfo, index) => {
if (index === 0) {
colorLUT.push([0, 0, 0, 0]);
return;
}
const {
SegmentedPropertyCategoryCodeSequence,
SegmentNumber,
SegmentLabel,
SegmentAlgorithmType,
SegmentAlgorithmName,
SegmentedPropertyTypeCodeSequence,
rgba,
} = segmentInfo;
colorLUT.push(rgba);
const segmentIndex = Number(SegmentNumber);
const centroid = segDisplaySet.centroids?.get(index);
const imageCentroidXYZ = centroid?.image || { x: 0, y: 0, z: 0 };
const worldCentroidXYZ = centroid?.world || { x: 0, y: 0, z: 0 };
segments[segmentIndex] = {
segmentIndex,
label: SegmentLabel || `Segment ${SegmentNumber}`,
locked: false,
active: false,
cachedStats: {
center: {
image: [imageCentroidXYZ.x, imageCentroidXYZ.y, imageCentroidXYZ.z],
world: [worldCentroidXYZ.x, worldCentroidXYZ.y, worldCentroidXYZ.z],
},
modifiedTime: segDisplaySet.SeriesDate,
category: SegmentedPropertyCategoryCodeSequence
? SegmentedPropertyCategoryCodeSequence.CodeMeaning
: '',
type: SegmentedPropertyTypeCodeSequence
? SegmentedPropertyTypeCodeSequence.CodeMeaning
: '',
algorithmType: SegmentAlgorithmType,
algorithmName: SegmentAlgorithmName,
},
};
});
// get next color lut index
const colorLUTIndex = getNextColorLUTIndex();
addColorLUT(colorLUT, colorLUTIndex);
this._segmentationIdToColorLUTIndexMap.set(segmentationId, colorLUTIndex);
this._broadcastEvent(EVENTS.SEGMENTATION_LOADING_COMPLETE, {
segmentationId,
segDisplaySet,
});
const seg: cstTypes.SegmentationPublicInput = {
segmentationId,
representation: {
type: LABELMAP,
data: {
imageIds: derivedImageIds,
referencedVolumeId: this._getVolumeIdForDisplaySet(referencedDisplaySet),
referencedImageIds: imageIds as string[],
},
},
config: {
label: segDisplaySet.SeriesDescription,
segments,
},
};
segDisplaySet.isLoaded = true;
this.addOrUpdateSegmentation(seg);
return segmentationId;
}
public async createSegmentationForRTDisplaySet(
rtDisplaySet,
options: {
segmentationId?: string;
type: SegmentationRepresentations;
} = {
type: CONTOUR,
}
): Promise<string> {
const { type } = options;
let { segmentationId } = options;
// Currently, only contour representation is supported for RT display
if (type !== CONTOUR) {
throw new Error('Only contour type is supported for RT display sets right now');
}
// Assign segmentationId if not provided
segmentationId = segmentationId ?? rtDisplaySet.displaySetInstanceUID;
const { structureSet } = rtDisplaySet;
if (!structureSet) {
throw new Error(
'To create the contours from RT displaySet, the displaySet should be loaded first. You can perform rtDisplaySet.load() before calling this method.'
);
}
const rtDisplaySetUID = rtDisplaySet.displaySetInstanceUID;
const referencedDisplaySet = this.servicesManager.services.displaySetService.getDisplaySetByUID(
rtDisplaySet.referencedDisplaySetInstanceUID
);
const referencedImageIdsWithGeometry = Array.from(structureSet.ReferencedSOPInstanceUIDsSet);
const referencedImageIds = referencedDisplaySet.imageIds;
// find the first image id that contains a referenced SOP instance UID
const firstSegmentedSliceImageId = referencedImageIds.find(imageId =>
referencedImageIdsWithGeometry.some(referencedId => imageId.includes(referencedId))
);
rtDisplaySet.firstSegmentedSliceImageId = firstSegmentedSliceImageId;
// Map ROI contours to RT Struct Data
const allRTStructData = mapROIContoursToRTStructData(structureSet, rtDisplaySetUID);
// Sort by segmentIndex for consistency
allRTStructData.sort((a, b) => a.segmentIndex - b.segmentIndex);
const geometryIds = allRTStructData.map(({ geometryId }) => geometryId);
// Initialize SegmentationPublicInput similar to SEG function
const segmentation: cstTypes.SegmentationPublicInput = {
segmentationId,
representation: {
type: CONTOUR,
data: {
geometryIds,
},
},
config: {
label: rtDisplaySet.SeriesDescription,
},
};
if (!structureSet.ROIContours?.length) {
throw new Error(
'The structureSet does not contain any ROIContours. Please ensure the structureSet is loaded first.'
);
}
const segments: { [segmentIndex: string]: cstTypes.Segment } = {};
let segmentsCachedStats = {};
// Process each segment similarly to the SEG function
for (const rtStructData of allRTStructData) {
const { data, id, color, segmentIndex, geometryId } = rtStructData;
try {
const geometry = await geometryLoader.createAndCacheGeometry(geometryId, {
geometryData: {
data,
id,
color,
frameOfReferenceUID: structureSet.frameOfReferenceUID,
segmentIndex,
},
type: csEnums.GeometryType.CONTOUR,
});
const contourSet = geometry.data as csTypes.IContourSet;
const centroid = contourSet.centroid;
segmentsCachedStats = {
center: { world: centroid },
modifiedTime: rtDisplaySet.SeriesDate, // Using SeriesDate as modifiedTime
};
segments[segmentIndex] = {
label: id,
segmentIndex,
cachedStats: segmentsCachedStats,
locked: false,
active: false,
};
// Broadcast segment loading progress
const numInitialized = Object.keys(segmentsCachedStats).length;
const percentComplete = Math.round((numInitialized / allRTStructData.length) * 100);
this._broadcastEvent(EVENTS.SEGMENT_LOADING_COMPLETE, {
percentComplete,
numSegments: allRTStructData.length,
});
} catch (e) {
console.warn(`Error initializing contour for segment ${segmentIndex}:`, e);
continue; // Continue processing other segments even if one fails
}
}
// Assign processed segments to segmentation config
segmentation.config.segments = segments;
// Broadcast segmentation loading complete event
this._broadcastEvent(EVENTS.SEGMENTATION_LOADING_COMPLETE, {
segmentationId,
rtDisplaySet,
});
// Mark the RT display set as loaded
rtDisplaySet.isLoaded = true;
// Add or update the segmentation in the state
this.addOrUpdateSegmentation(segmentation);
return segmentationId;
}
/**
* Adds or updates a segmentation in the state
* @param segmentationId - The ID of the segmentation to add or update
* @param data - The data to add or update the segmentation with
*
* @remarks
* This method handles the addition or update of a segmentation in the state.
* If the segmentation already exists, it updates the existing segmentation.
* If the segmentation does not exist, it adds a new segmentation.
*/
public addOrUpdateSegmentation(
data: cstTypes.SegmentationPublicInput | Partial<cstTypes.Segmentation>
) {
const segmentationId = data.segmentationId;
const existingSegmentation = cstSegmentation.state.getSegmentation(segmentationId);
if (existingSegmentation) {
// Update the existing segmentation
this.updateSegmentationInSource(segmentationId, data as Partial<cstTypes.Segmentation>);
} else {
// Add a new segmentation
this.addSegmentationToSource(data as cstTypes.SegmentationPublicInput);
}
}
public setActiveSegmentation(viewportId: string, segmentationId: string): void {
cstSegmentation.activeSegmentation.setActiveSegmentation(viewportId, segmentationId);
}
/**
* Gets the active segmentation for a viewport
* @param viewportId - The ID of the viewport to get the active segmentation for
* @returns The active segmentation object, or null if no segmentation is active
*
* @remarks
* This method retrieves the currently active segmentation for the specified viewport.
* The active segmentation is the one that is currently selected for editing operations.
* Returns null if no segmentation is active in the viewport.
*/
public getActiveSegmentation(viewportId: string): cstTypes.Segmentation | null {
return cstSegmentation.activeSegmentation.getActiveSegmentation(viewportId);
}
/**
* Gets the active segment from the active segmentation in a viewport
* @param viewportId - The ID of the viewport to get the active segment from
* @returns The active segment object, or undefined if no segment is active
*
* @remarks
* This method retrieves the currently active segment from the active segmentation
* in the specified viewport. The active segment is the one that is currently
* selected for editing operations. Returns undefined if no segment is active or
* if there is no active segmentation.
*/
public getActiveSegment(viewportId: string): cstTypes.Segment | undefined {
const activeSegmentation = this.getActiveSegmentation(viewportId);
if (!activeSegmentation) {
return;
}
const { segments } = activeSegmentation;
let activeSegment;
for (const segment of Object.values(segments)) {
if (segment.active) {
activeSegment = segment;
break;
}
}
return activeSegment;
}
public hasCustomStyles(specifier: {
viewportId: string;
segmentationId: string;
type: SegmentationRepresentations;
}): boolean {
return cstSegmentation.config.style.hasCustomStyle(specifier);
}
public getStyle = (specifier: {
viewportId: string;
segmentationId: string;
type: SegmentationRepresentations;
segmentIndex?: number;
}) => {
const style = cstSegmentation.config.style.getStyle(specifier);
return style;
};
public setStyle = (
specifier: {
type: SegmentationRepresentations;
viewportId?: string;
segmentationId?: string;
segmentIndex?: number;
},
style: LabelmapStyle | ContourStyle | SurfaceStyle
) => {
cstSegmentation.config.style.setStyle(specifier, style);
};
public resetToGlobalStyle = () => {
cstSegmentation.config.style.resetToGlobalStyle();
};
/**
* Adds a new segment to the specified segmentation.
* @param segmentationId - The ID of the segmentation to add the segment to.
* @param viewportId: The ID of the viewport to add the segment to, it is used to get the representation, if it is not
* provided, the first available representation for the segmentationId will be used.
* @param config - An object containing the configuration options for the new segment.
* - segmentIndex: (optional) The index of the segment to add. If not provided, the next available index will be used.
* - properties: (optional) An object containing the properties of the new segment.
* - label: (optional) The label of the new segment. If not provided, a default label will be used.
* - color: (optional) The color of the new segment in RGB format. If not provided, a default color will be used.
* - visibility: (optional) Whether the new segment should be visible. If not provided, the segment will be visible by default.
* - isLocked: (optional) Whether the new segment should be locked for editing. If not provided, the segment will not be locked by default.
* - active: (optional) Whether the new segment should be the active segment to be edited. If not provided, the segment will not be active by default.
*/
public addSegment(
segmentationId: string,
config: {
segmentIndex?: number;
label?: string;
isLocked?: boolean;
active?: boolean;
color?: csTypes.Color; // Add color type
visibility?: boolean; // Add visibility option
} = {}
): void {
if (config?.segmentIndex === 0) {
throw new Error(i18n.t('Segment') + ' index 0 is reserved for "no label"');
}
const csSegmentation = this.getCornerstoneSegmentation(segmentationId);
let segmentIndex = config.segmentIndex;
if (!segmentIndex) {
// grab the next available segment index based on the object keys,
// so basically get the highest segment index value + 1
const segmentKeys = Object.keys(csSegmentation.segments);
segmentIndex = segmentKeys.length === 0 ? 1 : Math.max(...segmentKeys.map(Number)) + 1;
}
// update the segmentation
if (!config.label) {
config.label = `${i18n.t('Segment')} ${segmentIndex}`;
}
const currentSegments = csSegmentation.segments;
cstSegmentation.updateSegmentations([
{
segmentationId,
payload: {
segments: {
...currentSegments,
[segmentIndex]: {
...currentSegments[segmentIndex],
segmentIndex,
cachedStats: {},
locked: false,
...config,
},
},
},
},
]);
this.setActiveSegment(segmentationId, segmentIndex);
// Apply additional configurations
if (config.isLocked !== undefined) {
this._setSegmentLockedStatus(segmentationId, segmentIndex, config.isLocked);
}
// Get all viewports that have this segmentation
const viewportIds = this.getViewportIdsWithSegmentation(segmentationId);
viewportIds.forEach(viewportId => {
// Set color if provided
if (config.color !== undefined) {
this.setSegmentColor(viewportId, segmentationId, segmentIndex, config.color);
}
// Set visibility if provided
if (config.visibility !== undefined) {
this.setSegmentVisibility(viewportId, segmentationId, segmentIndex, config.visibility);
}
});
}
/**
* Removes a segment from a segmentation and updates the active segment index if necessary.
*
* @param segmentationId - The ID of the segmentation containing the segment to remove.
* @param segmentIndex - The index of the segment to remove.
*
* @remarks
* This method performs the following actions:
* 1. Clears the segment value in the Cornerstone segmentation.
* 2. Updates all related segmentation representations to remove the segment.
* 3. If the removed segment was the active segment, it updates the active segment index.
*
*/
public removeSegment(segmentationId: string, segmentIndex: number): void {
cstSegmentation.removeSegment(segmentationId, segmentIndex);
}
public setSegmentVisibility(
viewportId: string,
segmentationId: string,
segmentIndex: number,
isVisible: boolean,
type?: SegmentationRepresentations
): void {
this._setSegmentVisibility(viewportId, segmentationId, segmentIndex, isVisible, type);
}
/**
* Sets the locked status of a segment in a segmentation.
*
* @param segmentationId - The ID of the segmentation containing the segment.
* @param segmentIndex - The index of the segment to set the locked status for.
* @param isLocked - The new locked status of the segment.
*
* @remarks
* This method updates the locked status of a specific segment within a segmentation.
* A locked segment cannot be modified or edited.
*/
public setSegmentLocked(segmentationId: string, segmentIndex: number, isLocked: boolean): void {
this._setSegmentLockedStatus(segmentationId, segmentIndex, isLocked);
}
/**
* Toggles the locked state of a segment in a segmentation.
* @param segmentationId - The ID of the segmentation.
* @param segmentIndex - The index of the segment to toggle.
*/
public toggleSegmentLocked(segmentationId: string, segmentIndex: number): void {
const isLocked = cstSegmentation.segmentLocking.isSegmentIndexLocked(
segmentationId,
segmentIndex
);
this._setSegmentLockedStatus(segmentationId, segmentIndex, !isLocked);
}
public toggleSegmentVisibility(
viewportId: string,
segmentationId: string,
segmentIndex: number,
type: SegmentationRepresentations
): void {
const isVisible = cstSegmentation.config.visibility.getSegmentIndexVisibility(
viewportId,
{
segmentationId,
type,
},
segmentIndex
);
this._setSegmentVisibility(viewportId, segmentationId, segmentIndex, !isVisible, type);
}
/**
* Sets the color of a specific segment in a segmentation.
*
* @param viewportId - The ID of the viewport containing the segmentation
* @param segmentationId - The ID of the segmentation containing the segment
* @param segmentIndex - The index of the segment to set the color for
* @param color - The new color to apply to the segment as an array of RGBA values
*
* @remarks
* This method updates the color of a specific segment within a segmentation.
* The color parameter should be an array of 4 numbers representing RGBA values.
*/
public setSegmentColor(
viewportId: string,
segmentationId: string,
segmentIndex: number,
color: csTypes.Color
): void {
cstSegmentation.config.color.setSegmentIndexColor(
viewportId,
segmentationId,
segmentIndex,
color
);
}
/**
* Gets the current color of a specific segment in a segmentation.
*
* @param viewportId - The ID of the viewport containing the segmentation
* @param segmentationId - The ID of the segmentation containing the segment
* @param segmentIndex - The index of the segment to get the color for
* @returns An array of 4 numbers representing the RGBA color values of the segment
*
* @remarks
* This method retrieves the current color of a specific segment within a segmentation.
* The returned color is an array of 4 numbers representing RGBA values.
*/
public getSegmentColor(viewportId: string, segmentationId: string, segmentIndex: number) {
return cstSegmentation.config.color.getSegmentIndexColor(
viewportId,
segmentationId,
segmentIndex
);
}
/**
* Gets the labelmap volume for a segmentation
* @param segmentationId - The ID of the segmentation to get the labelmap volume for
* @returns The labelmap volume for the segmentation, or null if not found
*
* @remarks
* This method retrieves the labelmap volume data for a specific segmentation.
* The labelmap volume contains the actual segmentation data in the form of a 3D volume.
* Returns null if the segmentation does not have valid labelmap volume data.
*/
public getLabelmapVolume(segmentationId: string) {
const csSegmentation = cstSegmentation.state.getSegmentation(segmentationId);
const labelmapData = csSegmentation.representationData[
SegmentationRepresentations.Labelmap
] as cstTypes.LabelmapToolOperationDataVolume;
if (!labelmapData || !labelmapData.volumeId) {
return null;
}
const { volumeId } = labelmapData;
const labelmapVolume = cache.getVolume(volumeId);
return labelmapVolume;
}
/**
* Sets the label for a specific segment in a segmentation
* @param segmentationId - The ID of the segmentation containing the segment
* @param segmentIndex - The index of the segment to set the label for
* @param label - The new label to apply to the segment
*
* @remarks
* This method updates the text label of a specific segment within a segmentation.
* The label is used to identify and describe the segment in the UI.
*/
public setSegmentLabel(segmentationId: string, segmentIndex: number, label: string) {
this._setSegmentLabel(segmentationId, segmentIndex, label);
}
/**
* Sets the active segment for a segmentation
* @param segmentationId - The ID of the segmentation containing the segment
* @param segmentIndex - The index of the segment to set as active
*
* @remarks
* This method updates which segment is considered "active" within a segmentation.
* The active segment is typically highlighted and available for editing operations.
*/
public setActiveSegment(segmentationId: string, segmentIndex: number): void {
this._setActiveSegment(segmentationId, segmentIndex);
}
/**
* Controls whether inactive segmentations should be rendered in a viewport
* @param viewportId - The ID of the viewport to update
* @param renderInactive - Whether inactive segmentations should be rendered
*
* @remarks
* This method configures if segmentations that are not currently active
* should still be visible in the specified viewport. This can be useful
* for comparing or viewing multiple segmentations simultaneously.
*/
public setRenderInactiveSegmentations(viewportId: string, renderInactive: boolean): void {
cstSegmentation.config.style.setRenderInactiveSegmentations(viewportId, renderInactive);
}
/**
* Gets whether inactive segmentations are being rendered for a viewport
* @param viewportId - The ID of the viewport to check
* @returns boolean indicating if inactive segmentations are rendered
*
* @remarks
* This method retrieves the current rendering state for inactive segmentations
* in the specified viewport. Returns true if inactive segmentations are visible.
*/
public getRenderInactiveSegmentations(viewportId: string): boolean {
return cstSegmentation.config.style.getRenderInactiveSegmentations(viewportId);
}
/**
* Sets statistics for a group of segmentations
* @param segmentationIds - Array of segmentation IDs that form the group
* @param stats - Statistics object containing metrics for the segmentation group
*
* @remarks
* This method stores statistical data for a group of related segmentations.
* The stats are stored using a composite key created from the sorted and joined
*/
public setSegmentationGroupStats(segmentationIds: string[], stats: any) {
const groupId = this.getGroupId(segmentationIds);
this._segmentationGroupStatsMap.set(groupId, stats);
}
/**
* Gets statistics for a group of segmentations
* @param segmentationIds - Array of segmentation IDs that form the group
* @returns The stored statistics object for the segmentation group if found, undefined otherwise
*/
public getSegmentationGroupStats(segmentationIds: string[]) {
const groupId = this.getGroupId(segmentationIds);
return this._segmentationGroupStatsMap.get(groupId);
}
private getGroupId(segmentationIds: string[]) {
return segmentationIds.sort().join(',');
}
/**
* Toggles the visibility of a segmentation in the state, and broadcasts the event.
* Note: this method does not update the segmentation state in the source. It only
* updates the state, and there should be separate listeners for that.
* @param ids segmentation ids
*/
public toggleSegmentationRepresentationVisibility = (
viewportId: string,
{ segmentationId, type }: { segmentationId: string; type: SegmentationRepresentations }
): void => {
this._toggleSegmentationRepresentationVisibility(viewportId, segmentationId, type);
};
public getViewportIdsWithSegmentation = (segmentationId: string): string[] => {
const viewportIds = cstSegmentation.state.getViewportIdsWithSegmentation(segmentationId);
return viewportIds;
};
/**
* Clears segmentation representations from the viewport.
* Unlike removeSegmentationRepresentations, this doesn't update
* removed display set and representation maps.
* We track removed segmentations manually to avoid re-adding them
* when the display set is added again.
* @param viewportId - The viewport ID to clear segmentation representations from.
*/
public clearSegmentationRepresentations(viewportId: string): void {
this.removeSegmentationRepresentations(viewportId);
}
/**
* Completely removes a segmentation from the state
* @param segmentationId - The ID of the segmentation to remove.
*/
public remove(segmentationId: string): void {
cstSegmentation.state.removeSegmentation(segmentationId);
}
public removeAllSegmentations(): void {
cstSegmentation.state.removeAllSegmentations();
}
/**
* It removes the segmentation representations from the viewport.
* @param viewportId - The viewport id to remove the segmentation representations from.
* @param specifier - The specifier to remove the segmentation representations.
*
* @remarks
* If no specifier is provided, all segmentation representations for the viewport are removed.
* If a segmentationId specifier is provided, only the segmentation representation with the specified segmentationId and type are removed.
* If a type specifier is provided, only the segmentation representation with the specified type are removed.
* If both a segmentationId and type specifier are provided, only the segmentation representation with the specified segmentationId and type are removed.
*/
public removeSegmentationRepresentations(
viewportId: string,
specifier: {
segmentationId?: string;
type?: SegmentationRepresentations;
} = {}
): void {
cstSegmentation.removeSegmentationRepresentations(viewportId, specifier);
}
public jumpToSegmentCenter(
segmentationId: string,
segmentIndex: number,
viewportId?: string,
highlightAlpha = 0.9,
highlightSegment = true,
animationLength = 750,
highlightHideOthers = false,
highlightFunctionType = 'ease-in-out' // todo: make animation functions configurable from outside
): void {
const center = this._getSegmentCenter(segmentationId, segmentIndex);
if (!center) {
console.warn('No center found for segmentation', segmentationId, segmentIndex);
return;
}
const { world } = center as { world: csTypes.Point3 };
// need to find which viewports are displaying the segmentation
const viewportIds = viewportId
? [viewportId]
: this.getViewportIdsWithSegmentation(segmentationId);
viewportIds.forEach(viewportId => {
const { viewport } = getEnabledElementByViewportId(viewportId);
viewport.jumpToWorld(world);
highlightSegment &&
this.highlightSegment(
segmentationId,
segmentIndex,
viewportId,
highlightAlpha,
animationLength,
highlightHideOthers
);
});
}
public highlightSegment(
segmentationId: string,
segmentIndex: number,
viewportId?: string,
alpha = 0.9,
animationLength = 750,
hideOthers = true,
highlightFunctionType = 'ease-in-out'
): void {
if (this.highlightIntervalId) {
clearInterval(this.highlightIntervalId);
}
const csSegmentation = this.getCornerstoneSegmentation(segmentationId);
const viewportIds = viewportId
? [viewportId]
: this.getViewportIdsWithSegmentation(segmentationId);
viewportIds.forEach(viewportId => {
const segmentationRepresentation = this.getSegmentationRepresentations(viewportId, {
segmentationId,
});
const representation = segmentationRepresentation[0];
const { type } = representation;
const segments = csSegmentation.segments;
const highlightFn =
type === LABELMAP ? this._highlightLabelmap.bind(this) : this._highlightContour.bind(this);
const adjustedAlpha = type === LABELMAP ? alpha : 1 - alpha;
highlightFn(
segmentIndex,
adjustedAlpha,
hideOthers,
segments,
viewportId,
animationLength,
representation
);
});
}
private getAndValidateViewport(viewportId: string) {
const csViewport =
this.servicesManager.services.cornerstoneViewportService.getCornerstoneViewport(viewportId);
if (!csViewport) {
throw new Error(`Viewport with id ${viewportId} not found.`);
}
return csViewport;
}
/**
* Sets the visibility of a segmentation representation.
*
* @param viewportId - The ID of the viewport.
* @param segmentationId - The ID of the segmentation.
* @param isVisible - The new visibility state.
*/
private _setSegmentationRepresentationVisibility(
viewportId: string,
segmentationId: string,
type: SegmentationRepresentations,
isVisible: boolean
): void {
const representations = this.getSegmentationRepresentations(viewportId, {
segmentationId,
type,
});
const representation = representations[0];
if (!representation) {
console.debug(
'No segmentation representation found for the given viewportId and segmentationId'
);
return;
}
cstSegmentation.config.visibility.setSegmentationRepresentationVisibility(
viewportId,
{
segmentationId,
type,
},
isVisible
);
}
private determineViewportAndSegmentationType(csViewport, segmentation) {
const isVolumeViewport =
csViewport.type === ViewportType.ORTHOGRAPHIC || csViewport.type === ViewportType.VOLUME_3D;
const isVolumeSegmentation = 'volumeId' in segmentation.representationData[LABELMAP];
return { isVolumeViewport, isVolumeSegmentation };
}
private async handleViewportConversion(
isVolumeViewport: boolean,
isVolumeSegmentation: boolean,
csViewport: csTypes.IViewport,
segmentation: cstTypes.Segmentation,
viewportId: string,
segmentationId: string,
representationType: csToolsEnums.SegmentationRepresentations
) {
let representationTypeToUse = representationType;
let isConverted = false;
const handler = isVolumeViewport ? this.handleVolumeViewportCase : this.handleStackViewportCase;
({ representationTypeToUse, isConverted } = await handler.apply(this, [
csViewport,
segmentation,
isVolumeSegmentation,
viewportId,
segmentationId,
]));
return { representationTypeToUse, isConverted };
}
private async handleVolumeViewportCase(csViewport, segmentation, isVolumeSegmentation) {
if (csViewport.type === ViewportType.VOLUME_3D) {
return { representationTypeToUse: SegmentationRepresentations.Surface, isConverted: false };
} else {
await this.handleVolumeViewport(
csViewport as csTypes.IVolumeViewport,
segmentation,
isVolumeSegmentation
);
return { representationTypeToUse: SegmentationRepresentations.Labelmap, isConverted: false };
}
}
private async handleStackViewportCase(
csViewport: csTypes.IViewport,
segmentation: cstTypes.Segmentation,
isVolumeSegmentation: boolean,
viewportId: string,
segmentationId: string
): Promise<{ representationTypeToUse: SegmentationRepresentations; isConverted: boolean }> {
if (isVolumeSegmentation) {
const isConverted = await this.convertStackToVolumeViewport(csViewport);
return { representationTypeToUse: SegmentationRepresentations.Labelmap, isConverted };
}
if (updateLabelmapSegmentationImageReferences(viewportId, segmentationId)) {
return { representationTypeToUse: SegmentationRepresentations.Labelmap, isConverted: false };
}
const isConverted = await this.attemptStackToVolumeConversion(
csViewport as csTypes.IStackViewport,
segmentation,
viewportId,
segmentationId
);
return { representationTypeToUse: SegmentationRepresentations.Labelmap, isConverted };
}
private async _addSegmentationRepresentation(
viewportId: string,
segmentationId: string,
representationType: csToolsEnums.SegmentationRepresentations,
colorLUTIndex: number,
isConverted: boolean,
config?: {
blendMode?: csEnums.BlendModes;
}
): Promise<void> {
const representation = {
type: representationType,
segmentationId,
config: { colorLUTOrIndex: colorLUTIndex, ...config },
};
const addRepresentation = () =>
cstSegmentation.addSegmentationRepresentations(viewportId, [representation]);
if (isConverted) {
const { viewportGridService } = this.servicesManager.services;
await new Promise<void>(resolve => {
const { unsubscribe } = viewportGridService.subscribe(
viewportGridService.EVENTS.GRID_STATE_CHANGED,
() => {
addRepresentation();
unsubscribe();
resolve();
}
);
});
} else {
addRepresentation();
}
}
private async handleVolumeViewport(
viewport: csTypes.IVolumeViewport,
segmentation: SegmentationData,
isVolumeSegmentation: boolean
): Promise<void> {
if (isVolumeSegmentation) {
return; // Volume Labelmap on Volume Viewport is natively supported
}
const frameOfReferenceUID = viewport.getFrameOfReferenceUID();
const imageIds = getLabelmapImageIds(segmentation.segmentationId);
const segImage = cache.getImage(imageIds[0]);
if (segImage?.FrameOfReferenceUID === frameOfReferenceUID) {
await convertStackToVolumeLabelmap(segmentation);
}
}
private async convertStackToVolumeViewport(viewport: csTypes.IViewport): Promise<boolean> {
const { viewportGridService, cornerstoneViewportService } = this.servicesManager.services;
const state = viewportGridService.getState();
const gridViewport = state.viewports.get(viewport.id);
const prevViewPresentation = viewport.getViewPresentation();
const prevViewReference = viewport.getViewReference();
const stackViewport = cornerstoneViewportService.getCornerstoneViewport(viewport.id);
const { element } = stackViewport;
const volumeViewportNewVolumeHandler = () => {
const volumeViewport = cornerstoneViewportService.getCornerstoneViewport(viewport.id);
volumeViewport.setViewPresentation(prevViewPresentation);
volumeViewport.setViewReference(prevViewReference);
volumeViewport.render();
element.removeEventListener(
csEnums.Events.VOLUME_VIEWPORT_NEW_VOLUME,
volumeViewportNewVolumeHandler
);
};
element.addEventListener(
csEnums.Events.VOLUME_VIEWPORT_NEW_VOLUME,
volumeViewportNewVolumeHandler
);
viewportGridService.setDisplaySetsForViewport({
viewportId: viewport.id,
displaySetInstanceUIDs: gridViewport.displaySetInstanceUIDs,
viewportOptions: {
...gridViewport.viewportOptions,
viewportType: ViewportType.ORTHOGRAPHIC,
},
});
return true;
}
private async attemptStackToVolumeConversion(
viewport: csTypes.IStackViewport,
segmentation: SegmentationData,
viewportId: string,
segmentationId: string
): Promise<boolean> {
const imageIds = getLabelmapImageIds(segmentation.segmentationId);
const frameOfReferenceUID = viewport.getFrameOfReferenceUID();
const segImage = cache.getImage(imageIds[0]);
if (
segImage?.FrameOfReferenceUID &&
frameOfReferenceUID &&
segImage.FrameOfReferenceUID === frameOfReferenceUID
) {
const isConverted = await this.convertStackToVolumeViewport(viewport);
triggerSegmentationRepresentationModified(
viewportId,
segmentationId,
SegmentationRepresentations.Labelmap
);
return isConverted;
}
}
private addSegmentationToSource(segmentationPublicInput: cstTypes.SegmentationPublicInput) {
cstSegmentation.addSegmentations([segmentationPublicInput]);
}
private updateSegmentationInSource(
segmentationId: string,
payload: Partial<cstTypes.Segmentation>
) {
cstSegmentation.updateSegmentations([{ segmentationId, payload }]);
}
private _toOHIFSegmentationRepresentation(
viewportId: string,
csRepresentation: cstTypes.SegmentationRepresentation
): SegmentationRepresentation {
const { segmentationId, type, active, visible } = csRepresentation;
const { colorLUTIndex } = csRepresentation;
const segmentsRepresentations: { [segmentIndex: number]: SegmentRepresentation } = {};
const segmentation = cstSegmentation.state.getSegmentation(segmentationId);
if (!segmentation) {
throw new Error(`Segmentation with ID ${segmentationId} not found.`);
}
const segmentIds = Object.keys(segmentation.segments);
for (const segmentId of segmentIds) {
const segmentIndex = parseInt(segmentId, 10);
const color = cstSegmentation.config.color.getSegmentIndexColor(
viewportId,
segmentationId,
segmentIndex
);
const isVisible = cstSegmentation.config.visibility.getSegmentIndexVisibility(
viewportId,
{
segmentationId,
type,
},
segmentIndex
);
segmentsRepresentations[segmentIndex] = {
color,
segmentIndex,
opacity: color[3],
visible: isVisible,
};
}
const styles = cstSegmentation.config.style.getStyle({
viewportId,
segmentationId,
type,
});
const id = `${segmentationId}-${type}-${viewportId}`;
return {
id: id,
segmentationId,
label: segmentation.label,
active,
type,
visible,
segments: segmentsRepresentations,
styles,
viewportId,
colorLUTIndex,
config: {},
};
}
private _initSegmentationService() {
eventTarget.addEventListener(
csToolsEnums.Events.SEGMENTATION_MODIFIED,
this._onSegmentationModifiedFromSource
);
eventTarget.addEventListener(
csToolsEnums.Events.SEGMENTATION_REMOVED,
this._onSegmentationModifiedFromSource
);
eventTarget.addEventListener(
csToolsEnums.Events.SEGMENTATION_DATA_MODIFIED,
this._onSegmentationDataModifiedFromSource
);
eventTarget.addEventListener(
csToolsEnums.Events.SEGMENTATION_REPRESENTATION_MODIFIED,
this._onSegmentationRepresentationModifiedFromSource
);
eventTarget.addEventListener(
csToolsEnums.Events.SEGMENTATION_REPRESENTATION_ADDED,
this._onSegmentationRepresentationModifiedFromSource
);
eventTarget.addEventListener(
csToolsEnums.Events.SEGMENTATION_REPRESENTATION_REMOVED,
this._onSegmentationRepresentationModifiedFromSource
);
eventTarget.addEventListener(
csToolsEnums.Events.SEGMENTATION_ADDED,
this._onSegmentationAddedFromSource
);
}
private getCornerstoneSegmentation(segmentationId: string) {
return cstSegmentation.state.getSegmentation(segmentationId);
}
private _highlightLabelmap(
segmentIndex: number,
alpha: number,
hideOthers: boolean,
segments: Segment[],
viewportId: string,
animationLength: number,
representation: cstTypes.SegmentationRepresentation
) {
const { segmentationId } = representation;
const newSegmentSpecificConfig = {
fillAlpha: alpha,
};
if (hideOthers) {
throw new Error('hideOthers is not working right now');
for (let i = 0; i < segments.length; i++) {
if (i !== segmentIndex) {
newSegmentSpecificConfig[i] = {
fillAlpha: 0,
};
}
}
}
const { fillAlpha } = this.getStyle({
viewportId,
segmentationId,
type: LABELMAP,
segmentIndex,
}) as cstTypes.LabelmapStyle;
let startTime: number = null;
const animation = (timestamp: number) => {
if (startTime === null) {
startTime = timestamp;
}
const elapsed = timestamp - startTime;
const progress = Math.min(elapsed / animationLength, 1);
cstSegmentation.config.style.setStyle(
{
segmentationId,
segmentIndex,
type: LABELMAP,
},
{
fillAlpha: easeInOutBell(progress, fillAlpha),
}
);
if (progress < 1) {
requestAnimationFrame(animation);
} else {
cstSegmentation.config.style.setStyle(
{
segmentationId,
segmentIndex,
type: LABELMAP,
},
{}
);
}
};
requestAnimationFrame(animation);
}
private _highlightContour(
segmentIndex: number,
alpha: number,
hideOthers: boolean,
segments: Segment[],
viewportId: string,
animationLength: number,
representation: cstTypes.SegmentationRepresentation
) {
const { segmentationId } = representation;
const startTime = performance.now();
const prevStyle = cstSegmentation.config.style.getStyle({
type: CONTOUR,
}) as ContourStyle;
const prevOutlineWidth = prevStyle.outlineWidth;
// make this configurable
const baseline = Math.max(prevOutlineWidth * 3.5, 5);
const animate = (currentTime: number) => {
const progress = (currentTime - startTime) / animationLength;
if (progress >= 1) {
cstSegmentation.config.style.resetToGlobalStyle();
return;
}
const reversedProgress = easeInOutBellRelative(progress, baseline, prevOutlineWidth);
cstSegmentation.config.style.setStyle(
{
segmentationId,
segmentIndex,
type: CONTOUR,
},
{
outlineWidth: reversedProgress,
}
);
requestAnimationFrame(animate);
};
requestAnimationFrame(animate);
}
private _toggleSegmentationRepresentationVisibility = (
viewportId: string,
segmentationId: string,
type: SegmentationRepresentations
): void => {
const representations = this.getSegmentationRepresentations(viewportId, {
segmentationId,
type,
});
const representation = representations[0];
const segmentsHidden = cstSegmentation.config.visibility.getHiddenSegmentIndices(viewportId, {
segmentationId,
type: representation.type,
});
const currentVisibility = segmentsHidden.size === 0;
this._setSegmentationRepresentationVisibility(
viewportId,
segmentationId,
representation.type,
!currentVisibility
);
};
private _setActiveSegment(segmentationId: string, segmentIndex: number) {
cstSegmentation.segmentIndex.setActiveSegmentIndex(segmentationId, segmentIndex);
}
private _getVolumeIdForDisplaySet(displaySet) {
const volumeLoaderSchema = displaySet.volumeLoaderSchema ?? VOLUME_LOADER_SCHEME;
return `${volumeLoaderSchema}:${displaySet.displaySetInstanceUID}`;
}
private _getSegmentCenter(segmentationId, segmentIndex) {
const segmentation = this.getSegmentation(segmentationId);
if (!segmentation) {
return;
}
const { segments } = segmentation;
const { cachedStats } = segments[segmentIndex];
if (!cachedStats) {
return;
}
const { center } = cachedStats;
return center;
}
private _setSegmentLockedStatus(segmentationId: string, segmentIndex: number, isLocked: boolean) {
cstSegmentation.segmentLocking.setSegmentIndexLocked(segmentationId, segmentIndex, isLocked);
}
private _setSegmentVisibility(
viewportId: string,
segmentationId: string,
segmentIndex: number,
isVisible: boolean,
type?: SegmentationRepresentations
) {
cstSegmentation.config.visibility.setSegmentIndexVisibility(
viewportId,
{
segmentationId,
type,
},
segmentIndex,
isVisible
);
}
private _setSegmentLabel(segmentationId: string, segmentIndex: number, segmentLabel: string) {
const segmentation = this.getCornerstoneSegmentation(segmentationId);
const { segments } = segmentation;
segments[segmentIndex].label = segmentLabel;
cstSegmentation.updateSegmentations([
{
segmentationId,
payload: {
segments,
},
},
]);
}
private _onSegmentationDataModifiedFromSource = evt => {
const { segmentationId } = evt.detail;
this._broadcastEvent(this.EVENTS.SEGMENTATION_DATA_MODIFIED, {
segmentationId,
});
};
private _onSegmentationRepresentationModifiedFromSource = evt => {
const { segmentationId, viewportId } = evt.detail;
this._broadcastEvent(this.EVENTS.SEGMENTATION_REPRESENTATION_MODIFIED, {
segmentationId,
viewportId,
});
};
private _onSegmentationModifiedFromSource = (
evt: cstTypes.EventTypes.SegmentationModifiedEventType
) => {
const { segmentationId } = evt.detail;
this._broadcastEvent(this.EVENTS.SEGMENTATION_MODIFIED, {
segmentationId,
});
};
private _onSegmentationAddedFromSource = (
evt: cstTypes.EventTypes.SegmentationAddedEventType
) => {
const { segmentationId } = evt.detail;
this._broadcastEvent(this.EVENTS.SEGMENTATION_ADDED, {
segmentationId,
});
};
}
export default SegmentationService;
export { EVENTS, VALUE_TYPES };