1986 lines
54 KiB
TypeScript
1986 lines
54 KiB
TypeScript
|
|
import cloneDeep from 'lodash.clonedeep';
|
||
|
|
|
||
|
|
import { pubSubServiceInterface } from '@ohif/core';
|
||
|
|
import {
|
||
|
|
utilities as cstUtils,
|
||
|
|
segmentation as cstSegmentation,
|
||
|
|
CONSTANTS as cstConstants,
|
||
|
|
Enums as csToolsEnums,
|
||
|
|
Types as cstTypes,
|
||
|
|
} from '@cornerstonejs/tools';
|
||
|
|
import {
|
||
|
|
eventTarget,
|
||
|
|
cache,
|
||
|
|
utilities as csUtils,
|
||
|
|
volumeLoader,
|
||
|
|
Types,
|
||
|
|
metaData,
|
||
|
|
getEnabledElementByIds,
|
||
|
|
} from '@cornerstonejs/core';
|
||
|
|
import isEqual from 'lodash.isequal';
|
||
|
|
import { easeInOutBell } from '../../utils/transitions';
|
||
|
|
import {
|
||
|
|
Segmentation,
|
||
|
|
SegmentationConfig,
|
||
|
|
SegmentationSchema,
|
||
|
|
} from './SegmentationServiceTypes';
|
||
|
|
|
||
|
|
const { COLOR_LUT } = cstConstants;
|
||
|
|
const LABELMAP = csToolsEnums.SegmentationRepresentations.Labelmap;
|
||
|
|
|
||
|
|
const EVENTS = {
|
||
|
|
// fired when the segmentation is updated (e.g. when a segment is added, removed, or modified, locked, visibility changed etc.)
|
||
|
|
SEGMENTATION_UPDATED: 'event::segmentation_updated',
|
||
|
|
// fired when the segmentation data (e.g., labelmap pixels) is modified
|
||
|
|
SEGMENTATION_DATA_MODIFIED: 'event::segmentation_data_modified',
|
||
|
|
// fired when the segmentation is added to the cornerstone
|
||
|
|
SEGMENTATION_ADDED: 'event::segmentation_added',
|
||
|
|
// fired when the segmentation is removed
|
||
|
|
SEGMENTATION_REMOVED: 'event::segmentation_removed',
|
||
|
|
// fired when the configuration for the segmentation is changed (e.g., brush size, render fill, outline thickness, etc.)
|
||
|
|
SEGMENTATION_CONFIGURATION_CHANGED:
|
||
|
|
'event::segmentation_configuration_changed',
|
||
|
|
SEGMENT_PIXEL_DATA_CREATED: 'event::segment_pixel_data_created',
|
||
|
|
// for all segments
|
||
|
|
SEGMENTATION_PIXEL_DATA_CREATED: 'event::segmentation_pixel_data_created',
|
||
|
|
};
|
||
|
|
|
||
|
|
const VALUE_TYPES = {};
|
||
|
|
|
||
|
|
class SegmentationService {
|
||
|
|
listeners = {};
|
||
|
|
segmentations: Record<string, Segmentation>;
|
||
|
|
servicesManager = null;
|
||
|
|
highlightIntervalId = null;
|
||
|
|
_broadcastEvent: (eventName: string, callbackProps: any) => void;
|
||
|
|
readonly EVENTS = EVENTS;
|
||
|
|
|
||
|
|
constructor({ servicesManager }) {
|
||
|
|
this.segmentations = {};
|
||
|
|
this.listeners = {};
|
||
|
|
|
||
|
|
Object.assign(this, pubSubServiceInterface);
|
||
|
|
this.servicesManager = servicesManager;
|
||
|
|
|
||
|
|
this._initSegmentationService();
|
||
|
|
}
|
||
|
|
|
||
|
|
public destroy = () => {
|
||
|
|
eventTarget.removeEventListener(
|
||
|
|
csToolsEnums.Events.SEGMENTATION_MODIFIED,
|
||
|
|
this._onSegmentationModified
|
||
|
|
);
|
||
|
|
|
||
|
|
eventTarget.removeEventListener(
|
||
|
|
csToolsEnums.Events.SEGMENTATION_DATA_MODIFIED,
|
||
|
|
this._onSegmentationDataModified
|
||
|
|
);
|
||
|
|
|
||
|
|
// remove the segmentations from the cornerstone
|
||
|
|
Object.keys(this.segmentations).forEach(segmentationId => {
|
||
|
|
this._removeSegmentationFromCornerstone(segmentationId);
|
||
|
|
});
|
||
|
|
|
||
|
|
this.segmentations = {};
|
||
|
|
this.listeners = {};
|
||
|
|
};
|
||
|
|
|
||
|
|
/**
|
||
|
|
* It adds a segment to a segmentation, basically just setting the properties for
|
||
|
|
* the segment
|
||
|
|
* @param segmentationId - The ID of the segmentation you want to add a
|
||
|
|
* segment to.
|
||
|
|
* @param segmentIndex - The index of the segment to add.
|
||
|
|
* @param properties - The properties of the segment to add including
|
||
|
|
* -- label: the label of the segment
|
||
|
|
* -- color: the color of the segment
|
||
|
|
* -- opacity: the opacity of the segment
|
||
|
|
* -- visibility: the visibility of the segment (boolean)
|
||
|
|
* -- isLocked: whether the segment is locked for editing
|
||
|
|
* -- active: whether the segment is currently the active segment to be edited
|
||
|
|
*/
|
||
|
|
public addSegment(
|
||
|
|
segmentationId: string,
|
||
|
|
segmentIndex: number,
|
||
|
|
toolGroupId?: string,
|
||
|
|
properties?: {
|
||
|
|
label?: string;
|
||
|
|
color?: Types.Point3;
|
||
|
|
opacity?: number;
|
||
|
|
visibility?: boolean;
|
||
|
|
isLocked?: boolean;
|
||
|
|
active?: boolean;
|
||
|
|
}
|
||
|
|
): void {
|
||
|
|
if (segmentIndex === 0) {
|
||
|
|
throw new Error('Segment index 0 is reserved for "no label"');
|
||
|
|
}
|
||
|
|
|
||
|
|
toolGroupId = toolGroupId ?? this._getFirstToolGroupId();
|
||
|
|
|
||
|
|
const {
|
||
|
|
segmentationRepresentationUID,
|
||
|
|
segmentation,
|
||
|
|
} = this._getSegmentationInfo(segmentationId, toolGroupId);
|
||
|
|
|
||
|
|
if (this._getSegmentInfo(segmentation, segmentIndex)) {
|
||
|
|
throw new Error(`Segment ${segmentIndex} already exists`);
|
||
|
|
}
|
||
|
|
|
||
|
|
const rgbaColor = cstSegmentation.config.color.getColorForSegmentIndex(
|
||
|
|
toolGroupId,
|
||
|
|
segmentationRepresentationUID,
|
||
|
|
segmentIndex
|
||
|
|
);
|
||
|
|
|
||
|
|
segmentation.segments[segmentIndex] = {
|
||
|
|
label: properties.label,
|
||
|
|
segmentIndex: segmentIndex,
|
||
|
|
color: [rgbaColor[0], rgbaColor[1], rgbaColor[2]],
|
||
|
|
opacity: rgbaColor[3],
|
||
|
|
isVisible: true,
|
||
|
|
isLocked: false,
|
||
|
|
};
|
||
|
|
|
||
|
|
segmentation.segmentCount++;
|
||
|
|
|
||
|
|
const suppressEvents = true;
|
||
|
|
if (properties !== undefined) {
|
||
|
|
const {
|
||
|
|
color: newColor,
|
||
|
|
opacity,
|
||
|
|
isLocked,
|
||
|
|
visibility,
|
||
|
|
active,
|
||
|
|
} = properties;
|
||
|
|
|
||
|
|
if (newColor !== undefined) {
|
||
|
|
this._setSegmentColor(
|
||
|
|
segmentationId,
|
||
|
|
segmentIndex,
|
||
|
|
newColor,
|
||
|
|
toolGroupId,
|
||
|
|
suppressEvents
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
if (opacity !== undefined) {
|
||
|
|
this._setSegmentOpacity(
|
||
|
|
segmentationId,
|
||
|
|
segmentIndex,
|
||
|
|
opacity,
|
||
|
|
toolGroupId,
|
||
|
|
suppressEvents
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
if (visibility !== undefined) {
|
||
|
|
this._setSegmentVisibility(
|
||
|
|
segmentationId,
|
||
|
|
segmentIndex,
|
||
|
|
visibility,
|
||
|
|
toolGroupId,
|
||
|
|
suppressEvents
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
if (active !== undefined) {
|
||
|
|
this._setActiveSegment(segmentationId, segmentIndex, suppressEvents);
|
||
|
|
}
|
||
|
|
|
||
|
|
if (isLocked !== undefined) {
|
||
|
|
this._setSegmentLocked(
|
||
|
|
segmentationId,
|
||
|
|
segmentIndex,
|
||
|
|
isLocked,
|
||
|
|
suppressEvents
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if (segmentation.activeSegmentIndex === null) {
|
||
|
|
this._setActiveSegment(segmentationId, segmentIndex, suppressEvents);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Todo: this includes nonhydrated segmentations which might not be
|
||
|
|
// persisted in the store
|
||
|
|
this._broadcastEvent(this.EVENTS.SEGMENTATION_UPDATED, {
|
||
|
|
segmentation,
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
public removeSegment(segmentationId: string, segmentIndex: number): void {
|
||
|
|
const segmentation = this.getSegmentation(segmentationId);
|
||
|
|
|
||
|
|
if (segmentation === undefined) {
|
||
|
|
throw new Error(`no segmentation for segmentationId: ${segmentationId}`);
|
||
|
|
}
|
||
|
|
|
||
|
|
if (segmentIndex === 0) {
|
||
|
|
throw new Error('Segment index 0 is reserved for "no label"');
|
||
|
|
}
|
||
|
|
|
||
|
|
if (!this._getSegmentInfo(segmentation, segmentIndex)) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
segmentation.segmentCount--;
|
||
|
|
|
||
|
|
segmentation.segments[segmentIndex] = null;
|
||
|
|
|
||
|
|
// Get volume and delete the labels
|
||
|
|
// Todo: handle other segmentations other than labelmap
|
||
|
|
const labelmapVolume = this.getLabelmapVolume(segmentationId);
|
||
|
|
|
||
|
|
const { scalarData, dimensions } = labelmapVolume;
|
||
|
|
|
||
|
|
// Set all values of this segment to zero and get which frames have been edited.
|
||
|
|
const frameLength = dimensions[0] * dimensions[1];
|
||
|
|
const numFrames = dimensions[2];
|
||
|
|
|
||
|
|
let voxelIndex = 0;
|
||
|
|
|
||
|
|
const modifiedFrames = new Set() as Set<number>;
|
||
|
|
|
||
|
|
for (let frame = 0; frame < numFrames; frame++) {
|
||
|
|
for (let p = 0; p < frameLength; p++) {
|
||
|
|
if (scalarData[voxelIndex] === segmentIndex) {
|
||
|
|
scalarData[voxelIndex] = 0;
|
||
|
|
modifiedFrames.add(frame);
|
||
|
|
}
|
||
|
|
|
||
|
|
voxelIndex++;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
const modifiedFramesArray: number[] = Array.from(modifiedFrames);
|
||
|
|
|
||
|
|
// Trigger texture update of modified segmentation frames.
|
||
|
|
cstSegmentation.triggerSegmentationEvents.triggerSegmentationDataModified(
|
||
|
|
segmentationId,
|
||
|
|
modifiedFramesArray
|
||
|
|
);
|
||
|
|
|
||
|
|
if (segmentation.activeSegmentIndex === segmentIndex) {
|
||
|
|
const segmentIndices = Object.keys(segmentation.segments);
|
||
|
|
|
||
|
|
const newActiveSegmentIndex = segmentIndices.length
|
||
|
|
? Number(segmentIndices[0])
|
||
|
|
: 1;
|
||
|
|
|
||
|
|
this._setActiveSegment(segmentationId, newActiveSegmentIndex, true);
|
||
|
|
}
|
||
|
|
|
||
|
|
this._broadcastEvent(this.EVENTS.SEGMENTATION_UPDATED, {
|
||
|
|
segmentation,
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
public setSegmentVisibility(
|
||
|
|
segmentationId: string,
|
||
|
|
segmentIndex: number,
|
||
|
|
isVisible: boolean,
|
||
|
|
toolGroupId?: string,
|
||
|
|
suppressEvents = false
|
||
|
|
): void {
|
||
|
|
this._setSegmentVisibility(
|
||
|
|
segmentationId,
|
||
|
|
segmentIndex,
|
||
|
|
isVisible,
|
||
|
|
toolGroupId,
|
||
|
|
suppressEvents
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
public setSegmentLockedForSegmentation(
|
||
|
|
segmentationId: string,
|
||
|
|
segmentIndex: number,
|
||
|
|
isLocked: boolean
|
||
|
|
): void {
|
||
|
|
const suppressEvents = false;
|
||
|
|
this._setSegmentLocked(
|
||
|
|
segmentationId,
|
||
|
|
segmentIndex,
|
||
|
|
isLocked,
|
||
|
|
suppressEvents
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
public setSegmentLabel(
|
||
|
|
segmentationId: string,
|
||
|
|
segmentIndex: number,
|
||
|
|
segmentLabel: string
|
||
|
|
): void {
|
||
|
|
this._setSegmentLabel(segmentationId, segmentIndex, segmentLabel);
|
||
|
|
}
|
||
|
|
|
||
|
|
public setSegmentColor(
|
||
|
|
segmentationId: string,
|
||
|
|
segmentIndex: number,
|
||
|
|
color: Types.Point3,
|
||
|
|
toolGroupId?: string
|
||
|
|
): void {
|
||
|
|
this._setSegmentColor(segmentationId, segmentIndex, color, toolGroupId);
|
||
|
|
}
|
||
|
|
|
||
|
|
public setSegmentRGBA = (
|
||
|
|
segmentationId: string,
|
||
|
|
segmentIndex: number,
|
||
|
|
rgbaColor: cstTypes.Color,
|
||
|
|
toolGroupId?: string
|
||
|
|
): void => {
|
||
|
|
const segmentation = this.getSegmentation(segmentationId);
|
||
|
|
|
||
|
|
if (segmentation === undefined) {
|
||
|
|
throw new Error(`no segmentation for segmentationId: ${segmentationId}`);
|
||
|
|
}
|
||
|
|
|
||
|
|
const suppressEvents = true;
|
||
|
|
this._setSegmentOpacity(
|
||
|
|
segmentationId,
|
||
|
|
segmentIndex,
|
||
|
|
rgbaColor[3],
|
||
|
|
toolGroupId,
|
||
|
|
suppressEvents
|
||
|
|
);
|
||
|
|
|
||
|
|
this._setSegmentColor(
|
||
|
|
segmentationId,
|
||
|
|
segmentIndex,
|
||
|
|
[rgbaColor[0], rgbaColor[1], rgbaColor[2]],
|
||
|
|
toolGroupId,
|
||
|
|
suppressEvents
|
||
|
|
);
|
||
|
|
|
||
|
|
this._broadcastEvent(this.EVENTS.SEGMENTATION_UPDATED, {
|
||
|
|
segmentation,
|
||
|
|
});
|
||
|
|
};
|
||
|
|
|
||
|
|
public setSegmentOpacity(
|
||
|
|
segmentationId: string,
|
||
|
|
segmentIndex: number,
|
||
|
|
opacity: number,
|
||
|
|
toolGroupId?: string
|
||
|
|
): void {
|
||
|
|
this._setSegmentOpacity(segmentationId, segmentIndex, opacity, toolGroupId);
|
||
|
|
}
|
||
|
|
|
||
|
|
public setActiveSegmentationForToolGroup(
|
||
|
|
segmentationId: string,
|
||
|
|
toolGroupId?: string
|
||
|
|
): void {
|
||
|
|
toolGroupId = toolGroupId ?? this._getFirstToolGroupId();
|
||
|
|
|
||
|
|
const suppressEvents = false;
|
||
|
|
this._setActiveSegmentationForToolGroup(
|
||
|
|
segmentationId,
|
||
|
|
toolGroupId,
|
||
|
|
suppressEvents
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
public setActiveSegmentForSegmentation(
|
||
|
|
segmentationId: string,
|
||
|
|
segmentIndex: number
|
||
|
|
): void {
|
||
|
|
this._setActiveSegment(segmentationId, segmentIndex, false);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Get all segmentations.
|
||
|
|
*
|
||
|
|
* * @param filterNonHydratedSegmentations - If true, only return hydrated segmentations
|
||
|
|
* hydrated segmentations are those that have been loaded and persisted
|
||
|
|
* in the state, but non hydrated segmentations are those that are
|
||
|
|
* only created for the SEG displayset (SEG viewport) and the user might not
|
||
|
|
* have loaded them yet fully.
|
||
|
|
*
|
||
|
|
|
||
|
|
* @return Array of segmentations
|
||
|
|
*/
|
||
|
|
public getSegmentations(
|
||
|
|
filterNonHydratedSegmentations = true
|
||
|
|
): Segmentation[] {
|
||
|
|
const segmentations = this._getSegmentations();
|
||
|
|
|
||
|
|
return (
|
||
|
|
segmentations &&
|
||
|
|
segmentations.filter(segmentation => {
|
||
|
|
return !filterNonHydratedSegmentations || segmentation.hydrated;
|
||
|
|
})
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
private _getSegmentations(): Segmentation[] {
|
||
|
|
const segmentations = this.arrayOfObjects(this.segmentations);
|
||
|
|
return (
|
||
|
|
segmentations &&
|
||
|
|
segmentations.map(m => this.segmentations[Object.keys(m)[0]])
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Get specific segmentation by its id.
|
||
|
|
*
|
||
|
|
* @param segmentationId If of the segmentation
|
||
|
|
* @return segmentation instance
|
||
|
|
*/
|
||
|
|
public getSegmentation(segmentationId: string): Segmentation {
|
||
|
|
return this.segmentations[segmentationId];
|
||
|
|
}
|
||
|
|
|
||
|
|
public addOrUpdateSegmentation(
|
||
|
|
segmentationSchema: SegmentationSchema,
|
||
|
|
suppressEvents = false,
|
||
|
|
notYetUpdatedAtSource = false
|
||
|
|
): string {
|
||
|
|
const { id: segmentationId } = segmentationSchema;
|
||
|
|
let segmentation = this.segmentations[segmentationId];
|
||
|
|
if (segmentation) {
|
||
|
|
// Update the segmentation (mostly for assigning metadata/labels)
|
||
|
|
Object.assign(segmentation, segmentationSchema);
|
||
|
|
|
||
|
|
this._updateCornerstoneSegmentations({
|
||
|
|
segmentationId,
|
||
|
|
notYetUpdatedAtSource,
|
||
|
|
});
|
||
|
|
|
||
|
|
if (!suppressEvents) {
|
||
|
|
this._broadcastEvent(this.EVENTS.SEGMENTATION_UPDATED, {
|
||
|
|
segmentation,
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
return segmentationId;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Add the segmentation otherwise
|
||
|
|
cstSegmentation.addSegmentations([
|
||
|
|
{
|
||
|
|
segmentationId,
|
||
|
|
representation: {
|
||
|
|
type: LABELMAP,
|
||
|
|
// Todo: need to be generalized
|
||
|
|
data: {
|
||
|
|
volumeId: segmentationId,
|
||
|
|
},
|
||
|
|
},
|
||
|
|
},
|
||
|
|
]);
|
||
|
|
|
||
|
|
// Define a new color LUT and associate it with this segmentation.
|
||
|
|
|
||
|
|
// Todo: need to be generalized to accept custom color LUTs
|
||
|
|
const newColorLUT = this.generateNewColorLUT();
|
||
|
|
const newColorLUTIndex = this.getNextColorLUTIndex();
|
||
|
|
|
||
|
|
cstSegmentation.config.color.addColorLUT(newColorLUT, newColorLUTIndex);
|
||
|
|
|
||
|
|
if (
|
||
|
|
segmentationSchema.label === undefined ||
|
||
|
|
segmentationSchema.label === ''
|
||
|
|
) {
|
||
|
|
segmentationSchema.label = 'Segmentation';
|
||
|
|
}
|
||
|
|
|
||
|
|
this.segmentations[segmentationId] = {
|
||
|
|
...segmentationSchema,
|
||
|
|
segments: segmentationSchema.segments || [null],
|
||
|
|
activeSegmentIndex: segmentationSchema.activeSegmentIndex ?? null,
|
||
|
|
segmentCount: segmentationSchema.segmentCount ?? 0,
|
||
|
|
isActive: false,
|
||
|
|
colorLUTIndex: newColorLUTIndex,
|
||
|
|
isVisible: true,
|
||
|
|
};
|
||
|
|
|
||
|
|
segmentation = this.segmentations[segmentationId];
|
||
|
|
|
||
|
|
this._updateCornerstoneSegmentations({
|
||
|
|
segmentationId,
|
||
|
|
notYetUpdatedAtSource: true,
|
||
|
|
});
|
||
|
|
|
||
|
|
if (!suppressEvents) {
|
||
|
|
this._broadcastEvent(this.EVENTS.SEGMENTATION_ADDED, {
|
||
|
|
segmentation,
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
return segmentation.id;
|
||
|
|
}
|
||
|
|
|
||
|
|
public async createSegmentationForSEGDisplaySet(
|
||
|
|
segDisplaySet,
|
||
|
|
segmentationId?: string,
|
||
|
|
suppressEvents = false
|
||
|
|
): Promise<string> {
|
||
|
|
segmentationId = segmentationId ?? segDisplaySet.displaySetInstanceUID;
|
||
|
|
const { segments, referencedVolumeId } = segDisplaySet;
|
||
|
|
|
||
|
|
if (!segments || !referencedVolumeId) {
|
||
|
|
throw new Error(
|
||
|
|
'To create the segmentation from SEG displaySet, the displaySet should be loaded first, you can perform segDisplaySet.load() before calling this method.'
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
const segmentationSchema: SegmentationSchema = {
|
||
|
|
id: segmentationId,
|
||
|
|
volumeId: segmentationId,
|
||
|
|
displaySetInstanceUID: segDisplaySet.displaySetInstanceUID,
|
||
|
|
referencedVolumeURI: segDisplaySet.referencedVolumeURI,
|
||
|
|
activeSegmentIndex: 1,
|
||
|
|
cachedStats: {},
|
||
|
|
label: '',
|
||
|
|
segmentsLocked: [],
|
||
|
|
type: LABELMAP,
|
||
|
|
displayText: [],
|
||
|
|
hydrated: false, // by default we don't hydrate the segmentation for SEG displaySets
|
||
|
|
segmentCount: 0,
|
||
|
|
segments: [],
|
||
|
|
};
|
||
|
|
|
||
|
|
const labelmap = this.getLabelmapVolume(segmentationId);
|
||
|
|
const segmentation = this.getSegmentation(segmentationId);
|
||
|
|
|
||
|
|
if (labelmap && segmentation) {
|
||
|
|
// if the labalemap with the same segmentationId already exists, we can
|
||
|
|
// just assume that the segmentation is already created and move on with
|
||
|
|
// updating the state
|
||
|
|
return this.addOrUpdateSegmentation(
|
||
|
|
Object.assign(segmentationSchema, segmentation),
|
||
|
|
suppressEvents
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
// if the labelmap doesn't exist, we need to create it first from the
|
||
|
|
// DICOM SEG displaySet data
|
||
|
|
|
||
|
|
const referencedVolume = cache.getVolume(referencedVolumeId);
|
||
|
|
|
||
|
|
if (!referencedVolume) {
|
||
|
|
throw new Error(
|
||
|
|
`No volume found for referencedVolumeId: ${referencedVolumeId}`
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Force use of a Uint8Array SharedArrayBuffer for the segmentation to save space and so
|
||
|
|
// it is easily compressible in worker thread.
|
||
|
|
const derivedVolume = await volumeLoader.createAndCacheDerivedVolume(
|
||
|
|
referencedVolumeId,
|
||
|
|
{
|
||
|
|
volumeId: segmentationId,
|
||
|
|
targetBuffer: {
|
||
|
|
type: 'Uint8Array',
|
||
|
|
sharedArrayBuffer: true,
|
||
|
|
},
|
||
|
|
}
|
||
|
|
);
|
||
|
|
const [rows, columns] = derivedVolume.dimensions;
|
||
|
|
const derivedVolumeScalarData = derivedVolume.scalarData;
|
||
|
|
|
||
|
|
const { imageIds } = referencedVolume;
|
||
|
|
const sopUIDImageIdIndexMap = imageIds.reduce((acc, imageId, index) => {
|
||
|
|
const { sopInstanceUid } = metaData.get('generalImageModule', imageId);
|
||
|
|
acc[sopInstanceUid] = index;
|
||
|
|
return acc;
|
||
|
|
}, {} as { [sopUID: string]: number });
|
||
|
|
|
||
|
|
const numSegments = Object.keys(segments).length;
|
||
|
|
// Note: ideally we could use the TypedArray set method, but since each
|
||
|
|
// slice can have multiple segments, we need to loop over each slice and
|
||
|
|
// set the segment value for each segment.
|
||
|
|
const _segmentInfoUpdate = (segmentInfo, segmentIndex) => {
|
||
|
|
const { pixelData: segPixelData } = segmentInfo;
|
||
|
|
|
||
|
|
let segmentX = 0;
|
||
|
|
let segmentY = 0;
|
||
|
|
let segmentZ = 0;
|
||
|
|
let count = 0;
|
||
|
|
|
||
|
|
for (const [
|
||
|
|
functionalGroupIndex,
|
||
|
|
functionalGroup,
|
||
|
|
] of segmentInfo.functionalGroups.entries()) {
|
||
|
|
const {
|
||
|
|
ReferencedSOPInstanceUID,
|
||
|
|
} = functionalGroup.DerivationImageSequence.SourceImageSequence;
|
||
|
|
|
||
|
|
const imageIdIndex = sopUIDImageIdIndexMap[ReferencedSOPInstanceUID];
|
||
|
|
|
||
|
|
if (imageIdIndex === -1) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
const step = rows * columns;
|
||
|
|
|
||
|
|
// we need a faster way to get the pixel data for the current
|
||
|
|
// functional group, which we use typed array view
|
||
|
|
|
||
|
|
const functionGroupPixelData = new Uint8Array(
|
||
|
|
segPixelData.buffer,
|
||
|
|
functionalGroupIndex * step,
|
||
|
|
step
|
||
|
|
);
|
||
|
|
|
||
|
|
const functionalGroupStartIndex = imageIdIndex * step;
|
||
|
|
const functionalGroupEndIndex = (imageIdIndex + 1) * step;
|
||
|
|
|
||
|
|
// Note: this for loop is not optimized, since DICOM SEG stores
|
||
|
|
// each segment as a separate labelmap so if there is a slice
|
||
|
|
// that has multiple segments, we will have to loop over each
|
||
|
|
// segment and we cannot use the TypedArray set method.
|
||
|
|
for (
|
||
|
|
let i = functionalGroupStartIndex, j = 0;
|
||
|
|
i < functionalGroupEndIndex;
|
||
|
|
i++, j++
|
||
|
|
) {
|
||
|
|
if (functionGroupPixelData[j] !== 0) {
|
||
|
|
derivedVolumeScalarData[i] = segmentIndex;
|
||
|
|
|
||
|
|
// centroid calculations
|
||
|
|
segmentX += i % columns;
|
||
|
|
segmentY += Math.floor(i / columns) % rows;
|
||
|
|
segmentZ += Math.floor(i / (columns * rows));
|
||
|
|
count++;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// centroid calculations
|
||
|
|
const x = Math.floor(segmentX / count);
|
||
|
|
const y = Math.floor(segmentY / count);
|
||
|
|
const z = Math.floor(segmentZ / count);
|
||
|
|
|
||
|
|
const centerWorld = derivedVolume.imageData.indexToWorld([x, y, z]);
|
||
|
|
|
||
|
|
segmentationSchema.cachedStats = {
|
||
|
|
...segmentationSchema.cachedStats,
|
||
|
|
segmentCenter: {
|
||
|
|
...segmentationSchema.cachedStats.segmentCenter,
|
||
|
|
[segmentIndex]: {
|
||
|
|
center: {
|
||
|
|
image: [x, y, z],
|
||
|
|
world: centerWorld,
|
||
|
|
},
|
||
|
|
modifiedTime: Date.now(),
|
||
|
|
},
|
||
|
|
},
|
||
|
|
};
|
||
|
|
|
||
|
|
this._broadcastEvent(EVENTS.SEGMENT_PIXEL_DATA_CREATED, {
|
||
|
|
segmentIndex: Number(segmentIndex),
|
||
|
|
numSegments,
|
||
|
|
});
|
||
|
|
};
|
||
|
|
|
||
|
|
for (const segmentIndex in segments) {
|
||
|
|
const segmentInfo = segments[segmentIndex];
|
||
|
|
|
||
|
|
// Important: we need a non-blocking way to update the segmentation
|
||
|
|
// state, otherwise the UI will freeze and the user will not be able
|
||
|
|
// to interact with the app or progress bars will not be updated.
|
||
|
|
const promise = new Promise<void>((resolve, reject) => {
|
||
|
|
setTimeout(() => {
|
||
|
|
_segmentInfoUpdate(segmentInfo, segmentIndex);
|
||
|
|
resolve();
|
||
|
|
}, 0);
|
||
|
|
});
|
||
|
|
|
||
|
|
await promise;
|
||
|
|
}
|
||
|
|
|
||
|
|
segmentationSchema.segmentCount = Object.keys(segments).length;
|
||
|
|
segmentationSchema.segments = [null]; // segment 0
|
||
|
|
|
||
|
|
Object.keys(segments).forEach(segmentIndex => {
|
||
|
|
const segmentInfo = segments[segmentIndex];
|
||
|
|
const segIndex = Number(segmentIndex);
|
||
|
|
|
||
|
|
segmentationSchema.segments[segIndex] = {
|
||
|
|
label: segmentInfo.label || `Segment ${segIndex}`,
|
||
|
|
segmentIndex: Number(segmentIndex),
|
||
|
|
color: [
|
||
|
|
segmentInfo.color[0],
|
||
|
|
segmentInfo.color[1],
|
||
|
|
segmentInfo.color[2],
|
||
|
|
],
|
||
|
|
opacity: segmentInfo.color[3],
|
||
|
|
isVisible: true,
|
||
|
|
isLocked: false,
|
||
|
|
};
|
||
|
|
});
|
||
|
|
|
||
|
|
segDisplaySet.isLoaded = true;
|
||
|
|
|
||
|
|
this._broadcastEvent(EVENTS.SEGMENTATION_PIXEL_DATA_CREATED, {
|
||
|
|
segmentationId,
|
||
|
|
segDisplaySet,
|
||
|
|
});
|
||
|
|
|
||
|
|
return this.addOrUpdateSegmentation(segmentationSchema, suppressEvents);
|
||
|
|
}
|
||
|
|
|
||
|
|
public jumpToSegmentCenter(
|
||
|
|
segmentationId: string,
|
||
|
|
segmentIndex: number,
|
||
|
|
toolGroupId?: string,
|
||
|
|
highlightAlpha = 0.9,
|
||
|
|
highlightSegment = true,
|
||
|
|
animationLength = 750,
|
||
|
|
highlightHideOthers = false,
|
||
|
|
highlightFunctionType: 'ease-in-out' // todo: make animation functions configurable from outside
|
||
|
|
): void {
|
||
|
|
const { ToolGroupService } = this.servicesManager.services;
|
||
|
|
const center = this._getSegmentCenter(segmentationId, segmentIndex);
|
||
|
|
|
||
|
|
const { world } = center;
|
||
|
|
|
||
|
|
// todo: generalize
|
||
|
|
toolGroupId =
|
||
|
|
toolGroupId || this._getToolGroupIdsWithSegmentation(segmentationId);
|
||
|
|
|
||
|
|
const toolGroups = [];
|
||
|
|
|
||
|
|
if (Array.isArray(toolGroupId)) {
|
||
|
|
toolGroupId.forEach(toolGroup => {
|
||
|
|
toolGroups.push(ToolGroupService.getToolGroup(toolGroup));
|
||
|
|
});
|
||
|
|
} else {
|
||
|
|
toolGroups.push(ToolGroupService.getToolGroup(toolGroupId));
|
||
|
|
}
|
||
|
|
|
||
|
|
toolGroups.forEach(toolGroup => {
|
||
|
|
const viewportsInfo = toolGroup.getViewportsInfo();
|
||
|
|
|
||
|
|
// @ts-ignore
|
||
|
|
for (const { viewportId, renderingEngineId } of viewportsInfo) {
|
||
|
|
const { viewport } = getEnabledElementByIds(
|
||
|
|
viewportId,
|
||
|
|
renderingEngineId
|
||
|
|
);
|
||
|
|
cstUtils.viewport.jumpToWorld(viewport, world);
|
||
|
|
}
|
||
|
|
|
||
|
|
if (highlightSegment) {
|
||
|
|
this.highlightSegment(
|
||
|
|
segmentationId,
|
||
|
|
segmentIndex,
|
||
|
|
toolGroup.id,
|
||
|
|
highlightAlpha,
|
||
|
|
animationLength,
|
||
|
|
highlightHideOthers,
|
||
|
|
highlightFunctionType
|
||
|
|
);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
public highlightSegment(
|
||
|
|
segmentationId: string,
|
||
|
|
segmentIndex: number,
|
||
|
|
toolGroupId?: string,
|
||
|
|
alpha = 0.9,
|
||
|
|
animationLength = 750,
|
||
|
|
hideOthers = true,
|
||
|
|
highlightFunctionType: 'ease-in-out'
|
||
|
|
): void {
|
||
|
|
if (this.highlightIntervalId) {
|
||
|
|
clearInterval(this.highlightIntervalId);
|
||
|
|
}
|
||
|
|
|
||
|
|
const segmentation = this.getSegmentation(segmentationId);
|
||
|
|
toolGroupId = toolGroupId ?? this._getFirstToolGroupId();
|
||
|
|
|
||
|
|
const segmentationRepresentation = this._getSegmentationRepresentation(
|
||
|
|
segmentationId,
|
||
|
|
toolGroupId
|
||
|
|
);
|
||
|
|
|
||
|
|
const { segments } = segmentation;
|
||
|
|
|
||
|
|
const newSegmentSpecificConfig = {
|
||
|
|
[segmentIndex]: {
|
||
|
|
LABELMAP: {
|
||
|
|
fillAlpha: alpha,
|
||
|
|
},
|
||
|
|
},
|
||
|
|
};
|
||
|
|
|
||
|
|
if (hideOthers) {
|
||
|
|
for (let i = 0; i < segments.length; i++) {
|
||
|
|
if (i !== segmentIndex) {
|
||
|
|
newSegmentSpecificConfig[i] = {
|
||
|
|
LABELMAP: {
|
||
|
|
fillAlpha: 0,
|
||
|
|
},
|
||
|
|
};
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
const { fillAlpha } = this.getConfiguration(toolGroupId);
|
||
|
|
|
||
|
|
let count = 0;
|
||
|
|
const intervalTime = 16;
|
||
|
|
const numberOfFrames = Math.ceil(animationLength / intervalTime);
|
||
|
|
|
||
|
|
this.highlightIntervalId = setInterval(() => {
|
||
|
|
const x = (count * intervalTime) / animationLength;
|
||
|
|
cstSegmentation.config.setSegmentSpecificConfig(
|
||
|
|
toolGroupId,
|
||
|
|
segmentationRepresentation.segmentationRepresentationUID,
|
||
|
|
{
|
||
|
|
[segmentIndex]: {
|
||
|
|
LABELMAP: {
|
||
|
|
fillAlpha: easeInOutBell(x, fillAlpha),
|
||
|
|
},
|
||
|
|
},
|
||
|
|
}
|
||
|
|
);
|
||
|
|
|
||
|
|
count++;
|
||
|
|
|
||
|
|
if (count === numberOfFrames) {
|
||
|
|
clearInterval(this.highlightIntervalId);
|
||
|
|
cstSegmentation.config.setSegmentSpecificConfig(
|
||
|
|
toolGroupId,
|
||
|
|
segmentationRepresentation.segmentationRepresentationUID,
|
||
|
|
{}
|
||
|
|
);
|
||
|
|
|
||
|
|
this.highlightIntervalId = null;
|
||
|
|
}
|
||
|
|
}, intervalTime);
|
||
|
|
}
|
||
|
|
|
||
|
|
public createSegmentationForDisplaySet = async (
|
||
|
|
displaySetInstanceUID: string,
|
||
|
|
options?: {
|
||
|
|
segmentationId: string;
|
||
|
|
label: string;
|
||
|
|
}
|
||
|
|
): Promise<string> => {
|
||
|
|
const volumeLoaderScheme = 'cornerstoneStreamingImageVolume'; // Loader id which defines which volume loader to use
|
||
|
|
const volumeId = `${volumeLoaderScheme}:${displaySetInstanceUID}`; // VolumeId with loader id + volume id
|
||
|
|
|
||
|
|
const segmentationId = options?.segmentationId ?? `${csUtils.uuidv4()}`;
|
||
|
|
|
||
|
|
// Force use of a Uint8Array SharedArrayBuffer for the segmentation to save space and so
|
||
|
|
// it is easily compressible in worker thread.
|
||
|
|
await volumeLoader.createAndCacheDerivedVolume(volumeId, {
|
||
|
|
volumeId: segmentationId,
|
||
|
|
targetBuffer: {
|
||
|
|
type: 'Uint8Array',
|
||
|
|
sharedArrayBuffer: true,
|
||
|
|
},
|
||
|
|
});
|
||
|
|
|
||
|
|
const segmentationSchema: SegmentationSchema = {
|
||
|
|
id: segmentationId,
|
||
|
|
volumeId: segmentationId,
|
||
|
|
displaySetInstanceUID,
|
||
|
|
referencedVolumeURI: volumeId.split(':')[0], // Todo: this is so ugly
|
||
|
|
activeSegmentIndex: 1,
|
||
|
|
cachedStats: {},
|
||
|
|
label: options?.label,
|
||
|
|
segmentsLocked: [],
|
||
|
|
type: LABELMAP,
|
||
|
|
displayText: [],
|
||
|
|
hydrated: false,
|
||
|
|
segmentCount: 0,
|
||
|
|
segments: [],
|
||
|
|
};
|
||
|
|
|
||
|
|
this.addOrUpdateSegmentation(segmentationSchema);
|
||
|
|
|
||
|
|
return segmentationId;
|
||
|
|
};
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 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 toggleSegmentationVisibility = (segmentationId: string): void => {
|
||
|
|
this._toggleSegmentationVisibility(segmentationId, false);
|
||
|
|
};
|
||
|
|
|
||
|
|
public addSegmentationRepresentationToToolGroup = async (
|
||
|
|
toolGroupId: string,
|
||
|
|
segmentationId: string,
|
||
|
|
hydrateSegmentation = false,
|
||
|
|
representationType = csToolsEnums.SegmentationRepresentations.Labelmap
|
||
|
|
): Promise<void> => {
|
||
|
|
const segmentation = this.getSegmentation(segmentationId);
|
||
|
|
|
||
|
|
if (!segmentation) {
|
||
|
|
throw new Error(
|
||
|
|
`Segmentation with segmentationId ${segmentationId} not found.`
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
if (hydrateSegmentation) {
|
||
|
|
// hydrate the segmentation if it's not hydrated yet
|
||
|
|
segmentation.hydrated = true;
|
||
|
|
}
|
||
|
|
|
||
|
|
const { colorLUTIndex } = segmentation;
|
||
|
|
|
||
|
|
// Based on the segmentationId, set the colorLUTIndex.
|
||
|
|
const segmentationRepresentationUIDs = await cstSegmentation.addSegmentationRepresentations(
|
||
|
|
toolGroupId,
|
||
|
|
[
|
||
|
|
{
|
||
|
|
segmentationId,
|
||
|
|
type: representationType,
|
||
|
|
},
|
||
|
|
]
|
||
|
|
);
|
||
|
|
|
||
|
|
// set the latest segmentation representation as active one
|
||
|
|
this._setActiveSegmentationForToolGroup(
|
||
|
|
segmentationId,
|
||
|
|
toolGroupId,
|
||
|
|
segmentationRepresentationUIDs[0]
|
||
|
|
);
|
||
|
|
|
||
|
|
cstSegmentation.config.color.setColorLUT(
|
||
|
|
toolGroupId,
|
||
|
|
segmentationRepresentationUIDs[0],
|
||
|
|
colorLUTIndex
|
||
|
|
);
|
||
|
|
|
||
|
|
// add the segmentation segments properly
|
||
|
|
for (const segment of segmentation.segments) {
|
||
|
|
if (segment === null || segment === undefined) {
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
const {
|
||
|
|
segmentIndex,
|
||
|
|
color,
|
||
|
|
isLocked,
|
||
|
|
isVisible: visibility,
|
||
|
|
opacity,
|
||
|
|
} = segment;
|
||
|
|
|
||
|
|
const suppressEvents = true;
|
||
|
|
|
||
|
|
if (color !== undefined) {
|
||
|
|
this._setSegmentColor(
|
||
|
|
segmentationId,
|
||
|
|
segmentIndex,
|
||
|
|
color,
|
||
|
|
toolGroupId,
|
||
|
|
suppressEvents
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
if (opacity !== undefined) {
|
||
|
|
this._setSegmentOpacity(
|
||
|
|
segmentationId,
|
||
|
|
segmentIndex,
|
||
|
|
opacity,
|
||
|
|
toolGroupId,
|
||
|
|
suppressEvents
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
if (visibility !== undefined) {
|
||
|
|
this._setSegmentVisibility(
|
||
|
|
segmentationId,
|
||
|
|
segmentIndex,
|
||
|
|
visibility,
|
||
|
|
toolGroupId,
|
||
|
|
suppressEvents
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
if (isLocked !== undefined) {
|
||
|
|
this._setSegmentLocked(
|
||
|
|
segmentationId,
|
||
|
|
segmentIndex,
|
||
|
|
isLocked,
|
||
|
|
suppressEvents
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
public setSegmentRGBAColorForSegmentation = (
|
||
|
|
segmentationId: string,
|
||
|
|
segmentIndex: number,
|
||
|
|
rgbaColor,
|
||
|
|
toolGroupId?: string
|
||
|
|
) => {
|
||
|
|
const segmentation = this.getSegmentation(segmentationId);
|
||
|
|
|
||
|
|
if (segmentation === undefined) {
|
||
|
|
throw new Error(`no segmentation for segmentationId: ${segmentationId}`);
|
||
|
|
}
|
||
|
|
|
||
|
|
this._setSegmentOpacity(
|
||
|
|
segmentationId,
|
||
|
|
segmentIndex,
|
||
|
|
rgbaColor[3],
|
||
|
|
toolGroupId, // toolGroupId
|
||
|
|
true
|
||
|
|
);
|
||
|
|
this._setSegmentColor(
|
||
|
|
segmentationId,
|
||
|
|
segmentIndex,
|
||
|
|
[rgbaColor[0], rgbaColor[1], rgbaColor[2]],
|
||
|
|
toolGroupId, // toolGroupId
|
||
|
|
true
|
||
|
|
);
|
||
|
|
|
||
|
|
this._broadcastEvent(this.EVENTS.SEGMENTATION_UPDATED, {
|
||
|
|
segmentation,
|
||
|
|
});
|
||
|
|
};
|
||
|
|
|
||
|
|
public getToolGroupIdsWithSegmentation = (
|
||
|
|
segmentationId: string
|
||
|
|
): string[] => {
|
||
|
|
const toolGroupIds = cstSegmentation.state.getToolGroupIdsWithSegmentation(
|
||
|
|
segmentationId
|
||
|
|
);
|
||
|
|
return toolGroupIds;
|
||
|
|
};
|
||
|
|
|
||
|
|
public hydrateSegmentation = (
|
||
|
|
segmentationId: string,
|
||
|
|
suppressEvents = false
|
||
|
|
): void => {
|
||
|
|
const segmentation = this.getSegmentation(segmentationId);
|
||
|
|
|
||
|
|
if (!segmentation) {
|
||
|
|
throw new Error(
|
||
|
|
`Segmentation with segmentationId ${segmentationId} not found.`
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
segmentation.hydrated = true;
|
||
|
|
|
||
|
|
if (!suppressEvents) {
|
||
|
|
this._broadcastEvent(this.EVENTS.SEGMENTATION_UPDATED, {
|
||
|
|
segmentation,
|
||
|
|
});
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
public removeSegmentationRepresentationFromToolGroup(
|
||
|
|
toolGroupId: string,
|
||
|
|
segmentationIds?: string[]
|
||
|
|
): void {
|
||
|
|
segmentationIds =
|
||
|
|
segmentationIds ??
|
||
|
|
cstSegmentation.state
|
||
|
|
.getSegmentationRepresentations(toolGroupId)
|
||
|
|
.map(rep => rep.segmentationRepresentationUID);
|
||
|
|
|
||
|
|
cstSegmentation.removeSegmentationsFromToolGroup(
|
||
|
|
toolGroupId,
|
||
|
|
segmentationIds,
|
||
|
|
true // immediate render
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Removes a segmentation and broadcasts the removed event.
|
||
|
|
*
|
||
|
|
* @param {string} segmentationId The segmentation id
|
||
|
|
*/
|
||
|
|
public remove(segmentationId: string): void {
|
||
|
|
const segmentation = this.segmentations[segmentationId];
|
||
|
|
const wasActive = segmentation.isActive;
|
||
|
|
|
||
|
|
if (!segmentationId || !segmentation) {
|
||
|
|
console.warn(
|
||
|
|
`No segmentationId provided, or unable to find segmentation by id.`
|
||
|
|
);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
const { colorLUTIndex } = segmentation;
|
||
|
|
|
||
|
|
this._removeSegmentationFromCornerstone(segmentationId);
|
||
|
|
|
||
|
|
// Delete associated colormap
|
||
|
|
// Todo: bring this back
|
||
|
|
cstSegmentation.state.removeColorLUT(colorLUTIndex);
|
||
|
|
|
||
|
|
delete this.segmentations[segmentationId];
|
||
|
|
|
||
|
|
// If this segmentation was active, and there is another segmentation, set another one active.
|
||
|
|
|
||
|
|
if (wasActive) {
|
||
|
|
const remainingSegmentations = this._getSegmentations();
|
||
|
|
|
||
|
|
if (remainingSegmentations.length) {
|
||
|
|
const { id } = remainingSegmentations[0];
|
||
|
|
|
||
|
|
this._setActiveSegmentationForToolGroup(
|
||
|
|
id,
|
||
|
|
this._getFirstToolGroupId(),
|
||
|
|
false
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
this._broadcastEvent(this.EVENTS.SEGMENTATION_REMOVED, {
|
||
|
|
segmentationId,
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
public getConfiguration = (toolGroupId?: string): SegmentationConfig => {
|
||
|
|
toolGroupId = toolGroupId ?? this._getFirstToolGroupId();
|
||
|
|
|
||
|
|
const brushSize = 1;
|
||
|
|
// const brushSize = cstUtils.segmentation.getBrushSizeForToolGroup(
|
||
|
|
// toolGroupId
|
||
|
|
// );
|
||
|
|
|
||
|
|
const brushThresholdGate = 1;
|
||
|
|
// const brushThresholdGate = cstUtils.segmentation.getBrushThresholdForToolGroup(
|
||
|
|
// toolGroupId
|
||
|
|
// );
|
||
|
|
|
||
|
|
const config = cstSegmentation.config.getGlobalConfig();
|
||
|
|
const { renderInactiveSegmentations } = config;
|
||
|
|
|
||
|
|
const labelmapRepresentationConfig = config.representations.LABELMAP;
|
||
|
|
|
||
|
|
const {
|
||
|
|
renderOutline,
|
||
|
|
outlineWidthActive,
|
||
|
|
renderFill,
|
||
|
|
fillAlpha,
|
||
|
|
fillAlphaInactive,
|
||
|
|
outlineOpacity,
|
||
|
|
outlineOpacityInactive,
|
||
|
|
} = labelmapRepresentationConfig;
|
||
|
|
|
||
|
|
return {
|
||
|
|
brushSize,
|
||
|
|
brushThresholdGate,
|
||
|
|
fillAlpha,
|
||
|
|
fillAlphaInactive,
|
||
|
|
outlineWidthActive,
|
||
|
|
renderFill,
|
||
|
|
renderInactiveSegmentations,
|
||
|
|
renderOutline,
|
||
|
|
outlineOpacity,
|
||
|
|
outlineOpacityInactive,
|
||
|
|
};
|
||
|
|
};
|
||
|
|
|
||
|
|
public setConfiguration = (configuration: SegmentationConfig): void => {
|
||
|
|
const {
|
||
|
|
brushSize,
|
||
|
|
brushThresholdGate,
|
||
|
|
fillAlpha,
|
||
|
|
fillAlphaInactive,
|
||
|
|
outlineWidthActive,
|
||
|
|
outlineOpacity,
|
||
|
|
renderFill,
|
||
|
|
renderInactiveSegmentations,
|
||
|
|
renderOutline,
|
||
|
|
} = configuration;
|
||
|
|
|
||
|
|
if (renderOutline !== undefined) {
|
||
|
|
this._setLabelmapConfigValue('renderOutline', renderOutline);
|
||
|
|
}
|
||
|
|
|
||
|
|
if (outlineWidthActive !== undefined) {
|
||
|
|
this._setLabelmapConfigValue('outlineWidthActive', outlineWidthActive);
|
||
|
|
// this._setLabelmapConfigValue('outlineWidthInactive', outlineWidthActive);
|
||
|
|
}
|
||
|
|
|
||
|
|
if (outlineOpacity !== undefined) {
|
||
|
|
this._setLabelmapConfigValue('outlineOpacity', outlineOpacity / 100);
|
||
|
|
}
|
||
|
|
|
||
|
|
if (fillAlpha !== undefined) {
|
||
|
|
this._setLabelmapConfigValue('fillAlpha', fillAlpha / 100);
|
||
|
|
}
|
||
|
|
|
||
|
|
if (renderFill !== undefined) {
|
||
|
|
this._setLabelmapConfigValue('renderFill', renderFill);
|
||
|
|
}
|
||
|
|
|
||
|
|
if (renderInactiveSegmentations !== undefined) {
|
||
|
|
const config = cstSegmentation.config.getGlobalConfig();
|
||
|
|
|
||
|
|
config.renderInactiveSegmentations = renderInactiveSegmentations;
|
||
|
|
cstSegmentation.config.setGlobalConfig(config);
|
||
|
|
}
|
||
|
|
|
||
|
|
if (fillAlphaInactive !== undefined) {
|
||
|
|
this._setLabelmapConfigValue(
|
||
|
|
'fillAlphaInactive',
|
||
|
|
fillAlphaInactive / 100
|
||
|
|
);
|
||
|
|
|
||
|
|
// we assume that if the user changes the inactive fill alpha, they
|
||
|
|
// want the inactive outline to be also changed
|
||
|
|
this._setLabelmapConfigValue(
|
||
|
|
'outlineOpacityInactive',
|
||
|
|
Math.max(0.75, fillAlphaInactive / 100) // don't go below 0.7 for outline
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
// if (brushSize !== undefined) {
|
||
|
|
// const { ToolGroupService } = this.servicesManager.services;
|
||
|
|
|
||
|
|
// const toolGroupIds = ToolGroupService.getToolGroupIds();
|
||
|
|
|
||
|
|
// toolGroupIds.forEach(toolGroupId => {
|
||
|
|
// cstUtils.segmentation.setBrushSizeForToolGroup(toolGroupId, brushSize);
|
||
|
|
// });
|
||
|
|
// }
|
||
|
|
|
||
|
|
// if (brushThresholdGate !== undefined) {
|
||
|
|
// const { ToolGroupService } = this.servicesManager.services;
|
||
|
|
|
||
|
|
// const toolGroupIds = ToolGroupService.getFirstToolGroupIds();
|
||
|
|
|
||
|
|
// toolGroupIds.forEach(toolGroupId => {
|
||
|
|
// cstUtils.segmentation.setBrushThresholdForToolGroup(
|
||
|
|
// toolGroupId,
|
||
|
|
// brushThresholdGate
|
||
|
|
// );
|
||
|
|
// });
|
||
|
|
// }
|
||
|
|
|
||
|
|
this._broadcastEvent(
|
||
|
|
this.EVENTS.SEGMENTATION_CONFIGURATION_CHANGED,
|
||
|
|
this.getConfiguration()
|
||
|
|
);
|
||
|
|
};
|
||
|
|
|
||
|
|
public getLabelmapVolume = (segmentationId: string) => {
|
||
|
|
return cache.getVolume(segmentationId);
|
||
|
|
};
|
||
|
|
|
||
|
|
public getSegmentationRepresentationsForToolGroup = toolGroupId => {
|
||
|
|
return cstSegmentation.state.getSegmentationRepresentations(toolGroupId);
|
||
|
|
};
|
||
|
|
|
||
|
|
public setSegmentLabelForSegmentation(
|
||
|
|
segmentationId: string,
|
||
|
|
segmentIndex: number,
|
||
|
|
label: string
|
||
|
|
) {
|
||
|
|
this._setSegmentLabelForSegmentation(segmentationId, segmentIndex, label);
|
||
|
|
}
|
||
|
|
|
||
|
|
private _setSegmentLabelForSegmentation(
|
||
|
|
segmentationId: string,
|
||
|
|
segmentIndex: number,
|
||
|
|
label: string,
|
||
|
|
suppressEvents = false
|
||
|
|
) {
|
||
|
|
const segmentation = this.getSegmentation(segmentationId);
|
||
|
|
|
||
|
|
if (segmentation === undefined) {
|
||
|
|
throw new Error(`no segmentation for segmentationId: ${segmentationId}`);
|
||
|
|
}
|
||
|
|
|
||
|
|
const segmentInfo = segmentation.segments[segmentIndex];
|
||
|
|
|
||
|
|
if (segmentInfo === undefined) {
|
||
|
|
throw new Error(
|
||
|
|
`Segment ${segmentIndex} not yet added to segmentation: ${segmentationId}`
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
segmentInfo.label = label;
|
||
|
|
|
||
|
|
if (suppressEvents === false) {
|
||
|
|
// this._setSegmentationModified(segmentationId);
|
||
|
|
this._broadcastEvent(this.EVENTS.SEGMENTATION_UPDATED, {
|
||
|
|
segmentation,
|
||
|
|
});
|
||
|
|
}
|
||
|
|
}
|
||
|
|
public shouldRenderSegmentation(
|
||
|
|
viewportDisplaySetInstanceUIDs,
|
||
|
|
segDisplaySetInstanceUID
|
||
|
|
) {
|
||
|
|
if (
|
||
|
|
!viewportDisplaySetInstanceUIDs ||
|
||
|
|
!viewportDisplaySetInstanceUIDs.length
|
||
|
|
) {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
const { DisplaySetService } = this.servicesManager.services;
|
||
|
|
|
||
|
|
let shouldDisplaySeg = false;
|
||
|
|
|
||
|
|
const segDisplaySet = DisplaySetService.getDisplaySetByUID(
|
||
|
|
segDisplaySetInstanceUID
|
||
|
|
);
|
||
|
|
|
||
|
|
const {
|
||
|
|
FrameOfReferenceUID: segFrameOfReferenceUID,
|
||
|
|
} = segDisplaySet.instance;
|
||
|
|
|
||
|
|
viewportDisplaySetInstanceUIDs.forEach(displaySetInstanceUID => {
|
||
|
|
// check if the displaySet is sharing the same frameOfReferenceUID
|
||
|
|
// with the new segmentation
|
||
|
|
const displaySet = DisplaySetService.getDisplaySetByUID(
|
||
|
|
displaySetInstanceUID
|
||
|
|
);
|
||
|
|
|
||
|
|
// Todo: this might not be ideal for use cases such as 4D, since we
|
||
|
|
// don't want to show the segmentation for all the frames
|
||
|
|
if (
|
||
|
|
displaySet.isReconstructable &&
|
||
|
|
displaySet?.images?.[0]?.FrameOfReferenceUID === segFrameOfReferenceUID
|
||
|
|
) {
|
||
|
|
shouldDisplaySeg = true;
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
return shouldDisplaySeg;
|
||
|
|
}
|
||
|
|
|
||
|
|
private _setActiveSegmentationForToolGroup(
|
||
|
|
segmentationId: string,
|
||
|
|
toolGroupId: string,
|
||
|
|
suppressEvents = false
|
||
|
|
) {
|
||
|
|
const segmentations = this._getSegmentations();
|
||
|
|
const targetSegmentation = this.getSegmentation(segmentationId);
|
||
|
|
|
||
|
|
if (targetSegmentation === undefined) {
|
||
|
|
throw new Error(`no segmentation for segmentationId: ${segmentationId}`);
|
||
|
|
}
|
||
|
|
|
||
|
|
segmentations.forEach(segmentation => {
|
||
|
|
segmentation.isActive = segmentation.id === segmentationId;
|
||
|
|
});
|
||
|
|
|
||
|
|
const representation = this._getSegmentationRepresentation(
|
||
|
|
segmentationId,
|
||
|
|
toolGroupId
|
||
|
|
);
|
||
|
|
|
||
|
|
cstSegmentation.activeSegmentation.setActiveSegmentationRepresentation(
|
||
|
|
toolGroupId,
|
||
|
|
representation.segmentationRepresentationUID
|
||
|
|
);
|
||
|
|
|
||
|
|
if (suppressEvents === false) {
|
||
|
|
this._broadcastEvent(this.EVENTS.SEGMENTATION_UPDATED, {
|
||
|
|
segmentation: targetSegmentation,
|
||
|
|
});
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private _toggleSegmentationVisibility = (
|
||
|
|
segmentationId: string,
|
||
|
|
suppressEvents = false
|
||
|
|
) => {
|
||
|
|
const segmentation = this.segmentations[segmentationId];
|
||
|
|
|
||
|
|
if (!segmentation) {
|
||
|
|
throw new Error(
|
||
|
|
`Segmentation with segmentationId ${segmentationId} not found.`
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
segmentation.isVisible = !segmentation.isVisible;
|
||
|
|
|
||
|
|
this._updateCornerstoneSegmentationVisibility(segmentationId);
|
||
|
|
|
||
|
|
if (suppressEvents === false) {
|
||
|
|
this._broadcastEvent(this.EVENTS.SEGMENTATION_UPDATED, {
|
||
|
|
segmentation,
|
||
|
|
});
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
private _setActiveSegment(
|
||
|
|
segmentationId: string,
|
||
|
|
segmentIndex: number,
|
||
|
|
suppressEvents = false
|
||
|
|
) {
|
||
|
|
const segmentation = this.getSegmentation(segmentationId);
|
||
|
|
|
||
|
|
if (segmentation === undefined) {
|
||
|
|
throw new Error(`no segmentation for segmentationId: ${segmentationId}`);
|
||
|
|
}
|
||
|
|
|
||
|
|
cstSegmentation.segmentIndex.setActiveSegmentIndex(
|
||
|
|
segmentationId,
|
||
|
|
segmentIndex
|
||
|
|
);
|
||
|
|
|
||
|
|
segmentation.activeSegmentIndex = segmentIndex;
|
||
|
|
|
||
|
|
if (suppressEvents === false) {
|
||
|
|
this._broadcastEvent(this.EVENTS.SEGMENTATION_UPDATED, {
|
||
|
|
segmentation,
|
||
|
|
});
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private _getSegmentInfo(segmentation: Segmentation, segmentIndex: number) {
|
||
|
|
const segments = segmentation.segments;
|
||
|
|
|
||
|
|
if (!segments) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (segments && segments.length > 0) {
|
||
|
|
return segments[segmentIndex];
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private _setSegmentColor = (
|
||
|
|
segmentationId: string,
|
||
|
|
segmentIndex: number,
|
||
|
|
color: Types.Point3,
|
||
|
|
toolGroupId?: string,
|
||
|
|
suppressEvents = false
|
||
|
|
) => {
|
||
|
|
const segmentation = this.getSegmentation(segmentationId);
|
||
|
|
|
||
|
|
if (segmentation === undefined) {
|
||
|
|
throw new Error(`no segmentation for segmentationId: ${segmentationId}`);
|
||
|
|
}
|
||
|
|
|
||
|
|
const segmentInfo = this._getSegmentInfo(segmentation, segmentIndex);
|
||
|
|
|
||
|
|
if (segmentInfo === undefined) {
|
||
|
|
throw new Error(
|
||
|
|
`Segment ${segmentIndex} not yet added to segmentation: ${segmentationId}`
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
toolGroupId = toolGroupId ?? this._getFirstToolGroupId();
|
||
|
|
|
||
|
|
const segmentationRepresentation = this._getSegmentationRepresentation(
|
||
|
|
segmentationId,
|
||
|
|
toolGroupId
|
||
|
|
);
|
||
|
|
|
||
|
|
if (!segmentationRepresentation) {
|
||
|
|
throw new Error(
|
||
|
|
'Must add representation to toolgroup before setting segments'
|
||
|
|
);
|
||
|
|
}
|
||
|
|
const { segmentationRepresentationUID } = segmentationRepresentation;
|
||
|
|
|
||
|
|
const rgbaColor = cstSegmentation.config.color.getColorForSegmentIndex(
|
||
|
|
toolGroupId,
|
||
|
|
segmentationRepresentationUID,
|
||
|
|
segmentIndex
|
||
|
|
);
|
||
|
|
|
||
|
|
cstSegmentation.config.color.setColorForSegmentIndex(
|
||
|
|
toolGroupId,
|
||
|
|
segmentationRepresentationUID,
|
||
|
|
segmentIndex,
|
||
|
|
[...color, rgbaColor[3]]
|
||
|
|
);
|
||
|
|
|
||
|
|
segmentInfo.color = color;
|
||
|
|
|
||
|
|
if (suppressEvents === false) {
|
||
|
|
this._broadcastEvent(this.EVENTS.SEGMENTATION_UPDATED, {
|
||
|
|
segmentation,
|
||
|
|
});
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
private _getSegmentCenter(segmentationId, segmentIndex) {
|
||
|
|
const segmentation = this.getSegmentation(segmentationId);
|
||
|
|
|
||
|
|
if (!segmentation) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
const { cachedStats } = segmentation;
|
||
|
|
|
||
|
|
if (!cachedStats) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
const { segmentCenter } = cachedStats;
|
||
|
|
|
||
|
|
if (!segmentCenter) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
const { center } = segmentCenter[segmentIndex];
|
||
|
|
|
||
|
|
return center;
|
||
|
|
}
|
||
|
|
|
||
|
|
private _setSegmentLocked(
|
||
|
|
segmentationId: string,
|
||
|
|
segmentIndex: number,
|
||
|
|
isLocked: boolean,
|
||
|
|
suppressEvents = false
|
||
|
|
) {
|
||
|
|
const segmentation = this.getSegmentation(segmentationId);
|
||
|
|
|
||
|
|
if (segmentation === undefined) {
|
||
|
|
throw new Error(`no segmentation for segmentationId: ${segmentationId}`);
|
||
|
|
}
|
||
|
|
|
||
|
|
const segmentInfo = this._getSegmentInfo(segmentation, segmentIndex);
|
||
|
|
|
||
|
|
if (segmentInfo === undefined) {
|
||
|
|
throw new Error(
|
||
|
|
`Segment ${segmentIndex} not yet added to segmentation: ${segmentationId}`
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
segmentInfo.isLocked = isLocked;
|
||
|
|
|
||
|
|
cstSegmentation.segmentLocking.setSegmentIndexLocked(
|
||
|
|
segmentationId,
|
||
|
|
segmentIndex,
|
||
|
|
isLocked
|
||
|
|
);
|
||
|
|
|
||
|
|
if (suppressEvents === false) {
|
||
|
|
this._broadcastEvent(this.EVENTS.SEGMENTATION_UPDATED, {
|
||
|
|
segmentation,
|
||
|
|
});
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private _setSegmentVisibility(
|
||
|
|
segmentationId: string,
|
||
|
|
segmentIndex: number,
|
||
|
|
isVisible: boolean,
|
||
|
|
toolGroupId?: string,
|
||
|
|
suppressEvents = false
|
||
|
|
) {
|
||
|
|
toolGroupId = toolGroupId ?? this._getFirstToolGroupId();
|
||
|
|
|
||
|
|
const {
|
||
|
|
segmentationRepresentationUID,
|
||
|
|
segmentation,
|
||
|
|
} = this._getSegmentationInfo(segmentationId, toolGroupId);
|
||
|
|
|
||
|
|
if (segmentation === undefined) {
|
||
|
|
throw new Error(`no segmentation for segmentationId: ${segmentationId}`);
|
||
|
|
}
|
||
|
|
|
||
|
|
const segmentInfo = this._getSegmentInfo(segmentation, segmentIndex);
|
||
|
|
|
||
|
|
if (segmentInfo === undefined) {
|
||
|
|
throw new Error(
|
||
|
|
`Segment ${segmentIndex} not yet added to segmentation: ${segmentationId}`
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
segmentInfo.isVisible = isVisible;
|
||
|
|
|
||
|
|
cstSegmentation.config.visibility.setVisibilityForSegmentIndex(
|
||
|
|
toolGroupId,
|
||
|
|
segmentationRepresentationUID,
|
||
|
|
segmentIndex,
|
||
|
|
isVisible
|
||
|
|
);
|
||
|
|
|
||
|
|
if (suppressEvents === false) {
|
||
|
|
this._broadcastEvent(this.EVENTS.SEGMENTATION_UPDATED, {
|
||
|
|
segmentation,
|
||
|
|
});
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private _setSegmentOpacity = (
|
||
|
|
segmentationId: string,
|
||
|
|
segmentIndex: number,
|
||
|
|
opacity: number,
|
||
|
|
toolGroupId?: string,
|
||
|
|
suppressEvents = false
|
||
|
|
) => {
|
||
|
|
const segmentation = this.getSegmentation(segmentationId);
|
||
|
|
|
||
|
|
if (segmentation === undefined) {
|
||
|
|
throw new Error(`no segmentation for segmentationId: ${segmentationId}`);
|
||
|
|
}
|
||
|
|
|
||
|
|
const segmentInfo = this._getSegmentInfo(segmentation, segmentIndex);
|
||
|
|
|
||
|
|
if (segmentInfo === undefined) {
|
||
|
|
throw new Error(
|
||
|
|
`Segment ${segmentIndex} not yet added to segmentation: ${segmentationId}`
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
toolGroupId = toolGroupId ?? this._getFirstToolGroupId();
|
||
|
|
|
||
|
|
const segmentationRepresentation = this._getSegmentationRepresentation(
|
||
|
|
segmentationId,
|
||
|
|
toolGroupId
|
||
|
|
);
|
||
|
|
|
||
|
|
if (!segmentationRepresentation) {
|
||
|
|
throw new Error(
|
||
|
|
'Must add representation to toolgroup before setting segments'
|
||
|
|
);
|
||
|
|
}
|
||
|
|
const { segmentationRepresentationUID } = segmentationRepresentation;
|
||
|
|
|
||
|
|
const rgbaColor = cstSegmentation.config.color.getColorForSegmentIndex(
|
||
|
|
toolGroupId,
|
||
|
|
segmentationRepresentationUID,
|
||
|
|
segmentIndex
|
||
|
|
);
|
||
|
|
|
||
|
|
cstSegmentation.config.color.setColorForSegmentIndex(
|
||
|
|
toolGroupId,
|
||
|
|
segmentationRepresentationUID,
|
||
|
|
segmentIndex,
|
||
|
|
[rgbaColor[0], rgbaColor[1], rgbaColor[2], opacity]
|
||
|
|
);
|
||
|
|
|
||
|
|
segmentInfo.opacity = opacity;
|
||
|
|
|
||
|
|
if (suppressEvents === false) {
|
||
|
|
this._broadcastEvent(this.EVENTS.SEGMENTATION_UPDATED, {
|
||
|
|
segmentation,
|
||
|
|
});
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
private _setSegmentLabel(
|
||
|
|
segmentationId: string,
|
||
|
|
segmentIndex: number,
|
||
|
|
segmentLabel: string,
|
||
|
|
suppressEvents = false
|
||
|
|
) {
|
||
|
|
const segmentation = this.getSegmentation(segmentationId);
|
||
|
|
|
||
|
|
if (segmentation === undefined) {
|
||
|
|
throw new Error(`no segmentation for segmentationId: ${segmentationId}`);
|
||
|
|
}
|
||
|
|
|
||
|
|
const segmentInfo = this._getSegmentInfo(segmentation, segmentIndex);
|
||
|
|
|
||
|
|
if (segmentInfo === undefined) {
|
||
|
|
throw new Error(
|
||
|
|
`Segment ${segmentIndex} not yet added to segmentation: ${segmentationId}`
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
segmentInfo.label = segmentLabel;
|
||
|
|
|
||
|
|
if (suppressEvents === false) {
|
||
|
|
this._broadcastEvent(this.EVENTS.SEGMENTATION_UPDATED, {
|
||
|
|
segmentation,
|
||
|
|
});
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private _getSegmentationRepresentation(segmentationId, toolGroupId) {
|
||
|
|
const segmentationRepresentations = this.getSegmentationRepresentationsForToolGroup(
|
||
|
|
toolGroupId
|
||
|
|
);
|
||
|
|
|
||
|
|
if (segmentationRepresentations.length === 0) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Todo: this finds the first segmentation representation that matches the segmentationId
|
||
|
|
// If there are two labelmap representations from the same segmentation, this will not work
|
||
|
|
const representation = segmentationRepresentations.find(
|
||
|
|
representation => representation.segmentationId === segmentationId
|
||
|
|
);
|
||
|
|
|
||
|
|
return representation;
|
||
|
|
}
|
||
|
|
|
||
|
|
private _setLabelmapConfigValue = (property, value) => {
|
||
|
|
const { CornerstoneViewportService } = this.servicesManager.services;
|
||
|
|
|
||
|
|
const config = cstSegmentation.config.getGlobalConfig();
|
||
|
|
|
||
|
|
config.representations.LABELMAP[property] = value;
|
||
|
|
|
||
|
|
// Todo: add non global (representation specific config as well)
|
||
|
|
cstSegmentation.config.setGlobalConfig(config);
|
||
|
|
|
||
|
|
const renderingEngine = CornerstoneViewportService.getRenderingEngine();
|
||
|
|
const viewportIds = CornerstoneViewportService.getViewportIds();
|
||
|
|
|
||
|
|
renderingEngine.renderViewports(viewportIds);
|
||
|
|
};
|
||
|
|
|
||
|
|
private _initSegmentationService() {
|
||
|
|
// Connect Segmentation Service to Cornerstone3D.
|
||
|
|
eventTarget.addEventListener(
|
||
|
|
csToolsEnums.Events.SEGMENTATION_MODIFIED,
|
||
|
|
this._onSegmentationModified
|
||
|
|
);
|
||
|
|
|
||
|
|
eventTarget.addEventListener(
|
||
|
|
csToolsEnums.Events.SEGMENTATION_DATA_MODIFIED,
|
||
|
|
this._onSegmentationDataModified
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
private _onSegmentationDataModified = evt => {
|
||
|
|
const { segmentationId } = evt.detail;
|
||
|
|
|
||
|
|
const segmentation = this.getSegmentation(segmentationId);
|
||
|
|
|
||
|
|
if (segmentation === undefined) {
|
||
|
|
// Part of add operation, not update operation, exit early.
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
this._broadcastEvent(this.EVENTS.SEGMENTATION_DATA_MODIFIED, {
|
||
|
|
segmentation,
|
||
|
|
});
|
||
|
|
};
|
||
|
|
|
||
|
|
private _onSegmentationModified = evt => {
|
||
|
|
const { segmentationId } = evt.detail;
|
||
|
|
|
||
|
|
const segmentation = this.segmentations[segmentationId];
|
||
|
|
|
||
|
|
if (segmentation === undefined) {
|
||
|
|
// Part of add operation, not update operation, exit early.
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
const segmentationState = cstSegmentation.state.getSegmentation(
|
||
|
|
segmentationId
|
||
|
|
);
|
||
|
|
|
||
|
|
if (!segmentationState) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (!Object.keys(segmentationState.representationData).includes(LABELMAP)) {
|
||
|
|
throw new Error('Non-labelmap representations are not supported yet');
|
||
|
|
}
|
||
|
|
|
||
|
|
const {
|
||
|
|
activeSegmentIndex,
|
||
|
|
cachedStats,
|
||
|
|
segmentsLocked,
|
||
|
|
representationData,
|
||
|
|
label,
|
||
|
|
type,
|
||
|
|
} = segmentationState;
|
||
|
|
|
||
|
|
const labelmapRepresentationData = representationData[LABELMAP];
|
||
|
|
|
||
|
|
// TODO: handle other representations when available in cornerstone3D
|
||
|
|
const segmentationSchema = {
|
||
|
|
activeSegmentIndex,
|
||
|
|
cachedStats,
|
||
|
|
displayText: [],
|
||
|
|
id: segmentationId,
|
||
|
|
label,
|
||
|
|
segmentsLocked,
|
||
|
|
type,
|
||
|
|
volumeId: labelmapRepresentationData.volumeId,
|
||
|
|
};
|
||
|
|
|
||
|
|
try {
|
||
|
|
this.addOrUpdateSegmentation(segmentationSchema);
|
||
|
|
} catch (error) {
|
||
|
|
console.warn(
|
||
|
|
`Failed to add/update segmentation ${segmentationId}`,
|
||
|
|
error
|
||
|
|
);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
private _getSegmentationInfo(segmentationId: string, toolGroupId: string) {
|
||
|
|
const segmentation = this.getSegmentation(segmentationId);
|
||
|
|
|
||
|
|
if (segmentation === undefined) {
|
||
|
|
throw new Error(`no segmentation for segmentationId: ${segmentationId}`);
|
||
|
|
}
|
||
|
|
const segmentationRepresentation = this._getSegmentationRepresentation(
|
||
|
|
segmentationId,
|
||
|
|
toolGroupId
|
||
|
|
);
|
||
|
|
|
||
|
|
if (!segmentationRepresentation) {
|
||
|
|
throw new Error(
|
||
|
|
'Must add representation to toolgroup before setting segments'
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
const { segmentationRepresentationUID } = segmentationRepresentation;
|
||
|
|
|
||
|
|
return { segmentationRepresentationUID, segmentation };
|
||
|
|
}
|
||
|
|
|
||
|
|
private _removeSegmentationFromCornerstone(segmentationId: string) {
|
||
|
|
// TODO: This should be from the configuration
|
||
|
|
const removeFromCache = true;
|
||
|
|
const segmentationState = cstSegmentation.state;
|
||
|
|
const sourceSegState = segmentationState.getSegmentation(segmentationId);
|
||
|
|
|
||
|
|
if (!sourceSegState) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
const toolGroupIds = segmentationState.getToolGroupIdsWithSegmentation(
|
||
|
|
segmentationId
|
||
|
|
);
|
||
|
|
|
||
|
|
toolGroupIds.forEach(toolGroupId => {
|
||
|
|
const segmentationRepresentations = segmentationState.getSegmentationRepresentations(
|
||
|
|
toolGroupId
|
||
|
|
);
|
||
|
|
|
||
|
|
const UIDsToRemove = [];
|
||
|
|
segmentationRepresentations.forEach(representation => {
|
||
|
|
if (representation.segmentationId === segmentationId) {
|
||
|
|
UIDsToRemove.push(representation.segmentationRepresentationUID);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
// remove segmentation representations
|
||
|
|
cstSegmentation.removeSegmentationsFromToolGroup(
|
||
|
|
toolGroupId,
|
||
|
|
UIDsToRemove,
|
||
|
|
true // immediate
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
// cleanup the segmentation state too
|
||
|
|
segmentationState.removeSegmentation(segmentationId);
|
||
|
|
|
||
|
|
if (removeFromCache) {
|
||
|
|
cache.removeVolumeLoadObject(segmentationId);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private _updateCornerstoneSegmentations({
|
||
|
|
segmentationId,
|
||
|
|
notYetUpdatedAtSource,
|
||
|
|
}) {
|
||
|
|
if (notYetUpdatedAtSource === false) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
const segmentationState = cstSegmentation.state;
|
||
|
|
const sourceSegmentation = segmentationState.getSegmentation(
|
||
|
|
segmentationId
|
||
|
|
);
|
||
|
|
const segmentation = this.segmentations[segmentationId];
|
||
|
|
const { label, cachedStats } = segmentation;
|
||
|
|
|
||
|
|
// Update the label in the source if necessary
|
||
|
|
if (sourceSegmentation.label !== label) {
|
||
|
|
sourceSegmentation.label = label;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (!isEqual(sourceSegmentation.cachedStats, cachedStats)) {
|
||
|
|
sourceSegmentation.cachedStats = cachedStats;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private _updateCornerstoneSegmentationVisibility = segmentationId => {
|
||
|
|
const segmentationState = cstSegmentation.state;
|
||
|
|
const toolGroupIds = segmentationState.getToolGroupIdsWithSegmentation(
|
||
|
|
segmentationId
|
||
|
|
);
|
||
|
|
|
||
|
|
toolGroupIds.forEach(toolGroupId => {
|
||
|
|
const segmentationRepresentations = cstSegmentation.state.getSegmentationRepresentations(
|
||
|
|
toolGroupId
|
||
|
|
);
|
||
|
|
|
||
|
|
if (segmentationRepresentations.length === 0) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Todo: this finds the first segmentation representation that matches the segmentationId
|
||
|
|
// If there are two labelmap representations from the same segmentation, this will not work
|
||
|
|
const representation = segmentationRepresentations.find(
|
||
|
|
representation => representation.segmentationId === segmentationId
|
||
|
|
);
|
||
|
|
|
||
|
|
const visibility = cstSegmentation.config.visibility.getSegmentationVisibility(
|
||
|
|
toolGroupId,
|
||
|
|
representation.segmentationRepresentationUID
|
||
|
|
);
|
||
|
|
|
||
|
|
cstSegmentation.config.visibility.setSegmentationVisibility(
|
||
|
|
toolGroupId,
|
||
|
|
representation.segmentationRepresentationUID,
|
||
|
|
!visibility
|
||
|
|
);
|
||
|
|
|
||
|
|
// set all segments to visible as well
|
||
|
|
const segments = this.getSegmentation(segmentationId).segments;
|
||
|
|
Object.keys(segments).forEach(segmentIndex => {
|
||
|
|
if (segmentIndex !== '0') {
|
||
|
|
this._setSegmentVisibility(
|
||
|
|
segmentationId,
|
||
|
|
Number(segmentIndex),
|
||
|
|
!visibility,
|
||
|
|
toolGroupId
|
||
|
|
);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
});
|
||
|
|
};
|
||
|
|
|
||
|
|
private _getToolGroupIdsWithSegmentation(segmentationId: string) {
|
||
|
|
const segmentationState = cstSegmentation.state;
|
||
|
|
const toolGroupIds = segmentationState.getToolGroupIdsWithSegmentation(
|
||
|
|
segmentationId
|
||
|
|
);
|
||
|
|
|
||
|
|
return toolGroupIds;
|
||
|
|
}
|
||
|
|
|
||
|
|
private _getFirstToolGroupId = () => {
|
||
|
|
const { ToolGroupService } = this.servicesManager.services;
|
||
|
|
const toolGroupIds = ToolGroupService.getToolGroupIds();
|
||
|
|
|
||
|
|
return toolGroupIds[0];
|
||
|
|
};
|
||
|
|
|
||
|
|
private getNextColorLUTIndex = (): number => {
|
||
|
|
let i = 0;
|
||
|
|
while (true) {
|
||
|
|
if (cstSegmentation.state.getColorLUT(i) === undefined) {
|
||
|
|
return i;
|
||
|
|
}
|
||
|
|
|
||
|
|
i++;
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
private generateNewColorLUT() {
|
||
|
|
const newColorLUT = cloneDeep(COLOR_LUT);
|
||
|
|
|
||
|
|
return newColorLUT;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Converts object of objects to array.
|
||
|
|
*
|
||
|
|
* @return {Array} Array of objects
|
||
|
|
*/
|
||
|
|
private arrayOfObjects = obj => {
|
||
|
|
return Object.entries(obj).map(e => ({ [e[0]]: e[1] }));
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
export default SegmentationService;
|
||
|
|
export { EVENTS, VALUE_TYPES };
|