feat: Add DICOM SEG support and MPR, referenceLines and StackSync and more (#3015)

* feat: add initial sop class handler for SEG

* feat: move segmentation service

* update segmentation service methods

* fix: viewport data structure

* feat: Add initial render for the DICOM SEG for each displaySet

* fix: various wrong architectural dependencies between services

* feat: initial separate SEG display in each viewport

* feat: refactore viewport action bar

* initial work for SEG hydration

* fix: various bugs regarding drag and dropping different viewports

* fix: rendering issues for multiple seg displaysets

* fix: bugs for thumbnail and hydration

* feat: fix the initial segment color

* update after rebase

* feat: initial design for the segmentation group table

* feat: initial new design for the segmentation panel

* feat: segmentation panel

* feat: make segmentation appear on all related viewports

* fix: segmentation load bug based on functional groups

* initial work for segmentation crosshairs

* fix: various stylings and functionality

* fix: various stylings for the seg panel

* fix: overflow styles

* feat: add more ui components

* feat: added segmentation config

* feat: add jump to segment

* feat: add jump to segment for DICOM SEG viewport

* fix: bugs after rebase

* feat: jump in segmentation viewport

* fix: mpr support for seg

* feat: add more icons

* feat: new icons

* feat: add new side panel

* feat: add segmentation config

* feat: add loading indicator to ohif

* feat: make hanging protocols follow matching rules for viewports

* feat: enhance drag and drop to be hanging protocol aware

* fix: mpr restore previous layout

* fix: crosshairs toggle

* fix: add auth headers to the dicom loader via dicomwebclient

* fix: bug for crosshairs toggle in mpr

* fix: seg viewport reusing old toolGroup

* feat: add loading animation with lottie

* fix: various bugs for Segmentation hydration in mpr

* feat: change outline alpha to outline opacity

* feat: loading indicator for seg viewport

* fix: various segmentation group styles

* fix: local mode for seg

* feat: add animation for the highlight

* fix: loading indicatro to show segment indices

* feat: enhance modality drop down ui

* fix: panels

* fix: download form

* fix: layout shift in loading indicator

* fix: loading indicator to have correct values

* fix: update software number

* fix: image jump between MPR and default

* fix: segmentation cleanup and cine service cleanup

* fix: segmentation toolgroup clena up

* fix: highlight interval should not trigger again

* fix: issue with multiframe sorting

* rename: change onSeriesChange to onArrowsClick

* fix: buttons for tmtv and layout shift

* fix: various bugs wrt crosshairs

* fix: crosshairs re init on reset camera

* fix: middle slice calculation different from cs middle reset camera

* fix: reset camera should reset viewport camera

* fix: loading segments in MPR mode

* fix: tmtv hp back to before

* fix: layout shift

* feat: orientation markers for volume viewport

* fix: capture for volume viewports

* fix: memory leak for back to worklist

* fix: loading bar bg color

* fix: side panel for only one panel

* fix: react select style in production

* fix: various styling for segmentation groups

* fix: various ui styles

* feat: add new segmentation config styles

* fix: hover state for segmentation item

* fix: side panel layout shift

* temp add panels

* try to fix scrollbar for thimbnail

* fix: scroll not appearing for side panels

* feat: changed styles for scrollbar

* feat: add cpu fallback warning

* fix: webworker destroy

* fix: select styles

* fix: orientation marker color and position

* fix: capture screenshot for volume viewports

* fix: hide segment visibility on mpr

* fix: reloading an already loaded seg displayset

* feat: add is equal

* fix: mpr jump to measurements

* apply review comments

* fix: optimization for the segmentation load

* fix: remove unnecessary context menu and hp service reset

* fix: segmentation service wrt brush settings

* feat: initial work for the stack synchronization

* fix: add validateDisplaySetSelectorsForNewDisplaySets to make sure drag and drop can follow requirements

* fix: various react proptypes

* fix: thumbnails for the tmtv and change default props to default params

* feat: make showing loading indicator configurable and add docs

* bump versions and add more docs

* fix demo

* update cornerstone versions

* apply review comments

* feat: add reference lines

* fix build

* fix unit tests

* add reference lines icon

* fix: e2e tests

* fix static wado config

* docs: add segmentation service docs

* fix: docker build

* fix: keep camera and bump versions

* Try re-enabling minification to fix deploy previews

Co-authored-by: Erik Ziegler <erik.sweed@gmail.com>
This commit is contained in:
AlirezaandErik Ziegler authored and GitHub committed 2022-11-16 14:19:52 -05:00
1 parent 08c58b0634
commit 83ea228d86
254 files changed
+12856 -3141

No files matched your search

@@ -0,0 +1,252 @@
import { cache as cs3DCache, Enums, volumeLoader } from '@cornerstonejs/core';
import { utils } from '@ohif/core';
import getCornerstoneViewportType from '../../utils/getCornerstoneViewportType';
import {
StackViewportData,
VolumeViewportData,
} from '../../types/CornerstoneCacheService';
const VOLUME_IMAGE_LOADER_SCHEME = 'streaming-wadors';
const VOLUME_LOADER_SCHEME = 'cornerstoneStreamingImageVolume';
class CornerstoneCacheService {
stackImageIds: Map<string, string[]> = new Map();
volumeImageIds: Map<string, string[]> = new Map();
constructor(servicesManager) {
this.servicesManager = servicesManager;
}
public getCacheSize() {
return cs3DCache.getCacheSize();
}
public getCacheFreeSpace() {
return cs3DCache.getBytesAvailable();
}
public async createViewportData(
displaySets: unknown[],
viewportOptions: Record<string, unknown>,
dataSource: unknown,
initialImageIndex?: number
): Promise<StackViewportData | VolumeViewportData> {
let viewportType = viewportOptions.viewportType as string;
// Todo: Since Cornerstone 3D currently doesn't support segmentation
// on stack viewport, we should check if whether the the displaySets
// that are about to be displayed are referenced in a segmentation
// as a reference volume, if so, we should hang a volume viewport
// instead of a stack viewport
if (this._shouldRenderSegmentation(displaySets)) {
viewportType = 'volume';
// update viewportOptions to reflect the new viewport type
viewportOptions.viewportType = viewportType;
}
const cs3DViewportType = getCornerstoneViewportType(viewportType);
let viewportData: StackViewportData | VolumeViewportData;
if (cs3DViewportType === Enums.ViewportType.STACK) {
viewportData = await this._getStackViewportData(
dataSource,
displaySets,
initialImageIndex
);
}
if (cs3DViewportType === Enums.ViewportType.ORTHOGRAPHIC) {
viewportData = await this._getVolumeViewportData(dataSource, displaySets);
}
viewportData.viewportType = cs3DViewportType;
return viewportData;
}
public async invalidateViewportData(
viewportData: VolumeViewportData,
invalidatedDisplaySetInstanceUID: string,
dataSource,
DisplaySetService
) {
if (viewportData.viewportType === Enums.ViewportType.STACK) {
throw new Error('Invalidation of StackViewport is not supported yet');
}
// Todo: grab the volume and get the id from the viewport itself
const volumeId = `${VOLUME_LOADER_SCHEME}:${invalidatedDisplaySetInstanceUID}`;
const volume = cs3DCache.getVolume(volumeId);
if (volume) {
cs3DCache.removeVolumeLoadObject(volumeId);
}
const displaySets = viewportData.data.map(({ displaySetInstanceUID }) =>
DisplaySetService.getDisplaySetByUID(displaySetInstanceUID)
);
const newViewportData = await this._getVolumeViewportData(
dataSource,
displaySets
);
return newViewportData;
}
private _getStackViewportData(
dataSource,
displaySets,
initialImageIndex
): StackViewportData {
// For Stack Viewport we don't have fusion currently
const displaySet = displaySets[0];
let stackImageIds = this.stackImageIds.get(
displaySet.displaySetInstanceUID
);
if (!stackImageIds) {
stackImageIds = this._getCornerstoneStackImageIds(displaySet, dataSource);
this.stackImageIds.set(displaySet.displaySetInstanceUID, stackImageIds);
}
const { displaySetInstanceUID, StudyInstanceUID } = displaySet;
const StackViewportData: StackViewportData = {
viewportType: Enums.ViewportType.STACK,
data: {
StudyInstanceUID,
displaySetInstanceUID,
imageIds: stackImageIds,
},
};
if (typeof initialImageIndex === 'number') {
StackViewportData.data.initialImageIndex = initialImageIndex;
}
return StackViewportData;
}
private async _getVolumeViewportData(
dataSource,
displaySets
): Promise<VolumeViewportData> {
// Todo: Check the cache for multiple scenarios to see if we need to
// decache the volume data from other viewports or not
const volumeData = [];
for (const displaySet of displaySets) {
// Don't create volumes for the displaySets that have custom load
// function (e.g., SEG, RT, since they rely on the reference volumes
// and they take care of their own loading after they are created in their
// getSOPClassHandler method
if (displaySet.load && displaySet.load instanceof Function) {
const { UserAuthenticationService } = this.servicesManager.services;
const headers = UserAuthenticationService.getAuthorizationHeader();
await displaySet.load({ headers });
volumeData.push({
studyInstanceUID: displaySet.StudyInstanceUID,
displaySetInstanceUID: displaySet.displaySetInstanceUID,
});
// Todo: do some cache check and empty the cache if needed
continue;
}
const volumeLoaderSchema =
displaySet.volumeLoaderSchema ?? VOLUME_LOADER_SCHEME;
const volumeId = `${volumeLoaderSchema}:${displaySet.displaySetInstanceUID}`;
let volumeImageIds = this.volumeImageIds.get(
displaySet.displaySetInstanceUID
);
let volume = cs3DCache.getVolume(volumeId);
if (!volumeImageIds || !volume) {
volumeImageIds = this._getCornerstoneVolumeImageIds(
displaySet,
dataSource
);
volume = await volumeLoader.createAndCacheVolume(volumeId, {
imageIds: volumeImageIds,
});
this.volumeImageIds.set(
displaySet.displaySetInstanceUID,
volumeImageIds
);
}
volumeData.push({
StudyInstanceUID: displaySet.StudyInstanceUID,
displaySetInstanceUID: displaySet.displaySetInstanceUID,
volume,
volumeId,
imageIds: volumeImageIds,
});
}
return {
viewportType: Enums.ViewportType.ORTHOGRAPHIC,
data: volumeData,
};
}
private _shouldRenderSegmentation(displaySets) {
const { SegmentationService } = this.servicesManager.services;
const viewportDisplaySetInstanceUIDs = displaySets.map(
({ displaySetInstanceUID }) => displaySetInstanceUID
);
// check inside segmentations if any of them are referencing the displaySets
// that are about to be displayed
const segmentations = SegmentationService.getSegmentations();
for (const segmentation of segmentations) {
const segDisplaySetInstanceUID = segmentation.displaySetInstanceUID;
const shouldDisplaySeg = SegmentationService.shouldRenderSegmentation(
viewportDisplaySetInstanceUIDs,
segDisplaySetInstanceUID
);
if (shouldDisplaySeg) {
return true;
}
}
}
private _getCornerstoneStackImageIds(displaySet, dataSource): string[] {
return dataSource.getImageIdsForDisplaySet(displaySet);
}
private _getCornerstoneVolumeImageIds(displaySet, dataSource): string[] {
const stackImageIds = this._getCornerstoneStackImageIds(
displaySet,
dataSource
);
if (stackImageIds[0].startsWith('dicomfile')) {
return stackImageIds;
}
return stackImageIds.map(imageId => {
const imageURI = utils.imageIdToURI(imageId);
return `${VOLUME_IMAGE_LOADER_SCHEME}:${imageURI}`;
});
}
}
export default CornerstoneCacheService;
@@ -0,0 +1,10 @@
import CornerstoneCacheService from './CornerstoneCacheService';
export default function ExtendedCornerstoneCacheService(serviceManager) {
return {
name: 'CornerstoneCacheService',
create: ({ configuration = {} }) => {
return new CornerstoneCacheService(serviceManager);
},
};
}
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,88 @@
import { Enums as csToolsEnums, Types as cstTypes } from '@cornerstonejs/tools';
import { Types } from '@cornerstonejs/core';
type SegmentationConfig = cstTypes.LabelmapTypes.LabelmapConfig & {
renderInactiveSegmentations: boolean;
brushSize: number;
brushThresholdGate: number;
};
type Segment = {
// the label for the segment
label: string;
// the index of the segment in the segmentation
segmentIndex: number;
// the color of the segment
color: Types.Point3;
// the opacity of the segment
opacity: number;
// whether the segment is visible
isVisible: boolean;
// whether the segment is locked
isLocked: boolean;
};
type Segmentation = {
// active segment index is the index of the segment that is currently being edited.
activeSegmentIndex: number;
// colorLUTIndex is the index of the color LUT that is currently being used.
colorLUTIndex: number;
// if segmentation contains any data (often calculated from labelmap)
cachedStats: Record<string, any>;
// displayText is the text that is displayed on the segmentation panel (often derived from the data)
displayText?: string[];
// the id of the segmentation
id: string;
// if the segmentation is the active segmentation being used in the viewer
isActive: boolean;
// if the segmentation is visible in the viewer
isVisible: boolean;
// the label of the segmentation
label: string;
// the number of segments in the segmentation
segmentCount: number;
// the array of segments with their details, [null, segment1, segment2, ...]
segments: Array<Segment>;
// the set of segments that are locked
segmentsLocked: Array<number>;
// the segmentation representation type
type: csToolsEnums.SegmentationRepresentations;
// if labelmap, the id of the volume that the labelmap is associated with
volumeId?: string;
// whether the segmentation is hydrated or not (non-hydrated SEG -> temporary segmentation for display in SEG Viewport
// but hydrated SEG -> segmentation that is persisted in the store)
hydrated: boolean;
};
// Schema to generate a segmentation
type SegmentationSchema = {
// active segment index for the segmentation
activeSegmentIndex: number;
// statistics that are derived from the segmentation
cachedStats: Record<string, any>;
// the displayText for the segmentation in the panels
displayText?: string[];
// segmentation id
id: string;
// displaySetInstanceUID
displaySetInstanceUID: string;
// segmentation label
label: string;
// segment indices that are locked for the segmentation
segmentsLocked: Array<number>;
// the type of the segmentation (e.g., Labelmap etc.)
type: csToolsEnums.SegmentationRepresentations;
// the volume id of the volume that the labelmap is associated with, this only exists for the labelmap representation
volumeId: string;
// the referenced volumeURI for the segmentation
referencedVolumeURI: string;
// whether the segmentation is hydrated or not (non-hydrated SEG -> temporary segmentation for display in SEG Viewport
// but hydrated SEG -> segmentation that is persisted in the store)
hydrated: boolean;
// the number of segments in the segmentation
segmentCount: number;
// the array of segments with their details
segments: Array<Segment>;
};
export { SegmentationConfig, Segment, Segmentation, SegmentationSchema };
@@ -0,0 +1,10 @@
import SegmentationService from './SegmentationService';
export default function ExtendedSegmentationService(servicesManager) {
return {
name: 'SegmentationService',
create: ({ configuration = {} }) => {
return new SegmentationService({ servicesManager });
},
};
}
@@ -31,6 +31,7 @@ export type SyncGroup = {
const POSITION = 'cameraposition';
const VOI = 'voi';
const ZOOMPAN = 'zoompan';
const STACKIMAGE = 'stackimage';
const asSyncGroup = (syncGroup: string | SyncGroup): SyncGroup =>
typeof syncGroup === 'string' ? { type: syncGroup } : syncGroup;
@@ -43,6 +44,7 @@ export default class SyncGroupService {
[POSITION]: synchronizers.createCameraPositionSynchronizer,
[VOI]: synchronizers.createVOISynchronizer,
[ZOOMPAN]: synchronizers.createZoomPanSynchronizer,
[STACKIMAGE]: synchronizers.createStackImageSynchronizer,
};
constructor(serviceManager) {
@@ -72,7 +74,7 @@ export default class SyncGroupService {
* @param creator
*/
public setSynchronizer(type: string, creator: SyncCreator): void {
this.synchronizerCreators[type] = creator;
this.synchronizerCreators[type.toLowerCase()] = creator;
}
protected _getOrCreateSynchronizer(
@@ -91,20 +93,29 @@ export default class SyncGroupService {
public addViewportToSyncGroup(
viewportId: string,
renderingEngineId: string,
syncGroups?: (SyncGroup | string)[]
syncGroups?: SyncGroup | string | SyncGroup[] | string[]
): void {
if (!syncGroups || !syncGroups.length) {
if (!syncGroups) {
return;
}
syncGroups.forEach(syncGroup => {
const syncGroupsArray = Array.isArray(syncGroups)
? syncGroups
: [syncGroups];
syncGroupsArray.forEach(syncGroup => {
const syncGroupObj = asSyncGroup(syncGroup);
const { type, target = true, source = true, options = {} } = syncGroupObj;
const { id = type } = syncGroupObj;
const {
type,
target = true,
source = true,
options = {},
id = type,
} = syncGroupObj;
const synchronizer = this._getOrCreateSynchronizer(type, id, options);
synchronizer.setOptions(viewportId, options);
const viewportInfo = { viewportId, renderingEngineId };
if (target && source) {
synchronizer.add(viewportInfo);
@@ -123,11 +134,16 @@ export default class SyncGroupService {
public removeViewportFromSyncGroup(
viewportId: string,
renderingEngineId: string
renderingEngineId: string,
syncGroupId?: string
): void {
const synchronizers = SynchronizerManager.getAllSynchronizers();
synchronizers.forEach(synchronizer => {
const filteredSynchronizers = syncGroupId
? synchronizers.filter(s => s.id === syncGroupId)
: synchronizers;
filteredSynchronizers.forEach(synchronizer => {
if (!synchronizer) {
return;
}
@@ -72,7 +72,16 @@ export default class ToolGroupService {
this.toolGroupIds = new Set();
}
public disable(viewportId: string, renderingEngineId: string): void {
public destroyToolGroup(toolGroupId: string) {
ToolGroupManager.destroyToolGroup(toolGroupId);
this.toolGroupIds.delete(toolGroupId);
}
public removeViewportFromToolGroup(
viewportId: string,
renderingEngineId: string,
deleteToolGroupIfEmpty?: boolean
): void {
const toolGroup = ToolGroupManager.getToolGroupForViewport(
viewportId,
renderingEngineId
@@ -85,9 +94,10 @@ export default class ToolGroupService {
toolGroup.removeViewports(renderingEngineId, viewportId);
const viewportIds = toolGroup.getViewportIds();
// if (viewportIds.length === 0) {
// ToolGroupManager.destroyToolGroup(toolGroup.id);
// }
if (viewportIds.length === 0 && deleteToolGroupIfEmpty) {
ToolGroupManager.destroyToolGroup(toolGroup.id);
}
}
public addViewportToToolGroup(
@@ -110,7 +120,10 @@ export default class ToolGroupService {
toolGroup.addViewport(viewportId, renderingEngineId);
}
this._broadcastEvent(EVENTS.VIEWPORT_ADDED, { viewportId, toolGroupId });
this._broadcastEvent(EVENTS.VIEWPORT_ADDED, {
viewportId,
toolGroupId,
});
}
public createToolGroup(toolGroupId: string): Types.IToolGroup {
@@ -122,7 +135,9 @@ export default class ToolGroupService {
const toolGroup = ToolGroupManager.createToolGroup(toolGroupId);
this.toolGroupIds.add(toolGroupId);
this._broadcastEvent(EVENTS.TOOLGROUP_CREATED, { toolGroupId });
this._broadcastEvent(EVENTS.TOOLGROUP_CREATED, {
toolGroupId,
});
return toolGroup;
}
@@ -195,9 +210,11 @@ export default class ToolGroupService {
private _getToolNames(toolGroupTools: Tools): string[] {
const toolNames = [];
toolGroupTools.active.forEach(tool => {
toolNames.push(tool.toolName);
});
if (toolGroupTools.active) {
toolGroupTools.active.forEach(tool => {
toolNames.push(tool.toolName);
});
}
if (toolGroupTools.passive) {
toolGroupTools.passive.forEach(tool => {
toolNames.push(tool.toolName);
@@ -221,9 +238,12 @@ export default class ToolGroupService {
private _setToolsMode(toolGroup, tools) {
const { active, passive, enabled, disabled } = tools;
active.forEach(({ toolName, bindings }) => {
toolGroup.setToolActive(toolName, { bindings });
});
if (active) {
active.forEach(({ toolName, bindings }) => {
toolGroup.setToolActive(toolName, { bindings });
});
}
if (passive) {
passive.forEach(({ toolName }) => {
@@ -1,231 +0,0 @@
import {
cache as cs3DCache,
Enums,
Types,
volumeLoader,
} from '@cornerstonejs/core';
import { utils, pubSubServiceInterface } from '@ohif/core';
import getCornerstoneViewportType from '../../utils/getCornerstoneViewportType';
export type StackData = {
StudyInstanceUID: string;
displaySetInstanceUID: string;
imageIds: string[];
frameRate?: number;
isClip?: boolean;
initialImageIndex?: number | string | null;
viewportType: Enums.ViewportType;
};
export type VolumeData = {
StudyInstanceUID: string;
displaySetInstanceUIDs: string[]; // can have more than one displaySet (fusion)
imageIds: string[][]; // can have more than one imageId list (fusion)
volumes: Types.IVolume[];
viewportType: Enums.ViewportType;
};
const VOLUME_LOADER_SCHEME = 'streaming-wadors';
const EVENTS = {
VIEWPORT_DATA_CHANGED: 'event::cornerstone::viewportdatachanged',
};
export type IViewportData = StackData | VolumeData;
class CornerstoneCacheService {
stackImageIds: Map<string, string[]> = new Map();
volumeImageIds: Map<string, string[]> = new Map();
listeners: { [key: string]: (...args: any[]) => void } = {};
EVENTS: { [key: string]: string };
constructor() {
this.listeners = {};
this.EVENTS = EVENTS;
Object.assign(this, pubSubServiceInterface);
}
public getCacheSize() {
return cs3DCache.getCacheSize();
}
public getCacheFreeSpace() {
return cs3DCache.getBytesAvailable();
}
public async getViewportData(
viewportIndex: number,
displaySets: unknown[],
viewportType: string,
dataSource: unknown,
callback: (val: IViewportData) => unknown,
initialImageIndex?: number
): Promise<StackData | VolumeData> {
const cs3DViewportType = getCornerstoneViewportType(viewportType);
let viewportData: IViewportData;
if (cs3DViewportType === Enums.ViewportType.STACK) {
viewportData = await this._getStackViewportData(
dataSource,
displaySets,
initialImageIndex
);
}
if (cs3DViewportType === Enums.ViewportType.ORTHOGRAPHIC) {
viewportData = await this._getVolumeViewportData(dataSource, displaySets);
}
viewportData.viewportType = cs3DViewportType;
await callback(viewportData);
this._broadcastEvent(this.EVENTS.VIEWPORT_DATA_CHANGED, {
viewportData,
viewportIndex,
});
return viewportData;
}
public async invalidateViewportData(
viewportData: VolumeData,
invalidatedDisplaySetInstanceUID: string,
dataSource,
DisplaySetService
) {
if (viewportData.viewportType === Enums.ViewportType.STACK) {
throw new Error('Invalidation of StackViewport is not supported yet');
}
const volumeId = invalidatedDisplaySetInstanceUID;
const volume = cs3DCache.getVolume(volumeId);
if (volume) {
cs3DCache.removeVolumeLoadObject(volumeId);
}
const displaySets = viewportData.displaySetInstanceUIDs.map(
DisplaySetService.getDisplaySetByUID
);
const newViewportData = await this._getVolumeViewportData(
dataSource,
displaySets
);
return newViewportData;
}
private _getStackViewportData(
dataSource,
displaySets,
initialImageIndex
): StackData {
// For Stack Viewport we don't have fusion currently
const displaySet = displaySets[0];
let stackImageIds = this.stackImageIds.get(
displaySet.displaySetInstanceUID
);
if (!stackImageIds) {
stackImageIds = this._getCornerstoneStackImageIds(displaySet, dataSource);
this.stackImageIds.set(displaySet.displaySetInstanceUID, stackImageIds);
}
const { displaySetInstanceUID, StudyInstanceUID } = displaySet;
const stackData: StackData = {
StudyInstanceUID,
displaySetInstanceUID,
viewportType: Enums.ViewportType.STACK,
imageIds: stackImageIds,
};
if (typeof initialImageIndex === 'number') {
stackData.initialImageIndex = initialImageIndex;
}
return stackData;
}
private async _getVolumeViewportData(
dataSource,
displaySets
): Promise<VolumeData> {
// Check the cache for multiple scenarios to see if we need to
// decache the volume data from other viewports or not
const volumeImageIdsArray = [];
const volumes = [];
for (const displaySet of displaySets) {
const volumeId = displaySet.displaySetInstanceUID;
let volumeImageIds = this.volumeImageIds.get(
displaySet.displaySetInstanceUID
);
let volume = cs3DCache.getVolume(volumeId);
if (!volumeImageIds || !volume) {
volumeImageIds = this._getCornerstoneVolumeImageIds(
displaySet,
dataSource
);
volume = await volumeLoader.createAndCacheVolume(volumeId, {
imageIds: volumeImageIds,
});
this.volumeImageIds.set(
displaySet.displaySetInstanceUID,
volumeImageIds
);
}
volumeImageIdsArray.push(volumeImageIds);
volumes.push(volume);
}
// assert displaySets are from the same study
const { StudyInstanceUID } = displaySets[0];
const displaySetInstanceUIDs = [];
displaySets.forEach(displaySet => {
if (displaySet.StudyInstanceUID !== StudyInstanceUID) {
throw new Error('Display sets are not from the same study');
}
displaySetInstanceUIDs.push(displaySet.displaySetInstanceUID);
});
return {
StudyInstanceUID,
displaySetInstanceUIDs,
imageIds: volumeImageIdsArray,
viewportType: Enums.ViewportType.ORTHOGRAPHIC,
volumes,
};
}
private _getCornerstoneStackImageIds(displaySet, dataSource): string[] {
return dataSource.getImageIdsForDisplaySet(displaySet);
}
private _getCornerstoneVolumeImageIds(displaySet, dataSource): string[] {
const stackImageIds = this._getCornerstoneStackImageIds(
displaySet,
dataSource
);
return stackImageIds.map(imageId => {
const imageURI = utils.imageIdToURI(imageId);
return `${VOLUME_LOADER_SCHEME}:${imageURI}`;
});
}
}
const CacheService = new CornerstoneCacheService();
export default CacheService;
@@ -2,7 +2,6 @@ import { pubSubServiceInterface } from '@ohif/core';
import {
RenderingEngine,
StackViewport,
Enums,
Types,
getRenderingEngine,
utilities as csUtils,
@@ -18,7 +17,10 @@ import ViewportInfo, {
DisplaySetOptions,
PublicViewportOptions,
} from './Viewport';
import { StackData, VolumeData } from './CornerstoneCacheService';
import {
StackViewportData,
VolumeViewportData,
} from '../../types/CornerstoneCacheService';
import {
setColormap,
setLowerUpperColorTransferFunction,
@@ -27,8 +29,8 @@ import {
import JumpPresets from '../../utils/JumpPresets';
const EVENTS = {
VIEWPORT_INFO_CREATED:
'event::cornerstone::viewportservice:viewportinfocreated',
VIEWPORT_DATA_CHANGED:
'event::cornerstoneViewportService:viewportDataChanged',
};
/**
@@ -39,8 +41,7 @@ class CornerstoneViewportService implements IViewportService {
renderingEngine: Types.IRenderingEngine | null;
viewportsInfo: Map<number, ViewportInfo>;
viewportGridResizeObserver: ResizeObserver | null;
// TODO - get the right type here.
hangingProtocolService: object;
viewportsDisplaySets: Map<string, string[]> = new Map();
/**
* Service-specific
@@ -52,6 +53,7 @@ class CornerstoneViewportService implements IViewportService {
enableResizeDetector: true;
resizeRefreshRateMs: 200;
resizeRefreshMode: 'debounce';
servicesManager = null;
constructor(servicesManager) {
this.renderingEngine = null;
@@ -60,8 +62,7 @@ class CornerstoneViewportService implements IViewportService {
//
this.listeners = {};
this.EVENTS = EVENTS;
const { HangingProtocolService } = servicesManager.services;
this.hangingProtocolService = HangingProtocolService;
this.servicesManager = servicesManager;
Object.assign(this, pubSubServiceInterface);
//
}
@@ -71,7 +72,7 @@ class CornerstoneViewportService implements IViewportService {
* @param {*} viewportIndex
* @param {*} elementRef
*/
public enableElement(
public enableViewport(
viewportIndex: number,
viewportOptions: PublicViewportOptions,
elementRef: HTMLDivElement
@@ -87,6 +88,16 @@ class CornerstoneViewportService implements IViewportService {
return `viewport-${viewportIndex}`;
}
public getViewportIds(): string[] {
const viewportIds = [];
this.viewportsInfo.forEach(viewportInfo => {
viewportIds.push(viewportInfo.getViewportId());
});
return viewportIds;
}
/**
* It retrieves the renderingEngine if it does exist, or creates one otherwise
* @returns {RenderingEngine} rendering engine
@@ -112,10 +123,9 @@ class CornerstoneViewportService implements IViewportService {
*/
public resize() {
const immediate = true;
const resetPan = false;
const resetZoom = false;
const keepCamera = true;
this.renderingEngine.resize(immediate, resetPan, resetZoom);
this.renderingEngine.resize(immediate, keepCamera);
this.renderingEngine.render();
}
@@ -126,8 +136,9 @@ class CornerstoneViewportService implements IViewportService {
this._removeResizeObserver();
this.viewportGridResizeObserver = null;
this.renderingEngine.destroy();
this.viewportsDisplaySets.clear();
this.renderingEngine = null;
cache.purgeVolumeCache();
cache.purgeCache();
}
/**
@@ -142,12 +153,11 @@ class CornerstoneViewportService implements IViewportService {
}
const viewportId = viewportInfo.getViewportId();
this.renderingEngine.disableElement(viewportId);
this.viewportsInfo.delete(viewportIndex);
if (this.viewportsInfo.size === 0) {
this.destroy();
}
this.renderingEngine && this.renderingEngine.disableElement(viewportId);
this.viewportsInfo.get(viewportIndex).destroy();
this.viewportsInfo.delete(viewportIndex);
}
/**
@@ -158,9 +168,9 @@ class CornerstoneViewportService implements IViewportService {
* @param {*} dataSource
* @returns
*/
public setViewportDisplaySets(
public setViewportData(
viewportIndex: number,
viewportData: StackData | VolumeData,
viewportData: StackViewportData | VolumeViewportData,
publicViewportOptions: PublicViewportOptions,
publicDisplaySetOptions: DisplaySetOptions[]
): void {
@@ -179,8 +189,12 @@ class CornerstoneViewportService implements IViewportService {
viewportInfo.setViewportOptions(viewportOptions);
viewportInfo.setDisplaySetOptions(displaySetOptions);
viewportInfo.setViewportData(viewportData);
this._broadcastEvent(EVENTS.VIEWPORT_INFO_CREATED, viewportInfo);
this._broadcastEvent(this.EVENTS.VIEWPORT_DATA_CHANGED, {
viewportData,
viewportIndex,
});
const viewportId = viewportInfo.getViewportId();
const element = viewportInfo.getElement();
@@ -203,6 +217,7 @@ class CornerstoneViewportService implements IViewportService {
// renderingEngine is designed to be used like this. This will trigger
// ENABLED_ELEMENT again and again, which will run onEnableElement callbacks
renderingEngine.enableElement(viewportInput);
this._setDisplaySets(viewportId, viewportData, viewportInfo);
}
@@ -265,11 +280,18 @@ class CornerstoneViewportService implements IViewportService {
_setStackViewport(
viewport: Types.IStackViewport,
viewportData: StackData,
viewportData: StackViewportData,
viewportInfo: ViewportInfo
) {
const displaySetOptions = viewportInfo.getDisplaySetOptions();
const { imageIds, initialImageIndex } = viewportData;
const {
imageIds,
initialImageIndex,
displaySetInstanceUID,
} = viewportData.data;
this.viewportsDisplaySets.set(viewport.id, [displaySetInstanceUID]);
let initialImageIndexToUse = initialImageIndex;
@@ -331,7 +353,15 @@ class CornerstoneViewportService implements IViewportService {
}
if (preset === JumpPresets.Middle) {
return Math.floor(lastSliceIndex / 2);
// Note: this is a simple but yet very important formula.
// since viewport reset works with the middle slice
// if the below formula is not correct, on a viewport reset
// it will jump to a different slice than the middle one which
// was the initial slice, and we have some tools such as Crosshairs
// which rely on a relative camera modifications and those will break.
return lastSliceIndex % 2 === 0
? lastSliceIndex / 2
: (lastSliceIndex + 1) / 2;
}
return 0;
@@ -339,7 +369,7 @@ class CornerstoneViewportService implements IViewportService {
async _setVolumeViewport(
viewport: Types.IVolumeViewport,
viewportData: VolumeData,
viewportData: VolumeViewportData,
viewportInfo: ViewportInfo
): Promise<void> {
// TODO: We need to overhaul the way data sources work so requests can be made
@@ -355,21 +385,24 @@ class CornerstoneViewportService implements IViewportService {
// (This call may or may not create sub-requests for series metadata)
const volumeInputArray = [];
const displaySetOptionsArray = viewportInfo.getDisplaySetOptions();
const { hangingProtocolService } = this;
const { HangingProtocolService } = this.servicesManager.services;
for (let i = 0; i < viewportData.imageIds.length; i++) {
const imageIds = viewportData.imageIds[i];
const displaySetInstanceUID = viewportData.displaySetInstanceUIDs[i];
const displaySetOptions = displaySetOptionsArray[i];
const volumeToLoad = [];
const displaySetInstanceUIDs = [];
const volumeId = displaySetInstanceUID;
for (const [index, data] of viewportData.data.entries()) {
const { volume, imageIds, displaySetInstanceUID } = data;
// if (displaySet.needsRerendering) {
// console.warn('Removing volume from cache', volumeId);
// cache.removeVolumeLoadObject(volumeId);
// displaySet.needsRerendering = false;
// this.displaySetsNeedRerendering.add(displaySet.displaySetInstanceUID);
// }
displaySetInstanceUIDs.push(displaySetInstanceUID);
if (!volume) {
continue;
}
volumeToLoad.push(volume);
const displaySetOptions = displaySetOptionsArray[index];
const { volumeId } = volume;
const voiCallbacks = this._getVOICallbacks(volumeId, displaySetOptions);
@@ -386,18 +419,20 @@ class CornerstoneViewportService implements IViewportService {
});
}
this.viewportsDisplaySets.set(viewport.id, displaySetInstanceUIDs);
if (
hangingProtocolService.hasCustomImageLoadStrategy() &&
!hangingProtocolService.customImageLoadPerformed
HangingProtocolService.hasCustomImageLoadStrategy() &&
!HangingProtocolService.customImageLoadPerformed
) {
// delegate the volume loading to the hanging protocol service if it has a custom image load strategy
return hangingProtocolService.runImageLoadStrategy({
return HangingProtocolService.runImageLoadStrategy({
viewportId: viewport.id,
volumeInputArray,
});
}
viewportData.volumes.forEach(volume => {
volumeToLoad.forEach(volume => {
volume.load();
});
@@ -406,9 +441,98 @@ class CornerstoneViewportService implements IViewportService {
}
public async setVolumesForViewport(viewport, volumeInputArray) {
const {
DisplaySetService,
SegmentationService,
ToolGroupService,
} = this.servicesManager.services;
await viewport.setVolumes(volumeInputArray);
// load any secondary displaySets
const displaySetInstanceUIDs = this.viewportsDisplaySets.get(viewport.id);
const segDisplaySet = displaySetInstanceUIDs
.map(DisplaySetService.getDisplaySetByUID)
.find(displaySet => displaySet && displaySet.Modality === 'SEG');
if (segDisplaySet) {
const { referencedVolumeId } = segDisplaySet;
const referencedVolume = cache.getVolume(referencedVolumeId);
const segmentationId = segDisplaySet.displaySetInstanceUID;
const toolGroup = ToolGroupService.getToolGroupForViewport(viewport.id);
if (referencedVolume) {
SegmentationService.addSegmentationRepresentationToToolGroup(
toolGroup.id,
segmentationId
);
}
} else {
const toolGroup = ToolGroupService.getToolGroupForViewport(viewport.id);
const toolGroupSegmentationRepresentations =
SegmentationService.getSegmentationRepresentationsForToolGroup(
toolGroup.id
) || [];
// csToolsUtils.segmentation.triggerSegmentationRender(toolGroup.id);
// If the displaySet is not a SEG displaySet we assume it is a primary displaySet
// and we can look into hydrated segmentations to check if any of them are
// associated with the primary displaySet
// get segmentations only returns the hydrated segmentations
const segmentations = SegmentationService.getSegmentations();
for (const segmentation of segmentations) {
// if there is already a segmentation representation for this segmentation
// for this toolGroup, don't bother at all
if (
toolGroupSegmentationRepresentations.find(
representation => representation.segmentationId === segmentation.id
)
) {
continue;
}
// otherwise, check if the hydrated segmentations are in the same FOR
// as the primary displaySet, if so add the representation (since it was not there)
const { id: segDisplaySetInstanceUID } = segmentation;
const segFrameOfReferenceUID = this._getFrameOfReferenceUID(
segDisplaySetInstanceUID
);
let shouldDisplaySeg = false;
for (const displaySetInstanceUID of displaySetInstanceUIDs) {
const primaryFrameOfReferenceUID = this._getFrameOfReferenceUID(
displaySetInstanceUID
);
if (segFrameOfReferenceUID === primaryFrameOfReferenceUID) {
shouldDisplaySeg = true;
break;
}
}
if (shouldDisplaySeg) {
const toolGroup = ToolGroupService.getToolGroupForViewport(
viewport.id
);
SegmentationService.addSegmentationRepresentationToToolGroup(
toolGroup.id,
segmentation.id
);
}
}
}
const viewportInfo = this.getViewportInfo(viewport.id);
const toolGroup = ToolGroupService.getToolGroupForViewport(viewport.id);
csToolsUtils.segmentation.triggerSegmentationRender(toolGroup.id);
const initialImageOptions = viewportInfo.getInitialImageOptions();
if (
@@ -493,17 +617,21 @@ class CornerstoneViewportService implements IViewportService {
_setDisplaySets(
viewportId: string,
viewportData: StackData | VolumeData,
viewportData: StackViewportData | VolumeViewportData,
viewportInfo: ViewportInfo
): void {
const viewport = this.getCornerstoneViewport(viewportId);
if (viewport instanceof StackViewport) {
this._setStackViewport(viewport, viewportData as StackData, viewportInfo);
this._setStackViewport(
viewport,
viewportData as StackViewportData,
viewportInfo
);
} else if (viewport instanceof VolumeViewport) {
this._setVolumeViewport(
viewport,
viewportData as VolumeData,
viewportData as VolumeViewportData,
viewportInfo
);
} else {
@@ -559,6 +687,10 @@ class CornerstoneViewportService implements IViewportService {
} {
const viewportIndex = viewportInfo.getViewportIndex();
if (!publicViewportOptions.viewportId) {
publicViewportOptions.viewportId = this.getViewportId(viewportIndex);
}
// Creating a temporary viewportInfo to handle defaults
const newViewportInfo = new ViewportInfo(
viewportIndex,
@@ -578,6 +710,31 @@ class CornerstoneViewportService implements IViewportService {
displaySetOptions: newDisplaySetOptions,
};
}
_getFrameOfReferenceUID(displaySetInstanceUID) {
const { DisplaySetService } = this.servicesManager.services;
const displaySet = DisplaySetService.getDisplaySetByUID(
displaySetInstanceUID
);
if (!displaySet) {
return;
}
if (displaySet.frameOfReferenceUID) {
return displaySet.frameOfReferenceUID;
}
if (displaySet.Modality === 'SEG') {
const { instance } = displaySet;
return instance.FrameOfReferenceUID;
}
const { images } = displaySet;
if (images && images.length) {
return images[0].FrameOfReferenceUID;
}
}
}
export default function ExtendedCornerstoneViewportService(serviceManager) {
@@ -1,5 +1,5 @@
import { Types } from '@cornerstonejs/core';
import { StackData, VolumeData } from './CornerstoneCacheService';
import { StackData, VolumeData } from '../../types/CornerstoneCacheService';
import {
DisplaySetOptions,
PublicViewportOptions,
@@ -31,7 +31,7 @@ export interface IViewportService {
* @param {*} viewportIndex
* @param {*} elementRef
*/
enableElement(
enableViewport(
viewportIndex: number,
viewportOptions: ViewportOptions,
elementRef: HTMLDivElement
@@ -65,7 +65,7 @@ export interface IViewportService {
* @param {*} dataSource
* @returns
*/
setViewportDisplaySets(
setViewportData(
viewportIndex: number,
viewportData: StackData | VolumeData,
publicViewportOptions: PublicViewportOptions,
@@ -4,6 +4,10 @@ import getCornerstoneOrientation from '../../utils/getCornerstoneOrientation';
import getCornerstoneViewportType from '../../utils/getCornerstoneViewportType';
import JumpPresets from '../../utils/JumpPresets';
import { SyncGroup } from '../SyncGroupService/SyncGroupService';
import {
StackViewportData,
VolumeViewportData,
} from './CornerstoneCacheService';
export type InitialImageOptions = {
index?: number;
@@ -67,6 +71,7 @@ class ViewportInfo {
private element: HTMLDivElement;
private viewportOptions: ViewportOptions;
private displaySetOptions: Array<DisplaySetOptions>;
private viewportData: StackViewportData | VolumeViewportData;
private renderingEngineId: string;
constructor(viewportIndex: number, viewportId: string) {
@@ -76,6 +81,13 @@ class ViewportInfo {
this.setPublicDisplaySetOptions([{}]);
}
public destroy = (): void => {
this.element = null;
this.viewportData = null;
this.viewportOptions = null;
this.displaySetOptions = null;
};
public setRenderingEngineId(renderingEngineId: string): void {
this.renderingEngineId = renderingEngineId;
}
@@ -95,6 +107,16 @@ class ViewportInfo {
this.element = element;
}
public setViewportData(
viewportData: StackViewportData | VolumeViewportData
): void {
this.viewportData = viewportData;
}
public getViewportData(): StackViewportData | VolumeViewportData {
return this.viewportData;
}
public getViewportIndex(): number {
return this.viewportIndex;
}
@@ -118,6 +140,23 @@ class ViewportInfo {
this.setDisplaySetOptions(displaySetOptions);
}
public hasDisplaySet(displaySetInstanceUID: string): boolean {
// Todo: currently this does not work for non image & referenceImage displaySets.
// Since SEG and other derived displaySets are loaded in a different way, and not
// via cornerstoneViewportService
let viewportData = this.getViewportData();
if (viewportData.viewportType === Enums.ViewportType.ORTHOGRAPHIC) {
viewportData = viewportData as VolumeViewportData;
return viewportData.data.some(
({ displaySetInstanceUID: dsUID }) => dsUID === displaySetInstanceUID
);
}
viewportData = viewportData as StackViewportData;
return viewportData.data.displaySetInstanceUID === displaySetInstanceUID;
}
public setPublicViewportOptions(
viewportOptionsEntry: PublicViewportOptions
): void {
@@ -201,12 +240,21 @@ class ViewportInfo {
const displaySetOptions: Array<DisplaySetOptions> = [];
publicDisplaySetOptions.forEach(option => {
if (!option) {
option = {
blendMode: undefined,
slabThickness: undefined,
colormap: undefined,
voi: {},
voiInverted: false,
};
}
const blendMode = getCornerstoneBlendMode(option.blendMode);
displaySetOptions.push({
voi: option.voi || ({} as VOI),
voiInverted: option.voiInverted || false,
colormap: option.colormap || undefined,
voi: option.voi,
voiInverted: option.voiInverted,
colormap: option.colormap,
slabThickness: option.slabThickness,
blendMode,
});