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

+5 -5
View File
@@ -43,11 +43,11 @@
},
"dependencies": {
"@babel/runtime": "7.17.9",
"@cornerstonejs/core": "^0.16.8",
"@cornerstonejs/streaming-image-volume-loader": "^0.4.23",
"@cornerstonejs/tools": "^0.24.1",
"@kitware/vtk.js": "^24.18.7",
"dom-to-image": "^2.6.0",
"@cornerstonejs/core": "^0.21.0",
"@cornerstonejs/streaming-image-volume-loader": "^0.6.1",
"@cornerstonejs/tools": "^0.29.2",
"@kitware/vtk.js": "25.9.0",
"html2canvas": "^1.4.1",
"lodash.debounce": "4.0.8",
"lodash.merge": "^4.6.2",
"shader-loader": "^1.3.1",
@@ -8,10 +8,11 @@ import {
eventTarget,
getEnabledElement,
StackViewport,
utilities as csUtils,
CONSTANTS,
} from '@cornerstonejs/core';
import { setEnabledElement } from '../state';
import CornerstoneCacheService from '../services/ViewportService/CornerstoneCacheService';
import './OHIFCornerstoneViewport.css';
import CornerstoneOverlays from './Overlays/CornerstoneOverlays';
@@ -46,24 +47,44 @@ function areEqual(prevProps, nextProps) {
return false;
}
const prevDisplaySets = prevProps.displaySets[0];
const nextDisplaySets = nextProps.displaySets[0];
const prevDisplaySets = prevProps.displaySets;
const nextDisplaySets = nextProps.displaySets;
if (prevDisplaySets && nextDisplaySets) {
const areSameDisplaySetInstanceUIDs =
prevDisplaySets.displaySetInstanceUID ===
nextDisplaySets.displaySetInstanceUID;
const areSameImageLength =
prevDisplaySets.images.length === nextDisplaySets.images.length;
const areSameImageIds = prevDisplaySets.images.every(
(prevImage, index) =>
prevImage.imageId === nextDisplaySets.images[index].imageId
);
return (
areSameDisplaySetInstanceUIDs && areSameImageLength && areSameImageIds
);
if (prevDisplaySets.length !== nextDisplaySets.length) {
return false;
}
return false;
for (let i = 0; i < prevDisplaySets.length; i++) {
const prevDisplaySet = prevDisplaySets[i];
const foundDisplaySet = nextDisplaySets.find(
nextDisplaySet =>
nextDisplaySet.displaySetInstanceUID ===
prevDisplaySet.displaySetInstanceUID
);
if (!foundDisplaySet) {
return false;
}
// check they contain the same image
if (foundDisplaySet.images?.length !== prevDisplaySet.images?.length) {
return false;
}
// check if their imageIds are the same
if (foundDisplaySet.images?.length) {
for (let j = 0; j < foundDisplaySet.images.length; j++) {
if (
foundDisplaySet.images[j].imageId !== prevDisplaySet.images[j].imageId
) {
return false;
}
}
}
}
return true;
}
// Todo: This should be done with expose of internal API similar to react-vtkjs-viewport
@@ -85,8 +106,6 @@ const OHIFCornerstoneViewport = React.memo(props => {
} = props;
const [scrollbarHeight, setScrollbarHeight] = useState('100px');
const [viewportData, setViewportData] = useState(null);
const [_, viewportGridService] = useViewportGrid();
const elementRef = useRef();
@@ -97,6 +116,8 @@ const OHIFCornerstoneViewport = React.memo(props => {
ToolGroupService,
SyncGroupService,
CornerstoneViewportService,
CornerstoneCacheService,
ViewportGridService,
} = servicesManager.services;
// useCallback for scroll bar height calculation
@@ -153,7 +174,7 @@ const OHIFCornerstoneViewport = React.memo(props => {
// disable the element upon unmounting
useEffect(() => {
CornerstoneViewportService.enableElement(
CornerstoneViewportService.enableViewport(
viewportIndex,
viewportOptions,
elementRef.current
@@ -175,7 +196,11 @@ const OHIFCornerstoneViewport = React.memo(props => {
const renderingEngineId = viewportInfo.getRenderingEngineId();
const syncGroups = viewportInfo.getSyncGroups();
ToolGroupService.disable(viewportId, renderingEngineId);
ToolGroupService.removeViewportFromToolGroup(
viewportId,
renderingEngineId
);
SyncGroupService.removeViewportFromSyncGroup(
viewportId,
renderingEngineId,
@@ -184,14 +209,14 @@ const OHIFCornerstoneViewport = React.memo(props => {
CornerstoneViewportService.disableElement(viewportIndex);
if (onElementDisabled) {
onElementDisabled(viewportInfo);
}
eventTarget.removeEventListener(
Enums.Events.ELEMENT_ENABLED,
elementEnabledHandler
);
if (onElementDisabled) {
onElementDisabled();
}
};
}, []);
@@ -207,11 +232,12 @@ const OHIFCornerstoneViewport = React.memo(props => {
const { unsubscribe } = DisplaySetService.subscribe(
DisplaySetService.EVENTS.DISPLAY_SET_SERIES_METADATA_INVALIDATED,
async invalidatedDisplaySetInstanceUID => {
if (
viewportData.displaySetInstanceUIDs.includes(
invalidatedDisplaySetInstanceUID
)
) {
const viewportInfo = CornerstoneViewportService.getViewportInfoByIndex(
viewportIndex
);
if (viewportInfo.hasDisplaySet(invalidatedDisplaySetInstanceUID)) {
const viewportData = viewportInfo.getViewportData();
const newViewportData = await CornerstoneCacheService.invalidateViewportData(
viewportData,
invalidatedDisplaySetInstanceUID,
@@ -225,15 +251,13 @@ const OHIFCornerstoneViewport = React.memo(props => {
newViewportData,
keepCamera
);
setViewportData(newViewportData);
}
}
);
return () => {
unsubscribe();
};
}, [viewportData, viewportIndex]);
}, [viewportIndex]);
useEffect(() => {
// handle the default viewportType to be stack
@@ -242,22 +266,19 @@ const OHIFCornerstoneViewport = React.memo(props => {
}
const loadViewportData = async () => {
await CornerstoneCacheService.getViewportData(
viewportIndex,
const viewportData = await CornerstoneCacheService.createViewportData(
displaySets,
viewportOptions.viewportType,
viewportOptions,
dataSource,
viewportDataLoaded => {
CornerstoneViewportService.setViewportDisplaySets(
viewportIndex,
viewportDataLoaded,
viewportOptions,
displaySetOptions
);
setViewportData(viewportDataLoaded);
},
initialImageIndex
);
CornerstoneViewportService.setViewportData(
viewportIndex,
viewportData,
viewportOptions,
displaySetOptions
);
};
loadViewportData();
@@ -280,7 +301,8 @@ const OHIFCornerstoneViewport = React.memo(props => {
elementRef,
viewportIndex,
displaySets,
viewportGridService
ViewportGridService,
CornerstoneViewportService
);
_checkForCachedJumpToMeasurementEvents(
@@ -289,13 +311,14 @@ const OHIFCornerstoneViewport = React.memo(props => {
elementRef,
viewportIndex,
displaySets,
viewportGridService
ViewportGridService,
CornerstoneViewportService
);
return () => {
unsubscribeFromJumpToMeasurementEvents();
};
}, [displaySets, elementRef, viewportIndex, viewportData]);
}, [displaySets, elementRef, viewportIndex]);
return (
<div className="viewport-wrapper">
@@ -303,7 +326,7 @@ const OHIFCornerstoneViewport = React.memo(props => {
handleWidth
handleHeight
skipOnMount={true} // Todo: make these configurable
refreshMode={'debounce'}
refreshMode={'throttle'}
refreshRate={100}
onResize={onResize}
targetRef={elementRef.current}
@@ -332,7 +355,8 @@ function _subscribeToJumpToMeasurementEvents(
elementRef,
viewportIndex,
displaySets,
viewportGridService
viewportGridService,
CornerstoneViewportService
) {
const displaysUIDs = displaySets.map(
displaySet => displaySet.displaySetInstanceUID
@@ -350,7 +374,8 @@ function _subscribeToJumpToMeasurementEvents(
viewportIndex,
MeasurementService,
DisplaySetService,
viewportGridService
ViewportGridService,
CornerstoneViewportService
);
}
}
@@ -366,7 +391,8 @@ function _checkForCachedJumpToMeasurementEvents(
elementRef,
viewportIndex,
displaySets,
viewportGridService
ViewportGridService,
CornerstoneViewportService
) {
const displaysUIDs = displaySets.map(
displaySet => displaySet.displaySetInstanceUID
@@ -389,7 +415,8 @@ function _checkForCachedJumpToMeasurementEvents(
viewportIndex,
MeasurementService,
DisplaySetService,
viewportGridService
ViewportGridService,
CornerstoneViewportService
);
}
}
@@ -401,7 +428,8 @@ function _jumpToMeasurement(
viewportIndex,
MeasurementService,
DisplaySetService,
viewportGridService
ViewportGridService,
CornerstoneViewportService
) {
const targetElement = targetElementRef.current;
const { displaySetInstanceUID, SOPInstanceUID, frameNumber } = measurement;
@@ -418,14 +446,20 @@ function _jumpToMeasurement(
// to set it properly
// setCornerstoneMeasurementActive(measurement);
viewportGridService.setActiveViewportIndex(viewportIndex);
ViewportGridService.setActiveViewportIndex(viewportIndex);
const enableElement = getEnabledElement(targetElement);
const viewportInfo = CornerstoneViewportService.getViewportInfoByIndex(
viewportIndex
);
if (enableElement) {
// See how the jumpToSlice() of Cornerstone3D deals with imageIdx param.
const viewport = enableElement.viewport as IStackViewport | IVolumeViewport;
let imageIdIndex = 0;
let viewportCameraDirectionMatch = true;
if (viewport instanceof StackViewport) {
const imageIds = viewport.getImageIds();
@@ -440,9 +474,29 @@ function _jumpToMeasurement(
);
});
} else {
// for volume viewport we can't rely on the imageIdIndex since it can be
// a reconstructed view that doesn't match the original slice numbers etc.
const { viewPlaneNormal } = measurement.metadata;
imageIdIndex = referencedDisplaySet.images.findIndex(
i => i.SOPInstanceUID === SOPInstanceUID
);
const { orientation } = viewportInfo.getViewportOptions();
if (
orientation &&
viewPlaneNormal &&
!csUtils.isEqual(
CONSTANTS.MPR_CAMERA_VALUES[orientation]?.viewPlaneNormal,
viewPlaneNormal
)
) {
viewportCameraDirectionMatch = false;
}
}
if (!viewportCameraDirectionMatch || imageIdIndex === -1) {
return;
}
cs3DTools.utilities.jumpToSlice(targetElement, {
@@ -3,8 +3,7 @@ import React, { useEffect, useState } from 'react';
import ViewportImageScrollbar from './ViewportImageScrollbar';
import ViewportOverlay from './ViewportOverlay';
import ViewportOrientationMarkers from './ViewportOrientationMarkers';
import ViewportLoadingIndicator from './ViewportLoadingIndicator';
import CornerstoneCacheService from '../../services/ViewportService/CornerstoneCacheService';
import ViewportImageSliceLoadingIndicator from './ViewportImageSliceLoadingIndicator';
function CornerstoneOverlays(props) {
const { viewportIndex, element, scrollbarHeight, servicesManager } = props;
@@ -16,8 +15,8 @@ function CornerstoneOverlays(props) {
const [viewportData, setViewportData] = useState(null);
useEffect(() => {
const { unsubscribe } = CornerstoneCacheService.subscribe(
CornerstoneCacheService.EVENTS.VIEWPORT_DATA_CHANGED,
const { unsubscribe } = CornerstoneViewportService.subscribe(
CornerstoneViewportService.EVENTS.VIEWPORT_DATA_CHANGED,
props => {
if (props.viewportIndex !== viewportIndex) {
return;
@@ -47,7 +46,7 @@ function CornerstoneOverlays(props) {
}
return (
<div>
<div className="noselect">
<ViewportImageScrollbar
viewportIndex={viewportIndex}
viewportData={viewportData}
@@ -64,11 +63,15 @@ function CornerstoneOverlays(props) {
servicesManager={servicesManager}
element={element}
/>
<ViewportLoadingIndicator viewportData={viewportData} element={element} />
<ViewportImageSliceLoadingIndicator
viewportData={viewportData}
element={element}
/>
<ViewportOrientationMarkers
imageSliceData={imageSliceData}
element={element}
viewportData={viewportData}
servicesManager={servicesManager}
viewportIndex={viewportIndex}
/>
</div>
@@ -57,7 +57,7 @@ function CornerstoneImageScrollbar({
setImageSliceData({
imageIndex: imageIndex,
numberOfSlices: viewportData.imageIds.length,
numberOfSlices: viewportData.data.imageIds.length,
});
return;
@@ -87,7 +87,7 @@ function CornerstoneImageScrollbar({
// find the index of imageId in the imageIds
setImageSliceData({
imageIndex: newImageIdIndex,
numberOfSlices: viewportData.imageIds.length,
numberOfSlices: viewportData.data.imageIds.length,
});
};
@@ -2,7 +2,7 @@ import React, { useEffect, useState, useRef } from 'react';
import PropTypes from 'prop-types';
import { Enums } from '@cornerstonejs/core';
function ViewportLoadingIndicator({ viewportData, element }) {
function ViewportImageSliceLoadingIndicator({ viewportData, element }) {
const [loading, setLoading] = useState(false);
const [error, setError] = useState(false);
@@ -89,15 +89,15 @@ function ViewportLoadingIndicator({ viewportData, element }) {
return null;
}
ViewportLoadingIndicator.propTypes = {
ViewportImageSliceLoadingIndicator.propTypes = {
percentComplete: PropTypes.number,
error: PropTypes.object,
element: PropTypes.object,
};
ViewportLoadingIndicator.defaultProps = {
ViewportImageSliceLoadingIndicator.defaultProps = {
percentComplete: 0,
error: null,
};
export default ViewportLoadingIndicator;
export default ViewportImageSliceLoadingIndicator;
@@ -11,7 +11,7 @@
position: absolute;
}
.ViewportOrientationMarkers .top-mid {
top: 5px;
top: 0.6rem;
left: 50%;
}
.ViewportOrientationMarkers .left-mid {
@@ -23,7 +23,7 @@
left: calc(100% - var(--marker-width) - var(--scrollbar-width));
}
.ViewportOrientationMarkers .bottom-mid {
top: calc(100% - var(--marker-height) - 5px);
top: calc(100% - var(--marker-height) - 0.6rem);
left: 47%;
}
.ViewportOrientationMarkers .right-mid .orientation-marker-value {
@@ -1,10 +1,158 @@
import React, { useEffect, useState, useCallback } from 'react';
import PropTypes from 'prop-types';
import { metaData, Enums, Types } from '@cornerstonejs/core';
import React, { useEffect, useState, useMemo } from 'react';
import {
metaData,
Enums,
Types,
getEnabledElement,
utilities as csUtils,
} from '@cornerstonejs/core';
import { utilities } from '@cornerstonejs/tools';
import PropTypes from 'prop-types';
import { vec3 } from 'gl-matrix';
import './ViewportOrientationMarkers.css';
import { getEnabledElement } from '../../state';
const {
getOrientationStringLPS,
invertOrientationStringLPS,
} = utilities.orientation;
function ViewportOrientationMarkers({
element,
viewportData,
imageSliceData,
viewportIndex,
servicesManager,
orientationMarkers = ['top', 'left'],
}) {
// Rotation is in degrees
const [rotation, setRotation] = useState(0);
const [flipHorizontal, setFlipHorizontal] = useState(false);
const [flipVertical, setFlipVertical] = useState(false);
const { CornerstoneViewportService } = servicesManager.services;
useEffect(() => {
const cameraModifiedListener = (
evt: Types.EventTypes.CameraModifiedEvent
) => {
const { rotation, previousCamera, camera } = evt.detail;
if (rotation !== undefined) {
setRotation(rotation);
}
if (
camera.flipHorizontal !== undefined &&
previousCamera.flipHorizontal !== camera.flipHorizontal
) {
setFlipHorizontal(camera.flipHorizontal);
}
if (
camera.flipVertical !== undefined &&
previousCamera.flipVertical !== camera.flipVertical
) {
setFlipVertical(camera.flipVertical);
}
};
element.addEventListener(
Enums.Events.CAMERA_MODIFIED,
cameraModifiedListener
);
return () => {
element.removeEventListener(
Enums.Events.CAMERA_MODIFIED,
cameraModifiedListener
);
};
}, []);
const markers = useMemo(() => {
if (!viewportData) {
return '';
}
let rowCosines, columnCosines;
if (viewportData.viewportType === 'stack') {
const imageIndex = imageSliceData.imageIndex;
const imageId = viewportData.data.imageIds?.[imageIndex];
// Workaround for below TODO stub
if (!imageId) {
return false;
}
({ rowCosines, columnCosines } =
metaData.get('imagePlaneModule', imageId) || {});
} else {
if (!element || !getEnabledElement(element)) {
return '';
}
const { viewport } = getEnabledElement(element);
const { viewUp, viewPlaneNormal } = viewport.getCamera();
const viewRight = vec3.create();
vec3.cross(viewRight, viewUp, viewPlaneNormal);
columnCosines = [-viewUp[0], -viewUp[1], -viewUp[2]];
rowCosines = viewRight;
}
if (!rowCosines || !columnCosines || rotation === undefined) {
return '';
}
const markers = _getOrientationMarkers(
rowCosines,
columnCosines,
rotation,
flipVertical,
flipHorizontal
);
const ohifViewport = CornerstoneViewportService.getViewportInfoByIndex(
viewportIndex
);
const backgroundColor = ohifViewport.getViewportOptions().background;
const isLight = backgroundColor
? csUtils.isEqual(backgroundColor, [1, 1, 1])
: false;
return orientationMarkers.map((m, index) => (
<div
className={`${m}-mid orientation-marker`}
key={`${m}-mid orientation-marker`}
>
<div className="orientation-marker-value">{markers[m]}</div>
</div>
));
}, [
viewportData,
imageSliceData,
rotation,
flipVertical,
flipHorizontal,
orientationMarkers,
element,
]);
return <div className="ViewportOrientationMarkers noselect">{markers}</div>;
}
ViewportOrientationMarkers.propTypes = {
percentComplete: PropTypes.number,
error: PropTypes.object,
};
ViewportOrientationMarkers.defaultProps = {
percentComplete: 0,
error: null,
};
/**
*
@@ -16,17 +164,13 @@ import { getEnabledElement } from '../../state';
* @param {*} rotation in degrees
* @returns
*/
function getOrientationMarkers(
function _getOrientationMarkers(
rowCosines,
columnCosines,
rotation,
flipVertical,
flipHorizontal
) {
const {
getOrientationStringLPS,
invertOrientationStringLPS,
} = utilities.orientation;
const rowString = getOrientationStringLPS(rowCosines);
const columnString = getOrientationStringLPS(columnCosines);
const oppositeRowString = invertOrientationStringLPS(rowString);
@@ -79,117 +223,4 @@ function getOrientationMarkers(
return markers;
}
function ViewportOrientationMarkers({
element,
viewportData,
imageSliceData,
viewportIndex,
orientationMarkers = ['top', 'left'],
}) {
// Rotation is in degrees
const [rotation, setRotation] = useState(0);
const [flipHorizontal, setFlipHorizontal] = useState(false);
const [flipVertical, setFlipVertical] = useState(false);
useEffect(() => {
const cameraModifiedListener = (
evt: Types.EventTypes.CameraModifiedEvent
) => {
const { rotation, previousCamera, camera } = evt.detail;
if (rotation !== undefined) {
setRotation(rotation);
}
if (camera.flipHorizontal !== undefined &&
previousCamera.flipHorizontal !== camera.flipHorizontal) {
setFlipHorizontal(camera.flipHorizontal);
}
if (camera.flipVertical !== undefined &&
previousCamera.flipVertical !== camera.flipVertical) {
setFlipVertical(camera.flipVertical);
}
};
element.addEventListener(
Enums.Events.CAMERA_MODIFIED,
cameraModifiedListener
);
return () => {
element.removeEventListener(
Enums.Events.CAMERA_MODIFIED,
cameraModifiedListener
);
};
}, []);
const getMarkers = useCallback(
orientationMarkers => {
// Todo: support orientation markers for the volume viewports
if (
!viewportData ||
viewportData.viewportType === Enums.ViewportType.ORTHOGRAPHIC
) {
return '';
}
const imageIndex = imageSliceData.imageIndex;
const imageId = viewportData.imageIds?.[imageIndex];
// Workaround for below TODO stub
if (!imageId) {
return false;
}
const { rowCosines, columnCosines } =
metaData.get('imagePlaneModule', imageId) || {};
if (!rowCosines || !columnCosines || rotation === undefined) {
return false;
}
if (!rowCosines || !columnCosines) {
return '';
}
const markers = getOrientationMarkers(
rowCosines,
columnCosines,
rotation,
flipVertical,
flipHorizontal
);
return orientationMarkers.map((m, index) => (
<div
className={`${m}-mid orientation-marker`}
key={`${m}-mid orientation-marker`}
>
<div className="orientation-marker-value">{markers[m]}</div>
</div>
));
},
[flipHorizontal, flipVertical, rotation, viewportData, imageSliceData]
);
return (
<div className="ViewportOrientationMarkers noselect">
{getMarkers(orientationMarkers)}
</div>
);
}
ViewportOrientationMarkers.propTypes = {
percentComplete: PropTypes.number,
error: PropTypes.object,
};
ViewportOrientationMarkers.defaultProps = {
percentComplete: 0,
error: null,
};
export default ViewportOrientationMarkers;
@@ -28,6 +28,25 @@ function CornerstoneViewportOverlay({
setActiveTools(ToolBarService.getActiveTools());
}, []);
useEffect(() => {
let isMounted = true;
const { unsubscribe } = ToolBarService.subscribe(
ToolBarService.EVENTS.TOOL_BAR_STATE_MODIFIED,
() => {
if (!isMounted) {
return;
}
setActiveTools(ToolBarService.getActiveTools());
}
);
return () => {
isMounted = false;
unsubscribe();
};
}, []);
/**
* Updating the VOI when the viewport changes its voi
*/
@@ -100,23 +119,6 @@ function CornerstoneViewportOverlay({
};
}, [viewportIndex, viewportData]);
/**
* Updating the active tools when the toolbar changes
*/
// Todo: this should act on the toolGroups instead of the toolbar state
useEffect(() => {
const { unsubscribe } = ToolBarService.subscribe(
ToolBarService.EVENTS.TOOL_BAR_STATE_MODIFIED,
() => {
setActiveTools(ToolBarService.getActiveTools());
}
);
return () => {
unsubscribe();
};
}, [ToolBarService]);
const getTopLeftContent = useCallback(() => {
const { windowWidth, windowCenter } = voi;
@@ -186,12 +188,6 @@ function CornerstoneViewportOverlay({
return null;
}
if (viewportData.imageIds.length === 0) {
throw new Error(
'ViewportOverlay: only viewports with imageIds is supported at this time'
);
}
return (
<ViewportOverlay
topLeft={getTopLeftContent()}
@@ -201,7 +197,7 @@ function CornerstoneViewportOverlay({
}
function _getInstanceNumberFromStack(viewportData, imageIndex) {
const imageIds = viewportData.imageIds;
const imageIds = viewportData.data.imageIds;
const imageId = imageIds[imageIndex];
if (!imageId) {
+303 -105
View File
@@ -1,34 +1,44 @@
import {
getEnabledElement,
StackViewport,
volumeLoader,
cache,
utilities as csUtils,
} from '@cornerstonejs/core';
import {
ToolGroupManager,
Enums,
segmentation,
utilities as csToolsUtils,
utilities as cstUtils,
segmentation as cstSegmentation,
ReferenceLinesTool,
synchronizers as cstSynchronizers,
} from '@cornerstonejs/tools';
import { Types } from '@ohif/core';
import CornerstoneViewportDownloadForm from './utils/CornerstoneViewportDownloadForm';
import { getEnabledElement as OHIFgetEnabledElement } from './state';
import callInputDialog from './utils/callInputDialog';
import { setColormap } from './utils/colormap/transferFunctionHelpers';
import getProtocolViewportStructureFromGridViewports from './utils/getProtocolViewportStructureFromGridViewports';
import removeToolGroupSegmentationRepresentations from './utils/removeToolGroupSegmentationRepresentations';
import calculateViewportRegistrations from './utils/calculateViewportRegistrations';
const MPR_TOOLGROUP_ID = 'mpr';
// [ {
// synchronizerId: string,
// viewports: [ { viewportId: number, renderingEngineId: string, index: number } , ...]
// ]}
let STACK_IMAGE_SYNC_GROUPS_INFO = [];
const commandsModule = ({ servicesManager }) => {
const {
ViewportGridService,
ToolGroupService,
DisplaySetService,
SyncGroupService,
CineService,
ToolBarService,
UIDialogService,
CornerstoneViewportService,
SegmentationService,
DisplaySetService,
HangingProtocolService,
UINotificationService,
} = servicesManager.services;
@@ -133,34 +143,23 @@ const commandsModule = ({ servicesManager }) => {
});
viewport.render();
},
toggleCrosshairs({ toolGroupId, toggledState }) {
const toolName = 'Crosshairs';
// If it is Enabled
if (toggledState) {
actions.setToolActive({ toolName, toolGroupId });
return;
}
const toolGroup = _getToolGroup(toolGroupId);
if (!toolGroup) {
return;
}
toolGroup.setToolDisabled(toolName);
// Get the primary toolId from the ToolBarService and set it to active
// Since it was set to passive if not already active
const primaryActiveTool = ToolBarService.state.primaryToolId;
if (
toolGroup?.toolOptions[primaryActiveTool]?.mode ===
Enums.ToolModes.Passive
) {
toolGroup.setToolActive(primaryActiveTool, {
bindings: [{ mouseButton: Enums.MouseBindings.Primary }],
});
}
},
setToolActive: ({ toolName, toolGroupId = null }) => {
if (toolName === 'Crosshairs') {
const activeViewportToolGroup = _getToolGroup(null);
if (!activeViewportToolGroup._toolInstances.Crosshairs) {
UINotificationService.show({
title: 'Crosshairs',
message:
'You need to be in a MPR view to use Crosshairs. Click on MPR button in the toolbar to activate it.',
type: 'info',
duration: 3000,
});
throw new Error('Crosshairs tool is not available in this viewport');
}
}
const toolGroup = _getToolGroup(toolGroupId);
if (!toolGroup) {
@@ -298,8 +297,12 @@ const commandsModule = ({ servicesManager }) => {
if (viewport instanceof StackViewport) {
viewport.resetProperties();
viewport.resetCamera();
viewport.render();
} else {
// Todo: add reset properties for volume viewport
viewport.resetCamera();
}
viewport.render();
},
scaleViewport: ({ direction }) => {
const enabledElement = _getActiveViewportEnabledElement();
@@ -331,59 +334,7 @@ const commandsModule = ({ servicesManager }) => {
const { viewport } = enabledElement;
const options = { delta: direction };
csToolsUtils.scroll(viewport, options);
},
async createSegmentationForDisplaySet({ displaySetInstanceUID }) {
const volumeId = displaySetInstanceUID;
const segmentationUID = csUtils.uuidv4();
const segmentationId = `${volumeId}::${segmentationUID}`;
await volumeLoader.createAndCacheDerivedVolume(volumeId, {
volumeId: segmentationId,
});
// Add the segmentations to state
segmentation.addSegmentations([
{
segmentationId,
representation: {
// The type of segmentation
type: Enums.SegmentationRepresentations.Labelmap,
// The actual segmentation data, in the case of labelmap this is a
// reference to the source volume of the segmentation.
data: {
volumeId: segmentationId,
},
},
},
]);
return segmentationId;
},
async addSegmentationRepresentationToToolGroup({
segmentationId,
toolGroupId,
representationType,
}) {
// // Add the segmentation representation to the toolgroup
await segmentation.addSegmentationRepresentations(toolGroupId, [
{
segmentationId,
type: representationType,
},
]);
},
getLabelmapVolumes: ({ segmentations }) => {
if (!segmentations || !segmentations.length) {
segmentations = SegmentationService.getSegmentations();
}
const labelmapVolumes = segmentations.map(segmentation => {
return cache.getVolume(segmentation.id);
});
return labelmapVolumes;
cstUtils.scroll(viewport, options);
},
setViewportColormap: ({
viewportIndex,
@@ -398,7 +349,7 @@ const commandsModule = ({ servicesManager }) => {
const actorEntries = viewport.getActors();
const actorEntry = actorEntries.find(actorEntry => {
return actorEntry.uid === displaySetInstanceUID;
return actorEntry.uid.includes(displaySetInstanceUID);
});
const { actor: volumeActor } = actorEntry;
@@ -423,6 +374,254 @@ const commandsModule = ({ servicesManager }) => {
setHangingProtocol: ({ protocolId }) => {
HangingProtocolService.setProtocol(protocolId);
},
toggleMPR: ({ toggledState }) => {
const { activeViewportIndex, viewports } = ViewportGridService.getState();
const viewportDisplaySetInstanceUIDs =
viewports[activeViewportIndex].displaySetInstanceUIDs;
const errorCallback = error => {
UINotificationService.show({
title: 'Multiplanar reconstruction (MPR) ',
message:
'Cannot create MPR for this DisplaySet since it is not reconstructable.',
type: 'info',
duration: 3000,
});
};
const cacheId = 'beforeMPR';
if (toggledState) {
ViewportGridService.setCachedLayout({
cacheId,
cachedLayout: ViewportGridService.getState(),
});
const matchDetails = {
displaySetInstanceUIDs: viewportDisplaySetInstanceUIDs,
};
HangingProtocolService.setProtocol(
MPR_TOOLGROUP_ID,
matchDetails,
errorCallback
);
return;
}
const { cachedLayout } = ViewportGridService.getState();
if (!cachedLayout || !cachedLayout[cacheId]) {
return;
}
const { viewports: cachedViewports, numRows, numCols } = cachedLayout[
cacheId
];
// Todo: The following assumes that when turning off MPR we are applying the default
// protocol which might not be the one that was used before MPR was turned on
// In order to properly implement this logic, we should modify the hanging protocol
// upon layout change with layout selector, and cache and restore it when turning
// MPR on and off
const viewportStructure = getProtocolViewportStructureFromGridViewports({
viewports: cachedViewports,
numRows,
numCols,
});
const viewportSpecificMatch = cachedViewports.reduce(
(acc, viewport, index) => {
const {
displaySetInstanceUIDs,
viewportOptions,
displaySetOptions,
} = viewport;
acc[index] = {
displaySetInstanceUIDs,
viewportOptions,
displaySetOptions,
};
return acc;
},
{}
);
const defaultProtocol = HangingProtocolService.getProtocolById('default');
// Todo: this assumes there is only one stage in the default protocol
const defaultProtocolStage = defaultProtocol.stages[0];
defaultProtocolStage.viewportStructure = viewportStructure;
const { primaryToolId } = ToolBarService.state;
const mprToolGroup = _getToolGroup(MPR_TOOLGROUP_ID);
// turn off crosshairs if it is on
if (
primaryToolId === 'Crosshairs' ||
mprToolGroup.getToolInstance('Crosshairs')?.mode ===
Enums.ToolModes.Active
) {
const toolGroup = _getToolGroup(MPR_TOOLGROUP_ID);
toolGroup.setToolDisabled('Crosshairs');
ToolBarService.recordInteraction({
groupId: 'WindowLevel',
itemId: 'WindowLevel',
interactionType: 'tool',
commands: [
{
commandName: 'setToolActive',
commandOptions: {
toolName: 'WindowLevel',
},
context: 'CORNERSTONE',
},
],
});
}
// clear segmentations if they exist
removeToolGroupSegmentationRepresentations(MPR_TOOLGROUP_ID);
HangingProtocolService.setProtocol(
'default',
viewportSpecificMatch,
error => {
UINotificationService.show({
title: 'Multiplanar reconstruction (MPR) ',
message:
'Something went wrong while trying to restore the previous layout.',
type: 'info',
duration: 3000,
});
}
);
},
toggleStackImageSync: ({ toggledState }) => {
if (!toggledState) {
STACK_IMAGE_SYNC_GROUPS_INFO.forEach(syncGroupInfo => {
const { viewports, synchronizerId } = syncGroupInfo;
viewports.forEach(({ viewportId, renderingEngineId }) => {
SyncGroupService.removeViewportFromSyncGroup(
viewportId,
renderingEngineId,
synchronizerId
);
});
});
return;
}
STACK_IMAGE_SYNC_GROUPS_INFO = [];
// create synchronization groups and add viewports
let { viewports } = ViewportGridService.getState();
// filter empty viewports
viewports = viewports.filter(
viewport =>
viewport.displaySetInstanceUIDs &&
viewport.displaySetInstanceUIDs.length
);
// filter reconstructable viewports
viewports = viewports.filter(viewport => {
const { displaySetInstanceUIDs } = viewport;
for (const displaySetInstanceUID of displaySetInstanceUIDs) {
const displaySet = DisplaySetService.getDisplaySetByUID(
displaySetInstanceUID
);
if (displaySet && displaySet.isReconstructable) {
return true;
}
return false;
}
});
const viewportsByOrientation = viewports.reduce((acc, viewport) => {
const { viewportId, viewportType } = viewport.viewportOptions;
if (viewportType !== 'stack') {
console.warn('Viewport is not a stack, cannot sync images yet');
return acc;
}
const { element } = CornerstoneViewportService.getViewportInfo(
viewportId
);
const { viewport: csViewport, renderingEngineId } = getEnabledElement(
element
);
const { viewPlaneNormal } = csViewport.getCamera();
// Should we round here? I guess so, but not sure how much precision we need
const orientation = viewPlaneNormal.map(v => Math.round(v)).join(',');
if (!acc[orientation]) {
acc[orientation] = [];
}
acc[orientation].push({ viewportId, renderingEngineId });
return acc;
}, {});
// create synchronizer for each group
Object.values(viewportsByOrientation).map(viewports => {
let synchronizerId = viewports
.map(({ viewportId }) => viewportId)
.join(',');
synchronizerId = `imageSync_${synchronizerId}`;
calculateViewportRegistrations(viewports);
viewports.forEach(({ viewportId, renderingEngineId }) => {
SyncGroupService.addViewportToSyncGroup(
viewportId,
renderingEngineId,
{
type: 'stackimage',
id: synchronizerId,
source: true,
target: true,
}
);
});
STACK_IMAGE_SYNC_GROUPS_INFO.push({
synchronizerId,
viewports,
});
});
},
toggleReferenceLines: ({ toggledState }) => {
const { activeViewportIndex } = ViewportGridService.getState();
const viewportInfo = CornerstoneViewportService.getViewportInfoByIndex(
activeViewportIndex
);
const viewportId = viewportInfo.getViewportId();
const toolGroup = ToolGroupService.getToolGroupForViewport(viewportId);
if (!toggledState) {
toolGroup.setToolDisabled(ReferenceLinesTool.toolName);
}
toolGroup.setToolConfiguration(
ReferenceLinesTool.toolName,
{
sourceViewportId: viewportId,
},
true // overwrite
);
toolGroup.setToolEnabled(ReferenceLinesTool.toolName);
},
};
const definitions = {
@@ -524,22 +723,6 @@ const commandsModule = ({ servicesManager }) => {
storeContexts: [],
options: {},
},
createSegmentationForDisplaySet: {
commandFn: actions.createSegmentationForDisplaySet,
storeContexts: [],
options: {},
},
addSegmentationRepresentationToToolGroup: {
commandFn: actions.addSegmentationRepresentationToToolGroup,
storeContexts: [],
options: {},
},
getLabelmapVolumes: {
commandFn: actions.getLabelmapVolumes,
storeContexts: [],
options: {},
},
setViewportColormap: {
commandFn: actions.setViewportColormap,
storeContexts: [],
@@ -550,6 +733,21 @@ const commandsModule = ({ servicesManager }) => {
storeContexts: [],
options: {},
},
toggleMPR: {
commandFn: actions.toggleMPR,
storeContexts: [],
options: {},
},
toggleStackImageSync: {
commandFn: actions.toggleStackImageSync,
storeContexts: [],
options: {},
},
toggleReferenceLines: {
commandFn: actions.toggleReferenceLines,
storeContexts: [],
options: {},
},
};
return {
@@ -1,144 +1,140 @@
import { Types } from '@ohif/core';
const MPRHangingProtocolGenerator: Types.HangingProtocol.ProtocolGenerator = ({
servicesManager,
commandsManager,
}) => {
const {
ViewportGridService,
UINotificationService,
DisplaySetService,
} = servicesManager.services;
const { activeViewportIndex, viewports } = ViewportGridService.getState();
const viewportDisplaySetInstanceUIDs =
viewports[activeViewportIndex].displaySetInstanceUIDs;
if (
!viewportDisplaySetInstanceUIDs ||
!viewportDisplaySetInstanceUIDs.length
) {
return;
}
const displaySetsToHang = viewportDisplaySetInstanceUIDs.map(
displaySetInstanceUID => {
const displaySet = DisplaySetService.getDisplaySetByUID(
displaySetInstanceUID
);
return displaySet;
}
);
if (displaySetsToHang.some(ds => !ds.isReconstructable)) {
UINotificationService.show({
title: 'Multiplanar reconstruction (MPR) ',
message:
'Cannot create MPR for this series since it is not reconstructable.',
type: 'warning',
displayTime: 3000,
});
return;
}
const matchingDisplaySets = {};
displaySetsToHang.forEach(displaySet => {
const {
displaySetInstanceUID,
SeriesInstanceUID,
StudyInstanceUID,
} = displaySet;
matchingDisplaySets[displaySetInstanceUID] = {
displaySetInstanceUID,
SeriesInstanceUID,
StudyInstanceUID,
} as Types.HangingProtocol.DisplaySetMatchDetails;
});
const hpViewports: Types.HangingProtocol.Viewport[] = [
'axial',
'sagittal',
'coronal',
].map(viewportOrientation => {
return {
viewportOptions: {
toolGroupId: 'mpr',
viewportType: 'volume',
orientation: viewportOrientation,
initialImageOptions: {
preset: 'middle',
},
syncGroups: [
{
type: 'voi',
id: 'mpr',
source: true,
target: true,
const mpr = {
id: 'mpr',
locked: true,
hasUpdatedPriorsInformation: false,
name: 'mpr',
createdDate: '2021-02-23T19:22:08.894Z',
modifiedDate: '2022-10-04T19:22:08.894Z',
availableTo: {},
editableBy: {},
protocolMatchingRules: [],
displaySetSelectors: {
mprDisplaySet: {
seriesMatchingRules: [
{
weight: 1,
attribute: 'isReconstructable',
constraint: {
equals: {
value: true,
},
},
],
required: true,
},
],
},
},
stages: [
{
id: 'mpr3Stage',
name: 'mpr',
viewportStructure: {
layoutType: 'grid',
properties: {
rows: 1,
columns: 3,
layoutOptions: [
{
x: 0,
y: 0,
width: 1 / 3,
height: 1,
},
{
x: 1 / 3,
y: 0,
width: 1 / 3,
height: 1,
},
{
x: 2 / 3,
y: 0,
width: 1 / 3,
height: 1,
},
],
},
},
displaySets: viewportDisplaySetInstanceUIDs.map(displaySetInstanceUID => {
return {
id: displaySetInstanceUID,
};
}),
};
});
const protocol = {
id: 'mpr',
displaySetSelectors: {},
stages: [
{
id: 'mprStage',
name: 'mpr',
viewportStructure: {
layoutType: 'grid',
properties: {
rows: 1,
columns: 3,
layoutOptions: [
viewports: [
{
viewportOptions: {
toolGroupId: 'mpr',
viewportType: 'volume',
orientation: 'axial',
initialImageOptions: {
preset: 'middle',
},
syncGroups: [
{
x: 0,
y: 0,
width: 1 / 3,
height: 1,
},
{
x: 1 / 3,
y: 0,
width: 1 / 3,
height: 1,
},
{
x: 2 / 3,
y: 0,
width: 1 / 3,
height: 1,
type: 'voi',
id: 'mpr',
source: true,
target: true,
},
],
},
displaySets: [
{
id: 'mprDisplaySet',
},
],
},
viewports: hpViewports,
},
],
};
return {
protocol,
matchingDisplaySets,
};
{
viewportOptions: {
toolGroupId: 'mpr',
viewportType: 'volume',
orientation: 'sagittal',
initialImageOptions: {
preset: 'middle',
},
syncGroups: [
{
type: 'voi',
id: 'mpr',
source: true,
target: true,
},
],
},
displaySets: [
{
id: 'mprDisplaySet',
},
],
},
{
viewportOptions: {
toolGroupId: 'mpr',
viewportType: 'volume',
orientation: 'coronal',
initialImageOptions: {
preset: 'middle',
},
syncGroups: [
{
type: 'voi',
id: 'mpr',
source: true,
target: true,
},
],
},
displaySets: [
{
id: 'mprDisplaySet',
},
],
},
],
},
],
};
function getHangingProtocolModule() {
return [
{
id: 'mpr',
protocol: MPRHangingProtocolGenerator,
protocol: mpr,
},
];
}
+12 -2
View File
@@ -7,18 +7,22 @@ import {
imageRetrievalPoolManager,
} from '@cornerstonejs/core';
import { Enums as cs3DToolsEnums } from '@cornerstonejs/tools';
import init from './init.js';
import init from './init';
import commandsModule from './commandsModule';
import getHangingProtocolModule from './getHangingProtocolModule';
import ToolGroupService from './services/ToolGroupService';
import SyncGroupService from './services/SyncGroupService';
import SegmentationService from './services/SegmentationService';
import CornerstoneCacheService from './services/CornerstoneCacheService';
import { toolNames } from './initCornerstoneTools';
import { getEnabledElement } from './state';
import { getEnabledElement, reset as enabledElementReset } from './state';
import CornerstoneViewportService from './services/ViewportService/CornerstoneViewportService';
import dicomLoaderService from './utils/dicomLoaderService';
import { registerColormap } from './utils/colormap/transferFunctionHelpers';
import { id } from './id';
import * as csWADOImageLoader from './initWADOImageLoader.js';
const Component = React.lazy(() => {
return import(
@@ -50,6 +54,9 @@ const cornerstoneExtension = {
imageLoadPoolManager.clearRequestStack(type);
imageRetrievalPoolManager.clearRequestStack(type);
});
csWADOImageLoader.destroy();
enabledElementReset();
},
/**
@@ -69,6 +76,9 @@ const cornerstoneExtension = {
);
servicesManager.registerService(ToolGroupService(servicesManager));
servicesManager.registerService(SyncGroupService(servicesManager));
servicesManager.registerService(SegmentationService(servicesManager));
servicesManager.registerService(CornerstoneCacheService(servicesManager));
await init({ servicesManager, commandsManager, configuration, appConfig });
},
getHangingProtocolModule,
@@ -1,4 +1,5 @@
import OHIF from '@ohif/core';
import React from 'react';
import { ContextMenuMeasurements } from '@ohif/ui';
import * as cornerstone from '@cornerstonejs/core';
@@ -13,7 +14,7 @@ import {
imageLoadPoolManager,
Settings,
} from '@cornerstonejs/core';
import { Enums, utilities } from '@cornerstonejs/tools';
import { Enums, utilities, ReferenceLinesTool } from '@cornerstonejs/tools';
import {
cornerstoneStreamingImageVolumeLoader,
sharedArrayBufferImageLoader,
@@ -27,7 +28,6 @@ import callInputDialog from './utils/callInputDialog';
import initCineService from './initCineService';
import interleaveCenterLoader from './utils/interleaveCenterLoader';
import interleaveTopToBottom from './utils/interleaveTopToBottom';
import initSegmentationService from './initSegmentationService';
const cs3DToolsEvents = Enums.Events;
@@ -59,25 +59,48 @@ export default async function init({
initCornerstoneTools();
Settings.getRuntimeSettings().set('useCursors', Boolean(appConfig.useCursors));
Settings.getRuntimeSettings().set(
'useCursors',
Boolean(appConfig.useCursors)
);
const {
UserAuthenticationService,
ToolGroupService,
MeasurementService,
DisplaySetService,
UIDialogService,
UIModalService,
CineService,
CornerstoneViewportService,
HangingProtocolService,
ToolGroupService,
SegmentationService,
ViewportGridService,
} = servicesManager.services;
window.SegmentationService = SegmentationService;
window.DisplaySetService = DisplaySetService;
window.services = servicesManager.services;
if (cornerstone.getShouldUseCPURendering()) {
_showCPURenderingModal(UIModalService, HangingProtocolService);
}
const labelmapRepresentation =
cornerstoneTools.Enums.SegmentationRepresentations.Labelmap;
cornerstoneTools.segmentation.config.setGlobalRepresentationConfig(
labelmapRepresentation,
{
fillAlpha: 0.3,
fillAlphaInactive: 0.2,
outlineOpacity: 1,
outlineOpacityInactive: 0.65,
}
);
const metadataProvider = OHIF.classes.MetadataProvider;
volumeLoader.registerUnknownVolumeLoader(
cornerstoneStreamingImageVolumeLoader
);
volumeLoader.registerVolumeLoader(
'cornerstoneStreamingImageVolume',
cornerstoneStreamingImageVolumeLoader
@@ -114,8 +137,6 @@ export default async function init({
CornerstoneViewportService
);
initSegmentationService(SegmentationService, CornerstoneViewportService);
initCineService(CineService);
const _getDefaultPosition = event => ({
@@ -137,8 +158,8 @@ export default async function init({
currentPoints.canvas
);
let menuItems = [];
if (nearbyToolData) {
const menuItems = [];
if (nearbyToolData && nearbyToolData.metadata.toolName !== 'Crosshairs') {
defaultMenuItems.forEach(item => {
item.value = nearbyToolData;
item.element = element;
@@ -266,6 +287,32 @@ export default async function init({
utilities.stackPrefetch.enable(element);
};
const resetCrosshairs = evt => {
const { element } = evt.detail;
const { viewportId, renderingEngineId } = cornerstone.getEnabledElement(
element
);
const toolGroup = cornerstoneTools.ToolGroupManager.getToolGroupForViewport(
viewportId,
renderingEngineId
);
if (!toolGroup || !toolGroup._toolInstances?.['Crosshairs']) {
return;
}
const mode = toolGroup._toolInstances['Crosshairs'].mode;
if (mode === Enums.ToolModes.Active) {
toolGroup.setToolActive('Crosshairs');
} else if (mode === Enums.ToolModes.Passive) {
toolGroup.setToolPassive('Crosshairs');
} else if (mode === Enums.ToolModes.Enabled) {
toolGroup.setToolEnabled('Crosshairs');
}
};
function elementEnabledHandler(evt) {
const { element } = evt.detail;
@@ -274,6 +321,8 @@ export default async function init({
contextMenuHandleClick
);
element.addEventListener(EVENTS.CAMERA_RESET, resetCrosshairs);
eventTarget.addEventListener(
EVENTS.STACK_VIEWPORT_NEW_STACK,
newStackCallback
@@ -281,16 +330,15 @@ export default async function init({
}
function elementDisabledHandler(evt) {
const { viewportId, element } = evt.detail;
const viewportInfo = CornerstoneViewportService.getViewportInfo(viewportId);
ToolGroupService.disable(viewportInfo);
const { element } = evt.detail;
element.removeEventListener(
cs3DToolsEvents.MOUSE_CLICK,
contextMenuHandleClick
);
element.removeEventListener(EVENTS.CAMERA_RESET, resetCrosshairs);
// TODO - consider removing the callback when all elements are gone
// eventTarget.removeEventListener(
// EVENTS.STACK_VIEWPORT_NEW_STACK,
@@ -307,4 +355,74 @@ export default async function init({
EVENTS.ELEMENT_DISABLED,
elementDisabledHandler.bind(null)
);
ViewportGridService.subscribe(
ViewportGridService.EVENTS.ACTIVE_VIEWPORT_INDEX_CHANGED,
({ viewportIndex }) => {
const viewportId = `viewport-${viewportIndex}`;
const toolGroup = ToolGroupService.getToolGroupForViewport(viewportId);
if (!toolGroup) {
return;
}
// check if reference lines are active
const referenceLinesEnabled =
toolGroup._toolInstances?.['ReferenceLines'].mode ===
Enums.ToolModes.Enabled;
if (!referenceLinesEnabled) {
return;
}
toolGroup.setToolConfiguration(
ReferenceLinesTool.toolName,
{
sourceViewportId: viewportId,
},
true // overwrite
);
// make sure to set it to enabled again since we want to recalculate
// the source-target lines
toolGroup.setToolEnabled(ReferenceLinesTool.toolName);
}
);
}
function CPUModal() {
return (
<div>
<p>
Your computer does not have enough GPU power to support the default GPU
rendering mode. OHIF has switched to CPU rendering mode. Please note
that CPU rendering does not support all features such as Volume
Rendering, Multiplanar Reconstruction, and Segmentation Overlays.
</p>
</div>
);
}
function _showCPURenderingModal(UIModalService, HangingProtocolService) {
const callback = progress => {
if (progress === 100) {
UIModalService.show({
content: CPUModal,
title: 'OHIF Fell Back to CPU Rendering',
});
return true;
}
};
const { unsubscribe } = HangingProtocolService.subscribe(
HangingProtocolService.EVENTS.HANGING_PROTOCOL_APPLIED_FOR_VIEWPORT,
({ progress }) => {
const done = callback(progress);
if (done) {
unsubscribe();
}
}
);
}
@@ -12,6 +12,7 @@ import {
BidirectionalTool,
ArrowAnnotateTool,
DragProbeTool,
ProbeTool,
AngleTool,
MagnifyTool,
CrosshairsTool,
@@ -19,6 +20,7 @@ import {
init,
addTool,
annotation,
ReferenceLinesTool,
} from '@cornerstonejs/tools';
export default function initCornerstoneTools(configuration = {}) {
@@ -28,6 +30,7 @@ export default function initCornerstoneTools(configuration = {}) {
addTool(StackScrollMouseWheelTool);
addTool(StackScrollTool);
addTool(ZoomTool);
addTool(ProbeTool);
addTool(VolumeRotateMouseWheelTool);
addTool(MIPJumpToClickTool);
addTool(LengthTool);
@@ -40,6 +43,7 @@ export default function initCornerstoneTools(configuration = {}) {
addTool(MagnifyTool);
addTool(CrosshairsTool);
addTool(SegmentationDisplayTool);
addTool(ReferenceLinesTool);
// Modify annotation tools to use dashed lines on SR
const annotationStyle = {
@@ -67,6 +71,7 @@ const toolNames = {
MipJumpToClick: MIPJumpToClickTool.toolName,
Length: LengthTool.toolName,
DragProbe: DragProbeTool.toolName,
Probe: ProbeTool.toolName,
RectangleROI: RectangleROITool.toolName,
EllipticalROI: EllipticalROITool.toolName,
Bidirectional: BidirectionalTool.toolName,
@@ -74,6 +79,7 @@ const toolNames = {
Magnify: MagnifyTool.toolName,
Crosshairs: CrosshairsTool.toolName,
SegmentationDisplay: SegmentationDisplayTool.toolName,
ReferenceLines: ReferenceLinesTool.toolName,
};
export { toolNames };
@@ -1,141 +0,0 @@
import { eventTarget, cache, triggerEvent } from '@cornerstonejs/core';
import * as csTools from '@cornerstonejs/tools';
import { Enums as csToolsEnums } from '@cornerstonejs/tools';
import Labelmap from './utils/segmentationServiceMappings/Labelmap';
function initSegmentationService(
SegmentationService,
CornerstoneViewportService
) {
connectToolsToSegmentationService(
SegmentationService,
CornerstoneViewportService
);
connectSegmentationServiceToTools(
SegmentationService,
CornerstoneViewportService
);
}
function connectToolsToSegmentationService(
SegmentationService,
CornerstoneViewportService
) {
connectSegmentationServiceToTools(
SegmentationService,
CornerstoneViewportService
);
const segmentationUpdated = csToolsEnums.Events.SEGMENTATION_MODIFIED;
eventTarget.addEventListener(segmentationUpdated, evt => {
const { segmentationId } = evt.detail;
const segmentationState = csTools.segmentation.state.getSegmentation(
segmentationId
);
if (!segmentationState) {
return;
}
if (
!Object.keys(segmentationState.representationData).includes(
csToolsEnums.SegmentationRepresentations.Labelmap
)
) {
throw new Error('Non-labelmap representations are not supported yet');
}
// Todo: handle other representations when available in cornerstone3D
const segmentationSchema = Labelmap.toSegmentation(segmentationState);
try {
SegmentationService.addOrUpdateSegmentation(
segmentationId,
segmentationSchema
);
} catch (error) {
console.warn(
`Failed to add/update segmentation ${segmentationId}`,
error
);
}
});
}
function connectSegmentationServiceToTools(
SegmentationService,
CornerstoneViewportService
) {
const {
SEGMENTATION_UPDATED,
SEGMENTATION_REMOVED,
} = SegmentationService.EVENTS;
SegmentationService.subscribe(SEGMENTATION_REMOVED, ({ id }) => {
// Todo: This should be from the configuration
const removeFromCache = true;
const sourceSegState = csTools.segmentation.state.getSegmentation(id);
if (!sourceSegState) {
return;
}
const toolGroupIds = csTools.segmentation.state.getToolGroupsWithSegmentation(
id
);
toolGroupIds.forEach(toolGroupId => {
const segmentationRepresentations = csTools.segmentation.state.getSegmentationRepresentations(
toolGroupId
);
const UIDsToRemove = [];
segmentationRepresentations.forEach(representation => {
if (representation.segmentationId === id) {
UIDsToRemove.push(representation.segmentationRepresentationUID);
}
});
csTools.segmentation.removeSegmentationsFromToolGroup(
toolGroupId,
UIDsToRemove
);
});
// cleanup the segmentation state too
csTools.segmentation.state.removeSegmentation(id);
if (removeFromCache) {
cache.removeVolumeLoadObject(id);
}
});
SegmentationService.subscribe(
SEGMENTATION_UPDATED,
({ id, segmentation, notYetUpdatedAtSource }) => {
if (notYetUpdatedAtSource === false) {
return;
}
const { label, text } = segmentation;
const sourceSegmentation = csTools.segmentation.state.getSegmentation(id);
// Update the label in the source if necessary
if (sourceSegmentation.label !== label) {
sourceSegmentation.label = label;
}
if (sourceSegmentation.text !== text) {
sourceSegmentation.text = text;
}
triggerEvent(eventTarget, csTools.Enums.Events.SEGMENTATION_MODIFIED, {
segmentationId: id,
});
}
);
}
export default initSegmentationService;
@@ -1,7 +1,9 @@
import * as cornerstone from '@cornerstonejs/core';
import { volumeLoader } from '@cornerstonejs/core';
import { cornerstoneStreamingImageVolumeLoader } from '@cornerstonejs/streaming-image-volume-loader';
import cornerstoneWADOImageLoader from 'cornerstone-wado-image-loader';
import cornerstoneWADOImageLoader, {
webWorkerManager,
} from 'cornerstone-wado-image-loader';
import dicomParser from 'dicom-parser';
import { errorHandler } from '@ohif/core';
@@ -80,3 +82,13 @@ export default function initWADOImageLoader(
initWebWorkers(appConfig);
}
export function destroy() {
// Note: we don't want to call .terminate on the webWorkerManager since
// that resets the config
const webWorkers = webWorkerManager.webWorkers;
for (let i = 0; i < webWorkers.length; i++) {
webWorkers[i].worker.terminate();
}
webWorkers.length = 0;
}
@@ -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,
});
+5 -1
View File
@@ -31,4 +31,8 @@ const getEnabledElement = viewportIndex => {
return state.enabledElements[viewportIndex];
};
export { setEnabledElement, getEnabledElement };
const reset = () => {
state.enabledElements = {};
};
export { setEnabledElement, getEnabledElement, reset };
@@ -0,0 +1,38 @@
import {
Enums,
Types,
} from '@cornerstonejs/core';
type StackData = {
StudyInstanceUID: string;
displaySetInstanceUID: string;
imageIds: string[];
frameRate?: number;
isClip?: boolean;
initialImageIndex?: number | string | null;
};
type VolumeData = {
studyInstanceUID: string;
displaySetInstanceUID: string;
volume?: Types.IVolume;
imageIds?: string[];
};
type StackViewportData = {
viewportType: Enums.ViewportType;
data: StackData;
};
type VolumeViewportData = {
viewportType: Enums.ViewportType;
data: VolumeData[];
};
export type {
StackViewportData,
VolumeViewportData,
StackData,
VolumeData,
};
@@ -0,0 +1,5 @@
import * as CornerstoneCacheService from './CornerstoneCacheService'
export type {
CornerstoneCacheService
}
@@ -1,11 +1,11 @@
import React from 'react';
import domtoimage from 'dom-to-image';
import React, { useEffect, useState } from 'react';
import html2canvas from 'html2canvas';
import {
Enums,
getEnabledElement,
getOrCreateCanvas,
StackViewport,
VolumeViewport,
} from '@cornerstonejs/core';
import { ToolGroupManager } from '@cornerstonejs/tools';
import PropTypes from 'prop-types';
@@ -17,7 +17,6 @@ const MINIMUM_SIZE = 100;
const DEFAULT_SIZE = 512;
const MAX_TEXTURE_SIZE = 10000;
const VIEWPORT_ID = 'cornerstone-viewport-download-form';
const TOOLGROUP_ID = 'cornerstone-viewport-download-form-toolgroup';
const CornerstoneViewportDownloadForm = ({
onClose,
@@ -26,15 +25,57 @@ const CornerstoneViewportDownloadForm = ({
}) => {
const enabledElement = OHIFgetEnabledElement(activeViewportIndex);
const activeViewportElement = enabledElement?.element;
const activeViewportEnabledElement = getEnabledElement(activeViewportElement);
const {
viewportId: activeViewportId,
renderingEngineId,
} = activeViewportEnabledElement;
const toolGroup = ToolGroupManager.getToolGroupForViewport(
activeViewportId,
renderingEngineId
);
const toolModeAndBindings = Object.keys(toolGroup.toolOptions).reduce(
(acc, toolName) => {
const tool = toolGroup.toolOptions[toolName];
const { mode, bindings } = tool;
return {
...acc,
[toolName]: {
mode,
bindings,
},
};
},
{}
);
useEffect(() => {
return () => {
Object.keys(toolModeAndBindings).forEach(toolName => {
const { mode, bindings } = toolModeAndBindings[toolName];
toolGroup.setToolMode(toolName, mode, { bindings });
});
};
}, []);
const enableViewport = viewportElement => {
if (viewportElement) {
const renderingEngine = CornerstoneViewportService.getRenderingEngine();
const { renderingEngine, viewport } = getEnabledElement(
activeViewportElement
);
const viewportInput = {
viewportId: VIEWPORT_ID,
element: viewportElement,
type: Enums.ViewportType.STACK,
type: viewport.type,
defaultOptions: {
background: viewport.defaultOptions.background,
orientation: viewport.defaultOptions.orientation,
},
};
renderingEngine.enableElement(viewportInput);
@@ -43,11 +84,9 @@ const CornerstoneViewportDownloadForm = ({
const disableViewport = viewportElement => {
if (viewportElement) {
const renderingEngine = CornerstoneViewportService.getRenderingEngine();
const { renderingEngine } = getEnabledElement(viewportElement);
return new Promise(resolve => {
renderingEngine.disableElement(VIEWPORT_ID);
ToolGroupManager.destroyToolGroup(TOOLGROUP_ID);
});
}
};
@@ -115,26 +154,39 @@ const CornerstoneViewportDownloadForm = ({
const { viewport } = activeViewportEnabledElement;
if (!(viewport instanceof StackViewport)) {
throw new Error('Viewport is not a StackViewport');
}
const imageId = viewport.getCurrentImageId();
const renderingEngine = CornerstoneViewportService.getRenderingEngine();
const downloadViewport = renderingEngine.getViewport(
VIEWPORT_ID
) as StackViewport;
const downloadViewport = renderingEngine.getViewport(VIEWPORT_ID);
downloadViewport.setStack([imageId]).then(() => {
if (downloadViewport instanceof StackViewport) {
const imageId = viewport.getCurrentImageId();
const properties = viewport.getProperties();
downloadViewport.setProperties(properties);
downloadViewport.setStack([imageId]).then(() => {
downloadViewport.setProperties(properties);
const newWidth = Math.min(width || image.width, MAX_TEXTURE_SIZE);
const newHeight = Math.min(
height || image.height,
MAX_TEXTURE_SIZE
);
resolve({ width: newWidth, height: newHeight });
});
} else if (downloadViewport instanceof VolumeViewport) {
const actors = viewport.getActors();
// downloadViewport.setActors(actors);
actors.forEach(actor => {
downloadViewport.addActor(actor);
});
downloadViewport.setCamera(viewport.getCamera());
downloadViewport.render();
const newWidth = Math.min(width || image.width, MAX_TEXTURE_SIZE);
const newHeight = Math.min(height || image.height, MAX_TEXTURE_SIZE);
resolve({ width: newWidth, height: newHeight });
});
}
}
});
@@ -164,31 +216,20 @@ const CornerstoneViewportDownloadForm = ({
renderingEngineId
);
let downloadToolGroup = ToolGroupManager.getToolGroupForViewport(
downloadViewportId,
renderingEngineId
);
// add the viewport to the toolGroup
toolGroup.addViewport(downloadViewportId);
if (downloadToolGroup === undefined) {
downloadToolGroup = ToolGroupManager.createToolGroup(TOOLGROUP_ID);
// what tools were in the active viewport?
// make them all enabled instances so that they can not be interacted
// with in the download viewport
Object.values(toolGroup._toolInstances).forEach(tool => {
downloadToolGroup.addTool(tool.getToolName());
});
// add the viewport to the toolGroup
downloadToolGroup.addViewport(downloadViewportId);
}
Object.values(downloadToolGroup._toolInstances).forEach(tool => {
const toolName = tool.getToolName();
if (toggle) {
downloadToolGroup.setToolEnabled(toolName);
Object.keys(toolGroup._toolInstances).forEach(toolName => {
// make all tools Enabled so that they can not be interacted with
// in the download viewport
if (toggle && toolName !== 'Crosshairs') {
try {
toolGroup.setToolEnabled(toolName);
} catch (e) {
console.log(e);
}
} else {
downloadToolGroup.setToolDisabled(toolName);
toolGroup.setToolDisabled(toolName);
}
});
};
@@ -199,10 +240,10 @@ const CornerstoneViewportDownloadForm = ({
`div[data-viewport-uid="${VIEWPORT_ID}"]`
);
domtoimage.toPng(divForDownloadViewport).then(dataUrl => {
html2canvas(divForDownloadViewport).then(canvas => {
const link = document.createElement('a');
link.download = file;
link.href = dataUrl;
link.href = canvas.toDataURL(fileType, 1.0);
link.click();
});
};
@@ -0,0 +1,30 @@
import { Types, getRenderingEngine, utilities } from '@cornerstonejs/core';
export default function calculateViewportRegistrations(
viewports: Types.IViewportId[]
) {
const viewportPairs = _getViewportPairs(viewports);
for (const [viewport, nextViewport] of viewportPairs) {
// check if they are in the same Frame of Reference
const renderingEngine1 = getRenderingEngine(viewport.renderingEngineId);
const renderingEngine2 = getRenderingEngine(nextViewport.renderingEngineId);
const csViewport1 = renderingEngine1.getViewport(viewport.viewportId);
const csViewport2 = renderingEngine2.getViewport(nextViewport.viewportId);
utilities.calculateViewportsSpatialRegistration(csViewport1, csViewport2);
}
}
const _getViewportPairs = (viewports: Types.IViewportId[]) => {
const viewportPairs = [];
for (let i = 0; i < viewports.length; i++) {
for (let j = i + 1; j < viewports.length; j++) {
viewportPairs.push([viewports[i], viewports[j]]);
}
}
return viewportPairs;
};
@@ -38,6 +38,10 @@ const getImageInstance = dataset => {
return dataset && dataset.images && dataset.images[0];
};
const getNonImageInstance = dataset => {
return dataset && dataset.instance;
};
const getImageInstanceId = imageInstance => {
return getImageId(imageInstance);
};
@@ -89,19 +93,28 @@ const getImageLoaderType = imageId => {
class DicomLoaderService {
getLocalData(dataset, studies) {
if (dataset && dataset.localFile) {
// Use referenced imageInstance
const imageInstance = getImageInstance(dataset);
let imageId = getImageInstanceId(imageInstance);
// Use referenced imageInstance
const imageInstance = getImageInstance(dataset);
const nonImageInstance = getNonImageInstance(dataset);
// or Try to get it from studies
if (someInvalidStrings(imageId)) {
imageId = findImageIdOnStudies(studies, dataset.displaySetInstanceUID);
}
if (
(!imageInstance && !nonImageInstance) ||
!nonImageInstance.imageId.startsWith('dicomfile')
) {
return;
}
if (!someInvalidStrings(imageId)) {
return cornerstoneWADOImageLoader.wadouri.loadFileRequest(imageId);
}
const instance = imageInstance || nonImageInstance;
let imageId = getImageInstanceId(instance);
// or Try to get it from studies
if (someInvalidStrings(imageId)) {
imageId = findImageIdOnStudies(studies, dataset.displaySetInstanceUID);
}
if (!someInvalidStrings(imageId)) {
return cornerstoneWADOImageLoader.wadouri.loadFileRequest(imageId);
}
}
@@ -182,13 +195,14 @@ class DicomLoaderService {
}
}
*getLoaderIterator(dataset, studies) {
*getLoaderIterator(dataset, studies, headers) {
yield this.getLocalData(dataset, studies);
yield this.getDataByImageType(dataset);
yield this.getDataByDatasetType(dataset);
}
findDicomDataPromise(dataset, studies) {
findDicomDataPromise(dataset, studies, headers) {
dataset.authorizationHeaders = headers;
const loaderIterator = this.getLoaderIterator(dataset, studies);
// it returns first valid retriever method.
for (const loader of loaderIterator) {
@@ -8,15 +8,19 @@ const CORONAL = 'coronal';
export default function getCornerstoneOrientation(
orientation: string
): Enums.OrientationAxis {
switch (orientation.toLowerCase()) {
case AXIAL:
return Enums.OrientationAxis.AXIAL;
case SAGITTAL:
return Enums.OrientationAxis.SAGITTAL;
case CORONAL:
return Enums.OrientationAxis.CORONAL;
default:
log.wanr('Choosing acquisition plane orientation');
return Enums.OrientationAxis.ACQUISITION;
if (orientation) {
switch (orientation.toLowerCase()) {
case AXIAL:
return Enums.OrientationAxis.AXIAL;
case SAGITTAL:
return Enums.OrientationAxis.SAGITTAL;
case CORONAL:
return Enums.OrientationAxis.CORONAL;
default:
log.wanr('Choosing acquisition plane orientation');
return Enums.OrientationAxis.ACQUISITION;
}
}
return Enums.OrientationAxis.ACQUISITION;
}
@@ -0,0 +1,74 @@
/**
* Given the ViewportGridService state it will re create the protocol viewport structure
* that was used at the hanging protocol creation time. This is used to re create the
* viewport structure when the user decides to go back to a previous cached
* layout in the viewport grid.
*
*
* viewportGrid's viewports look like
*
* viewports = [
* {
* displaySetInstanceUIDs: string[],
* displaySetOptions: [],
* viewportOptions: {}
* height: number,
* width: number,
* x: number,
* y: number
* },
* ]
*
* and hanging protocols viewport structure looks like
*
* viewportStructure: {
* layoutType: 'grid',
* properties: {
* rows: 3,
* columns: 4,
* layoutOptions: [
* {
* x: 0,
* y: 0,
* width: 1 / 4,
* height: 1 / 3,
* },
* {
* x: 1 / 4,
* y: 0,
* width: 1 / 4,
* height: 1 / 3,
* },
* ],
* },
* },
*/
export default function getProtocolViewportStructureFromGridViewports({
numRows,
numCols,
viewports,
}: {
numRows: number;
numCols: number;
viewports: any[];
}) {
const viewportStructure = {
layoutType: 'grid',
properties: {
rows: numRows,
columns: numCols,
layoutOptions: [],
},
};
viewports.forEach(viewport => {
viewportStructure.properties.layoutOptions.push({
x: viewport.x,
y: viewport.y,
width: viewport.width,
height: viewport.height,
});
});
return viewportStructure;
}
@@ -51,7 +51,7 @@ export default function interleaveCenterLoader({
* listen to it and as the other viewports are created we can set the volumes for them
* since volumes are already started loading.
*/
if (matchDetails.length !== viewportIdVolumeInputArrayMap.size) {
if (matchDetails.size !== viewportIdVolumeInputArrayMap.size) {
return;
}
@@ -50,7 +50,7 @@ export default function interleaveTopToBottom({
* listen to it and as the other viewports are created we can set the volumes for them
* since volumes are already started loading.
*/
if (matchDetails.length !== viewportIdVolumeInputArrayMap.size) {
if (matchDetails.size !== viewportIdVolumeInputArrayMap.size) {
return;
}
@@ -0,0 +1,20 @@
import { segmentation } from '@cornerstonejs/tools';
function removeToolGroupSegmentationRepresentations(toolGroupId) {
const representations = segmentation.state.getSegmentationRepresentations(
toolGroupId
);
if (!representations || !representations.length) {
return;
}
representations.forEach(representation => {
segmentation.state.removeSegmentationRepresentation(
toolGroupId,
representation.segmentationRepresentationUID
);
});
}
export default removeToolGroupSegmentationRepresentations;
@@ -1,30 +0,0 @@
import { Enums as csToolsEnums } from '@cornerstonejs/tools';
const Labelmap = {
toSegmentation: segmentationState => {
const {
activeSegmentIndex,
cachedStats: data,
segmentsLocked,
representationData,
label,
segmentationId,
text,
} = segmentationState;
const labelmapRepresentationData =
representationData[csToolsEnums.SegmentationRepresentations.Labelmap];
return {
id: segmentationId,
activeSegmentIndex,
segmentsLocked,
data,
label,
volumeId: labelmapRepresentationData.volumeId,
displayText: text || [],
};
},
};
export default Labelmap;
@@ -1,16 +0,0 @@
import Labelmap from './Labelmap';
const segmentationServiceMappingsFactory = (
SegmentationService,
DisplaySetService
) => {
return {
Labelmap: {
matchingCriteria: {},
toSegmentation: csToolsSegmentation =>
Labelmap.toSegmentation(csToolsSegmentation, DisplaySetService),
},
};
};
export default segmentationServiceMappingsFactory;
@@ -0,0 +1,22 @@
/**
* It is a bell curved function that uses ease in out quadratic for css
* transition timing function for each side of the curve.
*
* @param {number} x - The current time, in the range [0, 1].
* @param {number} baseline - The baseline value to start from and return to.
* @returns the value of the transition at time x.
*/
export function easeInOutBell(x: number, baseline: number): number {
const alpha = 1 - baseline;
// prettier-ignore
if (x < 1 / 4) {
return 4 * Math.pow(2 * x, 3) * alpha + baseline;
} else if (x < 1 / 2) {
return (1 - Math.pow(-4 * x + 2, 3) / 2) * alpha + baseline;
} else if (x < 3 / 4) {
return (1 - Math.pow(4 * x - 2, 3) / 2) * alpha + baseline;
} else {
return (- 4 * Math.pow(2 * x - 2, 3)) * alpha + baseline;
}
}