feat(4D): Add 4D dynamic volume rendering and new pre-clinical 4d pt/ct mode (#3664)
Co-authored-by: Alireza <ar.sedghi@gmail.com> Co-authored-by: Neil <neil.a.macphee@gmail.com>
This commit is contained in:
200 files changed
+9589
-2178
No files matched your search
@@ -118,6 +118,14 @@ const OHIFCornerstoneViewport = React.memo(props => {
|
||||
} = props;
|
||||
|
||||
const viewportId = viewportOptions.viewportId;
|
||||
|
||||
// Since we only have support for dynamic data in volume viewports, we should
|
||||
// handle this case here and set the viewportType to volume if any of the
|
||||
// displaySets are dynamic volumes
|
||||
viewportOptions.viewportType = displaySets.some(ds => ds.isDynamicVolume)
|
||||
? 'volume'
|
||||
: viewportOptions.viewportType;
|
||||
|
||||
const [scrollbarHeight, setScrollbarHeight] = useState('100px');
|
||||
const [enabledVPElement, setEnabledVPElement] = useState(null);
|
||||
const elementRef = useRef();
|
||||
|
||||
@@ -322,7 +322,7 @@ function _getViewportInstances(viewportData) {
|
||||
} else if (viewportData.viewportType === Enums.ViewportType.ORTHOGRAPHIC) {
|
||||
const volumes = viewportData.data;
|
||||
volumes.forEach(volume => {
|
||||
if (!volume?.imageIds) {
|
||||
if (!volume?.imageIds || volume.imageIds.length === 0) {
|
||||
return;
|
||||
}
|
||||
imageIds.push(volume.imageIds[0]);
|
||||
|
||||
@@ -126,16 +126,12 @@ function commandsModule({
|
||||
? nearbyToolData
|
||||
: null;
|
||||
},
|
||||
|
||||
// Measurement tool commands:
|
||||
|
||||
/** Delete the given measurement */
|
||||
deleteMeasurement: ({ uid }) => {
|
||||
if (uid) {
|
||||
measurementServiceSource.remove(uid);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Show the measurement labelling input dialog and update the label
|
||||
* on the measurement with a response if not cancelled.
|
||||
@@ -302,6 +298,43 @@ function commandsModule({
|
||||
const renderingEngine = cornerstoneViewportService.getRenderingEngine();
|
||||
renderingEngine.render();
|
||||
},
|
||||
toggleEnabledDisabledToolbar({ value, itemId, toolGroupIds = [] }) {
|
||||
const toolName = itemId || value;
|
||||
toolGroupIds = toolGroupIds.length ? toolGroupIds : toolGroupService.getToolGroupIds();
|
||||
toolGroupIds.forEach(toolGroupId => {
|
||||
const toolGroup = toolGroupService.getToolGroup(toolGroupId);
|
||||
if (!toolGroup || !toolGroup.hasTool(toolName)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const toolIsEnabled = toolGroup.getToolOptions(toolName).mode === Enums.ToolModes.Enabled;
|
||||
|
||||
toolIsEnabled ? toolGroup.setToolDisabled(toolName) : toolGroup.setToolEnabled(toolName);
|
||||
});
|
||||
},
|
||||
toggleActiveDisabledToolbar({ value, itemId, toolGroupIds = [] }) {
|
||||
const toolName = itemId || value;
|
||||
toolGroupIds = toolGroupIds.length ? toolGroupIds : toolGroupService.getToolGroupIds();
|
||||
toolGroupIds.forEach(toolGroupId => {
|
||||
const toolGroup = toolGroupService.getToolGroup(toolGroupId);
|
||||
if (!toolGroup || !toolGroup.hasTool(toolName)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const toolIsActive = toolGroup.getToolOptions(toolName).mode === Enums.ToolModes.Active;
|
||||
|
||||
toolIsActive
|
||||
? toolGroup.setToolDisabled(toolName)
|
||||
: actions.setToolActive({ toolName, toolGroupId });
|
||||
|
||||
// we should set the previously active tool to active after we set the
|
||||
// current tool disabled
|
||||
if (toolIsActive) {
|
||||
const prevToolName = toolGroup.getPrevActivePrimaryToolName();
|
||||
actions.setToolActive({ toolName: prevToolName, toolGroupId });
|
||||
}
|
||||
});
|
||||
},
|
||||
setToolActiveToolbar: ({ value, itemId, toolGroupIds = [] }) => {
|
||||
// Sometimes it is passed as value (tools with options), sometimes as itemId (toolbar buttons)
|
||||
const toolName = itemId || value;
|
||||
@@ -639,6 +672,17 @@ function commandsModule({
|
||||
storePresentation: ({ viewportId }) => {
|
||||
cornerstoneViewportService.storePresentation({ viewportId });
|
||||
},
|
||||
updateVolumeData: ({ volume }) => {
|
||||
// update vtkOpenGLTexture and imageData of computed volume
|
||||
const { imageData, vtkOpenGLTexture } = volume;
|
||||
const numSlices = imageData.getDimensions()[2];
|
||||
const slicesToUpdate = [...Array(numSlices).keys()];
|
||||
slicesToUpdate.forEach(i => {
|
||||
vtkOpenGLTexture.setUpdatedFrame(i);
|
||||
});
|
||||
imageData.modified();
|
||||
},
|
||||
|
||||
attachProtocolViewportDataListener: ({ protocol, stageIndex }) => {
|
||||
const EVENT = cornerstoneViewportService.EVENTS.VIEWPORT_DATA_CHANGED;
|
||||
const command = protocol.callbacks.onViewportDataInitialized;
|
||||
@@ -928,6 +972,15 @@ function commandsModule({
|
||||
toggleSynchronizer: {
|
||||
commandFn: actions.toggleSynchronizer,
|
||||
},
|
||||
updateVolumeData: {
|
||||
commandFn: actions.updateVolumeData,
|
||||
},
|
||||
toggleEnabledDisabledToolbar: {
|
||||
commandFn: actions.toggleEnabledDisabledToolbar,
|
||||
},
|
||||
toggleActiveDisabledToolbar: {
|
||||
commandFn: actions.toggleActiveDisabledToolbar,
|
||||
},
|
||||
};
|
||||
|
||||
return {
|
||||
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
import React, { ReactElement } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { ServicesManager } from '@ohif/core';
|
||||
import { useViewportGrid } from '@ohif/ui';
|
||||
import ViewportWindowLevel from '../ViewportWindowLevel/ViewportWindowLevel';
|
||||
|
||||
const ActiveViewportWindowLevel = ({
|
||||
servicesManager,
|
||||
}: {
|
||||
servicesManager: ServicesManager;
|
||||
}): ReactElement => {
|
||||
const [viewportGrid] = useViewportGrid();
|
||||
const { activeViewportId } = viewportGrid;
|
||||
|
||||
return (
|
||||
<>
|
||||
{activeViewportId && (
|
||||
<ViewportWindowLevel
|
||||
servicesManager={servicesManager}
|
||||
viewportId={activeViewportId}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
ActiveViewportWindowLevel.propTypes = {
|
||||
servicesManager: PropTypes.instanceOf(ServicesManager),
|
||||
};
|
||||
|
||||
export default ActiveViewportWindowLevel;
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from './ActiveViewportWindowLevel';
|
||||
@@ -1,73 +1,108 @@
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import React, { useCallback, useEffect, useState, useRef } from 'react';
|
||||
import { CinePlayer, useCine } from '@ohif/ui';
|
||||
import { Enums, eventTarget } from '@cornerstonejs/core';
|
||||
import { Enums, eventTarget, cache } from '@cornerstonejs/core';
|
||||
import { Enums as StreamingEnums } from '@cornerstonejs/streaming-image-volume-loader';
|
||||
import { useAppConfig } from '@state';
|
||||
|
||||
function WrappedCinePlayer({ enabledVPElement, viewportId, servicesManager }) {
|
||||
const { customizationService, displaySetService, viewportGridService } = servicesManager.services;
|
||||
const [{ isCineEnabled, cines }, api] = useCine();
|
||||
const [{ isCineEnabled, cines }, cineService] = useCine();
|
||||
const [newStackFrameRate, setNewStackFrameRate] = useState(24);
|
||||
const [dynamicInfo, setDynamicInfo] = useState(null);
|
||||
const [appConfig] = useAppConfig();
|
||||
const isMountedRef = useRef(null);
|
||||
|
||||
const { component: CinePlayerComponent = CinePlayer } =
|
||||
customizationService.get('cinePlayer') ?? {};
|
||||
|
||||
const cineHandler = () => {
|
||||
if (!cines || !cines[viewportId] || !enabledVPElement) {
|
||||
if (!cines?.[viewportId] || !enabledVPElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
const cine = cines[viewportId];
|
||||
const isPlaying = cine.isPlaying || false;
|
||||
const frameRate = cine.frameRate || 24;
|
||||
|
||||
const { isPlaying = false, frameRate = 24 } = cines[viewportId];
|
||||
const validFrameRate = Math.max(frameRate, 1);
|
||||
|
||||
if (isPlaying) {
|
||||
api.playClip(enabledVPElement, {
|
||||
framesPerSecond: validFrameRate,
|
||||
});
|
||||
} else {
|
||||
api.stopClip(enabledVPElement);
|
||||
}
|
||||
return isPlaying
|
||||
? cineService.playClip(enabledVPElement, { framesPerSecond: validFrameRate })
|
||||
: cineService.stopClip(enabledVPElement);
|
||||
};
|
||||
|
||||
const newStackCineHandler = useCallback(() => {
|
||||
const newDisplaySetHandler = useCallback(() => {
|
||||
if (!enabledVPElement || !isCineEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { viewports } = viewportGridService.getState();
|
||||
const { displaySetInstanceUIDs } = viewports.get(viewportId);
|
||||
|
||||
let frameRate = 24;
|
||||
let isPlaying = cines[viewportId].isPlaying;
|
||||
let isPlaying = cines[viewportId]?.isPlaying || false;
|
||||
displaySetInstanceUIDs.forEach(displaySetInstanceUID => {
|
||||
const displaySet = displaySetService.getDisplaySetByUID(displaySetInstanceUID);
|
||||
|
||||
if (displaySet.FrameRate) {
|
||||
// displaySet.FrameRate corresponds to DICOM tag (0018,1063) which is defined as the the frame time in milliseconds
|
||||
// So a bit of math to get the actual frame rate.
|
||||
frameRate = Math.round(1000 / displaySet.FrameRate);
|
||||
isPlaying ||= !!appConfig.autoPlayCine;
|
||||
}
|
||||
|
||||
// check if the displaySet is dynamic and set the dynamic info
|
||||
if (displaySet.isDynamicVolume) {
|
||||
const { dynamicVolumeInfo } = displaySet;
|
||||
const numTimePoints = dynamicVolumeInfo.timePoints.length;
|
||||
const label = dynamicVolumeInfo.splittingTag;
|
||||
const timePointIndex = dynamicVolumeInfo.timePointIndex || 0;
|
||||
setDynamicInfo({
|
||||
volumeId: displaySet.displaySetInstanceUID,
|
||||
timePointIndex,
|
||||
numTimePoints,
|
||||
label,
|
||||
});
|
||||
} else {
|
||||
setDynamicInfo(null);
|
||||
}
|
||||
});
|
||||
|
||||
if (isPlaying) {
|
||||
api.setIsCineEnabled(isPlaying);
|
||||
cineService.setIsCineEnabled(isPlaying);
|
||||
}
|
||||
api.setCine({ id: viewportId, isPlaying, frameRate });
|
||||
cineService.setCine({ id: viewportId, isPlaying, frameRate });
|
||||
setNewStackFrameRate(frameRate);
|
||||
}, [displaySetService, viewportId, viewportGridService, cines]);
|
||||
}, [displaySetService, viewportId, viewportGridService, cines, isCineEnabled]);
|
||||
|
||||
useEffect(() => {
|
||||
isMountedRef.current = true;
|
||||
|
||||
eventTarget.addEventListener(Enums.Events.STACK_VIEWPORT_NEW_STACK, newStackCineHandler);
|
||||
newDisplaySetHandler();
|
||||
|
||||
return () => {
|
||||
isMountedRef.current = false;
|
||||
api.stopClip(enabledVPElement);
|
||||
api.setCine({ id: viewportId, isPlaying: false });
|
||||
eventTarget.removeEventListener(Enums.Events.STACK_VIEWPORT_NEW_STACK, newStackCineHandler);
|
||||
};
|
||||
}, [enabledVPElement, newStackCineHandler]);
|
||||
}, [isCineEnabled, newDisplaySetHandler]);
|
||||
|
||||
/**
|
||||
* Use effect for handling new display set
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (!enabledVPElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
eventTarget.addEventListener(Enums.Events.STACK_VIEWPORT_NEW_STACK, newDisplaySetHandler);
|
||||
// this doesn't makes sense that we are listening to this event on viewport element
|
||||
enabledVPElement.addEventListener(
|
||||
Enums.Events.VOLUME_VIEWPORT_NEW_VOLUME,
|
||||
newDisplaySetHandler
|
||||
);
|
||||
|
||||
return () => {
|
||||
cineService.setCine({ id: viewportId, isPlaying: false });
|
||||
|
||||
eventTarget.removeEventListener(Enums.Events.STACK_VIEWPORT_NEW_STACK, newDisplaySetHandler);
|
||||
enabledVPElement.removeEventListener(
|
||||
Enums.Events.VOLUME_VIEWPORT_NEW_VOLUME,
|
||||
newDisplaySetHandler
|
||||
);
|
||||
};
|
||||
}, [enabledVPElement, newDisplaySetHandler, viewportId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!cines || !cines[viewportId] || !enabledVPElement || !isMountedRef.current) {
|
||||
@@ -77,41 +112,118 @@ function WrappedCinePlayer({ enabledVPElement, viewportId, servicesManager }) {
|
||||
cineHandler();
|
||||
|
||||
return () => {
|
||||
api.stopClip(enabledVPElement);
|
||||
cineService.stopClip(enabledVPElement);
|
||||
};
|
||||
}, [cines, viewportId, enabledVPElement, cineHandler]);
|
||||
}, [cines, viewportId, cineService, enabledVPElement, cineHandler]);
|
||||
|
||||
if (!isCineEnabled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const cine = cines[viewportId];
|
||||
const isPlaying = (cine && cine.isPlaying) || false;
|
||||
const isPlaying = cine?.isPlaying || false;
|
||||
|
||||
return (
|
||||
isCineEnabled && (
|
||||
<CinePlayerComponent
|
||||
className="absolute left-1/2 bottom-3 -translate-x-1/2"
|
||||
frameRate={newStackFrameRate}
|
||||
isPlaying={isPlaying}
|
||||
onClose={() => {
|
||||
// also stop the clip
|
||||
api.setCine({
|
||||
id: viewportId,
|
||||
isPlaying: false,
|
||||
});
|
||||
api.setIsCineEnabled(false);
|
||||
}}
|
||||
onPlayPauseChange={isPlaying => {
|
||||
api.setCine({
|
||||
id: viewportId,
|
||||
isPlaying,
|
||||
});
|
||||
}}
|
||||
onFrameRateChange={frameRate =>
|
||||
api.setCine({
|
||||
id: viewportId,
|
||||
frameRate,
|
||||
})
|
||||
}
|
||||
/>
|
||||
)
|
||||
<RenderCinePlayer
|
||||
viewportId={viewportId}
|
||||
cineService={cineService}
|
||||
newStackFrameRate={newStackFrameRate}
|
||||
isPlaying={isPlaying}
|
||||
dynamicInfo={dynamicInfo}
|
||||
customizationService={customizationService}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function RenderCinePlayer({
|
||||
viewportId,
|
||||
cineService,
|
||||
newStackFrameRate,
|
||||
isPlaying,
|
||||
dynamicInfo: dynamicInfoProp,
|
||||
customizationService,
|
||||
}) {
|
||||
const { component: CinePlayerComponent = CinePlayer } =
|
||||
customizationService.get('cinePlayer') ?? {};
|
||||
|
||||
const [dynamicInfo, setDynamicInfo] = useState(dynamicInfoProp);
|
||||
|
||||
useEffect(() => {
|
||||
setDynamicInfo(dynamicInfoProp);
|
||||
}, [dynamicInfoProp]);
|
||||
|
||||
/**
|
||||
* Use effect for handling 4D time index changed
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (!dynamicInfo) {
|
||||
return;
|
||||
}
|
||||
|
||||
const handleTimePointIndexChange = evt => {
|
||||
const { volumeId, timePointIndex, numTimePoints, splittingTag } = evt.detail;
|
||||
setDynamicInfo({ volumeId, timePointIndex, numTimePoints, label: splittingTag });
|
||||
};
|
||||
|
||||
eventTarget.addEventListener(
|
||||
StreamingEnums.Events.DYNAMIC_VOLUME_TIME_POINT_INDEX_CHANGED,
|
||||
handleTimePointIndexChange
|
||||
);
|
||||
|
||||
return () => {
|
||||
eventTarget.removeEventListener(
|
||||
StreamingEnums.Events.DYNAMIC_VOLUME_TIME_POINT_INDEX_CHANGED,
|
||||
handleTimePointIndexChange
|
||||
);
|
||||
};
|
||||
}, [dynamicInfo]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!dynamicInfo) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { volumeId, timePointIndex, numTimePoints, splittingTag } = dynamicInfo || {};
|
||||
const volume = cache.getVolume(volumeId);
|
||||
volume.timePointIndex = timePointIndex;
|
||||
|
||||
setDynamicInfo({ volumeId, timePointIndex, numTimePoints, label: splittingTag });
|
||||
}, []);
|
||||
|
||||
const updateDynamicInfo = useCallback(props => {
|
||||
const { volumeId, timePointIndex } = props;
|
||||
const volume = cache.getVolume(volumeId);
|
||||
volume.timePointIndex = timePointIndex;
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<CinePlayerComponent
|
||||
className="absolute left-1/2 bottom-3 -translate-x-1/2"
|
||||
frameRate={newStackFrameRate}
|
||||
isPlaying={isPlaying}
|
||||
onClose={() => {
|
||||
// also stop the clip
|
||||
cineService.setCine({
|
||||
id: viewportId,
|
||||
isPlaying: false,
|
||||
});
|
||||
cineService.setIsCineEnabled(false);
|
||||
}}
|
||||
onPlayPauseChange={isPlaying => {
|
||||
cineService.setCine({
|
||||
id: viewportId,
|
||||
isPlaying,
|
||||
});
|
||||
}}
|
||||
onFrameRateChange={frameRate =>
|
||||
cineService.setCine({
|
||||
id: viewportId,
|
||||
frameRate,
|
||||
})
|
||||
}
|
||||
dynamicInfo={dynamicInfo}
|
||||
updateDynamicInfo={updateDynamicInfo}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,387 @@
|
||||
import React, { useEffect, useCallback, useState, ReactElement } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import debounce from 'lodash.debounce';
|
||||
import { ServicesManager } from '@ohif/core';
|
||||
import { PanelSection, WindowLevel } from '@ohif/ui';
|
||||
import vtkColorMaps from '@kitware/vtk.js/Rendering/Core/ColorTransferFunction/ColorMaps';
|
||||
import { Enums, eventTarget, cache as cs3DCache, utilities as csUtils } from '@cornerstonejs/core';
|
||||
import { getViewportVolumeHistogram } from './getViewportVolumeHistogram';
|
||||
|
||||
const { Events } = Enums;
|
||||
|
||||
const ViewportWindowLevel = ({
|
||||
servicesManager,
|
||||
viewportId,
|
||||
}: {
|
||||
servicesManager: ServicesManager;
|
||||
viewportId: string;
|
||||
}): ReactElement => {
|
||||
const { cornerstoneViewportService } = servicesManager.services;
|
||||
const [windowLevels, setWindowLevels] = useState([]);
|
||||
const [cachedHistograms, setCachedHistograms] = useState({});
|
||||
|
||||
/**
|
||||
* Looks for all viewports that has exactly all volumeIds passed as parameter.
|
||||
*/
|
||||
const getViewportsWithVolumeIds = useCallback(
|
||||
(volumeIds: string[]) => {
|
||||
const renderingEngine = cornerstoneViewportService.getRenderingEngine();
|
||||
const viewports = renderingEngine.getVolumeViewports();
|
||||
|
||||
return viewports.filter(vp => {
|
||||
const viewportVolumeIds = vp.getActors().map(actor => actor.uid);
|
||||
|
||||
return (
|
||||
volumeIds.length === viewportVolumeIds.length &&
|
||||
volumeIds.every(volumeId => viewportVolumeIds.includes(volumeId))
|
||||
);
|
||||
});
|
||||
},
|
||||
[cornerstoneViewportService]
|
||||
);
|
||||
|
||||
const getNodeOpacity = (volumeActor, nodeIndex) => {
|
||||
const volumeOpacity = volumeActor.getProperty().getScalarOpacity(0);
|
||||
const nodeValue = [];
|
||||
|
||||
volumeOpacity.getNodeValue(nodeIndex, nodeValue);
|
||||
|
||||
return nodeValue[1];
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks if the opacity applied to the PET volume is something like
|
||||
* [{x: 0, y: 0}, {x: 0.1, y: [C]}, {x: [ANY], y: [C]}] where C is a
|
||||
* constant opacity value for all x's greater than 0.1
|
||||
*/
|
||||
const isPetVolumeWithDefaultOpacity = (volumeId, volumeActor) => {
|
||||
const volume = cs3DCache.getVolume(volumeId);
|
||||
|
||||
if (!volume) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const modality = volume.metadata.Modality;
|
||||
|
||||
if (modality !== 'PT') {
|
||||
return false;
|
||||
}
|
||||
|
||||
const volumeOpacity = volumeActor.getProperty().getScalarOpacity(0);
|
||||
|
||||
// It must have at least two points (0 and 0.1)
|
||||
if (volumeOpacity.getSize() < 2) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const node1Value = [];
|
||||
const node2Value = [];
|
||||
|
||||
volumeOpacity.getNodeValue(0, node1Value);
|
||||
volumeOpacity.getNodeValue(1, node2Value);
|
||||
|
||||
// First node must be (x:0, y:0} and the second one {x:0.1, y:any}
|
||||
if (node1Value[0] !== 0 || node1Value[1] !== 0 || node2Value[0] !== 0.1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const expectedOpacity = node2Value[1];
|
||||
const opacitySize = volumeOpacity.getSize();
|
||||
const currentNodeValue = [];
|
||||
|
||||
// Any point after 0.1 must have the same opacity
|
||||
for (let i = 2; i < opacitySize; i++) {
|
||||
volumeOpacity.getNodeValue(i, currentNodeValue);
|
||||
|
||||
if (currentNodeValue[1] !== expectedOpacity) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks if the opacity function has a constance opacity value for all x's
|
||||
*/
|
||||
const isVolumeWithConstantOpacity = volumeActor => {
|
||||
const volumeOpacity = volumeActor.getProperty().getScalarOpacity(0);
|
||||
const opacitySize = volumeOpacity.getSize();
|
||||
const firstNodeValue = [];
|
||||
|
||||
volumeOpacity.getNodeValue(0, firstNodeValue);
|
||||
|
||||
const firstNodeOpacity = firstNodeValue[1];
|
||||
|
||||
for (let i = 0; i < opacitySize; i++) {
|
||||
const currentNodeValue = [];
|
||||
|
||||
volumeOpacity.getNodeValue(0, currentNodeValue);
|
||||
|
||||
if (currentNodeValue[1] !== firstNodeOpacity) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
const getVolumeOpacity = useCallback((viewport, volumeId) => {
|
||||
const volumeActor = viewport.getActor(volumeId).actor;
|
||||
|
||||
if (isPetVolumeWithDefaultOpacity(volumeId, volumeActor)) {
|
||||
// Get the opacity from the second node at 0.1
|
||||
return getNodeOpacity(volumeActor, 1);
|
||||
} else if (isVolumeWithConstantOpacity(volumeActor)) {
|
||||
return getNodeOpacity(volumeActor, 0);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}, []);
|
||||
|
||||
const getWindowLevelsData = useCallback(
|
||||
async (viewportId: number) => {
|
||||
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
|
||||
|
||||
if (!viewport) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const viewportInfo = cornerstoneViewportService.getViewportInfo(viewportId);
|
||||
|
||||
const volumeIds = viewport.getActors().map(actor => actor.uid);
|
||||
const viewportProperties = viewport.getProperties();
|
||||
const { voiRange } = viewportProperties;
|
||||
const viewportVoi = voiRange
|
||||
? {
|
||||
windowWidth: voiRange.upper - voiRange.lower,
|
||||
windowCenter: voiRange.lower + (voiRange.upper - voiRange.lower) / 2,
|
||||
}
|
||||
: undefined;
|
||||
|
||||
const windowLevels = await Promise.all(
|
||||
volumeIds.map(async (volumeId, volumeIndex) => {
|
||||
const volume = cs3DCache.getVolume(volumeId);
|
||||
|
||||
if (!volume) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const opacity = getVolumeOpacity(viewport, volumeId);
|
||||
const { metadata, scaling } = volume;
|
||||
const modality = metadata.Modality;
|
||||
|
||||
// TODO: find a proper way to fix the histogram
|
||||
const options = {
|
||||
min: modality === 'PT' ? 0.1 : -999,
|
||||
max: modality === 'PT' ? 5 : 10000,
|
||||
};
|
||||
|
||||
const histogram =
|
||||
cachedHistograms[volumeId] ??
|
||||
(await getViewportVolumeHistogram(viewport, volume, options));
|
||||
const { voi: displaySetVOI, colormap: displaySetColormap } =
|
||||
viewportInfo.displaySetOptions[volumeIndex];
|
||||
let colormap;
|
||||
if (displaySetColormap) {
|
||||
colormap =
|
||||
csUtils.colormap.getColormap(displaySetColormap.name) ??
|
||||
vtkColorMaps.getPresetByName(displaySetColormap.name);
|
||||
}
|
||||
|
||||
const voi = !volumeIndex ? viewportVoi ?? displaySetVOI : displaySetVOI;
|
||||
|
||||
return {
|
||||
viewportId,
|
||||
modality,
|
||||
volumeId,
|
||||
volumeIndex,
|
||||
voi,
|
||||
histogram,
|
||||
colormap,
|
||||
step: scaling?.PT ? 0.05 : 1,
|
||||
opacity,
|
||||
showOpacitySlider: volumeIndex === 1 && opacity !== undefined,
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
const data = windowLevels.filter(Boolean);
|
||||
return data;
|
||||
},
|
||||
|
||||
[cachedHistograms, cornerstoneViewportService, getVolumeOpacity]
|
||||
);
|
||||
|
||||
const updateViewportHistograms = useCallback(() => {
|
||||
getWindowLevelsData(viewportId).then(windowLevels => {
|
||||
setWindowLevels(windowLevels);
|
||||
});
|
||||
}, [viewportId, getWindowLevelsData]);
|
||||
|
||||
const handleCornerstoneVOIModified = useCallback(
|
||||
e => {
|
||||
const { detail } = e;
|
||||
const { volumeId, range } = detail;
|
||||
const oldWindowLevel = windowLevels.find(wl => wl.volumeId === volumeId);
|
||||
|
||||
if (!oldWindowLevel) {
|
||||
return;
|
||||
}
|
||||
|
||||
const oldVOI = oldWindowLevel.voi;
|
||||
const windowWidth = range.upper - range.lower;
|
||||
const windowCenter = range.lower + windowWidth / 2;
|
||||
|
||||
if (windowWidth === oldVOI.windowWidth && windowCenter === oldVOI.windowCenter) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newWindowLevel = {
|
||||
...oldWindowLevel,
|
||||
voi: {
|
||||
windowWidth,
|
||||
windowCenter,
|
||||
},
|
||||
};
|
||||
|
||||
setWindowLevels(
|
||||
windowLevels.map(windowLevel =>
|
||||
windowLevel === oldWindowLevel ? newWindowLevel : windowLevel
|
||||
)
|
||||
);
|
||||
},
|
||||
[windowLevels]
|
||||
);
|
||||
|
||||
const debouncedHandleCornerstoneVOIModified = useCallback(
|
||||
debounce(handleCornerstoneVOIModified, 100),
|
||||
[handleCornerstoneVOIModified]
|
||||
);
|
||||
|
||||
const handleVOIChange = useCallback(
|
||||
(volumeId, voi) => {
|
||||
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
|
||||
|
||||
const newRange = {
|
||||
lower: voi.windowCenter - voi.windowWidth / 2,
|
||||
upper: voi.windowCenter + voi.windowWidth / 2,
|
||||
};
|
||||
|
||||
viewport.setProperties({ voiRange: newRange }, volumeId);
|
||||
viewport.render();
|
||||
},
|
||||
[cornerstoneViewportService, viewportId]
|
||||
);
|
||||
|
||||
const handleOpacityChange = useCallback(
|
||||
(viewportId, _volumeIndex, volumeId, opacity) => {
|
||||
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
|
||||
|
||||
if (!viewport) {
|
||||
return;
|
||||
}
|
||||
|
||||
const viewportVolumeIds = viewport.getActors().map(actor => actor.uid);
|
||||
const viewports = getViewportsWithVolumeIds(viewportVolumeIds);
|
||||
|
||||
viewports.forEach(vp => {
|
||||
vp.setProperties({ colormap: { opacity } }, volumeId);
|
||||
vp.render();
|
||||
});
|
||||
},
|
||||
[getViewportsWithVolumeIds, cornerstoneViewportService]
|
||||
);
|
||||
|
||||
// Listen to windowLevels changes and caches all the new ones
|
||||
useEffect(() => {
|
||||
const newVolumeHistograms = windowLevels
|
||||
.filter(windowLevel => !cachedHistograms[windowLevel.volumeId] && windowLevel.histogram)
|
||||
.reduce((volumeHistograms, windowLevel) => {
|
||||
// If the histogram exists, add it to the volumeHistograms object
|
||||
if (windowLevel?.histogram) {
|
||||
volumeHistograms[windowLevel.volumeId] = windowLevel.histogram;
|
||||
}
|
||||
return volumeHistograms;
|
||||
}, {});
|
||||
|
||||
if (Object.keys(newVolumeHistograms).length) {
|
||||
setCachedHistograms(prev => ({ ...prev, ...newVolumeHistograms }));
|
||||
}
|
||||
}, [windowLevels, cachedHistograms]);
|
||||
|
||||
// Updates the histogram when the viewport index prop has changed
|
||||
useEffect(() => updateViewportHistograms(), [viewportId, updateViewportHistograms]);
|
||||
|
||||
// Listen to cornerstone events on "eventTarget" and at the document level
|
||||
useEffect(() => {
|
||||
eventTarget.addEventListener(Events.IMAGE_VOLUME_LOADING_COMPLETED, updateViewportHistograms);
|
||||
|
||||
document.addEventListener(Events.VOI_MODIFIED, debouncedHandleCornerstoneVOIModified, true);
|
||||
|
||||
return () => {
|
||||
eventTarget.removeEventListener(
|
||||
Events.IMAGE_VOLUME_LOADING_COMPLETED,
|
||||
updateViewportHistograms
|
||||
);
|
||||
|
||||
document.removeEventListener(
|
||||
Events.VOI_MODIFIED,
|
||||
debouncedHandleCornerstoneVOIModified,
|
||||
true
|
||||
);
|
||||
};
|
||||
}, [updateViewportHistograms, debouncedHandleCornerstoneVOIModified]);
|
||||
|
||||
// Updates the viewport when the context of the viewport has changed. This is
|
||||
// necessary when moving across different stages because the viewport index
|
||||
// may not change but the volumes loaded on it may change.
|
||||
useEffect(() => {
|
||||
const { unsubscribe } = cornerstoneViewportService.subscribe(
|
||||
cornerstoneViewportService.EVENTS.VIEWPORT_VOLUMES_CHANGED,
|
||||
({ viewportInfo }) => {
|
||||
if (viewportInfo.viewportId === viewportId) {
|
||||
updateViewportHistograms();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return () => {
|
||||
unsubscribe();
|
||||
};
|
||||
}, [viewportId, cornerstoneViewportService, updateViewportHistograms]);
|
||||
|
||||
return (
|
||||
<PanelSection title="Window Level">
|
||||
{windowLevels.map((windowLevel, i) => {
|
||||
if (!windowLevel.histogram) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<WindowLevel
|
||||
key={windowLevel.volumeId}
|
||||
title={`${windowLevel.modality}`}
|
||||
histogram={windowLevel.histogram}
|
||||
voi={windowLevel.voi}
|
||||
step={windowLevel.step}
|
||||
showOpacitySlider={windowLevel.showOpacitySlider}
|
||||
colormap={windowLevel.colormap}
|
||||
onVOIChange={voi => handleVOIChange(windowLevel.volumeId, voi)}
|
||||
opacity={windowLevel.opacity}
|
||||
onOpacityChange={opacity =>
|
||||
handleOpacityChange(windowLevel.viewportId, i, windowLevel.volumeId, opacity)
|
||||
}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</PanelSection>
|
||||
);
|
||||
};
|
||||
|
||||
ViewportWindowLevel.propTypes = {
|
||||
servicesManager: PropTypes.instanceOf(ServicesManager),
|
||||
viewportId: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default ViewportWindowLevel;
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
import { getWebWorkerManager } from '@cornerstonejs/core';
|
||||
|
||||
const workerManager = getWebWorkerManager();
|
||||
|
||||
const options = {
|
||||
// maxWorkerInstances: 1,
|
||||
// overwrite: false
|
||||
autoTerminationOnIdle: 1000,
|
||||
};
|
||||
|
||||
// Register the task
|
||||
const workerFn = () => {
|
||||
return new Worker(new URL('./histogramWorker.js', import.meta.url), {
|
||||
name: 'histogram-worker', // name used by the browser to name the worker
|
||||
});
|
||||
};
|
||||
|
||||
workerManager.registerWorker('histogram-worker', workerFn, options);
|
||||
|
||||
const getViewportVolumeHistogram = async (viewport, volume, options?) => {
|
||||
if (!volume?.loadStatus.loaded) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const volumeImageData = viewport.getImageData(volume.volumeId);
|
||||
|
||||
if (!volumeImageData) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
let scalarData = volume.scalarData;
|
||||
|
||||
let prevTimePoint;
|
||||
if (volume.numTimePoints > 1) {
|
||||
prevTimePoint = volume.timePointIndex;
|
||||
const middleTimePoint = Math.round(volume.numTimePoints / 2);
|
||||
volume.timePointIndex = middleTimePoint;
|
||||
scalarData = volume.getScalarData(middleTimePoint);
|
||||
}
|
||||
|
||||
const { dimensions, origin, direction, spacing } = volume;
|
||||
|
||||
const range = await workerManager.executeTask('histogram-worker', 'getRange', {
|
||||
dimensions,
|
||||
origin,
|
||||
direction,
|
||||
spacing,
|
||||
scalarData,
|
||||
});
|
||||
|
||||
// after we calculate the range let's reset the timePointIndex
|
||||
if (volume.numTimePoints > 1) {
|
||||
volume.timePointIndex = prevTimePoint;
|
||||
}
|
||||
const { minimum: min, maximum: max } = range;
|
||||
const calcHistOptions = {
|
||||
numBins: 256,
|
||||
min: Math.max(min, options?.min ?? min),
|
||||
max: Math.min(max, options?.max ?? max),
|
||||
};
|
||||
|
||||
const histogram = await workerManager.executeTask('histogram-worker', 'calcHistogram', {
|
||||
data: scalarData,
|
||||
options: calcHistOptions,
|
||||
});
|
||||
|
||||
return histogram;
|
||||
};
|
||||
|
||||
export { getViewportVolumeHistogram };
|
||||
@@ -0,0 +1,97 @@
|
||||
import { expose } from 'comlink';
|
||||
import vtkImageData from '@kitware/vtk.js/Common/DataModel/ImageData';
|
||||
import vtkDataArray from '@kitware/vtk.js/Common/Core/DataArray';
|
||||
|
||||
/**
|
||||
* This object simulates a heavy task by implementing a sleep function and a recursive Fibonacci function.
|
||||
* It's used for testing or demonstrating purposes where a heavy or time-consuming task is needed.
|
||||
*/
|
||||
const obj = {
|
||||
getRange: ({ dimensions, origin, direction, spacing, scalarData }) => {
|
||||
const imageData = vtkImageData.newInstance();
|
||||
imageData.setDimensions(dimensions);
|
||||
imageData.setOrigin(origin);
|
||||
imageData.setDirection(direction);
|
||||
imageData.setSpacing(spacing);
|
||||
|
||||
const scalarArray = vtkDataArray.newInstance({
|
||||
name: 'Pixels',
|
||||
numberOfComponents: 1,
|
||||
values: scalarData,
|
||||
});
|
||||
|
||||
imageData.getPointData().setScalars(scalarArray);
|
||||
|
||||
imageData.modified();
|
||||
|
||||
const range = imageData.computeHistogram(imageData.getBounds());
|
||||
|
||||
return range;
|
||||
},
|
||||
calcHistogram: ({ data, options }) => {
|
||||
if (options === undefined) {
|
||||
options = {};
|
||||
}
|
||||
const histogram = {
|
||||
numBins: options.numBins || 256,
|
||||
range: { min: 0, max: 0 },
|
||||
bins: new Int32Array(1),
|
||||
maxBin: 0,
|
||||
maxBinValue: 0,
|
||||
};
|
||||
|
||||
let minToUse = options.min;
|
||||
let maxToUse = options.max;
|
||||
|
||||
if (minToUse === undefined || maxToUse === undefined) {
|
||||
let min = Infinity;
|
||||
let max = -Infinity;
|
||||
let index = data.length;
|
||||
|
||||
while (index--) {
|
||||
const value = data[index];
|
||||
if (value < min) {
|
||||
min = value;
|
||||
}
|
||||
if (value > max) {
|
||||
max = value;
|
||||
}
|
||||
}
|
||||
|
||||
minToUse = min;
|
||||
maxToUse = max;
|
||||
}
|
||||
|
||||
histogram.range = { min: minToUse, max: maxToUse };
|
||||
|
||||
const bins = new Int32Array(histogram.numBins);
|
||||
const binScale = histogram.numBins / (maxToUse - minToUse);
|
||||
|
||||
for (let index = 0; index < data.length; index++) {
|
||||
const value = data[index];
|
||||
if (value < minToUse) {
|
||||
continue;
|
||||
}
|
||||
if (value > maxToUse) {
|
||||
continue;
|
||||
}
|
||||
const bin = Math.floor((value - minToUse) * binScale);
|
||||
bins[bin] += 1;
|
||||
}
|
||||
|
||||
histogram.bins = bins;
|
||||
histogram.maxBin = 0;
|
||||
histogram.maxBinValue = 0;
|
||||
|
||||
for (let bin = 0; bin < histogram.numBins; bin++) {
|
||||
if (histogram.bins[bin] > histogram.maxBinValue) {
|
||||
histogram.maxBin = bin;
|
||||
histogram.maxBinValue = histogram.bins[bin];
|
||||
}
|
||||
}
|
||||
|
||||
return histogram;
|
||||
},
|
||||
};
|
||||
|
||||
expose(obj);
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from './ViewportWindowLevel';
|
||||
@@ -192,7 +192,7 @@ export default function getToolbarModule({ commandsManager, servicesManager }) {
|
||||
}
|
||||
|
||||
const displaySets = displaySetUIDs.map(displaySetService.getDisplaySetByUID);
|
||||
const isUS = displaySets.some(displaySet => displaySet.Modality === 'US');
|
||||
const isUS = displaySets.some(displaySet => displaySet?.Modality === 'US');
|
||||
if (!isUS) {
|
||||
return {
|
||||
disabled: true,
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
imageLoadPoolManager,
|
||||
imageRetrievalPoolManager,
|
||||
} from '@cornerstonejs/core';
|
||||
import * as csStreamingImageVolumeLoader from '@cornerstonejs/streaming-image-volume-loader';
|
||||
import { Enums as cs3DToolsEnums } from '@cornerstonejs/tools';
|
||||
import { ServicesManager, Types } from '@ohif/core';
|
||||
|
||||
@@ -34,6 +35,10 @@ import ImageOverlayViewerTool from './tools/ImageOverlayViewerTool';
|
||||
import { showLabelAnnotationPopup } from './utils/callInputDialog';
|
||||
import ViewportActionCornersService from './services/ViewportActionCornersService/ViewportActionCornersService';
|
||||
import { ViewportActionCornersProvider } from './contextProviders/ViewportActionCornersProvider';
|
||||
import ActiveViewportWindowLevel from './components/ActiveViewportWindowLevel';
|
||||
|
||||
const { helpers: volumeLoaderHelpers } = csStreamingImageVolumeLoader;
|
||||
const { getDynamicVolumeInfo } = volumeLoaderHelpers ?? {};
|
||||
|
||||
const Component = React.lazy(() => {
|
||||
return import(/* webpackPrefetch: true */ './Viewport/OHIFCornerstoneViewport');
|
||||
@@ -90,6 +95,16 @@ const cornerstoneExtension: Types.Extensions.Extension = {
|
||||
},
|
||||
|
||||
getToolbarModule,
|
||||
getPanelModule({ servicesManager }) {
|
||||
return [
|
||||
{
|
||||
name: 'activeViewportWindowLevel',
|
||||
component: () => {
|
||||
return <ActiveViewportWindowLevel servicesManager={servicesManager} />;
|
||||
},
|
||||
},
|
||||
];
|
||||
},
|
||||
getHangingProtocolModule,
|
||||
getViewportModule({ servicesManager, commandsManager }) {
|
||||
const ExtendedOHIFCornerstoneViewport = props => {
|
||||
@@ -127,7 +142,7 @@ const cornerstoneExtension: Types.Extensions.Extension = {
|
||||
},
|
||||
getEnabledElement,
|
||||
dicomLoaderService,
|
||||
showLabelAnnotationPopup
|
||||
showLabelAnnotationPopup,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -143,6 +158,12 @@ const cornerstoneExtension: Types.Extensions.Extension = {
|
||||
Enums: cs3DToolsEnums,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'volumeLoader',
|
||||
exports: {
|
||||
getDynamicVolumeInfo,
|
||||
},
|
||||
},
|
||||
];
|
||||
},
|
||||
};
|
||||
|
||||
@@ -15,7 +15,11 @@ import {
|
||||
utilities as csUtilities,
|
||||
Enums as csEnums,
|
||||
} from '@cornerstonejs/core';
|
||||
import { cornerstoneStreamingImageVolumeLoader } from '@cornerstonejs/streaming-image-volume-loader';
|
||||
import { utilities } from '@cornerstonejs/tools';
|
||||
import {
|
||||
cornerstoneStreamingImageVolumeLoader,
|
||||
cornerstoneStreamingDynamicImageVolumeLoader,
|
||||
} from '@cornerstonejs/streaming-image-volume-loader';
|
||||
|
||||
import initWADOImageLoader from './initWADOImageLoader';
|
||||
import initCornerstoneTools from './initCornerstoneTools';
|
||||
@@ -29,10 +33,9 @@ import initContextMenu from './initContextMenu';
|
||||
import initDoubleClick from './initDoubleClick';
|
||||
import { CornerstoneServices } from './types';
|
||||
import initViewTiming from './utils/initViewTiming';
|
||||
import { utilities } from '@cornerstonejs/core';
|
||||
import { colormaps } from './utils/colormaps';
|
||||
|
||||
const { registerColormap } = utilities.colormap;
|
||||
const { registerColormap } = csUtilities.colormap;
|
||||
|
||||
// TODO: Cypress tests are currently grabbing this from the window?
|
||||
window.cornerstone = cornerstone;
|
||||
@@ -93,7 +96,6 @@ export default async function init({
|
||||
customizationService,
|
||||
uiModalService,
|
||||
uiNotificationService,
|
||||
cineService,
|
||||
cornerstoneViewportService,
|
||||
hangingProtocolService,
|
||||
toolbarService,
|
||||
@@ -172,6 +174,11 @@ export default async function init({
|
||||
cornerstoneStreamingImageVolumeLoader
|
||||
);
|
||||
|
||||
volumeLoader.registerVolumeLoader(
|
||||
'cornerstoneStreamingDynamicImageVolume',
|
||||
cornerstoneStreamingDynamicImageVolumeLoader
|
||||
);
|
||||
|
||||
hangingProtocolService.registerImageLoadStrategy('interleaveCenter', interleaveCenterLoader);
|
||||
hangingProtocolService.registerImageLoadStrategy('interleaveTopToBottom', interleaveTopToBottom);
|
||||
hangingProtocolService.registerImageLoadStrategy('nth', nthLoader);
|
||||
@@ -195,7 +202,7 @@ export default async function init({
|
||||
/* Measurement Service */
|
||||
this.measurementServiceSource = connectToolsToMeasurementService(servicesManager);
|
||||
|
||||
initCineService(cineService);
|
||||
initCineService(servicesManager);
|
||||
|
||||
// When a custom image load is performed, update the relevant viewports
|
||||
hangingProtocolService.subscribe(
|
||||
|
||||
@@ -1,6 +1,60 @@
|
||||
import { cache } from '@cornerstonejs/core';
|
||||
import { utilities } from '@cornerstonejs/tools';
|
||||
|
||||
function initCineService(cineService) {
|
||||
function _getVolumesFromViewport(viewport) {
|
||||
return viewport ? viewport.getActors().map(actor => cache.getVolume(actor.uid)) : [];
|
||||
}
|
||||
|
||||
function _getVolumeFromViewport(viewport) {
|
||||
const volumes = _getVolumesFromViewport(viewport);
|
||||
const dynamicVolume = volumes.find(volume => volume.isDynamicVolume());
|
||||
|
||||
return dynamicVolume ?? volumes[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Return all viewports that needs to be synchronized with the source
|
||||
* viewport passed as parameter when cine is updated.
|
||||
* @param servicesManager ServiceManager
|
||||
* @param srcViewportIndex Source viewport index
|
||||
* @returns array with viewport information.
|
||||
*/
|
||||
function _getSyncedViewports(servicesManager, srcViewportId) {
|
||||
const { viewportGridService, cornerstoneViewportService } = servicesManager.services;
|
||||
|
||||
const { viewports: viewportsStates } = viewportGridService.getState();
|
||||
const srcViewportState = viewportsStates.get(srcViewportId);
|
||||
|
||||
if (srcViewportState?.viewportOptions?.viewportType !== 'volume') {
|
||||
return [];
|
||||
}
|
||||
|
||||
const srcViewport = cornerstoneViewportService.getCornerstoneViewport(srcViewportId);
|
||||
|
||||
const srcVolume = srcViewport ? _getVolumeFromViewport(srcViewport) : null;
|
||||
|
||||
if (!srcVolume?.isDynamicVolume()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const { volumeId: srcVolumeId } = srcVolume;
|
||||
|
||||
return Array.from(viewportsStates.values())
|
||||
.filter(({ viewportId }) => {
|
||||
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
|
||||
|
||||
return viewportId !== srcViewportId && viewport?.hasVolumeId(srcVolumeId);
|
||||
})
|
||||
.map(({ viewportId }) => ({ viewportId }));
|
||||
}
|
||||
|
||||
function initCineService(servicesManager) {
|
||||
const { cineService } = servicesManager.services;
|
||||
|
||||
const getSyncedViewports = viewportId => {
|
||||
return _getSyncedViewports(servicesManager, viewportId);
|
||||
};
|
||||
|
||||
const playClip = (element, playClipOptions) => {
|
||||
return utilities.cine.playClip(element, playClipOptions);
|
||||
};
|
||||
@@ -9,7 +63,11 @@ function initCineService(cineService) {
|
||||
return utilities.cine.stopClip(element);
|
||||
};
|
||||
|
||||
cineService.setServiceImplementation({ playClip, stopClip });
|
||||
cineService.setServiceImplementation({
|
||||
getSyncedViewports,
|
||||
playClip,
|
||||
stopClip,
|
||||
});
|
||||
}
|
||||
|
||||
export default initCineService;
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
MIPJumpToClickTool,
|
||||
LengthTool,
|
||||
RectangleROITool,
|
||||
RectangleROIThresholdTool,
|
||||
EllipticalROITool,
|
||||
CircleROITool,
|
||||
BidirectionalTool,
|
||||
@@ -19,14 +20,16 @@ import {
|
||||
MagnifyTool,
|
||||
CrosshairsTool,
|
||||
SegmentationDisplayTool,
|
||||
RectangleScissorsTool,
|
||||
SphereScissorsTool,
|
||||
CircleScissorsTool,
|
||||
BrushTool,
|
||||
PaintFillTool,
|
||||
init,
|
||||
addTool,
|
||||
annotation,
|
||||
ReferenceLinesTool,
|
||||
TrackballRotateTool,
|
||||
CircleScissorsTool,
|
||||
RectangleScissorsTool,
|
||||
SphereScissorsTool,
|
||||
AdvancedMagnifyTool,
|
||||
UltrasoundDirectionalTool,
|
||||
PlanarFreehandROITool,
|
||||
@@ -52,6 +55,7 @@ export default function initCornerstoneTools(configuration = {}) {
|
||||
addTool(MIPJumpToClickTool);
|
||||
addTool(LengthTool);
|
||||
addTool(RectangleROITool);
|
||||
addTool(RectangleROIThresholdTool);
|
||||
addTool(EllipticalROITool);
|
||||
addTool(CircleROITool);
|
||||
addTool(BidirectionalTool);
|
||||
@@ -62,12 +66,14 @@ export default function initCornerstoneTools(configuration = {}) {
|
||||
addTool(MagnifyTool);
|
||||
addTool(CrosshairsTool);
|
||||
addTool(SegmentationDisplayTool);
|
||||
addTool(RectangleScissorsTool);
|
||||
addTool(SphereScissorsTool);
|
||||
addTool(CircleScissorsTool);
|
||||
addTool(BrushTool);
|
||||
addTool(PaintFillTool);
|
||||
addTool(ReferenceLinesTool);
|
||||
addTool(CalibrationLineTool);
|
||||
addTool(TrackballRotateTool);
|
||||
addTool(CircleScissorsTool);
|
||||
addTool(RectangleScissorsTool);
|
||||
addTool(SphereScissorsTool);
|
||||
addTool(ImageOverlayViewerTool);
|
||||
addTool(AdvancedMagnifyTool);
|
||||
addTool(UltrasoundDirectionalTool);
|
||||
@@ -103,6 +109,7 @@ const toolNames = {
|
||||
DragProbe: DragProbeTool.toolName,
|
||||
Probe: ProbeTool.toolName,
|
||||
RectangleROI: RectangleROITool.toolName,
|
||||
RectangleROIThreshold: RectangleROIThresholdTool.toolName,
|
||||
EllipticalROI: EllipticalROITool.toolName,
|
||||
CircleROI: CircleROITool.toolName,
|
||||
Bidirectional: BidirectionalTool.toolName,
|
||||
@@ -111,6 +118,8 @@ const toolNames = {
|
||||
Magnify: MagnifyTool.toolName,
|
||||
Crosshairs: CrosshairsTool.toolName,
|
||||
SegmentationDisplay: SegmentationDisplayTool.toolName,
|
||||
Brush: BrushTool.toolName,
|
||||
PaintFill: PaintFillTool.toolName,
|
||||
ReferenceLines: ReferenceLinesTool.toolName,
|
||||
CalibrationLine: CalibrationLineTool.toolName,
|
||||
TrackballRotateTool: TrackballRotateTool.toolName,
|
||||
|
||||
@@ -49,6 +49,20 @@ const initMeasurementService = (
|
||||
Length.toMeasurement
|
||||
);
|
||||
|
||||
measurementService.addMapping(
|
||||
csTools3DVer1MeasurementSource,
|
||||
'Crosshairs',
|
||||
Length.matchingCriteria,
|
||||
() => {
|
||||
console.warn('Crosshairs mapping not implemented.');
|
||||
return {};
|
||||
},
|
||||
() => {
|
||||
console.warn('Crosshairs mapping not implemented.');
|
||||
return {};
|
||||
}
|
||||
);
|
||||
|
||||
measurementService.addMapping(
|
||||
csTools3DVer1MeasurementSource,
|
||||
'Bidirectional',
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import * as cornerstone from '@cornerstonejs/core';
|
||||
import { volumeLoader } from '@cornerstonejs/core';
|
||||
import { cornerstoneStreamingImageVolumeLoader } from '@cornerstonejs/streaming-image-volume-loader';
|
||||
import {
|
||||
cornerstoneStreamingImageVolumeLoader,
|
||||
cornerstoneStreamingDynamicImageVolumeLoader,
|
||||
} from '@cornerstonejs/streaming-image-volume-loader';
|
||||
import dicomImageLoader, { webWorkerManager } from '@cornerstonejs/dicom-image-loader';
|
||||
import dicomParser from 'dicom-parser';
|
||||
import { errorHandler, utils } from '@ohif/core';
|
||||
@@ -41,6 +44,11 @@ export default function initWADOImageLoader(
|
||||
|
||||
registerVolumeLoader('cornerstoneStreamingImageVolume', cornerstoneStreamingImageVolumeLoader);
|
||||
|
||||
registerVolumeLoader(
|
||||
'cornerstoneStreamingDynamicImageVolume',
|
||||
cornerstoneStreamingDynamicImageVolumeLoader
|
||||
);
|
||||
|
||||
dicomImageLoader.configure({
|
||||
decodeConfig: {
|
||||
// !! IMPORTANT !!
|
||||
|
||||
+2
-1
@@ -200,6 +200,7 @@ class CornerstoneCacheService {
|
||||
volume,
|
||||
volumeId,
|
||||
imageIds: volumeImageIds,
|
||||
isDynamicVolume: displaySet.isDynamicVolume,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -228,7 +229,7 @@ class CornerstoneCacheService {
|
||||
|
||||
const shouldDisplaySeg = segmentationService.shouldRenderSegmentation(
|
||||
viewportDisplaySetInstanceUIDs,
|
||||
instance.FrameOfReferenceUID
|
||||
instance?.FrameOfReferenceUID || segDisplaySet.FrameOfReferenceUID
|
||||
);
|
||||
|
||||
if (shouldDisplaySeg) {
|
||||
|
||||
@@ -1289,7 +1289,7 @@ class SegmentationService extends PubSubService {
|
||||
}
|
||||
|
||||
const { colorLUTIndex } = segmentation;
|
||||
this._removeSegmentationFromCornerstone(segmentationId);
|
||||
const { updatedToolGroupIds } = this._removeSegmentationFromCornerstone(segmentationId);
|
||||
|
||||
// Delete associated colormap
|
||||
// Todo: bring this back
|
||||
@@ -1309,7 +1309,9 @@ class SegmentationService extends PubSubService {
|
||||
if (remainingHydratedSegmentations.length) {
|
||||
const { id } = remainingHydratedSegmentations[0];
|
||||
|
||||
this._setActiveSegmentationForToolGroup(id, this._getApplicableToolGroupId(), false);
|
||||
updatedToolGroupIds.forEach(toolGroupId => {
|
||||
this._setActiveSegmentationForToolGroup(id, toolGroupId, false);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1966,6 +1968,7 @@ class SegmentationService extends PubSubService {
|
||||
const removeFromCache = true;
|
||||
const segmentationState = cstSegmentation.state;
|
||||
const sourceSegState = segmentationState.getSegmentation(segmentationId);
|
||||
const updatedToolGroupIds: Set<string> = new Set();
|
||||
|
||||
if (!sourceSegState) {
|
||||
return;
|
||||
@@ -1981,6 +1984,7 @@ class SegmentationService extends PubSubService {
|
||||
segmentationRepresentations.forEach(representation => {
|
||||
if (representation.segmentationId === segmentationId) {
|
||||
UIDsToRemove.push(representation.segmentationRepresentationUID);
|
||||
updatedToolGroupIds.add(toolGroupId);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1998,6 +2002,8 @@ class SegmentationService extends PubSubService {
|
||||
if (removeFromCache && cache.getVolumeLoadObject(segmentationId)) {
|
||||
cache.removeVolumeLoadObject(segmentationId);
|
||||
}
|
||||
|
||||
return { updatedToolGroupIds: Array.from(updatedToolGroupIds) };
|
||||
}
|
||||
|
||||
private _updateCornerstoneSegmentations({ segmentationId, notYetUpdatedAtSource }) {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { ToolGroupManager, Enums, Types } from '@cornerstonejs/tools';
|
||||
import { eventTarget } from '@cornerstonejs/core';
|
||||
|
||||
import { Types as OhifTypes, pubSubServiceInterface } from '@ohif/core';
|
||||
import getActiveViewportEnabledElement from '../../utils/getActiveViewportEnabledElement';
|
||||
@@ -6,6 +7,8 @@ import getActiveViewportEnabledElement from '../../utils/getActiveViewportEnable
|
||||
const EVENTS = {
|
||||
VIEWPORT_ADDED: 'event::cornerstone::toolgroupservice:viewportadded',
|
||||
TOOLGROUP_CREATED: 'event::cornerstone::toolgroupservice:toolgroupcreated',
|
||||
TOOL_ACTIVATED: 'event::cornerstone::toolgroupservice:toolactivated',
|
||||
PRIMARY_TOOL_ACTIVATED: 'event::cornerstone::toolgroupservice:primarytoolactivated',
|
||||
};
|
||||
|
||||
type Tool = {
|
||||
@@ -38,18 +41,26 @@ export default class ToolGroupService {
|
||||
EVENTS: { [key: string]: string };
|
||||
|
||||
constructor(serviceManager) {
|
||||
const { cornerstoneViewportService, viewportGridService } = serviceManager.services;
|
||||
const { cornerstoneViewportService, viewportGridService, uiNotificationService } =
|
||||
serviceManager.services;
|
||||
this.cornerstoneViewportService = cornerstoneViewportService;
|
||||
this.viewportGridService = viewportGridService;
|
||||
this.uiNotificationService = uiNotificationService;
|
||||
this.listeners = {};
|
||||
this.EVENTS = EVENTS;
|
||||
Object.assign(this, pubSubServiceInterface);
|
||||
|
||||
this._init();
|
||||
}
|
||||
|
||||
onModeExit() {
|
||||
this.destroy();
|
||||
}
|
||||
|
||||
private _init() {
|
||||
eventTarget.addEventListener(Enums.Events.TOOL_ACTIVATED, this._onToolActivated);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a tool group from the ToolGroupManager by tool group ID.
|
||||
* If no tool group ID is provided, it retrieves the tool group of the active viewport.
|
||||
@@ -105,12 +116,14 @@ export default class ToolGroupService {
|
||||
return toolGroup.getActivePrimaryMouseButtonTool();
|
||||
}
|
||||
|
||||
public destroy() {
|
||||
public destroy(): void {
|
||||
ToolGroupManager.destroy();
|
||||
this.toolGroupIds = new Set();
|
||||
|
||||
eventTarget.removeEventListener(Enums.Events.TOOL_ACTIVATED, this._onToolActivated);
|
||||
}
|
||||
|
||||
public destroyToolGroup(toolGroupId: string) {
|
||||
public destroyToolGroup(toolGroupId: string): void {
|
||||
ToolGroupManager.destroyToolGroup(toolGroupId);
|
||||
this.toolGroupIds.delete(toolGroupId);
|
||||
}
|
||||
@@ -222,6 +235,10 @@ export default class ToolGroupService {
|
||||
toolInstance.configuration = config;
|
||||
}
|
||||
|
||||
public getActivePrimaryMouseButtonTool(toolGroupId?: string): string {
|
||||
return this.getToolGroup(toolGroupId)?.getActivePrimaryMouseButtonTool();
|
||||
}
|
||||
|
||||
private _setToolsMode(toolGroup, tools) {
|
||||
const { active, passive, enabled, disabled } = tools;
|
||||
|
||||
@@ -279,4 +296,23 @@ export default class ToolGroupService {
|
||||
addTools(tools.disabled);
|
||||
}
|
||||
}
|
||||
|
||||
private _onToolActivated = (evt: Types.EventTypes.ToolActivatedEventType) => {
|
||||
const { toolGroupId, toolName, toolBindingsOptions } = evt.detail;
|
||||
const isPrimaryTool = toolBindingsOptions.bindings?.some(
|
||||
binding => binding.mouseButton === Enums.MouseBindings.Primary
|
||||
);
|
||||
|
||||
const callbackProps = {
|
||||
toolGroupId,
|
||||
toolName,
|
||||
toolBindingsOptions,
|
||||
};
|
||||
|
||||
this._broadcastEvent(EVENTS.TOOL_ACTIVATED, callbackProps);
|
||||
|
||||
if (isPrimaryTool) {
|
||||
this._broadcastEvent(EVENTS.PRIMARY_TOOL_ACTIVATED, callbackProps);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -24,6 +24,7 @@ import JumpPresets from '../../utils/JumpPresets';
|
||||
|
||||
const EVENTS = {
|
||||
VIEWPORT_DATA_CHANGED: 'event::cornerstoneViewportService:viewportDataChanged',
|
||||
VIEWPORT_VOLUMES_CHANGED: 'event::cornerstoneViewportService:viewportVolumesChanged',
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -793,6 +794,10 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
|
||||
}
|
||||
|
||||
viewport.render();
|
||||
|
||||
this._broadcastEvent(this.EVENTS.VIEWPORT_VOLUMES_CHANGED, {
|
||||
viewportInfo,
|
||||
});
|
||||
}
|
||||
|
||||
private _addSegmentationRepresentationToToolGroupIfNecessary(
|
||||
|
||||
@@ -37,6 +37,31 @@ export default function interleaveTopToBottom({
|
||||
}
|
||||
}
|
||||
|
||||
const filteredMatchDetails = [];
|
||||
const displaySetsToLoad = new Set();
|
||||
|
||||
// Check all viewports that have a displaySet to be loaded. In some cases
|
||||
// (eg: line chart viewports which is not a Cornerstone viewport) the
|
||||
// displaySet is created on the client and there are no instances to be
|
||||
// downloaded. For those viewports the displaySet may have the `skipLoading`
|
||||
// option set to true otherwise it may block the download of all other
|
||||
// instances resulting in blank viewports.
|
||||
Array.from(matchDetails.values()).forEach(curMatchDetails => {
|
||||
const { displaySetsInfo } = curMatchDetails;
|
||||
let numDisplaySetsToLoad = 0;
|
||||
|
||||
displaySetsInfo.forEach(({ displaySetInstanceUID, displaySetOptions }) => {
|
||||
if (!displaySetOptions?.options?.skipLoading) {
|
||||
numDisplaySetsToLoad++;
|
||||
displaySetsToLoad.add(displaySetInstanceUID);
|
||||
}
|
||||
});
|
||||
|
||||
if (numDisplaySetsToLoad) {
|
||||
filteredMatchDetails.push(curMatchDetails);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* The following is checking if all the viewports that were matched in the HP has been
|
||||
* successfully created their cornerstone viewport or not. Todo: This can be
|
||||
@@ -50,16 +75,19 @@ 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.size !== viewportIdVolumeInputArrayMap.size) {
|
||||
if (filteredMatchDetails.length !== viewportIdVolumeInputArrayMap.size) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if all the matched volumes are loaded
|
||||
for (const [_, details] of displaySetsMatchDetails.entries()) {
|
||||
const { SeriesInstanceUID } = details;
|
||||
const { SeriesInstanceUID, displaySetInstanceUID } = details;
|
||||
|
||||
// HangingProtocol has matched, but don't have all the volumes created yet, so return
|
||||
if (!Array.from(volumeIdMapsToLoad.values()).includes(SeriesInstanceUID)) {
|
||||
if (
|
||||
displaySetsToLoad.has(displaySetInstanceUID) &&
|
||||
!Array.from(volumeIdMapsToLoad.values()).includes(SeriesInstanceUID)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user