diff --git a/extensions/cornerstone-3d/src/Viewport/OHIFCornerstone3DViewport.tsx b/extensions/cornerstone-3d/src/Viewport/OHIFCornerstone3DViewport.tsx index 6620b1b070a..6ff6bafb4a1 100644 --- a/extensions/cornerstone-3d/src/Viewport/OHIFCornerstone3DViewport.tsx +++ b/extensions/cornerstone-3d/src/Viewport/OHIFCornerstone3DViewport.tsx @@ -1,21 +1,15 @@ import React, { useEffect, useRef, useCallback, useState } from 'react'; import ReactResizeDetector from 'react-resize-detector'; -import { useViewportGrid, ImageScrollbar } from '@ohif/ui'; -import OHIF from '@ohif/core'; +import PropTypes from 'prop-types'; +import { useViewportGrid } from '@ohif/ui'; import * as cs3DTools from '@cornerstonejs/tools'; import { Enums, eventTarget } from '@cornerstonejs/core'; -import PropTypes from 'prop-types'; - import { setEnabledElement } from '../state'; -import Cornerstone3DViewportService from '../services/ViewportService/Cornerstone3DViewportService'; -import CornerstoneOverlay from './CornerstoneOverlay'; -import ViewportLoadingIndicator from './ViewportLoadingIndicator'; -import ViewportOrientationMarkers from './ViewportOrientationMarkers'; +import Cornerstone3DCacheService from '../services/ViewportService/Cornerstone3DCacheService'; import './OHIFCornerstone3DViewport.css'; - -const { StackManager } = OHIF.utils; +import CornerstoneOverlays from './Overlays/CornerstoneOverlays'; const STACK = 'stack'; @@ -28,8 +22,6 @@ function areEqual(prevProps, nextProps) { return false; } - // Todo: handle fusion - // Todo: handle orientation const prevDisplaySets = prevProps.displaySets[0]; const nextDisplaySets = nextProps.displaySets[0]; @@ -67,9 +59,8 @@ const OHIFCornerstoneViewport = React.memo(props => { initialImageIdOrIndex, } = props; - const [viewportData, setViewportData] = useState(null); - const [imageIndex, setImageIndex] = useState(0); const [scrollbarHeight, setScrollbarHeight] = useState('100px'); + const [viewportData, setViewportData] = useState(null); const [_, viewportGridService] = useViewportGrid(); const elementRef = useRef(); @@ -79,6 +70,8 @@ const OHIFCornerstoneViewport = React.memo(props => { DisplaySetService, ToolBarService, ToolGroupService, + SyncGroupService, + Cornerstone3DViewportService, } = servicesManager.services; // useCallback for scroll bar height calculation @@ -103,7 +96,7 @@ const OHIFCornerstoneViewport = React.memo(props => { } const { viewportId, element } = evt.detail; - const viewportInfo = Cornerstone3DViewportService.getViewportInfoById( + const viewportInfo = Cornerstone3DViewportService.getViewportInfo( viewportId ); const viewportIndex = viewportInfo.getViewportIndex(); @@ -112,12 +105,20 @@ const OHIFCornerstoneViewport = React.memo(props => { const renderingEngineId = viewportInfo.getRenderingEngineId(); const toolGroupId = viewportInfo.getToolGroupId(); - ToolGroupService.addToolGroupViewport( + const syncGroups = viewportInfo.getSyncGroups(); + + ToolGroupService.addViewportToToolGroup( viewportId, renderingEngineId, toolGroupId ); + SyncGroupService.addViewportToSyncGroup( + viewportId, + renderingEngineId, + syncGroups + ); + if (onElementEnabled) { onElementEnabled(evt); } @@ -129,6 +130,7 @@ const OHIFCornerstoneViewport = React.memo(props => { useEffect(() => { Cornerstone3DViewportService.enableElement( viewportIndex, + viewportOptions, elementRef.current ); @@ -138,8 +140,25 @@ const OHIFCornerstoneViewport = React.memo(props => { ); setImageScrollBarHeight(); + return () => { + const viewportInfo = Cornerstone3DViewportService.getViewportInfoByIndex( + viewportIndex + ); + + const viewportId = viewportInfo.getViewportId(); + const renderingEngineId = viewportInfo.getRenderingEngineId(); + const syncGroups = viewportInfo.getSyncGroups(); + + ToolGroupService.disable(viewportId, renderingEngineId); + SyncGroupService.removeViewportFromSyncGroup( + viewportId, + renderingEngineId, + syncGroups + ); + Cornerstone3DViewportService.disableElement(viewportIndex); + eventTarget.removeEventListener( Enums.Events.ELEMENT_ENABLED, elementEnabledHandler @@ -147,40 +166,70 @@ const OHIFCornerstoneViewport = React.memo(props => { }; }, []); + // subscribe to displaySet metadata invalidation (updates) + // Currently, if the metadata changes we need to re-render the display set + // for it to take effect in the viewport. As we deal with scaling in the loading, + // we need to remove the old volume from the cache, and let the + // viewport to re-add it which will use the new metadata. Otherwise, the + // viewport will use the cached volume and the new metadata will not be used. + // Note: this approach does not actually end of sending network requests + // and it uses the network cache useEffect(() => { - const viewportData = _getViewportData( - dataSource, - displaySets, - viewportOptions.viewportType, - initialImageIdOrIndex + const { unsubscribe } = DisplaySetService.subscribe( + DisplaySetService.EVENTS.DISPLAY_SET_SERIES_METADATA_INVALIDATED, + async invalidatedDisplaySetInstanceUID => { + if ( + viewportData.displaySetInstanceUIDs.includes( + invalidatedDisplaySetInstanceUID + ) + ) { + const newViewportData = await Cornerstone3DCacheService.invalidateViewportData( + viewportData, + invalidatedDisplaySetInstanceUID, + dataSource, + DisplaySetService + ); + + Cornerstone3DViewportService.updateViewport( + viewportIndex, + newViewportData + ); + + setViewportData(newViewportData); + } + } ); + return () => { + unsubscribe(); + }; + }, [viewportData, viewportIndex]); - setViewportData(viewportData); - - Cornerstone3DViewportService.setViewportDisplaySets( - viewportIndex, - viewportData, - viewportOptions, - displaySetOptions - ); + useEffect(() => { + // handle the default viewportType to be stack + if (!viewportOptions.viewportType) { + viewportOptions.viewportType = STACK; + } - const element = elementRef.current; + const loadViewportData = async () => { + const viewportData = await Cornerstone3DCacheService.getViewportData( + viewportIndex, + displaySets, + viewportOptions.viewportType, + dataSource, + initialImageIdOrIndex + ); - const updateIndex = event => { - const { imageId } = event.detail; - // find the index of imageId in the imageIds - const index = viewportData.stack?.imageIds.indexOf(imageId); + Cornerstone3DViewportService.setViewportDisplaySets( + viewportIndex, + viewportData, + viewportOptions, + displaySetOptions + ); - if (index !== -1) { - setImageIndex(index); - } + setViewportData(viewportData); }; - element.addEventListener(Enums.Events.STACK_NEW_IMAGE, updateIndex); - - return () => { - element.removeEventListener(Enums.Events.STACK_NEW_IMAGE, updateIndex); - }; + loadViewportData(); }, [viewportOptions, displaySets, dataSource]); /** @@ -194,10 +243,6 @@ const OHIFCornerstoneViewport = React.memo(props => { * the cache for jumping to see if there is any jump queued, then we jump to the correct slice. */ useEffect(() => { - if (!viewportData) { - return; - } - const unsubscribeFromJumpToMeasurementEvents = _subscribeToJumpToMeasurementEvents( MeasurementService, DisplaySetService, @@ -219,34 +264,7 @@ const OHIFCornerstoneViewport = React.memo(props => { return () => { unsubscribeFromJumpToMeasurementEvents(); }; - }, [displaySets, elementRef, viewportIndex, viewportData]); - - const onImageScrollbarChange = useCallback( - (imageIndex, viewportIndex) => { - const viewportInfo = Cornerstone3DViewportService.getViewportInfoByIndex( - viewportIndex - ); - - const viewportId = viewportInfo.getViewportId(); - const viewport = Cornerstone3DViewportService.getCornerstone3DViewport( - viewportId - ); - - // if getCurrentImageId is not a method on viewport - if (!viewport.getCurrentImageId) { - throw new Error('cannot use scrollbar for non-stack viewports'); - } - - // Later scrollThroughStack should return two values the current index - // and the total number of indices (volume it is different) - viewport.setImageIdIndex(imageIndex).then(() => { - // Update scrollbar index - const currentIndex = viewport.getCurrentImageIdIndex(); - setImageIndex(currentIndex); - }); - }, - [viewportIndex, viewportData] - ); + }, [displaySets, elementRef, viewportIndex]); return (
@@ -266,78 +284,17 @@ const OHIFCornerstoneViewport = React.memo(props => { onMouseDown={e => e.preventDefault()} ref={elementRef} >
- onImageScrollbarChange(evt, viewportIndex)} - max={viewportData ? viewportData.stack?.imageIds?.length - 1 : 0} - height={scrollbarHeight} - value={imageIndex} - /> - - {viewportData && ( - <> - - - - )} ); }, areEqual); -function _getCornerstoneStack(displaySet, dataSource) { - // Get stack from Stack Manager - const storedStack = StackManager.findOrCreateStack(displaySet, dataSource); - - // Clone the stack here so we don't mutate it - const stack = Object.assign({}, storedStack); - - return stack; -} - -function _getViewportData( - dataSource, - displaySets, - viewportType, - initialImageIdOrIndex -) { - viewportType = viewportType || STACK; - if (viewportType !== STACK) { - throw new Error('Only STACK viewport type is supported now'); - } - - // For Stack Viewport we don't have fusion currently - const displaySet = displaySets[0]; - - const stack = _getCornerstoneStack(displaySet, dataSource); - - if (initialImageIdOrIndex !== undefined && stack?.imageIds) { - if (typeof initialImageIdOrIndex === 'number') { - stack.initialImageIdIndex = initialImageIdOrIndex; - } else { - stack.initialImageIdIndex = stack.imageIds.indexOf(initialImageIdOrIndex); - } - } - - const viewportData = { - StudyInstanceUID: displaySet.StudyInstanceUID, - displaySetInstanceUID: displaySet.displaySetInstanceUID, - stack, - }; - - return viewportData; -} - function _subscribeToJumpToMeasurementEvents( MeasurementService, DisplaySetService, diff --git a/extensions/cornerstone-3d/src/Viewport/Overlays/CornerstoneOverlays.tsx b/extensions/cornerstone-3d/src/Viewport/Overlays/CornerstoneOverlays.tsx new file mode 100644 index 00000000000..826d6f4de02 --- /dev/null +++ b/extensions/cornerstone-3d/src/Viewport/Overlays/CornerstoneOverlays.tsx @@ -0,0 +1,84 @@ +import React, { useEffect, useState } from 'react'; + +import ViewportImageScrollbar from './ViewportImageScrollbar'; +import ViewportOverlay from './ViewportOverlay'; +import ViewportOrientationMarkers from './ViewportOrientationMarkers'; +import ViewportLoadingIndicator from './ViewportLoadingIndicator'; +import Cornerstone3DCacheService from '../../services/ViewportService/Cornerstone3DCacheService'; + +function CornerstoneOverlays(props) { + const { + viewportIndex, + ToolBarService, + element, + scrollbarHeight, + Cornerstone3DViewportService, + } = props; + const [imageSliceData, setImageSliceData] = useState({ + imageIndex: 0, + numberOfSlices: 0, + }); + const [viewportData, setViewportData] = useState(null); + + useEffect(() => { + const { unsubscribe } = Cornerstone3DCacheService.subscribe( + Cornerstone3DCacheService.EVENTS.VIEWPORT_DATA_CHANGED, + props => { + if (props.viewportIndex !== viewportIndex) { + return; + } + + setViewportData(props.viewportData); + } + ); + + return () => { + unsubscribe(); + }; + }, [viewportIndex]); + + if (!element) { + return null; + } + + if (viewportData) { + const viewportInfo = Cornerstone3DViewportService.getViewportInfoByIndex( + viewportIndex + ); + + if (viewportInfo?.viewportOptions?.customViewportOptions?.hideOverlays) { + return null; + } + } + + return ( + <> + + + + + + ); +} + +export default CornerstoneOverlays; diff --git a/extensions/cornerstone-3d/src/Viewport/Overlays/ViewportImageScrollbar.tsx b/extensions/cornerstone-3d/src/Viewport/Overlays/ViewportImageScrollbar.tsx new file mode 100644 index 00000000000..7b79b7a4e22 --- /dev/null +++ b/extensions/cornerstone-3d/src/Viewport/Overlays/ViewportImageScrollbar.tsx @@ -0,0 +1,155 @@ +import React, { useCallback, useEffect } from 'react'; +import PropTypes from 'prop-types'; +import { Enums, Types, utilities } from '@cornerstonejs/core'; +import { utilities as csToolsUtils } from '@cornerstonejs/tools'; +import { ImageScrollbar } from '@ohif/ui'; + +function CornerstoneImageScrollbar({ + viewportData, + viewportIndex, + element, + imageSliceData, + setImageSliceData, + scrollbarHeight, + Cornerstone3DViewportService, +}) { + const onImageScrollbarChange = useCallback( + (imageIndex, viewportIndex) => { + const viewportInfo = Cornerstone3DViewportService.getViewportInfoByIndex( + viewportIndex + ); + + const viewportId = viewportInfo.getViewportId(); + const viewport = Cornerstone3DViewportService.getCornerstone3DViewport( + viewportId + ); + + csToolsUtils.jumpToSlice(viewport.element, { imageIndex }).then(() => { + setImageSliceData({ + ...imageSliceData, + imageIndex: imageIndex, + }); + }); + }, + [viewportIndex, viewportData, imageSliceData] + ); + + useEffect(() => { + if (!viewportData) { + return; + } + + const viewport = Cornerstone3DViewportService.getCornerstone3DViewportByIndex( + viewportIndex + ); + + if (!viewport) { + return; + } + + if (viewportData.viewportType === Enums.ViewportType.STACK) { + const imageId = viewport.getCurrentImageId(); + const index = viewportData?.imageIds?.indexOf(imageId); + + if (index === -1) { + return; + } + + setImageSliceData({ + imageIndex: index, + numberOfSlices: viewportData.imageIds.length, + }); + + return; + } + + if (viewportData.viewportType === Enums.ViewportType.ORTHOGRAPHIC) { + const sliceData = utilities.getImageSliceDataForVolumeViewport( + viewport as Types.IVolumeViewport + ); + + if (!sliceData) { + return; + } + + const { imageIndex, numberOfSlices } = sliceData; + setImageSliceData({ imageIndex, numberOfSlices }); + } + }, [viewportIndex, viewportData]); + + useEffect(() => { + if ( + !viewportData || + viewportData.viewportType !== Enums.ViewportType.STACK + ) { + return; + } + + const updateStackIndex = event => { + const { imageId } = event.detail; + // find the index of imageId in the imageIds + const index = viewportData?.imageIds?.indexOf(imageId); + if (index !== -1) { + setImageSliceData({ + imageIndex: index, + numberOfSlices: viewportData.imageIds.length, + }); + } + }; + + element.addEventListener(Enums.Events.STACK_NEW_IMAGE, updateStackIndex); + + return () => { + element.removeEventListener( + Enums.Events.STACK_NEW_IMAGE, + updateStackIndex + ); + }; + }, [viewportData, element]); + + useEffect(() => { + if ( + !viewportData || + viewportData.viewportType !== Enums.ViewportType.ORTHOGRAPHIC + ) { + return; + } + + const updateVolumeIndex = event => { + const { imageIndex, numberOfSlices } = event.detail; + // find the index of imageId in the imageIds + setImageSliceData({ imageIndex, numberOfSlices }); + }; + + element.addEventListener(Enums.Events.VOLUME_NEW_IMAGE, updateVolumeIndex); + + return () => { + element.removeEventListener( + Enums.Events.VOLUME_NEW_IMAGE, + updateVolumeIndex + ); + }; + }, [viewportData, element]); + + return ( + onImageScrollbarChange(evt, viewportIndex)} + max={ + imageSliceData.numberOfSlices ? imageSliceData.numberOfSlices - 1 : 0 + } + height={scrollbarHeight} + value={imageSliceData.imageIndex} + /> + ); +} + +CornerstoneImageScrollbar.propTypes = { + viewportData: PropTypes.object.isRequired, + viewportIndex: PropTypes.number.isRequired, + element: PropTypes.instanceOf(Element), + scrollbarHeight: PropTypes.string, + imageSliceData: PropTypes.object.isRequired, + setImageSliceData: PropTypes.func.isRequired, +}; + +export default CornerstoneImageScrollbar; diff --git a/extensions/cornerstone-3d/src/Viewport/ViewportLoadingIndicator.tsx b/extensions/cornerstone-3d/src/Viewport/Overlays/ViewportLoadingIndicator.tsx similarity index 100% rename from extensions/cornerstone-3d/src/Viewport/ViewportLoadingIndicator.tsx rename to extensions/cornerstone-3d/src/Viewport/Overlays/ViewportLoadingIndicator.tsx diff --git a/extensions/cornerstone-3d/src/Viewport/ViewportOrientationMarkers.css b/extensions/cornerstone-3d/src/Viewport/Overlays/ViewportOrientationMarkers.css similarity index 100% rename from extensions/cornerstone-3d/src/Viewport/ViewportOrientationMarkers.css rename to extensions/cornerstone-3d/src/Viewport/Overlays/ViewportOrientationMarkers.css diff --git a/extensions/cornerstone-3d/src/Viewport/ViewportOrientationMarkers.tsx b/extensions/cornerstone-3d/src/Viewport/Overlays/ViewportOrientationMarkers.tsx similarity index 92% rename from extensions/cornerstone-3d/src/Viewport/ViewportOrientationMarkers.tsx rename to extensions/cornerstone-3d/src/Viewport/Overlays/ViewportOrientationMarkers.tsx index fa712748104..7e87e0becdb 100644 --- a/extensions/cornerstone-3d/src/Viewport/ViewportOrientationMarkers.tsx +++ b/extensions/cornerstone-3d/src/Viewport/Overlays/ViewportOrientationMarkers.tsx @@ -4,7 +4,7 @@ import { metaData, Enums, Types } from '@cornerstonejs/core'; import { utilities } from '@cornerstonejs/tools'; import './ViewportOrientationMarkers.css'; -import { getEnabledElement } from '../state'; +import { getEnabledElement } from '../../state'; /** * @@ -80,8 +80,9 @@ function getOrientationMarkers( } function ViewportOrientationMarkers({ + element, viewportData, - imageIndex, + imageSliceData, viewportIndex, orientationMarkers = ['top', 'left'], }) { @@ -91,14 +92,6 @@ function ViewportOrientationMarkers({ const [flipVertical, setFlipVertical] = useState(false); useEffect(() => { - const ohifEnabledElement = getEnabledElement(viewportIndex); - - if (!ohifEnabledElement || !ohifEnabledElement.element) { - return; - } - - const { element } = ohifEnabledElement; - const cameraModifiedListener = ( evt: Types.EventTypes.CameraModifiedEventDetail ) => { @@ -132,7 +125,16 @@ function ViewportOrientationMarkers({ const getMarkers = useCallback( orientationMarkers => { - const imageId = viewportData?.stack?.imageIds[imageIndex]; + // 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) { @@ -167,7 +169,7 @@ function ViewportOrientationMarkers({ )); }, - [flipHorizontal, flipVertical, rotation, viewportData, imageIndex] + [flipHorizontal, flipVertical, rotation, viewportData, imageSliceData] ); return ( diff --git a/extensions/cornerstone-3d/src/Viewport/CornerstoneOverlay.tsx b/extensions/cornerstone-3d/src/Viewport/Overlays/ViewportOverlay.tsx similarity index 57% rename from extensions/cornerstone-3d/src/Viewport/CornerstoneOverlay.tsx rename to extensions/cornerstone-3d/src/Viewport/Overlays/ViewportOverlay.tsx index c1512ce1535..2b511ed20e4 100644 --- a/extensions/cornerstone-3d/src/Viewport/CornerstoneOverlay.tsx +++ b/extensions/cornerstone-3d/src/Viewport/Overlays/ViewportOverlay.tsx @@ -1,40 +1,23 @@ import React, { useCallback, useEffect, useState } from 'react'; +import { vec3 } from 'gl-matrix'; import PropTypes from 'prop-types'; import { metaData, Enums, utilities } from '@cornerstonejs/core'; import { ViewportOverlay } from '@ohif/ui'; -import Cornerstone3DViewportService from '../services/ViewportService/Cornerstone3DViewportService'; +const EPSILON = 1e-4; -function CornerstoneOverlay({ +function CornerstoneViewportOverlay({ + element, viewportData, - imageIndex, + imageSliceData, viewportIndex, + Cornerstone3DViewportService, ToolBarService, }) { const [voi, setVOI] = useState({ windowCenter: null, windowWidth: null }); const [scale, setScale] = useState(1); const [activeTools, setActiveTools] = useState([]); - const getCornerstoneViewport = useCallback( - viewportIndex => { - const viewportInfo = Cornerstone3DViewportService.getViewportInfoByIndex( - viewportIndex - ); - - if (!viewportInfo) { - return; - } - - const viewportId = viewportInfo.getViewportId(); - const viewport = Cornerstone3DViewportService.getCornerstone3DViewport( - viewportId - ); - - return viewport; - }, - [viewportIndex, viewportData] - ); - /** * Initial toolbar state */ @@ -46,14 +29,6 @@ function CornerstoneOverlay({ * Updating the VOI when the viewport changes its voi */ useEffect(() => { - const viewport = getCornerstoneViewport(viewportIndex); - - if (!viewport) { - return; - } - - const { element } = viewport; - const updateVOI = eventDetail => { const { range } = eventDetail.detail; @@ -75,24 +50,19 @@ function CornerstoneOverlay({ return () => { element.removeEventListener(Enums.Events.VOI_MODIFIED, updateVOI); }; - }, [viewportIndex, viewportData]); + }, [viewportIndex, viewportData, voi, element]); /** * Updating the scale when the viewport changes its zoom */ useEffect(() => { - const viewport = getCornerstoneViewport(viewportIndex); - if (!viewport) { - return; - } - - const { element } = viewport; - const updateScale = eventDetail => { const { previousCamera, camera } = eventDetail.detail; if (previousCamera.parallelScale !== camera.parallelScale) { - const viewport = getCornerstoneViewport(viewportIndex); + const viewport = Cornerstone3DViewportService.getCornerstone3DViewportByIndex( + viewportIndex + ); if (!viewport) { return; @@ -167,45 +137,45 @@ function CornerstoneOverlay({ }, [voi, scale, activeTools]); const getTopRightContent = useCallback(() => { - const { stack } = viewportData; - const imageId = stack.imageIds[imageIndex]; - - if (!imageId) { - return null; + const { imageIndex, numberOfSlices } = imageSliceData; + if (!viewportData) { + return; } - const generalImageModule = - metaData.get('generalImageModule', imageId) || {}; - const { instanceNumber } = generalImageModule; + let instanceNumber; - const stackSize = stack.imageIds ? stack.imageIds.length : 0; + if (viewportData.viewportType === Enums.ViewportType.STACK) { + instanceNumber = _getInstanceNumberFromStack(viewportData, imageIndex); - if (stackSize <= 1) { - return null; + if (!instanceNumber) { + return null; + } + } else if (viewportData.viewportType === Enums.ViewportType.ORTHOGRAPHIC) { + instanceNumber = _getInstanceNumberFromVolume( + viewportData, + imageIndex, + viewportIndex, + Cornerstone3DViewportService + ); } - const instanceNumberInt = parseInt(instanceNumber); - return (
I: - {instanceNumberInt !== undefined - ? `${instanceNumberInt} (${imageIndex + 1}/${stackSize})` - : `${imageIndex + 1}/${stackSize}`} + {instanceNumber !== undefined + ? `${instanceNumber} (${imageIndex + 1}/${numberOfSlices})` + : `${imageIndex + 1}/${numberOfSlices}`}
); - }, [imageIndex, viewportData]); + }, [imageSliceData, viewportData, viewportIndex]); if (!viewportData) { return null; } - // Todo: fix this for volume later - const { stack } = viewportData; - - if (!stack || stack.imageIds.length === 0) { + if (viewportData.imageIds.length === 0) { throw new Error( 'ViewportOverlay: only viewports with imageIds is supported at this time' ); @@ -219,10 +189,74 @@ function CornerstoneOverlay({ ); } -CornerstoneOverlay.propTypes = { +function _getInstanceNumberFromStack(viewportData, imageIndex) { + const imageIds = viewportData.imageIds; + const imageId = imageIds[imageIndex]; + + if (!imageId) { + return; + } + + const generalImageModule = metaData.get('generalImageModule', imageId) || {}; + const { instanceNumber } = generalImageModule; + + const stackSize = imageIds.length; + + if (stackSize <= 1) { + return; + } + + return parseInt(instanceNumber); +} + +// Since volume viewports can be in any view direction, they can render +// a reconstructed image which don't have imageIds; therefore, no instance and instanceNumber +// Here we check if viewport is in the acquisition direction and if so, we get the instanceNumber +function _getInstanceNumberFromVolume( + viewportData, + imageIndex, + viewportIndex, + Cornerstone3DViewportService +) { + const volumes = viewportData.volumes; + + // Todo: support fusion of acquisition plane which has instanceNumber + if (!volumes || volumes.length > 1) { + return; + } + + const volume = volumes[0]; + const { direction, imageIds } = volume; + + const cornerstoneViewport = Cornerstone3DViewportService.getCornerstone3DViewportByIndex( + viewportIndex + ); + + if (!cornerstoneViewport) { + return; + } + + const camera = cornerstoneViewport.getCamera(); + const { viewPlaneNormal } = camera; + // checking if camera is looking at the acquisition plane (defined by the direction on the volume) + + const scanAxisNormal = direction.slice(6, 9); + + // check if viewPlaneNormal is parallel to scanAxisNormal + const cross = vec3.cross(vec3.create(), viewPlaneNormal, scanAxisNormal); + const isAcquisitionPlane = vec3.length(cross) < EPSILON; + + if (isAcquisitionPlane) { + const { instanceNumber } = + metaData.get('generalImageModule', imageIds[imageIndex]) || {}; + return parseInt(instanceNumber); + } +} + +CornerstoneViewportOverlay.propTypes = { viewportData: PropTypes.object, imageIndex: PropTypes.number, viewportIndex: PropTypes.number, }; -export default CornerstoneOverlay; +export default CornerstoneViewportOverlay; diff --git a/extensions/cornerstone-3d/src/commandsModule.js b/extensions/cornerstone-3d/src/commandsModule.js index 15e23569a81..0eedba8a479 100644 --- a/extensions/cornerstone-3d/src/commandsModule.js +++ b/extensions/cornerstone-3d/src/commandsModule.js @@ -1,12 +1,12 @@ -import * as cornerstone3D from '@cornerstonejs/core'; -import * as cornerstone3DTools from '@cornerstonejs/tools'; -import Cornerstone3DViewportService from './services/ViewportService/Cornerstone3DViewportService'; +import * as cornerstone from '@cornerstonejs/core'; +import * as cornerstoneTools from '@cornerstonejs/tools'; import CornerstoneViewportDownloadForm from './utils/CornerstoneViewportDownloadForm'; import { Enums } from '@cornerstonejs/tools'; import { getEnabledElement } from './state'; import callInputDialog from './utils/callInputDialog'; +import { setColormap } from './utils/colormap/transferFunctionHelpers'; const commandsModule = ({ servicesManager }) => { const { @@ -15,19 +15,67 @@ const commandsModule = ({ servicesManager }) => { CineService, ToolBarService, UIDialogService, + Cornerstone3DViewportService, + SegmentationService, } = servicesManager.services; function _getActiveViewportEnabledElement() { const { activeViewportIndex } = ViewportGridService.getState(); const { element } = getEnabledElement(activeViewportIndex) || {}; - const enabledElement = cornerstone3D.getEnabledElement(element); + const enabledElement = cornerstone.getEnabledElement(element); return enabledElement; } + function _getToolGroup(toolGroupId) { + let toolGroupIdToUse = toolGroupId; + + if (!toolGroupIdToUse) { + // Use the active viewport's tool group if no tool group id is provided + const enabledElement = _getActiveViewportEnabledElement(); + + if (!enabledElement) { + return; + } + + const { renderingEngineId, viewportId } = enabledElement; + const toolGroup = cornerstoneTools.ToolGroupManager.getToolGroupForViewport( + viewportId, + renderingEngineId + ); + + if (!toolGroup) { + console.warn( + 'No tool group found for viewportId:', + viewportId, + 'and renderingEngineId:', + renderingEngineId + ); + return; + } + + toolGroupIdToUse = toolGroup.id; + } + + const toolGroup = ToolGroupService.getToolGroup(toolGroupIdToUse); + return toolGroup; + } + const actions = { getActiveViewportEnabledElement: () => { return _getActiveViewportEnabledElement(); }, + setViewportActive: ({ viewportId }) => { + const viewportInfo = Cornerstone3DViewportService.getViewportInfo( + viewportId + ); + if (!viewportInfo) { + console.warn('No viewport found for viewportId:', viewportId); + return; + } + + const viewportIndex = viewportInfo.getViewportIndex(); + ViewportGridService.setActiveViewportIndex(viewportIndex); + }, arrowTextCallback: ({ callback, data }) => { callInputDialog(UIDialogService, data, callback); }, @@ -61,7 +109,7 @@ const commandsModule = ({ servicesManager }) => { const lower = windowCenterNum - windowWidthNum / 2.0; const upper = windowCenterNum + windowWidthNum / 2.0; - if (viewport instanceof cornerstone3D.StackViewport) { + if (viewport instanceof cornerstone.StackViewport) { viewport.setProperties({ voiRange: { upper, @@ -72,35 +120,35 @@ const commandsModule = ({ servicesManager }) => { viewport.render(); } }, - setToolActive: ({ toolName, toolGroupId = null }) => { - let toolGroupIdToUse = toolGroupId; - if (!toolGroupIdToUse) { - // Use the active viewport's tool group if no tool group id is provided - const enabledElement = _getActiveViewportEnabledElement(); - - if (!enabledElement) { - return; - } + toggleCrosshairs({ toolGroupId, toggledState }) { + const toolName = 'Crosshairs'; + // If it is Enabled + if (toggledState) { + actions.setToolActive({ toolName, toolGroupId }); + return; + } + const toolGroup = _getToolGroup(toolGroupId); - const { renderingEngineId, viewportId } = enabledElement; - const toolGroup = cornerstone3DTools.ToolGroupManager.getToolGroupForViewport( - viewportId, - renderingEngineId - ); + if (!toolGroup) { + return; + } - if (!toolGroup) { - console.warn( - 'No tool group found for viewportId:', - viewportId, - 'and renderingEngineId:', - renderingEngineId - ); - } + toolGroup.setToolDisabled(toolName); - toolGroupIdToUse = toolGroup.id; + // 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 === + cornerstoneTools.Enums.ToolModes.Passive + ) { + toolGroup.setToolActive(primaryActiveTool, { + bindings: [{ mouseButton: Enums.MouseBindings.Primary }], + }); } - - const toolGroup = ToolGroupService.getToolGroup(toolGroupIdToUse); + }, + setToolActive: ({ toolName, toolGroupId = null }) => { + const toolGroup = _getToolGroup(toolGroupId); if (!toolGroup) { console.warn('No tool group found for toolGroupId:', toolGroupId); @@ -113,8 +161,6 @@ const commandsModule = ({ servicesManager }) => { viewports: [], }; - const toolGroupViewportIds = toolGroup.getViewportIds(); - // iterate over all viewports and set the tool active for the // viewports that belong to the toolGroup for (let index = 0; index < viewports.length; index++) { @@ -124,7 +170,7 @@ const commandsModule = ({ servicesManager }) => { continue; } - const viewport = cornerstone3D.getEnabledElement( + const viewport = cornerstone.getEnabledElement( ohifEnabledElement.element ); @@ -158,6 +204,7 @@ const commandsModule = ({ servicesManager }) => { contentProps: { activeViewportIndex, onClose: UIModalService.hide, + Cornerstone3DViewportService, }, }); } @@ -170,7 +217,7 @@ const commandsModule = ({ servicesManager }) => { const { viewport } = enabledElement; - if (viewport instanceof cornerstone3D.StackViewport) { + if (viewport instanceof cornerstone.StackViewport) { const { rotation: currentRotation } = viewport.getProperties(); const newRotation = (currentRotation + rotation) % 360; viewport.setProperties({ rotation: newRotation }); @@ -186,7 +233,7 @@ const commandsModule = ({ servicesManager }) => { const { viewport } = enabledElement; - if (viewport instanceof cornerstone3D.StackViewport) { + if (viewport instanceof cornerstone.StackViewport) { const { flipHorizontal } = viewport.getCamera(); viewport.setCamera({ flipHorizontal: !flipHorizontal }); viewport.render(); @@ -201,7 +248,7 @@ const commandsModule = ({ servicesManager }) => { const { viewport } = enabledElement; - if (viewport instanceof cornerstone3D.StackViewport) { + if (viewport instanceof cornerstone.StackViewport) { const { flipVertical } = viewport.getCamera(); viewport.setCamera({ flipVertical: !flipVertical }); viewport.render(); @@ -222,7 +269,7 @@ const commandsModule = ({ servicesManager }) => { const { viewport } = enabledElement; - if (viewport instanceof cornerstone3D.StackViewport) { + if (viewport instanceof cornerstone.StackViewport) { const { invert } = viewport.getProperties(); viewport.setProperties({ invert: !invert }); viewport.render(); @@ -237,7 +284,7 @@ const commandsModule = ({ servicesManager }) => { const { viewport } = enabledElement; - if (viewport instanceof cornerstone3D.StackViewport) { + if (viewport instanceof cornerstone.StackViewport) { viewport.resetProperties(); viewport.resetCamera(); viewport.render(); @@ -252,7 +299,7 @@ const commandsModule = ({ servicesManager }) => { } const { viewport } = enabledElement; - if (viewport instanceof cornerstone3D.StackViewport) { + if (viewport instanceof cornerstone.StackViewport) { if (direction) { const { parallelScale } = viewport.getCamera(); viewport.setCamera({ parallelScale: parallelScale * scaleFactor }); @@ -271,14 +318,92 @@ const commandsModule = ({ servicesManager }) => { } const { viewport } = enabledElement; - const options = { delta: direction }; - cornerstone3DTools.utilities.stackScrollTool.scrollThroughStack( + cornerstoneTools.utilities.stackScrollTool.scrollThroughStack( viewport, options ); }, + async createSegmentationForDisplaySet({ displaySetInstanceUID }) { + const volumeId = displaySetInstanceUID; + + const segmentationUID = cornerstone.utilities.uuidv4(); + const segmentationId = `${volumeId}::${segmentationUID}`; + + await cornerstone.volumeLoader.createAndCacheDerivedVolume(volumeId, { + volumeId: segmentationId, + }); + + // Add the segmentations to state + cornerstoneTools.segmentation.addSegmentations([ + { + segmentationId, + representation: { + // The type of segmentation + type: cornerstoneTools.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 cornerstoneTools.segmentation.addSegmentationRepresentations( + toolGroupId, + [ + { + segmentationId, + type: representationType, + }, + ] + ); + }, + getLabelmapVolumes: ({ segmentations }) => { + if (!segmentations || !segmentations.length) { + segmentations = SegmentationService.getSegmentations(); + } + + const labelmapVolumes = segmentations.map(segmentation => { + return cornerstone.cache.getVolume(segmentation.id); + }); + + return labelmapVolumes; + }, + setViewportColormap: ({ + viewportIndex, + displaySetInstanceUID, + colormap, + immediate = false, + }) => { + const viewport = Cornerstone3DViewportService.getCornerstone3DViewportByIndex( + viewportIndex + ); + + const actorEntries = viewport.getActors(); + + const actorEntry = actorEntries.find(actorEntry => { + return actorEntry.uid === displaySetInstanceUID; + }); + + const { actor: volumeActor } = actorEntry; + + setColormap(volumeActor, colormap); + + if (immediate) { + viewport.render(); + } + }, }; const definitions = { @@ -292,6 +417,11 @@ const commandsModule = ({ servicesManager }) => { storeContexts: [], options: {}, }, + toggleCrosshairs: { + commandFn: actions.toggleCrosshairs, + storeContexts: [], + options: {}, + }, rotateViewportCW: { commandFn: actions.rotateViewport, storeContexts: [], @@ -362,6 +492,32 @@ const commandsModule = ({ servicesManager }) => { storeContexts: [], options: {}, }, + setViewportActive: { + commandFn: actions.setViewportActive, + 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: [], + options: {}, + }, }; return { diff --git a/extensions/cornerstone-3d/src/index.tsx b/extensions/cornerstone-3d/src/index.tsx index 81ed450a70d..e8b5d4b115b 100644 --- a/extensions/cornerstone-3d/src/index.tsx +++ b/extensions/cornerstone-3d/src/index.tsx @@ -11,10 +11,12 @@ import { Enums as cs3DToolsEnums } from '@cornerstonejs/tools'; import init from './init.js'; import commandsModule from './commandsModule'; import ToolGroupService from './services/ToolGroupService'; +import SyncGroupService from './services/SyncGroupService'; import { toolNames } from './initCornerstoneTools'; import { getEnabledElement } from './state'; import Cornerstone3DViewportService from './services/ViewportService/Cornerstone3DViewportService'; import dicomLoaderService from './utils/dicomLoaderService'; +import { registerColormap } from './utils/colormap/transferFunctionHelpers'; import { id } from './id'; @@ -62,7 +64,11 @@ const cornerstone3DExtension = { configuration = {}, appConfig, }) { + servicesManager.registerService( + Cornerstone3DViewportService(servicesManager) + ); servicesManager.registerService(ToolGroupService(servicesManager)); + servicesManager.registerService(SyncGroupService(servicesManager)); await init({ servicesManager, commandsManager, configuration, appConfig }); }, getViewportModule({ servicesManager, commandsManager }) { @@ -105,8 +111,8 @@ const cornerstone3DExtension = { return { cornerstone3D, cornerstone3DTools }; }, getEnabledElement, - Cornerstone3DViewportService, dicomLoaderService, + registerColormap, }, }, { diff --git a/extensions/cornerstone-3d/src/init.js b/extensions/cornerstone-3d/src/init.js index bea513adc16..680f2b5b6e8 100644 --- a/extensions/cornerstone-3d/src/init.js +++ b/extensions/cornerstone-3d/src/init.js @@ -2,22 +2,31 @@ import OHIF from '@ohif/core'; import { ContextMenuMeasurements } from '@ohif/ui'; import * as cornerstone3D from '@cornerstonejs/core'; +import * as cornerstone3DTools from '@cornerstonejs/tools'; import { init as cs3DInit, eventTarget, EVENTS, + volumeLoader, + imageLoader, imageLoadPoolManager, Settings, } from '@cornerstonejs/core'; import { Enums, utilities } from '@cornerstonejs/tools'; +import { + cornerstoneStreamingImageVolumeLoader, + sharedArrayBufferImageLoader, +} from '@cornerstonejs/streaming-image-volume-loader'; import initWADOImageLoader from './initWADOImageLoader'; -import Cornerstone3DViewportService from './services/ViewportService/Cornerstone3DViewportService'; import initCornerstoneTools from './initCornerstoneTools'; import { connectToolsToMeasurementService } from './initMeasurementService'; 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; @@ -25,6 +34,7 @@ let CONTEXT_MENU_OPEN = false; // TODO: Cypress tests are currently grabbing this from the window? window.cornerstone = cornerstone3D; +window.cornerstoneTools = cornerstone3DTools; /** * */ @@ -48,10 +58,35 @@ export default async function init({ DisplaySetService, UIDialogService, CineService, + Cornerstone3DViewportService, + HangingProtocolService, + SegmentationService, } = servicesManager.services; const metadataProvider = OHIF.classes.MetadataProvider; + volumeLoader.registerUnknownVolumeLoader( + cornerstoneStreamingImageVolumeLoader + ); + volumeLoader.registerVolumeLoader( + 'cornerstoneStreamingImageVolume', + cornerstoneStreamingImageVolumeLoader + ); + + HangingProtocolService.registerImageLoadStrategy( + 'interleaveCenter', + interleaveCenterLoader + ); + HangingProtocolService.registerImageLoadStrategy( + 'interleaveTopToBottom', + interleaveTopToBottom + ); + + imageLoader.registerImageLoader( + 'streaming-wadors', + sharedArrayBufferImageLoader + ); + cornerstone3D.metaData.addProvider( metadataProvider.get.bind(metadataProvider), 9999 @@ -72,6 +107,8 @@ export default async function init({ Cornerstone3DViewportService ); + initSegmentationService(SegmentationService, Cornerstone3DViewportService); + initCineService(CineService); const _getDefaultPosition = event => ({ @@ -180,6 +217,24 @@ export default async function init({ UIDialogService.dismiss({ id: 'context-menu' }); }; + // When a custom image load is performed, update the relevant viewports + HangingProtocolService.subscribe( + HangingProtocolService.EVENTS.CUSTOM_IMAGE_LOAD_PERFORMED, + volumeInputArrayMap => { + for (const entry of volumeInputArrayMap.entries()) { + const [viewportId, volumeInputArray] = entry; + const viewport = Cornerstone3DViewportService.getCornerstone3DViewport( + viewportId + ); + + Cornerstone3DViewportService.setVolumesForViewport( + viewport, + volumeInputArray + ); + } + } + ); + /* * Because click gives us the native "mouse up", buttons will always be `0` * Need to fallback to event.which; @@ -211,7 +266,7 @@ export default async function init({ function elementDisabledHandler(evt) { const { viewportId, element } = evt.detail; - const viewportInfo = Cornerstone3DViewportService.getViewportInfoById( + const viewportInfo = Cornerstone3DViewportService.getViewportInfo( viewportId ); ToolGroupService.disable(viewportInfo); diff --git a/extensions/cornerstone-3d/src/initCornerstoneTools.js b/extensions/cornerstone-3d/src/initCornerstoneTools.js index e0bb399f641..a997e08dfb5 100644 --- a/extensions/cornerstone-3d/src/initCornerstoneTools.js +++ b/extensions/cornerstone-3d/src/initCornerstoneTools.js @@ -16,7 +16,9 @@ const { DragProbeTool, AngleTool, MagnifyTool, - // CrosshairsTool, + CrosshairsTool, + RectangleROIStartEndThresholdTool, + SegmentationDisplayTool, } = cornerstone3DTools; export default function initCornerstone3DTools(configuration = {}) { @@ -36,7 +38,8 @@ export default function initCornerstone3DTools(configuration = {}) { cornerstone3DTools.addTool(DragProbeTool); cornerstone3DTools.addTool(AngleTool); cornerstone3DTools.addTool(MagnifyTool); - // cornerstone3DTools.addTool(CrosshairsTool); + cornerstone3DTools.addTool(CrosshairsTool); + cornerstone3DTools.addTool(SegmentationDisplayTool); // Modify annotation tools to use dashed lines on SR const annotationStyle = { @@ -69,7 +72,8 @@ const toolNames = { Bidirectional: BidirectionalTool.toolName, Angle: AngleTool.toolName, Magnify: MagnifyTool.toolName, - // crosshairs: CrosshairsTool.toolName, + Crosshairs: CrosshairsTool.toolName, + SegmentationDisplay: SegmentationDisplayTool.toolName, }; export { toolNames }; diff --git a/extensions/cornerstone-3d/src/initMeasurementService.js b/extensions/cornerstone-3d/src/initMeasurementService.js index 2a9b2aa2908..fac2b120b97 100644 --- a/extensions/cornerstone-3d/src/initMeasurementService.js +++ b/extensions/cornerstone-3d/src/initMeasurementService.js @@ -215,14 +215,21 @@ const connectMeasurementServiceToTools = ( const { uid, label } = measurement; const sourceAnnotation = annotation.state.getAnnotation(uid); + const { data, metadata } = sourceAnnotation; - if (sourceAnnotation) { - sourceAnnotation.data.label = label; - if (sourceAnnotation.hasOwnProperty('text')) { - // Deal with the weird case of ArrowAnnotate. - sourceAnnotation.text = label; - } + if (!data) { + return; + } + + if (data.label !== label) { + data.label = label; } + + if (metadata.toolName === 'ArrowAnnotate') { + data.text = label; + } + + // Todo: trigger render for annotation } ); diff --git a/extensions/cornerstone-3d/src/initSegmentationService.js b/extensions/cornerstone-3d/src/initSegmentationService.js new file mode 100644 index 00000000000..bbf5d924288 --- /dev/null +++ b/extensions/cornerstone-3d/src/initSegmentationService.js @@ -0,0 +1,141 @@ +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, + Cornerstone3DViewportService +) { + connectToolsToSegmentationService( + SegmentationService, + Cornerstone3DViewportService + ); + + connectSegmentationServiceToTools( + SegmentationService, + Cornerstone3DViewportService + ); +} + +function connectToolsToSegmentationService( + SegmentationService, + Cornerstone3DViewportService +) { + connectSegmentationServiceToTools( + SegmentationService, + Cornerstone3DViewportService + ); + 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, + Cornerstone3DViewportService +) { + 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; diff --git a/extensions/cornerstone-3d/src/initWADOImageLoader.js b/extensions/cornerstone-3d/src/initWADOImageLoader.js index 5800c4c40d9..bc6a5435647 100644 --- a/extensions/cornerstone-3d/src/initWADOImageLoader.js +++ b/extensions/cornerstone-3d/src/initWADOImageLoader.js @@ -62,7 +62,6 @@ export default function initWADOImageLoader( // http://dicom.nema.org/medical/dicom/current/output/html/part18.html const xhrRequestHeaders = { accept: 'multipart/related; type=application/octet-stream', - //accept: 'multipart/related; type="image/x-jls"', // 'multipart/related; type="image/x-jls", multipart/related; type="image/jls"; transfer-syntax="1.2.840.10008.1.2.4.80", multipart/related; type="image/x-jls", multipart/related; type="application/octet-stream"; transfer-syntax=*', }; diff --git a/extensions/cornerstone-3d/src/services/SyncGroupService/SyncGroupService.ts b/extensions/cornerstone-3d/src/services/SyncGroupService/SyncGroupService.ts new file mode 100644 index 00000000000..4c8542caefa --- /dev/null +++ b/extensions/cornerstone-3d/src/services/SyncGroupService/SyncGroupService.ts @@ -0,0 +1,109 @@ +import { synchronizers, SynchronizerManager } from '@cornerstonejs/tools'; + +import { pubSubServiceInterface } from '@ohif/core'; + +const EVENTS = { + TOOL_GROUP_CREATED: + 'event::cornerstone-3d::syncgroupservice:toolgroupcreated', +}; + +export type SyncGroup = { + type: string; + id: string; + source: boolean; + target: boolean; +}; + +const POSITION = 'cameraposition'; +const VOI = 'voi'; + +export default class SyncGroupService { + serviceManager: any; + listeners: { [key: string]: (...args: any[]) => void } = {}; + EVENTS: { [key: string]: string }; + + constructor(serviceManager) { + this.serviceManager = serviceManager; + this.listeners = {}; + this.EVENTS = EVENTS; + // + Object.assign(this, pubSubServiceInterface); + } + + private _createSynchronizer(type: string, id: string) { + type = type.toLowerCase(); + if (type === POSITION) { + return synchronizers.createCameraPositionSynchronizer(id); + } else if (type === VOI) { + return synchronizers.createVOISynchronizer(id); + } + } + + public addViewportToSyncGroup( + viewportId: string, + renderingEngineId: string, + syncGroups?: SyncGroup[] + ): void { + if (!syncGroups || !syncGroups.length) { + return; + } + + syncGroups.forEach(syncGroup => { + const { type, id, target, source } = syncGroup; + + let synchronizer = SynchronizerManager.getSynchronizer(id); + + if (!synchronizer) { + synchronizer = this._createSynchronizer(type, id); + } + + if (target && source) { + synchronizer.add({ + viewportId, + renderingEngineId, + }); + return; + } else if (source) { + synchronizer.addSource({ + viewportId, + renderingEngineId, + }); + } else if (target) { + synchronizer.addTarget({ + viewportId, + renderingEngineId, + }); + } + }); + } + + public destroy() { + SynchronizerManager.destroy(); + } + + public removeViewportFromSyncGroup( + viewportId: string, + renderingEngineId: string + ): void { + const synchronizers = SynchronizerManager.getAllSynchronizers(); + + synchronizers.forEach(synchronizer => { + if (!synchronizer) { + return; + } + + synchronizer.remove({ + viewportId, + renderingEngineId, + }); + + // check if any viewport is left in any of the sync groups, if not, delete that sync group + const sourceViewports = synchronizer.getSourceViewports(); + const targetViewports = synchronizer.getTargetViewports(); + + if (!sourceViewports.length && !targetViewports.length) { + SynchronizerManager.destroySynchronizer(synchronizer.id); + } + }); + } +} diff --git a/extensions/cornerstone-3d/src/services/SyncGroupService/index.js b/extensions/cornerstone-3d/src/services/SyncGroupService/index.js new file mode 100644 index 00000000000..9a49fcca17a --- /dev/null +++ b/extensions/cornerstone-3d/src/services/SyncGroupService/index.js @@ -0,0 +1,10 @@ +import SyncGroupService from './SyncGroupService'; + +export default function ExtendedSyncGroupService(serviceManager) { + return { + name: 'SyncGroupService', + create: ({ configuration = {} }) => { + return new SyncGroupService(serviceManager); + }, + }; +} diff --git a/extensions/cornerstone-3d/src/services/ToolGroupService/ToolGroupService.ts b/extensions/cornerstone-3d/src/services/ToolGroupService/ToolGroupService.ts index bdd4888eafb..809acf01360 100644 --- a/extensions/cornerstone-3d/src/services/ToolGroupService/ToolGroupService.ts +++ b/extensions/cornerstone-3d/src/services/ToolGroupService/ToolGroupService.ts @@ -2,10 +2,9 @@ import { ToolGroupManager, Enums, Types } from '@cornerstonejs/tools'; import { pubSubServiceInterface } from '@ohif/core'; -import Cornerstone3DViewportService from '../ViewportService/Cornerstone3DViewportService'; - const EVENTS = { VIEWPORT_ADDED: 'event::cornerstone-3d::toolgroupservice:viewportadded', + TOOLGROUP_CREATED: 'event::cornerstone-3d::toolgroupservice:toolgroupcreated', }; type Tool = { @@ -30,7 +29,8 @@ export default class ToolGroupService { EVENTS: { [key: string]: string }; constructor(serviceManager) { - this.serviceManager = serviceManager; + const { Cornerstone3DViewportService } = serviceManager.services; + this.Cornerstone3DViewportService = Cornerstone3DViewportService; this.listeners = {}; this.EVENTS = EVENTS; Object.assign(this, pubSubServiceInterface); @@ -51,7 +51,7 @@ export default class ToolGroupService { } public getToolGroupForViewport(viewportId: string): Types.IToolGroup | void { - const renderingEngine = Cornerstone3DViewportService.getRenderingEngine(); + const renderingEngine = this.Cornerstone3DViewportService.getRenderingEngine(); return ToolGroupManager.getToolGroupForViewport( viewportId, renderingEngine.id @@ -90,7 +90,7 @@ export default class ToolGroupService { } } - public addToolGroupViewport( + public addViewportToToolGroup( viewportId: string, renderingEngineId: string, toolGroupId?: string @@ -110,7 +110,7 @@ export default class ToolGroupService { toolGroup.addViewport(viewportId, renderingEngineId); } - this._broadcastEvent(EVENTS.VIEWPORT_ADDED, { viewportId }); + this._broadcastEvent(EVENTS.VIEWPORT_ADDED, { viewportId, toolGroupId }); } public createToolGroup(toolGroupId: string): Types.IToolGroup { @@ -122,6 +122,8 @@ export default class ToolGroupService { const toolGroup = ToolGroupManager.createToolGroup(toolGroupId); this.toolGroupIds.add(toolGroupId); + this._broadcastEvent(EVENTS.TOOLGROUP_CREATED, { toolGroupId }); + return toolGroup; } @@ -156,7 +158,40 @@ export default class ToolGroupService { // } // }); } + */ + + /** + * Get the tool's configuration based on the tool name and tool group id + * @param toolGroupId - The id of the tool group that the tool instance belongs to. + * @param toolName - The name of the tool + * @returns The configuration of the tool. + */ + public getToolConfiguration(toolGroupId: string, toolName: string) { + const toolGroup = ToolGroupManager.getToolGroup(toolGroupId); + if (!toolGroup) { + return null; + } + + const tool = toolGroup.getToolInstance(toolName); + if (!tool) { + return null; + } + + return tool.configuration; + } + + /** + * Set the tool instance configuration. This will update the tool instance configuration + * on the toolGroup + * @param toolGroupId - The id of the tool group that the tool instance belongs to. + * @param toolName - The name of the tool + * @param config - The configuration object that you want to set. */ + public setToolConfiguration(toolGroupId, toolName, config) { + const toolGroup = ToolGroupManager.getToolGroup(toolGroupId); + const toolInstance = toolGroup.getToolInstance(toolName); + toolInstance.configuration = config; + } private _getToolNames(toolGroupTools: Tools): string[] { const toolNames = []; @@ -175,6 +210,12 @@ export default class ToolGroupService { }); } + if (toolGroupTools.disabled) { + toolGroupTools.disabled.forEach(tool => { + toolNames.push(tool.toolName); + }); + } + return toolNames; } diff --git a/extensions/cornerstone-3d/src/services/ViewportService/Cornerstone3DCacheService.ts b/extensions/cornerstone-3d/src/services/ViewportService/Cornerstone3DCacheService.ts new file mode 100644 index 00000000000..102f80a834a --- /dev/null +++ b/extensions/cornerstone-3d/src/services/ViewportService/Cornerstone3DCacheService.ts @@ -0,0 +1,235 @@ +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; + initialImageIdIndex?: 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-3d::viewportdatachanged', +}; + +class Cornerstone3DCacheService { + stackImageIds: Map = new Map(); + volumeImageIds: Map = 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, + initialImageIdOrIndex?: number | string + ): Promise { + const cs3DViewportType = getCornerstoneViewportType(viewportType); + let viewportData: StackData | VolumeData; + + if (cs3DViewportType === Enums.ViewportType.STACK) { + viewportData = await this._getStackViewportData( + dataSource, + displaySets, + initialImageIdOrIndex + ); + } + + if (cs3DViewportType === Enums.ViewportType.ORTHOGRAPHIC) { + viewportData = await this._getVolumeViewportData( + dataSource, + displaySets, + initialImageIdOrIndex + ); + } + + viewportData.viewportType = cs3DViewportType; + + 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, + initialImageIdOrIndex + ): 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, + imageIds: stackImageIds, + }; + + if (initialImageIdOrIndex !== undefined) { + if (typeof initialImageIdOrIndex === 'number') { + stackData.initialImageIdIndex = initialImageIdOrIndex; + } else { + stackData.initialImageIdIndex = stackData.imageIds.indexOf( + initialImageIdOrIndex + ); + } + } + + return stackData; + } + + private async _getVolumeViewportData( + dataSource, + displaySets + ): Promise { + // 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, + 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 Cornerstone3DCacheService(); +export default CacheService; diff --git a/extensions/cornerstone-3d/src/services/ViewportService/Cornerstone3DViewportService.ts b/extensions/cornerstone-3d/src/services/ViewportService/Cornerstone3DViewportService.ts index 289d2cfc24d..a57b3866def 100644 --- a/extensions/cornerstone-3d/src/services/ViewportService/Cornerstone3DViewportService.ts +++ b/extensions/cornerstone-3d/src/services/ViewportService/Cornerstone3DViewportService.ts @@ -2,14 +2,29 @@ import { pubSubServiceInterface } from '@ohif/core'; import { RenderingEngine, StackViewport, + Enums, Types, getRenderingEngine, utilities as csUtils, + VolumeViewport, + cache, } from '@cornerstonejs/core'; + +import { utilities as csToolsUtils } from '@cornerstonejs/tools'; import { IViewportService } from './IViewportService'; import { RENDERING_ENGINE_ID } from './constants'; -import ViewportInfo, { ViewportOptions, DisplaySetOptions } from './Viewport'; -import { IStackViewport } from '@cornerstonejs/core/dist/esm/types'; +import ViewportInfo, { + ViewportOptions, + DisplaySetOptions, + PublicViewportOptions, +} from './Viewport'; +import { StackData, VolumeData } from './Cornerstone3DCacheService'; +import { + setColormap, + setLowerUpperColorTransferFunction, +} from '../../utils/colormap/transferFunctionHelpers'; + +import JumpPresets from '../../utils/JumpPresets'; const EVENTS = { VIEWPORT_INFO_CREATED: @@ -29,20 +44,22 @@ class Cornerstone3DViewportService implements IViewportService { * Service-specific */ EVENTS: { [key: string]: string }; - listeners: { [key: string]: Function[] }; + listeners: { [key: string]: Array<(...args: any[]) => void> }; _broadcastEvent: unknown; // we should be able to extend the PubSub class to get this // Some configs enableResizeDetector: true; resizeRefreshRateMs: 200; resizeRefreshMode: 'debounce'; - constructor() { + constructor(servicesManager) { this.renderingEngine = null; this.viewportGridResizeObserver = null; this.viewportsInfo = new Map(); // this.listeners = {}; this.EVENTS = EVENTS; + const { HangingProtocolService } = servicesManager.services; + this.HangingProtocolService = HangingProtocolService; Object.assign(this, pubSubServiceInterface); // } @@ -52,8 +69,13 @@ class Cornerstone3DViewportService implements IViewportService { * @param {*} viewportIndex * @param {*} elementRef */ - public enableElement(viewportIndex: number, elementRef: HTMLDivElement) { - const viewportId = this.getViewportId(viewportIndex); + public enableElement( + viewportIndex: number, + viewportOptions: PublicViewportOptions, + elementRef: HTMLDivElement + ) { + const viewportId = + viewportOptions.viewportId || this.getViewportId(viewportIndex); const viewportInfo = new ViewportInfo(viewportIndex, viewportId); viewportInfo.setElement(elementRef); this.viewportsInfo.set(viewportIndex, viewportInfo); @@ -90,6 +112,7 @@ class Cornerstone3DViewportService implements IViewportService { const immediate = true; const resetPan = false; const resetZoom = false; + this.renderingEngine.resize(immediate, resetPan, resetZoom); this.renderingEngine.render(); } @@ -102,6 +125,7 @@ class Cornerstone3DViewportService implements IViewportService { this.viewportGridResizeObserver = null; this.renderingEngine.destroy(); this.renderingEngine = null; + cache.purgeCache(); } /** @@ -134,33 +158,25 @@ class Cornerstone3DViewportService implements IViewportService { */ public setViewportDisplaySets( viewportIndex: number, - viewportData: unknown, - viewportOptions: ViewportOptions, - displaySetOptions: DisplaySetOptions[] + viewportData: StackData | VolumeData, + publicViewportOptions: PublicViewportOptions, + publicDisplaySetOptions: DisplaySetOptions[] ): void { const renderingEngine = this.getRenderingEngine(); const viewportInfo = this.viewportsInfo.get(viewportIndex); viewportInfo.setRenderingEngineId(renderingEngine.id); - // If new viewportOptions are provided and have keys that are not in the - // current viewportOptions, then we need to update the viewportOptions, - // else we inherit the current viewportOptions. - const currentViewportOptions = viewportInfo.getViewportOptions(); - let viewportOptionsToUse = currentViewportOptions; - if (Object.keys(viewportOptions)) { - viewportOptionsToUse = { - ...currentViewportOptions, - ...viewportOptions, - }; - } - viewportInfo.setViewportOptions(viewportOptionsToUse); + const { + viewportOptions, + displaySetOptions, + } = this._getViewportAndDisplaySetOptions( + publicViewportOptions, + publicDisplaySetOptions, + viewportInfo + ); - const currentDisplaySetOptions = viewportInfo.getDisplaySetOptions(); - let displaySetOptionsToUse = currentDisplaySetOptions; - if (displaySetOptions?.length) { - displaySetOptionsToUse = [...displaySetOptions]; - } - viewportInfo.setDisplaySetOptions(displaySetOptionsToUse); + viewportInfo.setViewportOptions(viewportOptions); + viewportInfo.setDisplaySetOptions(displaySetOptions); this._broadcastEvent(EVENTS.VIEWPORT_INFO_CREATED, viewportInfo); @@ -188,8 +204,28 @@ class Cornerstone3DViewportService implements IViewportService { this._setDisplaySets(viewportId, viewportData, viewportInfo); } - public getCornerstone3DViewport(viewportId: string): IStackViewport | null { - const viewportInfo = this.getViewportInfoById(viewportId); + public getCornerstone3DViewport( + viewportId: string + ): Types.IStackViewport | Types.IVolumeViewport | null { + const viewportInfo = this.getViewportInfo(viewportId); + + if ( + !viewportInfo || + !this.renderingEngine || + this.renderingEngine.hasBeenDestroyed + ) { + return null; + } + + const viewport = this.renderingEngine.getViewport(viewportId); + + return viewport; + } + + public getCornerstone3DViewportByIndex( + viewportIndex: number + ): Types.IStackViewport | Types.IVolumeViewport | null { + const viewportInfo = this.getViewportInfoByIndex(viewportIndex); if ( !viewportInfo || @@ -200,8 +236,8 @@ class Cornerstone3DViewportService implements IViewportService { } const viewport = this.renderingEngine.getViewport( - viewportId - ) as IStackViewport; + viewportInfo.getViewportId() + ); return viewport; } @@ -215,7 +251,7 @@ class Cornerstone3DViewportService implements IViewportService { return this.viewportsInfo.get(viewportIndex); } - public getViewportInfoById(viewportId: string): ViewportInfo { + public getViewportInfo(viewportId: string): ViewportInfo { // @ts-ignore for (const [index, viewport] of this.viewportsInfo.entries()) { if (viewport.getViewportId() === viewportId) { @@ -225,13 +261,25 @@ class Cornerstone3DViewportService implements IViewportService { return null; } - _setStackViewport(viewport, viewportData, viewportInfo) { + _setStackViewport( + viewport: Types.IStackViewport, + viewportData: StackData, + viewportInfo: ViewportInfo + ) { const displaySetOptions = viewportInfo.getDisplaySetOptions(); - const { imageIds, initialImageIdIndex } = viewportData.stack; + const { imageIds, initialImageIdIndex } = viewportData; + + let initialImageIdIndexToUse = initialImageIdIndex; + + if (!initialImageIdIndexToUse) { + initialImageIdIndexToUse = + this._getInitialImageIndexForStackViewport(viewportInfo, imageIds) || 0; + } + const { voi, voiInverted } = displaySetOptions[0]; const properties = {}; - if (voi.windowWidth || voi.windowCenter) { + if (voi && (voi.windowWidth || voi.windowCenter)) { const { lower, upper } = csUtils.windowLevel.toLowHighRange( voi.windowWidth, voi.windowCenter @@ -243,23 +291,213 @@ class Cornerstone3DViewportService implements IViewportService { properties.invert = voiInverted; } - viewport.setStack(imageIds, initialImageIdIndex).then(() => { + viewport.setStack(imageIds, initialImageIdIndexToUse).then(() => { viewport.setProperties(properties); csUtils.prefetchStack(imageIds); }); } + private _getInitialImageIndexForStackViewport( + viewportInfo: ViewportInfo, + imageIds?: string[] + ): number { + const initialImageOptions = viewportInfo.getInitialImageOptions(); + + if (!initialImageOptions) { + return; + } + + const { index, preset } = initialImageOptions; + return this._getInitialImageIndex(imageIds.length, index, preset); + } + + _getInitialImageIndex( + numberOfSlices: number, + imageIndex?: number, + preset?: JumpPresets + ): number { + const lastSliceIndex = numberOfSlices - 1; + + if (imageIndex !== undefined) { + return csToolsUtils.clip(imageIndex, 0, lastSliceIndex); + } + + if (preset === JumpPresets.First) { + return 0; + } + + if (preset === JumpPresets.Last) { + return lastSliceIndex; + } + + if (preset === JumpPresets.Middle) { + return Math.floor(lastSliceIndex / 2); + } + + return 0; + } + + async _setVolumeViewport( + viewport: Types.IVolumeViewport, + viewportData: VolumeData, + viewportInfo: ViewportInfo + ): Promise { + // TODO: We need to overhaul the way data sources work so requests can be made + // async. I think we should follow the image loader pattern which is async and + // has a cache behind it. + // The problem is that to set this volume, we need the metadata, but the request is + // already in-flight, and the promise is not cached, so we have no way to wait for + // it and know when it has fully arrived. + // loadStudyMetadata(StudyInstanceUID) => Promise([instances for study]) + // loadSeriesMetadata(StudyInstanceUID, SeriesInstanceUID) => Promise([instances for series]) + // If you call loadStudyMetadata and it's not in the DicomMetadataStore cache, it should fire + // a request through the data source? + // (This call may or may not create sub-requests for series metadata) + const volumeInputArray = []; + const displaySetOptionsArray = viewportInfo.getDisplaySetOptions(); + const { HangingProtocolService } = this; + + for (let i = 0; i < viewportData.imageIds.length; i++) { + const imageIds = viewportData.imageIds[i]; + const displaySetInstanceUID = viewportData.displaySetInstanceUIDs[i]; + const displaySetOptions = displaySetOptionsArray[i]; + + const volumeId = displaySetInstanceUID; + + // if (displaySet.needsRerendering) { + // console.warn('Removing volume from cache', volumeId); + // cache.removeVolumeLoadObject(volumeId); + // displaySet.needsRerendering = false; + // this.displaySetsNeedRerendering.add(displaySet.displaySetInstanceUID); + // } + + const voiCallbacks = this._getVOICallbacks(volumeId, displaySetOptions); + + const callback = ({ volumeActor }) => { + voiCallbacks.forEach(callback => callback(volumeActor)); + }; + + volumeInputArray.push({ + imageIds, + volumeId, + callback, + blendMode: displaySetOptions.blendMode, + slabThickness: this._getSlabThickness(displaySetOptions, volumeId), + }); + } + + if ( + HangingProtocolService.hasCustomImageLoadStrategy() && + !HangingProtocolService.customImageLoadPerformed + ) { + // delegate the volume loading to the hanging protocol service if it has a custom image load strategy + return HangingProtocolService.runImageLoadStrategy({ + viewportId: viewport.id, + volumeInputArray, + }); + } + + viewportData.volumes.forEach(volume => { + volume.load(); + }); + + this.setVolumesForViewport(viewport, volumeInputArray); + } + + public setVolumesForViewport(viewport, volumeInputArray) { + viewport.setVolumes(volumeInputArray).then(() => { + const viewportInfo = this.getViewportInfo(viewport.id); + const initialImageOptions = viewportInfo.getInitialImageOptions(); + + if ( + initialImageOptions && + (initialImageOptions.preset !== undefined || + initialImageOptions.index !== undefined) + ) { + const { index, preset } = initialImageOptions; + + const { numberOfSlices } = csUtils.getImageSliceDataForVolumeViewport( + viewport + ); + + const imageIndex = this._getInitialImageIndex( + numberOfSlices, + index, + preset + ); + + csToolsUtils.jumpToSlice(viewport.element, { + imageIndex, + }); + } + + viewport.render(); + }); + } + + public updateViewport(viewportIndex, viewportData) { + const viewportInfo = this.getViewportInfoByIndex(viewportIndex); + + const viewportId = viewportInfo.getViewportId(); + const viewport = this.getCornerstone3DViewport(viewportId); + + if (viewport instanceof VolumeViewport) { + this._setVolumeViewport(viewport, viewportData, viewportInfo); + return; + } + + if (viewport instanceof StackViewport) { + this._setStackViewport(viewport, viewportData, viewportInfo); + return; + } + } + + _getVOICallbacks(volumeId, displaySetOptions) { + const { voi, voiInverted: inverted, colormap } = displaySetOptions; + + const voiCallbackArray = []; + + // If colormap is set, use it to set the color transfer function + if (colormap) { + voiCallbackArray.push(volumeActor => setColormap(volumeActor, colormap)); + } + + if (voi instanceof Object && voi.windowWidth && voi.windowCenter) { + const { windowWidth, windowCenter } = voi; + const { lower, upper } = csUtils.windowLevel.toLowHighRange( + windowWidth, + windowCenter + ); + voiCallbackArray.push(volumeActor => + setLowerUpperColorTransferFunction({ + volumeActor, + lower, + upper, + inverted, + }) + ); + } + + return voiCallbackArray; + } + _setDisplaySets( viewportId: string, - viewportData: unknown, + viewportData: StackData | VolumeData, viewportInfo: ViewportInfo ): void { const viewport = this.getCornerstone3DViewport(viewportId); if (viewport instanceof StackViewport) { - this._setStackViewport(viewport, viewportData, viewportInfo); + this._setStackViewport(viewport, viewportData as StackData, viewportInfo); + } else if (viewport instanceof VolumeViewport) { + this._setVolumeViewport( + viewport, + viewportData as VolumeData, + viewportInfo + ); } else { - throw new Error('Unsupported viewport type'); + throw new Error('Unknown viewport type'); } } @@ -271,6 +509,72 @@ class Cornerstone3DViewportService implements IViewportService { this.viewportGridResizeObserver.disconnect(); } } + + _getSlabThickness(displaySetOptions, volumeId) { + const { blendMode } = displaySetOptions; + if ( + blendMode === undefined || + displaySetOptions.slabThickness === undefined + ) { + return; + } + + // if there is a slabThickness set as a number then use it + if (typeof displaySetOptions.slabThickness === 'number') { + return displaySetOptions.slabThickness; + } + + if (displaySetOptions.slabThickness.toLowerCase() === 'fullvolume') { + // calculate the slab thickness based on the volume dimensions + const imageVolume = cache.getVolume(volumeId); + + const { dimensions } = imageVolume; + const slabThickness = Math.sqrt( + dimensions[0] * dimensions[0] + + dimensions[1] * dimensions[1] + + dimensions[2] * dimensions[2] + ); + + return slabThickness; + } + } + + _getViewportAndDisplaySetOptions( + publicViewportOptions: PublicViewportOptions, + publicDisplaySetOptions: DisplaySetOptions[], + viewportInfo: ViewportInfo + ): { + viewportOptions: ViewportOptions; + displaySetOptions: DisplaySetOptions[]; + } { + const viewportIndex = viewportInfo.getViewportIndex(); + + // Creating a temporary viewportInfo to handle defaults + const newViewportInfo = new ViewportInfo( + viewportIndex, + viewportInfo.getViewportId() + ); + + // To handle setting the default values if missing for the viewportOptions and + // displaySetOptions + newViewportInfo.setPublicViewportOptions(publicViewportOptions); + newViewportInfo.setPublicDisplaySetOptions(publicDisplaySetOptions); + + const newViewportOptions = newViewportInfo.getViewportOptions(); + const newDisplaySetOptions = newViewportInfo.getDisplaySetOptions(); + + return { + viewportOptions: newViewportOptions, + displaySetOptions: newDisplaySetOptions, + }; + } } -export default new Cornerstone3DViewportService(); +export default function ExtendedCornerstoneViewportService(serviceManager) { + return { + name: 'Cornerstone3DViewportService', + create: ({ configuration = {} }) => { + return new Cornerstone3DViewportService(serviceManager); + }, + }; +} diff --git a/extensions/cornerstone-3d/src/services/ViewportService/IViewportService.ts b/extensions/cornerstone-3d/src/services/ViewportService/IViewportService.ts index 808d35661a1..9a9decc8b23 100644 --- a/extensions/cornerstone-3d/src/services/ViewportService/IViewportService.ts +++ b/extensions/cornerstone-3d/src/services/ViewportService/IViewportService.ts @@ -1,4 +1,10 @@ import { Types } from '@cornerstonejs/core'; +import { StackData, VolumeData } from './Cornerstone3DCacheService'; +import { + DisplaySetOptions, + PublicViewportOptions, + ViewportOptions, +} from './Viewport'; /** * Handles cornerstone-3D viewport logic including enabling, disabling, and @@ -25,7 +31,11 @@ export interface IViewportService { * @param {*} viewportIndex * @param {*} elementRef */ - enableElement(viewportIndex: number, elementRef: HTMLDivElement): void; + enableElement( + viewportIndex: number, + viewportOptions: ViewportOptions, + elementRef: HTMLDivElement + ): void; /** * It retrieves the renderingEngine if it does exist, or creates one otherwise * @returns {RenderingEngine} rendering engine @@ -57,9 +67,8 @@ export interface IViewportService { */ setViewportDisplaySets( viewportIndex: number, - displaySets: unknown[], - viewportOptions: unknown, - displaySetOptions: unknown[], - dataSource: unknown + viewportData: StackData | VolumeData, + publicViewportOptions: PublicViewportOptions, + publicDisplaySetOptions: DisplaySetOptions[] ): void; } diff --git a/extensions/cornerstone-3d/src/services/ViewportService/Viewport.ts b/extensions/cornerstone-3d/src/services/ViewportService/Viewport.ts index 77d69fb58d3..4767242582a 100644 --- a/extensions/cornerstone-3d/src/services/ViewportService/Viewport.ts +++ b/extensions/cornerstone-3d/src/services/ViewportService/Viewport.ts @@ -1,29 +1,68 @@ -import { Types, Enums } from '@cornerstonejs/core'; +import { Types, Enums, CONSTANTS } from '@cornerstonejs/core'; +import getCornerstoneBlendMode from '../../utils/getCornerstoneBlendMode'; +import getCornerstoneOrientation from '../../utils/getCornerstoneOrientation'; +import getCornerstoneViewportType from '../../utils/getCornerstoneViewportType'; +import JumpPresets from '../../utils/JumpPresets'; +import { SyncGroup } from '../SyncGroupService/SyncGroupService'; + +export type InitialImageOptions = { + index?: number; + preset?: JumpPresets; +}; export type ViewportOptions = { viewportType: Enums.ViewportType; toolGroupId: string; - viewportId?: string; + viewportId: string; orientation?: Types.Orientation; background?: Types.Point3; - blendMode?: number; initialView?: string; + syncGroups?: SyncGroup[]; + initialImageOptions?: InitialImageOptions; + customViewportOptions?: Record; }; -type VOI = { - windowWidth: number; - windowCenter: number; +export type PublicViewportOptions = { + viewportType?: string; + toolGroupId?: string; + viewportId?: string; + orientation?: string; + background?: Types.Point3; + initialView?: string; + syncGroups?: SyncGroup[]; + initialImageOptions?: InitialImageOptions; + customViewportOptions?: Record; +}; + +export type PublicDisplaySetOptions = { + voi?: VOI; + voiInverted?: boolean; + blendMode?: string; + slabThickness?: number; + colormap?: string; }; export type DisplaySetOptions = { - voi: 'default' | VOI; + voi?: VOI; voiInverted: boolean; + blendMode?: Enums.BlendModes; + slabThickness?: number; + colormap?: string; +}; + +type VOI = { + windowWidth: number; + windowCenter: number; }; export type DisplaySet = { displaySetInstanceUID: string; }; +const STACK = 'stack'; +const VOLUME = 'volume'; +const DEFAULT_TOOLGROUP_ID = 'default'; + class ViewportInfo { private viewportId = ''; private viewportIndex: number; @@ -35,13 +74,8 @@ class ViewportInfo { constructor(viewportIndex: number, viewportId: string) { this.viewportIndex = viewportIndex; this.viewportId = viewportId; - const viewportOptions = { - toolGroupId: 'default', - viewportType: Enums.ViewportType.STACK, - }; - const displaySetOptions = [{} as DisplaySetOptions]; - this.setViewportOptions(viewportOptions); - this.setDisplaySetOptions(displaySetOptions); + this.setPublicViewportOptions({}); + this.setPublicDisplaySetOptions([{}]); } public setRenderingEngineId(renderingEngineId: string): void { @@ -75,6 +109,52 @@ class ViewportInfo { return this.viewportId; } + public setPublicDisplaySetOptions( + publicDisplaySetOptions: Array + ): void { + // map the displaySetOptions and check if they are undefined then set them to default values + const displaySetOptions = this.mapDisplaySetOptions( + publicDisplaySetOptions + ); + + this.setDisplaySetOptions(displaySetOptions); + } + + public setPublicViewportOptions( + viewportOptionsEntry: PublicViewportOptions + ): void { + let viewportType = viewportOptionsEntry.viewportType; + let toolGroupId = viewportOptionsEntry.toolGroupId; + let orientation; + + if (!viewportType) { + viewportType = getCornerstoneViewportType(STACK); + } else { + viewportType = getCornerstoneViewportType( + viewportOptionsEntry.viewportType + ); + } + + // map SAGITTAL, AXIAL, CORONAL orientation to be used by cornerstone + if (viewportOptionsEntry.viewportType?.toLowerCase() === VOLUME) { + orientation = getCornerstoneOrientation(viewportOptionsEntry.orientation); + } else { + orientation = CONSTANTS.ORIENTATION.AXIAL; + } + + if (!toolGroupId) { + toolGroupId = DEFAULT_TOOLGROUP_ID; + } + + this.setViewportOptions({ + ...viewportOptionsEntry, + viewportId: this.viewportId, + viewportType: viewportType as Enums.ViewportType, + orientation, + toolGroupId, + }); + } + public setViewportOptions(viewportOptions: ViewportOptions): void { this.viewportOptions = viewportOptions; } @@ -86,11 +166,13 @@ class ViewportInfo { public setDisplaySetOptions( displaySetOptions: Array ): void { - // validate the displaySetOptions and check if they are undefined then set them to default values - this.validateDisplaySetOptions(displaySetOptions); this.displaySetOptions = displaySetOptions; } + public getSyncGroups(): SyncGroup[] { + return this.viewportOptions.syncGroups || []; + } + public getDisplaySetOptions(): Array { return this.displaySetOptions; } @@ -111,13 +193,28 @@ class ViewportInfo { return this.viewportOptions.orientation; } - private validateDisplaySetOptions( - displaySetOptions: Array - ): void { - for (const displaySetOption of displaySetOptions) { - displaySetOption.voi = displaySetOption.voi || 'default'; - displaySetOption.voiInverted = displaySetOption.voiInverted || false; - } + public getInitialImageOptions(): InitialImageOptions { + return this.viewportOptions.initialImageOptions; + } + + private mapDisplaySetOptions( + publicDisplaySetOptions: Array + ): Array { + const displaySetOptions: Array = []; + + publicDisplaySetOptions.forEach(option => { + const blendMode = getCornerstoneBlendMode(option.blendMode); + + displaySetOptions.push({ + voi: option.voi || ({} as VOI), + voiInverted: option.voiInverted || false, + colormap: option.colormap || undefined, + slabThickness: option.slabThickness, + blendMode, + }); + }); + + return displaySetOptions; } } diff --git a/extensions/cornerstone-3d/src/services/ViewportService/ViewportService.ts b/extensions/cornerstone-3d/src/services/ViewportService/ViewportService.ts deleted file mode 100644 index a67c58b1c97..00000000000 --- a/extensions/cornerstone-3d/src/services/ViewportService/ViewportService.ts +++ /dev/null @@ -1,252 +0,0 @@ -import { pubSubServiceInterface } from '@ohif/core'; -import { - RenderingEngine, - StackViewport, - Types, - getRenderingEngine, - utilities as csUtils, -} from '@cornerstonejs/core'; -import { IViewportService } from './IViewportService'; -import { RENDERING_ENGINE_ID } from './constants'; -import ViewportInfo, { - ViewportOptions, - DisplaySet, - DisplaySetOptions, -} from './Viewport'; - -const EVENTS = { - VIEWPORT_INFO_CREATED: - 'event::cornerstone-3d::viewportservice:viewportinfocreated', -}; - -/** - * Handles cornerstone-3D viewport logic including enabling, disabling, and - * updating the viewport. - */ -class ViewportService implements IViewportService { - servicesManager: unknown; - HangingProtocolService: unknown; - renderingEngine: Types.IRenderingEngine | null; - viewportsInfo: Map; - viewportGridResizeObserver: ResizeObserver | null; - - /** - * Service-specific - */ - EVENTS: { [key: string]: string }; - listeners: { [key: string]: Function[] }; - _broadcastEvent: unknown; // we should be able to extend the PubSub class to get this - // Some configs - enableResizeDetector: true; - resizeRefreshRateMs: 200; - resizeRefreshMode: 'debounce'; - - constructor(servicesManager) { - this.servicesManager = servicesManager; - this.renderingEngine = null; - this.viewportGridResizeObserver = null; - this.viewportsInfo = new Map(); - // - this.listeners = {}; - this.EVENTS = EVENTS; - Object.assign(this, pubSubServiceInterface); - // - const { HangingProtocolService } = servicesManager.services; - this.HangingProtocolService = HangingProtocolService; - } - - /** - * Adds the HTML element to the viewportService - * @param {*} viewportIndex - * @param {*} elementRef - */ - public enableElement(viewportIndex: number, elementRef: HTMLDivElement) { - const viewportInfo = new ViewportInfo(viewportIndex); - viewportInfo.setElement(elementRef); - this.viewportsInfo.set(viewportIndex, viewportInfo); - } - - /** - * It retrieves the renderingEngine if it does exist, or creates one otherwise - * @returns {RenderingEngine} rendering engine - */ - public getRenderingEngine() { - // get renderingEngine from cache if it exists - const renderingEngine = getRenderingEngine(RENDERING_ENGINE_ID); - - if (renderingEngine) { - this.renderingEngine = renderingEngine; - return this.renderingEngine; - } - - if (!renderingEngine || renderingEngine.hasBeenDestroyed) { - this.renderingEngine = new RenderingEngine(RENDERING_ENGINE_ID); - } - - return this.renderingEngine; - } - - /** - * It triggers the resize on the rendering engine. - */ - public resize() { - const immediate = true; - const resetPanZoomForViewPlane = false; - this.renderingEngine.resize(immediate, resetPanZoomForViewPlane); - this.renderingEngine.render(); - } - - /** - * Removes the viewport from cornerstone-3D, and destroys the rendering engine - */ - public destroy() { - this._removeResizeObserver(); - this.viewportGridResizeObserver = null; - this.renderingEngine.destroy(); - this.renderingEngine = null; - } - - /** - * Disables the viewport inside the renderingEngine, if no viewport is left - * it destroys the renderingEngine. - * @param viewportIndex - */ - public disableElement(viewportIndex: number) { - const viewportInfo = this.viewportsInfo.get(viewportIndex); - if (!viewportInfo) { - return; - } - - const viewportId = viewportInfo.getViewportId(); - this.renderingEngine.disableElement(viewportId); - this.viewportsInfo.delete(viewportIndex); - - if (this.viewportsInfo.size === 0) { - this.destroy(); - } - } - - /** - * Uses the renderingEngine to enable the element for the given viewport index - * and sets the displaySet data to the viewport - * @param {*} viewportIndex - * @param {*} displaySet - * @param {*} dataSource - * @returns - */ - public setViewportDisplaySets( - viewportIndex: number, - displaySets: unknown[], - viewportOptions: ViewportOptions, - displaySetOptions: unknown[], - dataSource: unknown - ) { - const renderingEngine = this.getRenderingEngine(); - const viewportInfo = this.viewportsInfo.get(viewportIndex); - viewportInfo.setRenderingEngineId(renderingEngine.id); - - const currentViewportOptions = viewportInfo.getViewportOptions(); - // If new viewportOptions are provided and have keys that are not in the - // current viewportOptions, then we need to update the viewportOptions, - // else we inherit the current viewportOptions. - if (Object.keys(viewportOptions)) { - const newViewportOptions = { - ...currentViewportOptions, - ...viewportOptions, - }; - viewportInfo.setViewportOptions(newViewportOptions); - } else { - viewportInfo.setViewportOptions(currentViewportOptions); - } - - // Todo: handle changed displaySetOptions - - viewportInfo.setDisplaySets(displaySets, displaySetOptions); - - this._broadcastEvent(EVENTS.VIEWPORT_INFO_CREATED, viewportInfo); - - const viewportId = viewportInfo.getViewportId(); - const element = viewportInfo.getElement(); - const type = viewportInfo.getViewportType(); - const background = viewportInfo.getBackground(); - const orientation = viewportInfo.getOrientation(); - - const viewportInput: Types.PublicViewportInput = { - viewportId, - element, - type, - defaultOptions: { - background, - orientation, - }, - }; - - renderingEngine.enableElement(viewportInput); - this._setDisplaySets( - viewportId, - displaySets, - viewportOptions, - displaySetOptions, - dataSource - ); - } - - /** - * Returns the viewportIndex for the provided viewportId - * @param {string} viewportId - the viewportId - * @returns {number} - the viewportIndex - */ - public getViewportInfoByIndex(viewportIndex: number): ViewportInfo { - return this.viewportsInfo.get(viewportIndex); - } - - public getViewportInfoById(viewportId: string): ViewportInfo { - // @ts-ignore - for (const [index, viewport] of this.viewportsInfo.entries()) { - if (viewport.getViewportId() === viewportId) { - return viewport; - } - } - return null; - } - - _setStackViewport(viewport, displaySet, displaySetOptions, dataSource) { - const imageIds = dataSource.getImageIdsForDisplaySet(displaySet); - viewport.setStack(imageIds).then(() => { - csUtils.prefetchStack(imageIds); - }); - } - - _setDisplaySets( - viewportId: string, - displaySets: DisplaySet[], - viewportOptions: ViewportOptions, - displaySetOptions: DisplaySetOptions, - dataSource: unknown - ) { - const viewport = this.renderingEngine.getViewport(viewportId); - - if (viewport instanceof StackViewport) { - // Todo: No fusion on StackViewport Yet - this._setStackViewport( - viewport, - displaySets[0], - displaySetOptions[0], - dataSource - ); - } else { - throw new Error('Unsupported viewport type'); - } - } - - /** - * Removes the resize observer from the viewport element - */ - _removeResizeObserver() { - if (this.viewportGridResizeObserver) { - this.viewportGridResizeObserver.disconnect(); - } - } -} - -export default ViewportService; diff --git a/extensions/cornerstone-3d/src/services/ViewportService/index.js b/extensions/cornerstone-3d/src/services/ViewportService/index.js deleted file mode 100644 index 1097961542b..00000000000 --- a/extensions/cornerstone-3d/src/services/ViewportService/index.js +++ /dev/null @@ -1,10 +0,0 @@ -import ViewportService from './ViewportService'; - -export default function ExtendedViewportService(serviceManager) { - return { - name: 'ViewportService', - create: ({ configuration = {} }) => { - return new ViewportService(serviceManager); - }, - }; -} diff --git a/extensions/cornerstone-3d/src/utils/CornerstoneViewportDownloadForm.tsx b/extensions/cornerstone-3d/src/utils/CornerstoneViewportDownloadForm.tsx index 98fadd11bb9..0750d21b8ea 100644 --- a/extensions/cornerstone-3d/src/utils/CornerstoneViewportDownloadForm.tsx +++ b/extensions/cornerstone-3d/src/utils/CornerstoneViewportDownloadForm.tsx @@ -3,7 +3,6 @@ import domtoimage from 'dom-to-image'; import * as cornerstone from '@cornerstonejs/core'; import * as cornerstoneTools from '@cornerstonejs/tools'; -import Cornerstone3DViewportService from '../services/ViewportService/Cornerstone3DViewportService'; import PropTypes from 'prop-types'; import { ViewportDownloadForm } from '@ohif/ui'; @@ -15,7 +14,11 @@ const MAX_TEXTURE_SIZE = 10000; const VIEWPORT_ID = 'cornerstone-viewport-download-form'; const TOOLGROUP_ID = 'cornerstone-viewport-download-form-toolgroup'; -const CornerstoneViewportDownloadForm = ({ onClose, activeViewportIndex }) => { +const CornerstoneViewportDownloadForm = ({ + onClose, + activeViewportIndex, + Cornerstone3DViewportService, +}) => { const enabledElement = getEnabledElement(activeViewportIndex); const activeViewportElement = enabledElement?.element; diff --git a/extensions/cornerstone-3d/src/utils/JumpPresets.ts b/extensions/cornerstone-3d/src/utils/JumpPresets.ts new file mode 100644 index 00000000000..e9417deff47 --- /dev/null +++ b/extensions/cornerstone-3d/src/utils/JumpPresets.ts @@ -0,0 +1,14 @@ +/** + * Jump Presets - This enum defines the 3 jump states which are available + * to be used with the jumpToSlice utility function. + */ +enum JumpPresets { + /** Jumps to first slice */ + First = 'first', + /** Jumps to last slice */ + Last = 'last', + /** Jumps to the middle slice */ + Middle = 'middle', +} + +export default JumpPresets; diff --git a/extensions/cornerstone-3d/src/utils/colormap/applyPreset.js b/extensions/cornerstone-3d/src/utils/colormap/applyPreset.js new file mode 100644 index 00000000000..f0159737d30 --- /dev/null +++ b/extensions/cornerstone-3d/src/utils/colormap/applyPreset.js @@ -0,0 +1,139 @@ +import vtkColorTransferFunction from '@kitware/vtk.js/Rendering/Core/ColorTransferFunction'; +import vtkPiecewiseFunction from '@kitware/vtk.js/Common/DataModel/PiecewiseFunction'; + +import presets from './presets'; + +// get preset from preset name +function getPreset(name) { + return presets.find(preset => preset.name === name); +} + +// Todo: we should be able to register a new preset via the utilityModule +// of the cornerstone3D extension +export default function applyPreset(actor, presetName) { + const preset = getPreset(presetName); + // Create color transfer function + const colorTransferArray = preset.colorTransfer + .split(' ') + .splice(1) + .map(parseFloat); + + const { shiftRange } = getShiftRange(colorTransferArray); + let min = shiftRange[0]; + const width = shiftRange[1] - shiftRange[0]; + const cfun = vtkColorTransferFunction.newInstance(); + const normColorTransferValuePoints = []; + for (let i = 0; i < colorTransferArray.length; i += 4) { + let value = colorTransferArray[i]; + const r = colorTransferArray[i + 1]; + const g = colorTransferArray[i + 2]; + const b = colorTransferArray[i + 3]; + + value = (value - min) / width; + normColorTransferValuePoints.push([value, r, g, b]); + } + + applyPointsToRGBFunction(normColorTransferValuePoints, shiftRange, cfun); + + actor.getProperty().setRGBTransferFunction(0, cfun); + + // Create scalar opacity function + const scalarOpacityArray = preset.scalarOpacity + .split(' ') + .splice(1) + .map(parseFloat); + + const ofun = vtkPiecewiseFunction.newInstance(); + const normPoints = []; + for (let i = 0; i < scalarOpacityArray.length; i += 2) { + let value = scalarOpacityArray[i]; + const opacity = scalarOpacityArray[i + 1]; + + value = (value - min) / width; + + normPoints.push([value, opacity]); + } + + applyPointsToPiecewiseFunction(normPoints, shiftRange, ofun); + + actor.getProperty().setScalarOpacity(0, ofun); + + const [ + gradientMinValue, + gradientMinOpacity, + gradientMaxValue, + gradientMaxOpacity, + ] = preset.gradientOpacity + .split(' ') + .splice(1) + .map(parseFloat); + + actor.getProperty().setUseGradientOpacity(0, true); + actor.getProperty().setGradientOpacityMinimumValue(0, gradientMinValue); + actor.getProperty().setGradientOpacityMinimumOpacity(0, gradientMinOpacity); + actor.getProperty().setGradientOpacityMaximumValue(0, gradientMaxValue); + actor.getProperty().setGradientOpacityMaximumOpacity(0, gradientMaxOpacity); + + if (preset.interpolation === '1') { + actor.getProperty().setInterpolationTypeToFastLinear(); + //actor.getProperty().setInterpolationTypeToLinear() + } + + const ambient = parseFloat(preset.ambient); + //const shade = preset.shade === '1' + const diffuse = parseFloat(preset.diffuse); + const specular = parseFloat(preset.specular); + const specularPower = parseFloat(preset.specularPower); + + //actor.getProperty().setShade(shade) + actor.getProperty().setAmbient(ambient); + actor.getProperty().setDiffuse(diffuse); + actor.getProperty().setSpecular(specular); + actor.getProperty().setSpecularPower(specularPower); +} + +function getShiftRange(colorTransferArray) { + // Credit to paraview-glance + // https://github.com/Kitware/paraview-glance/blob/3fec8eeff31e9c19ad5b6bff8e7159bd745e2ba9/src/components/controls/ColorBy/script.js#L133 + + // shift range is original rgb/opacity range centered around 0 + let min = Infinity; + let max = -Infinity; + for (let i = 0; i < colorTransferArray.length; i += 4) { + min = Math.min(min, colorTransferArray[i]); + max = Math.max(max, colorTransferArray[i]); + } + + const center = (max - min) / 2; + + return { + shiftRange: [-center, center], + min, + max, + }; +} + +function applyPointsToRGBFunction(points, range, cfun) { + const width = range[1] - range[0]; + const rescaled = points.map(([x, r, g, b]) => [ + x * width + range[0], + r, + g, + b, + ]); + + cfun.removeAllPoints(); + rescaled.forEach(([x, r, g, b]) => cfun.addRGBPoint(x, r, g, b)); + + return rescaled; +} + +function applyPointsToPiecewiseFunction(points, range, pwf) { + const width = range[1] - range[0]; + const rescaled = points.map(([x, y]) => [x * width + range[0], y]); + + pwf.removeAllPoints(); + rescaled.forEach(([x, y]) => pwf.addPoint(x, y)); + + return rescaled; +} diff --git a/extensions/cornerstone-3d/src/utils/colormap/colors.js b/extensions/cornerstone-3d/src/utils/colormap/colors.js new file mode 100644 index 00000000000..2cc80a9a2c1 --- /dev/null +++ b/extensions/cornerstone-3d/src/utils/colormap/colors.js @@ -0,0 +1,29 @@ +// https://www.slicer.org/w/index.php/Slicer3:2010_GenericAnatomyColors#Lookup_table +const colors = [ + { + integerLabel: 0, + textLabel: 'background', + color: [0, 0, 0, 0], + }, + { + integerLabel: 1, + textLabel: 'tissue', + // color: [255, 174, 128, 255], + color: [255, 0, 0, 255], + }, + { + integerLabel: 2, + textLabel: 'bone', + // color: [241, 214, 145, 255], + color: [0, 255, 0, 255], + }, + { + integerLabel: 3, + textLabel: 'skin', + // color: [177, 122, 101, 255], + color: [0, 0, 255, 255], + }, + // .... +]; + +export default colors; diff --git a/extensions/cornerstone-3d/src/utils/colormap/presets.js b/extensions/cornerstone-3d/src/utils/colormap/presets.js new file mode 100644 index 00000000000..4db2da8e362 --- /dev/null +++ b/extensions/cornerstone-3d/src/utils/colormap/presets.js @@ -0,0 +1,435 @@ +const presets = [ + { + name: 'CT-AAA', + gradientOpacity: '4 0 1 255 1', + specularPower: '10', + scalarOpacity: + '12 -3024 0 143.556 0 166.222 0.686275 214.389 0.696078 419.736 0.833333 3071 0.803922', + id: 'vtkMRMLVolumePropertyNode1', + specular: '0.2', + shade: '1', + ambient: '0.1', + colorTransfer: + '24 -3024 0 0 0 143.556 0.615686 0.356863 0.184314 166.222 0.882353 0.603922 0.290196 214.389 1 1 1 419.736 1 0.937033 0.954531 3071 0.827451 0.658824 1', + selectable: 'true', + diffuse: '0.9', + interpolation: '1', + effectiveRange: '143.556 419.736', + }, + { + name: 'CT-AAA2', + gradientOpacity: '4 0 1 255 1', + specularPower: '10', + scalarOpacity: + '16 -3024 0 129.542 0 145.244 0.166667 157.02 0.5 169.918 0.627451 395.575 0.8125 1578.73 0.8125 3071 0.8125', + id: 'vtkMRMLVolumePropertyNode2', + specular: '0.2', + shade: '1', + ambient: '0.1', + colorTransfer: + '32 -3024 0 0 0 129.542 0.54902 0.25098 0.14902 145.244 0.6 0.627451 0.843137 157.02 0.890196 0.47451 0.6 169.918 0.992157 0.870588 0.392157 395.575 1 0.886275 0.658824 1578.73 1 0.829256 0.957922 3071 0.827451 0.658824 1', + selectable: 'true', + diffuse: '0.9', + interpolation: '1', + effectiveRange: '0 1600', + }, + { + name: 'CT-Bone', + gradientOpacity: '4 0 1 255 1', + specularPower: '10', + scalarOpacity: '8 -3024 0 -16.4458 0 641.385 0.715686 3071 0.705882', + id: 'vtkMRMLVolumePropertyNode3', + specular: '0.2', + shade: '1', + ambient: '0.1', + colorTransfer: + '16 -3024 0 0 0 -16.4458 0.729412 0.254902 0.301961 641.385 0.905882 0.815686 0.552941 3071 1 1 1', + selectable: 'true', + diffuse: '0.9', + interpolation: '1', + effectiveRange: '-16.4458 641.385', + }, + { + name: 'CT-Bones', + gradientOpacity: '4 0 1 985.12 1', + specularPower: '1', + scalarOpacity: '8 -1000 0 152.19 0 278.93 0.190476 952 0.2', + id: 'vtkMRMLVolumePropertyNode4', + specular: '0', + shade: '1', + ambient: '0.2', + colorTransfer: + '20 -1000 0.3 0.3 1 -488 0.3 1 0.3 463.28 1 0 0 659.15 1 0.912535 0.0374849 953 1 0.3 0.3', + selectable: 'true', + diffuse: '1', + interpolation: '1', + effectiveRange: '152.19 952', + }, + { + name: 'CT-Cardiac', + gradientOpacity: '4 0 1 255 1', + specularPower: '10', + scalarOpacity: + '12 -3024 0 -77.6875 0 94.9518 0.285714 179.052 0.553571 260.439 0.848214 3071 0.875', + id: 'vtkMRMLVolumePropertyNode5', + specular: '0.2', + shade: '1', + ambient: '0.1', + colorTransfer: + '24 -3024 0 0 0 -77.6875 0.54902 0.25098 0.14902 94.9518 0.882353 0.603922 0.290196 179.052 1 0.937033 0.954531 260.439 0.615686 0 0 3071 0.827451 0.658824 1', + selectable: 'true', + diffuse: '0.9', + interpolation: '1', + effectiveRange: '-77.6875 260.439', + }, + { + name: 'CT-Cardiac2', + gradientOpacity: '4 0 1 255 1', + specularPower: '10', + scalarOpacity: + '12 -3024 0 42.8964 0 163.488 0.428571 277.642 0.776786 1587 0.754902 3071 0.754902', + id: 'vtkMRMLVolumePropertyNode6', + specular: '0.2', + shade: '1', + ambient: '0.1', + colorTransfer: + '24 -3024 0 0 0 42.8964 0.54902 0.25098 0.14902 163.488 0.917647 0.639216 0.0588235 277.642 1 0.878431 0.623529 1587 1 1 1 3071 0.827451 0.658824 1', + selectable: 'true', + diffuse: '0.9', + interpolation: '1', + effectiveRange: '42.8964 1587', + }, + { + name: 'CT-Cardiac3', + gradientOpacity: '4 0 1 255 1', + specularPower: '10', + scalarOpacity: + '14 -3024 0 -86.9767 0 45.3791 0.169643 139.919 0.589286 347.907 0.607143 1224.16 0.607143 3071 0.616071', + id: 'vtkMRMLVolumePropertyNode7', + specular: '0.2', + shade: '1', + ambient: '0.1', + colorTransfer: + '28 -3024 0 0 0 -86.9767 0 0.25098 1 45.3791 1 0 0 139.919 1 0.894893 0.894893 347.907 1 1 0.25098 1224.16 1 1 1 3071 0.827451 0.658824 1', + selectable: 'true', + diffuse: '0.9', + interpolation: '1', + effectiveRange: '-86.9767 1224.16', + }, + { + name: 'CT-Chest-Contrast-Enhanced', + gradientOpacity: '4 0 1 255 1', + specularPower: '10', + scalarOpacity: + '10 -3024 0 67.0106 0 251.105 0.446429 439.291 0.625 3071 0.616071', + id: 'vtkMRMLVolumePropertyNode8', + specular: '0.2', + shade: '1', + ambient: '0.1', + colorTransfer: + '20 -3024 0 0 0 67.0106 0.54902 0.25098 0.14902 251.105 0.882353 0.603922 0.290196 439.291 1 0.937033 0.954531 3071 0.827451 0.658824 1', + selectable: 'true', + diffuse: '0.9', + interpolation: '1', + effectiveRange: '67.0106 439.291', + }, + { + name: 'CT-Chest-Vessels', + gradientOpacity: '4 0 1 255 1', + specularPower: '1', + scalarOpacity: + '10 -3024 0 -1278.35 0 22.8277 0.428571 439.291 0.625 3071 0.616071', + id: 'vtkMRMLVolumePropertyNode9', + specular: '0', + shade: '1', + ambient: '0.2', + colorTransfer: + '20 -3024 0 0 0 -1278.35 0.54902 0.25098 0.14902 22.8277 0.882353 0.603922 0.290196 439.291 1 0.937033 0.954531 3071 0.827451 0.658824 1', + selectable: 'true', + diffuse: '1', + interpolation: '1', + effectiveRange: '-1278.35 439.291', + }, + { + name: 'CT-Coronary-Arteries', + gradientOpacity: '4 0 1 255 1', + specularPower: '1', + scalarOpacity: + '12 -2048 0 136.47 0 159.215 0.258929 318.43 0.571429 478.693 0.776786 3661 1', + id: 'vtkMRMLVolumePropertyNode10', + specular: '0', + shade: '0', + ambient: '0.2', + colorTransfer: + '24 -2048 0 0 0 136.47 0 0 0 159.215 0.159804 0.159804 0.159804 318.43 0.764706 0.764706 0.764706 478.693 1 1 1 3661 1 1 1', + selectable: 'true', + diffuse: '1', + interpolation: '1', + effectiveRange: '136.47 478.693', + }, + { + name: 'CT-Coronary-Arteries-2', + gradientOpacity: '4 0 1 255 1', + specularPower: '1', + scalarOpacity: + '14 -2048 0 142.677 0 145.016 0.116071 192.174 0.5625 217.24 0.776786 384.347 0.830357 3661 0.830357', + id: 'vtkMRMLVolumePropertyNode11', + specular: '0', + shade: '1', + ambient: '0.2', + colorTransfer: + '28 -2048 0 0 0 142.677 0 0 0 145.016 0.615686 0 0.0156863 192.174 0.909804 0.454902 0 217.24 0.972549 0.807843 0.611765 384.347 0.909804 0.909804 1 3661 1 1 1', + selectable: 'true', + diffuse: '1', + interpolation: '1', + effectiveRange: '142.677 384.347', + }, + { + name: 'CT-Coronary-Arteries-3', + gradientOpacity: '4 0 1 255 1', + specularPower: '10', + scalarOpacity: + '14 -2048 0 128.643 0 129.982 0.0982143 173.636 0.669643 255.884 0.857143 584.878 0.866071 3661 1', + id: 'vtkMRMLVolumePropertyNode12', + specular: '0.2', + shade: '1', + ambient: '0.1', + colorTransfer: + '28 -2048 0 0 0 128.643 0 0 0 129.982 0.615686 0 0.0156863 173.636 0.909804 0.454902 0 255.884 0.886275 0.886275 0.886275 584.878 0.968627 0.968627 0.968627 3661 1 1 1', + selectable: 'true', + diffuse: '0.9', + interpolation: '1', + effectiveRange: '128.643 584.878', + }, + { + name: 'CT-Cropped-Volume-Bone', + gradientOpacity: '4 0 1 255 1', + specularPower: '1', + scalarOpacity: '10 -2048 0 -451 0 -450 1 1050 1 3661 1', + id: 'vtkMRMLVolumePropertyNode13', + specular: '0', + shade: '0', + ambient: '0.2', + colorTransfer: + '20 -2048 0 0 0 -451 0 0 0 -450 0.0556356 0.0556356 0.0556356 1050 1 1 1 3661 1 1 1', + selectable: 'true', + diffuse: '1', + interpolation: '1', + effectiveRange: '-451 1050', + }, + { + name: 'CT-Fat', + gradientOpacity: '6 0 1 985.12 1 988 1', + specularPower: '1', + scalarOpacity: '14 -1000 0 -100 0 -99 0.15 -60 0.15 -59 0 101.2 0 952 0', + id: 'vtkMRMLVolumePropertyNode14', + specular: '0', + references: '0', + shade: '0', + ambient: '0.2', + colorTransfer: + '36 -1000 0.3 0.3 1 -497.5 0.3 1 0.3 -99 0 0 1 -76.946 0 1 0 -65.481 0.835431 0.888889 0.0165387 83.89 1 0 0 463.28 1 0 0 659.15 1 0.912535 0.0374849 2952 1 0.300267 0.299886', + selectable: 'true', + diffuse: '1', + interpolation: '1', + effectiveRange: '-100 101.2', + }, + { + name: 'CT-Liver-Vasculature', + gradientOpacity: '4 0 1 255 1', + specularPower: '1', + scalarOpacity: + '14 -2048 0 149.113 0 157.884 0.482143 339.96 0.660714 388.526 0.830357 1197.95 0.839286 3661 0.848214', + id: 'vtkMRMLVolumePropertyNode15', + specular: '0', + shade: '0', + ambient: '0.2', + colorTransfer: + '28 -2048 0 0 0 149.113 0 0 0 157.884 0.501961 0.25098 0 339.96 0.695386 0.59603 0.36886 388.526 0.854902 0.85098 0.827451 1197.95 1 1 1 3661 1 1 1', + selectable: 'true', + diffuse: '1', + interpolation: '1', + effectiveRange: '149.113 1197.95', + }, + { + name: 'CT-Lung', + gradientOpacity: '6 0 1 985.12 1 988 1', + specularPower: '1', + scalarOpacity: '12 -1000 0 -600 0 -599 0.15 -400 0.15 -399 0 2952 0', + id: 'vtkMRMLVolumePropertyNode16', + specular: '0', + references: '0', + shade: '1', + ambient: '0.2', + colorTransfer: + '24 -1000 0.3 0.3 1 -600 0 0 1 -530 0.134704 0.781726 0.0724558 -460 0.929244 1 0.109473 -400 0.888889 0.254949 0.0240258 2952 1 0.3 0.3', + selectable: 'true', + diffuse: '1', + interpolation: '1', + effectiveRange: '-600 -399', + }, + { + name: 'CT-MIP', + gradientOpacity: '4 0 1 255 1', + specularPower: '10', + scalarOpacity: '8 -3024 0 -637.62 0 700 1 3071 1', + id: 'vtkMRMLVolumePropertyNode17', + specular: '0.2', + shade: '1', + ambient: '0.1', + colorTransfer: '16 -3024 0 0 0 -637.62 1 1 1 700 1 1 1 3071 1 1 1', + selectable: 'true', + diffuse: '0.9', + interpolation: '1', + effectiveRange: '-637.62 700', + }, + { + name: 'CT-Muscle', + gradientOpacity: '4 0 1 255 1', + specularPower: '10', + scalarOpacity: + '10 -3024 0 -155.407 0 217.641 0.676471 419.736 0.833333 3071 0.803922', + id: 'vtkMRMLVolumePropertyNode18', + specular: '0.2', + shade: '1', + ambient: '0.1', + colorTransfer: + '20 -3024 0 0 0 -155.407 0.54902 0.25098 0.14902 217.641 0.882353 0.603922 0.290196 419.736 1 0.937033 0.954531 3071 0.827451 0.658824 1', + selectable: 'true', + diffuse: '0.9', + interpolation: '1', + effectiveRange: '-155.407 419.736', + }, + { + name: 'CT-Pulmonary-Arteries', + gradientOpacity: '4 0 1 255 1', + specularPower: '1', + scalarOpacity: + '14 -2048 0 -568.625 0 -364.081 0.0714286 -244.813 0.401786 18.2775 0.607143 447.798 0.830357 3592.73 0.839286', + id: 'vtkMRMLVolumePropertyNode19', + specular: '0', + shade: '1', + ambient: '0.2', + colorTransfer: + '28 -2048 0 0 0 -568.625 0 0 0 -364.081 0.396078 0.301961 0.180392 -244.813 0.611765 0.352941 0.0705882 18.2775 0.843137 0.0156863 0.156863 447.798 0.752941 0.752941 0.752941 3592.73 1 1 1', + selectable: 'true', + diffuse: '1', + interpolation: '1', + effectiveRange: '-568.625 447.798', + }, + { + name: 'CT-Soft-Tissue', + gradientOpacity: '4 0 1 255 1', + specularPower: '1', + scalarOpacity: '10 -2048 0 -167.01 0 -160 1 240 1 3661 1', + id: 'vtkMRMLVolumePropertyNode20', + specular: '0', + shade: '0', + ambient: '0.2', + colorTransfer: + '20 -2048 0 0 0 -167.01 0 0 0 -160 0.0556356 0.0556356 0.0556356 240 1 1 1 3661 1 1 1', + selectable: 'true', + diffuse: '1', + interpolation: '1', + effectiveRange: '-167.01 240', + }, + { + name: 'CT-Air', + gradientOpacity: '4 0 1 255 1', + specularPower: '10', + scalarOpacity: '8 -3024 0.705882 -900.0 0.715686 -500.0 0 3071 0', + id: 'vtkMRMLVolumePropertyNode21', + specular: '0.2', + shade: '1', + ambient: '0.1', + colorTransfer: + '16 -3024 1 1 1 -900.0 0.2 1.0 1.0 -500.0 0.3 0.3 1.0 3071 0 0 0 ', + selectable: 'true', + diffuse: '0.9', + interpolation: '1', + effectiveRange: '-1200.0 -200.0', + }, + { + name: 'MR-Angio', + gradientOpacity: '4 0 1 255 1', + specularPower: '1', + scalarOpacity: + '12 -2048 0 151.354 0 158.279 0.4375 190.112 0.580357 200.873 0.732143 3661 0.741071', + id: 'vtkMRMLVolumePropertyNode22', + specular: '0', + shade: '1', + ambient: '0.2', + colorTransfer: + '24 -2048 0 0 0 151.354 0 0 0 158.279 0.74902 0.376471 0 190.112 1 0.866667 0.733333 200.873 0.937255 0.937255 0.937255 3661 1 1 1', + selectable: 'true', + diffuse: '1', + interpolation: '1', + effectiveRange: '151.354 200.873', + }, + { + name: 'MR-Default', + gradientOpacity: '4 0 1 255 1', + specularPower: '1', + scalarOpacity: '12 0 0 20 0 40 0.15 120 0.3 220 0.375 1024 0.5', + id: 'vtkMRMLVolumePropertyNode23', + specular: '0', + shade: '1', + ambient: '0.2', + colorTransfer: + '24 0 0 0 0 20 0.168627 0 0 40 0.403922 0.145098 0.0784314 120 0.780392 0.607843 0.380392 220 0.847059 0.835294 0.788235 1024 1 1 1', + selectable: 'true', + diffuse: '1', + interpolation: '1', + effectiveRange: '0 220', + }, + { + name: 'MR-MIP', + gradientOpacity: '4 0 1 255 1', + specularPower: '1', + scalarOpacity: '8 0 0 98.3725 0 416.637 1 2800 1', + id: 'vtkMRMLVolumePropertyNode24', + specular: '0', + shade: '0', + ambient: '0.2', + colorTransfer: '16 0 1 1 1 98.3725 1 1 1 416.637 1 1 1 2800 1 1 1', + selectable: 'true', + diffuse: '1', + interpolation: '1', + effectiveRange: '0 416.637', + }, + { + name: 'MR-T2-Brain', + gradientOpacity: '4 0 1 160.25 1', + specularPower: '40', + scalarOpacity: '10 0 0 36.05 0 218.302 0.171429 412.406 1 641 1', + id: 'vtkMRMLVolumePropertyNode25', + specular: '0.5', + shade: '1', + ambient: '0.3', + colorTransfer: + '16 0 0 0 0 98.7223 0.956863 0.839216 0.192157 412.406 0 0.592157 0.807843 641 1 1 1', + selectable: 'true', + diffuse: '0.6', + interpolation: '1', + effectiveRange: '0 412.406', + }, + { + name: 'DTI-FA-Brain', + gradientOpacity: '4 0 1 0.9950 1', + specularPower: '40', + scalarOpacity: + '16 0 0 0 0 0.3501 0.0158 0.49379 0.7619 0.6419 1 0.9920 1 0.9950 0 0.9950 0', + id: 'vtkMRMLVolumePropertyNode26', + specular: '0.5', + shade: '1', + ambient: '0.3', + colorTransfer: + '28 0 1 0 0 0 1 0 0 0.24974 0.4941 1 0 0.49949 0 0.9882 1 0.7492 0.51764 0 1 0.9950 1 0 0 0.9950 1 0 0', + selectable: 'true', + diffuse: '0.9', + interpolation: '1', + effectiveRange: '0 1', + }, +]; + +export default presets; diff --git a/extensions/cornerstone-3d/src/utils/colormap/transferFunctionHelpers.js b/extensions/cornerstone-3d/src/utils/colormap/transferFunctionHelpers.js new file mode 100644 index 00000000000..4bbc25713e8 --- /dev/null +++ b/extensions/cornerstone-3d/src/utils/colormap/transferFunctionHelpers.js @@ -0,0 +1,93 @@ +import { cache, utilities } from '@cornerstonejs/core'; +import vtkColorMaps from '@kitware/vtk.js/Rendering/Core/ColorTransferFunction/ColorMaps'; +import vtkColorTransferFunction from '@kitware/vtk.js/Rendering/Core/ColorTransferFunction'; +import vtkPiecewiseFunction from '@kitware/vtk.js/Common/DataModel/PiecewiseFunction'; + +const colormaps = {}; + +export function registerColormap(colormap) { + colormaps[colormap.Name] = colormap; +} + +function setColorTransferFunctionFromVolumeMetadata({ + volumeActor, + volumeId, + inverted, +}) { + let lower, upper, windowWidth, windowCenter; + + if (volumeId) { + const volume = cache.getVolume(volumeId); + const voiLutModule = volume.metadata.voiLut[0]; + if (voiLutModule) { + windowWidth = voiLutModule.windowWidth; + windowCenter = voiLutModule.windowCenter; + } + } else { + windowWidth = 400; + windowCenter = 40; + } + + if (windowWidth == undefined || windowCenter === undefined) { + // Set to something so we can window level it manually. + lower = 200; + upper = 400; + } else { + lower = windowCenter - windowWidth / 2.0; + upper = windowCenter + windowWidth / 2.0; + } + + setLowerUpperColorTransferFunction({ volumeActor, lower, upper, inverted }); +} + +function setLowerUpperColorTransferFunction({ + volumeActor, + lower, + upper, + inverted, +}) { + volumeActor + .getProperty() + .getRGBTransferFunction(0) + .setMappingRange(lower, upper); + + if (inverted) { + utilities.invertRgbTransferFunction( + volumeActor.getProperty().getRGBTransferFunction(0) + ); + } +} + +function setColormap(volumeActor, colormap) { + const mapper = volumeActor.getMapper(); + mapper.setSampleDistance(1.0); + + const cfun = vtkColorTransferFunction.newInstance(); + + // if we have a custom colormap, use it + let preset; + if (colormaps[colormap]) { + preset = colormaps[colormap]; + } else { + preset = vtkColorMaps.getPresetByName(colormap); + } + + cfun.applyColorMap(preset); + cfun.setMappingRange(0, 5); + + volumeActor.getProperty().setRGBTransferFunction(0, cfun); + + // Create scalar opacity function + const ofun = vtkPiecewiseFunction.newInstance(); + ofun.addPoint(0, 0.0); + ofun.addPoint(0.1, 0.9); + ofun.addPoint(5, 1.0); + + volumeActor.getProperty().setScalarOpacity(0, ofun); +} + +export { + setColormap, + setColorTransferFunctionFromVolumeMetadata, + setLowerUpperColorTransferFunction, +}; diff --git a/extensions/cornerstone-3d/src/utils/getCornerstoneBlendMode.ts b/extensions/cornerstone-3d/src/utils/getCornerstoneBlendMode.ts new file mode 100644 index 00000000000..4bc7cb449f3 --- /dev/null +++ b/extensions/cornerstone-3d/src/utils/getCornerstoneBlendMode.ts @@ -0,0 +1,17 @@ +import { Enums } from '@cornerstonejs/core'; + +const MIP = 'mip'; + +export default function getCornerstoneBlendMode( + blendMode: string +): Enums.BlendModes { + if (!blendMode) { + return Enums.BlendModes.COMPOSITE; + } + + if (blendMode.toLowerCase() === MIP) { + return Enums.BlendModes.MAXIMUM_INTENSITY_BLEND; + } + + throw new Error(); +} diff --git a/extensions/cornerstone-3d/src/utils/getCornerstoneOrientation.ts b/extensions/cornerstone-3d/src/utils/getCornerstoneOrientation.ts new file mode 100644 index 00000000000..ace5bcef331 --- /dev/null +++ b/extensions/cornerstone-3d/src/utils/getCornerstoneOrientation.ts @@ -0,0 +1,22 @@ +import { CONSTANTS } from '@cornerstonejs/core'; +import { log } from '@ohif/core'; + +const AXIAL = 'axial'; +const SAGITTAL = 'sagittal'; +const CORONAL = 'coronal'; + +export default function getCornerstoneOrientation( + orientation: string +): CONSTANTS.ORIENTATION { + switch (orientation.toLowerCase()) { + case AXIAL: + return CONSTANTS.ORIENTATION.AXIAL; + case SAGITTAL: + return CONSTANTS.ORIENTATION.SAGITTAL; + case CORONAL: + return CONSTANTS.ORIENTATION.CORONAL; + default: + log.wanr('Choosing default orientation: axial'); + return CONSTANTS.ORIENTATION.AXIAL; + } +} diff --git a/extensions/cornerstone-3d/src/utils/getCornerstoneViewportType.ts b/extensions/cornerstone-3d/src/utils/getCornerstoneViewportType.ts new file mode 100644 index 00000000000..2e19312b6fd --- /dev/null +++ b/extensions/cornerstone-3d/src/utils/getCornerstoneViewportType.ts @@ -0,0 +1,20 @@ +import { Enums } from '@cornerstonejs/core'; + +const STACK = 'stack'; +const VOLUME = 'volume'; + +export default function getCornerstoneViewportType( + viewportType: string +): Enums.ViewportType { + if (viewportType.toLowerCase() === STACK) { + return Enums.ViewportType.STACK; + } + + if (viewportType.toLowerCase() === VOLUME) { + return Enums.ViewportType.ORTHOGRAPHIC; + } + + throw new Error( + `Invalid viewport type: ${viewportType}. Valid types are: stack, volume` + ); +} diff --git a/extensions/cornerstone-3d/src/utils/getInterleavedFrames.js b/extensions/cornerstone-3d/src/utils/getInterleavedFrames.js new file mode 100644 index 00000000000..d8eae131a21 --- /dev/null +++ b/extensions/cornerstone-3d/src/utils/getInterleavedFrames.js @@ -0,0 +1,60 @@ +export default function getInterleavedFrames(imageIds) { + const minImageIdIndex = 0; + const maxImageIdIndex = imageIds.length - 1; + + const middleImageIdIndex = Math.floor(imageIds.length / 2); + + let lowerImageIdIndex = middleImageIdIndex; + let upperImageIdIndex = middleImageIdIndex; + + // Build up an array of images to prefetch, starting with the current image. + const imageIdsToPrefetch = [ + { imageId: imageIds[middleImageIdIndex], imageIdIndex: middleImageIdIndex }, + ]; + + const prefetchQueuedFilled = { + currentPositionDownToMinimum: false, + currentPositionUpToMaximum: false, + }; + + // Check if on edges and some criteria is already fulfilled + + if (middleImageIdIndex === minImageIdIndex) { + prefetchQueuedFilled.currentPositionDownToMinimum = true; + } else if (middleImageIdIndex === maxImageIdIndex) { + prefetchQueuedFilled.currentPositionUpToMaximum = true; + } + + while ( + !prefetchQueuedFilled.currentPositionDownToMinimum || + !prefetchQueuedFilled.currentPositionUpToMaximum + ) { + if (!prefetchQueuedFilled.currentPositionDownToMinimum) { + // Add imageId bellow + lowerImageIdIndex--; + imageIdsToPrefetch.push({ + imageId: imageIds[lowerImageIdIndex], + imageIdIndex: lowerImageIdIndex, + }); + + if (lowerImageIdIndex === minImageIdIndex) { + prefetchQueuedFilled.currentPositionDownToMinimum = true; + } + } + + if (!prefetchQueuedFilled.currentPositionUpToMaximum) { + // Add imageId above + upperImageIdIndex++; + imageIdsToPrefetch.push({ + imageId: imageIds[upperImageIdIndex], + imageIdIndex: upperImageIdIndex, + }); + + if (upperImageIdIndex === maxImageIdIndex) { + prefetchQueuedFilled.currentPositionUpToMaximum = true; + } + } + } + + return imageIdsToPrefetch; +} diff --git a/extensions/cornerstone-3d/src/utils/interleaveCenterLoader.ts b/extensions/cornerstone-3d/src/utils/interleaveCenterLoader.ts new file mode 100644 index 00000000000..dcf19ffd765 --- /dev/null +++ b/extensions/cornerstone-3d/src/utils/interleaveCenterLoader.ts @@ -0,0 +1,150 @@ +import { cache, imageLoadPoolManager, Enums } from '@cornerstonejs/core'; +import getInterleavedFrames from './getInterleavedFrames'; +import { compact, flatten, zip } from 'lodash'; + +// Map of volumeId and SeriesInstanceId +const volumeIdMapsToLoad = new Map(); +const viewportIdVolumeInputArrayMap = new Map(); + +/** + * This function caches the volumeUIDs until all the volumes inside the + * hangging protocol are initialized. Then it goes through the imageIds + * of the volumes, and interleav them, in order for the volumes to be loaded + * together from middle to the start and the end. + * @param {Object} props image loading properties from Cornerstone ViewportService + * @returns + */ +export default function interleaveCenterLoader({ + data: { viewportId, volumeInputArray }, + displaySetsMatchDetails, + matchDetails, +}) { + viewportIdVolumeInputArrayMap.set(viewportId, volumeInputArray); + + // Based on the volumeInputs store the volumeIds and SeriesInstanceIds + // to keep track of the volumes being loaded + for (const volumeInput of volumeInputArray) { + const { volumeId } = volumeInput; + const volume = cache.getVolume(volumeId); + + if (!volume) { + return; + } + + // if the volumeUID is not in the volumeUIDs array, add it + if (!volumeIdMapsToLoad.has(volumeId)) { + const { metadata } = volume; + volumeIdMapsToLoad.set(volumeId, metadata.SeriesInstanceUID); + } + } + + /** + * 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 + * improved by not checking it, and as soon as the matched DisplaySets have their + * volume loaded, we start the loading, but that comes at the cost of viewports + * not being created yet (e.g., in a 10 viewport ptCT fusion, when one ct viewport and one + * pt viewport are created we have a guarantee that the volumes are created in the cache + * but the rest of the viewports (fusion, mip etc.) are not created yet. So + * we can't initiate setting the volumes for those viewports. One solution can be + * to add an event when a viewport is created (not enabled element event) and then + * 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) { + return; + } + + // Check if all the matched volumes are loaded + for (const [_, details] of displaySetsMatchDetails.entries()) { + const { SeriesInstanceUID } = details; + + // HangingProtocol has matched, but don't have all the volumes created yet, so return + if (!Array.from(volumeIdMapsToLoad.values()).includes(SeriesInstanceUID)) { + return; + } + } + + const volumeIds = Array.from(volumeIdMapsToLoad.keys()).slice(); + // get volumes from cache + const volumes = volumeIds.map(volumeId => { + return cache.getVolume(volumeId); + }); + + // iterate over all volumes, and get their imageIds, and interleave + // the imageIds and save them in AllRequests for later use + const AllRequests = []; + volumes.forEach(volume => { + const requests = volume.getImageLoadRequests(); + + if (!requests.length || !requests[0] || !requests[0].imageId) { + return; + } + + const requestImageIds = requests.map(request => { + return request.imageId; + }); + + const imageIds = getInterleavedFrames(requestImageIds); + + const reOrderedRequests = imageIds.map(({ imageId }) => { + const request = requests.find(req => req.imageId === imageId); + return request; + }); + + AllRequests.push(reOrderedRequests); + }); + + // flatten the AllRequests array, which will result in a list of all the + // imageIds for all the volumes but interleaved + const interleavedRequests = compact(flatten(zip(...AllRequests))); + + // set the finalRequests to the imageLoadPoolManager + const finalRequests = []; + interleavedRequests.forEach(request => { + const { imageId } = request; + + AllRequests.forEach(volumeRequests => { + const volumeImageIdRequest = volumeRequests.find( + req => req.imageId === imageId + ); + if (volumeImageIdRequest) { + finalRequests.push(volumeImageIdRequest); + } + }); + }); + + const requestType = Enums.RequestType.Prefetch; + const priority = 0; + + finalRequests.forEach( + ({ callLoadImage, additionalDetails, imageId, imageIdIndex, options }) => { + const callLoadImageBound = callLoadImage.bind( + null, + imageId, + imageIdIndex, + options + ); + + imageLoadPoolManager.addRequest( + callLoadImageBound, + requestType, + additionalDetails, + priority + ); + } + ); + + // clear the volumeIdMapsToLoad + volumeIdMapsToLoad.clear(); + + // copy the viewportIdVolumeInputArrayMap + const viewportIdVolumeInputArrayMapCopy = new Map( + viewportIdVolumeInputArrayMap + ); + + // reset the viewportIdVolumeInputArrayMap + viewportIdVolumeInputArrayMap.clear(); + + return viewportIdVolumeInputArrayMapCopy; +} diff --git a/extensions/cornerstone-3d/src/utils/interleaveTopToBottom.ts b/extensions/cornerstone-3d/src/utils/interleaveTopToBottom.ts new file mode 100644 index 00000000000..2c7df60bc38 --- /dev/null +++ b/extensions/cornerstone-3d/src/utils/interleaveTopToBottom.ts @@ -0,0 +1,139 @@ +import { cache, imageLoadPoolManager, Enums } from '@cornerstonejs/core'; +import { compact, flatten, zip } from 'lodash'; + +// Map of volumeId and SeriesInstanceId +const volumeIdMapsToLoad = new Map(); +const viewportIdVolumeInputArrayMap = new Map(); + +/** + * This function caches the volumeIds until all the volumes inside the + * hanging protocol are initialized. Then it goes through the imageIds + * of the volumes, and interleave them, in order for the volumes to be loaded + * together from middle to the start and the end. + * @param {Object} {viewportData, displaySetMatchDetails} + * @returns + */ +export default function interleaveTopToBottom({ + data: { viewportId, volumeInputArray }, + displaySetsMatchDetails, + matchDetails, +}) { + viewportIdVolumeInputArrayMap.set(viewportId, volumeInputArray); + + // Based on the volumeInputs store the volumeIds and SeriesInstanceIds + // to keep track of the volumes being loaded + for (const volumeInput of volumeInputArray) { + const { volumeId } = volumeInput; + const volume = cache.getVolume(volumeId); + + if (!volume) { + return; + } + + // if the volumeUID is not in the volumeUIDs array, add it + if (!volumeIdMapsToLoad.has(volumeId)) { + const { metadata } = volume; + volumeIdMapsToLoad.set(volumeId, metadata.SeriesInstanceUID); + } + } + + /** + * 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 + * improved by not checking it, and as soon as the matched DisplaySets have their + * volume loaded, we start the loading, but that comes at the cost of viewports + * not being created yet (e.g., in a 10 viewport ptCT fusion, when one ct viewport and one + * pt viewport are created we have a guarantee that the volumes are created in the cache + * but the rest of the viewports (fusion, mip etc.) are not created yet. So + * we can't initiate setting the volumes for those viewports. One solution can be + * to add an event when a viewport is created (not enabled element event) and then + * 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) { + return; + } + + // Check if all the matched volumes are loaded + for (const [_, details] of displaySetsMatchDetails.entries()) { + const { SeriesInstanceUID } = details; + + // HangingProtocol has matched, but don't have all the volumes created yet, so return + if (!Array.from(volumeIdMapsToLoad.values()).includes(SeriesInstanceUID)) { + return; + } + } + + const volumeIds = Array.from(volumeIdMapsToLoad.keys()).slice(); + // get volumes from cache + const volumes = volumeIds.map(volumeId => { + return cache.getVolume(volumeId); + }); + + // iterate over all volumes, and get their imageIds, and interleave + // the imageIds and save them in AllRequests for later use + const AllRequests = []; + volumes.forEach(volume => { + const requests = volume.getImageLoadRequests(); + + if (!requests.length || !requests[0] || !requests[0].imageId) { + return; + } + + // reverse the requests + AllRequests.push(requests.reverse()); + }); + + // flatten the AllRequests array, which will result in a list of all the + // imageIds for all the volumes but interleaved + const interleavedRequests = compact(flatten(zip(...AllRequests))); + + // set the finalRequests to the imageLoadPoolManager + const finalRequests = []; + interleavedRequests.forEach(request => { + const { imageId } = request; + + AllRequests.forEach(volumeRequests => { + const volumeImageIdRequest = volumeRequests.find( + req => req.imageId === imageId + ); + if (volumeImageIdRequest) { + finalRequests.push(volumeImageIdRequest); + } + }); + }); + + const requestType = Enums.RequestType.Prefetch; + const priority = 0; + + finalRequests.forEach( + ({ callLoadImage, additionalDetails, imageId, imageIdIndex, options }) => { + const callLoadImageBound = callLoadImage.bind( + null, + imageId, + imageIdIndex, + options + ); + + imageLoadPoolManager.addRequest( + callLoadImageBound, + requestType, + additionalDetails, + priority + ); + } + ); + + // clear the volumeIdMapsToLoad + volumeIdMapsToLoad.clear(); + + // copy the viewportIdVolumeInputArrayMap + const viewportIdVolumeInputArrayMapCopy = new Map( + viewportIdVolumeInputArrayMap + ); + + // reset the viewportIdVolumeInputArrayMap + viewportIdVolumeInputArrayMap.clear(); + + return viewportIdVolumeInputArrayMapCopy; +} diff --git a/extensions/cornerstone-3d/src/utils/measurementServiceMappings/ArrowAnnotate.js b/extensions/cornerstone-3d/src/utils/measurementServiceMappings/ArrowAnnotate.js index 96eea1bddd5..8aedcc36c06 100644 --- a/extensions/cornerstone-3d/src/utils/measurementServiceMappings/ArrowAnnotate.js +++ b/extensions/cornerstone-3d/src/utils/measurementServiceMappings/ArrowAnnotate.js @@ -1,4 +1,3 @@ -import { annotation } from '@cornerstonejs/tools'; import SUPPORTED_TOOLS from './constants/supportedTools'; import getSOPInstanceAttributes from './utils/getSOPInstanceAttributes'; diff --git a/extensions/cornerstone-3d/src/utils/measurementServiceMappings/Bidirectional.js b/extensions/cornerstone-3d/src/utils/measurementServiceMappings/Bidirectional.js index 93913330751..619f11ec677 100644 --- a/extensions/cornerstone-3d/src/utils/measurementServiceMappings/Bidirectional.js +++ b/extensions/cornerstone-3d/src/utils/measurementServiceMappings/Bidirectional.js @@ -94,29 +94,27 @@ function getMappedAnnotations(annotation, DisplaySetService) { let displaySet; - let SeriesInstanceUID, SOPInstanceUID; - if (targetId.startsWith('imageId:')) { - ({ SOPInstanceUID, SeriesInstanceUID } = getSOPInstanceAttributes( + if (referencedImageId) { + const { SOPInstanceUID, SeriesInstanceUID } = getSOPInstanceAttributes( referencedImageId - )); + ); displaySet = DisplaySetService.getDisplaySetForSOPInstanceUID( SOPInstanceUID, SeriesInstanceUID ); } else { - // Todo: separate imageId and volumeId, for now just implementing the - // referenceImageId - throw new Error('Not implemented'); + throw new Error( + 'Non-acquisition plane measurement mapping not supported' + ); } - const { SeriesNumber } = displaySet; + const { SeriesNumber, SeriesInstanceUID } = displaySet; const { length, width } = targetStats; const unit = 'mm'; annotations.push({ SeriesInstanceUID, - SOPInstanceUID, SeriesNumber, unit, length, diff --git a/extensions/cornerstone-3d/src/utils/measurementServiceMappings/EllipticalROI.js b/extensions/cornerstone-3d/src/utils/measurementServiceMappings/EllipticalROI.js index 57cb1a5ca3a..7affe67f8b8 100644 --- a/extensions/cornerstone-3d/src/utils/measurementServiceMappings/EllipticalROI.js +++ b/extensions/cornerstone-3d/src/utils/measurementServiceMappings/EllipticalROI.js @@ -1,5 +1,3 @@ -import { annotation } from '@cornerstonejs/tools'; - import SUPPORTED_TOOLS from './constants/supportedTools'; import getSOPInstanceAttributes from './utils/getSOPInstanceAttributes'; import getModalityUnit from './utils/getModalityUnit'; @@ -95,29 +93,28 @@ function getMappedAnnotations(annotation, DisplaySetService) { let displaySet; - let SeriesInstanceUID, SOPInstanceUID; - if (targetId.startsWith('imageId:')) { - ({ SOPInstanceUID, SeriesInstanceUID } = getSOPInstanceAttributes( + if (referencedImageId) { + const { SOPInstanceUID, SeriesInstanceUID } = getSOPInstanceAttributes( referencedImageId - )); + ); displaySet = DisplaySetService.getDisplaySetForSOPInstanceUID( SOPInstanceUID, SeriesInstanceUID ); } else { - // Todo: separate imageId and volumeId, for now just implementing the - // referenceImageId - throw new Error('Not implemented'); + // Todo: Non-acquisition plane measurement mapping not supported yet + throw new Error( + 'Non-acquisition plane measurement mapping not supported' + ); } - const { SeriesNumber } = displaySet; + const { SeriesNumber, SeriesInstanceUID } = displaySet; const { mean, stdDev, max, area, Modality } = targetStats; const unit = getModalityUnit(Modality); annotations.push({ SeriesInstanceUID, - SOPInstanceUID, SeriesNumber, Modality, unit, diff --git a/extensions/cornerstone-3d/src/utils/measurementServiceMappings/Length.js b/extensions/cornerstone-3d/src/utils/measurementServiceMappings/Length.js index 87a688fcc06..60ad232f19c 100644 --- a/extensions/cornerstone-3d/src/utils/measurementServiceMappings/Length.js +++ b/extensions/cornerstone-3d/src/utils/measurementServiceMappings/Length.js @@ -99,29 +99,27 @@ function getMappedAnnotations(annotation, DisplaySetService) { let displaySet; - let SeriesInstanceUID, SOPInstanceUID; - if (targetId.startsWith('imageId:')) { - ({ SOPInstanceUID, SeriesInstanceUID } = getSOPInstanceAttributes( + if (referencedImageId) { + const { SOPInstanceUID, SeriesInstanceUID } = getSOPInstanceAttributes( referencedImageId - )); + ); displaySet = DisplaySetService.getDisplaySetForSOPInstanceUID( SOPInstanceUID, SeriesInstanceUID ); } else { - // Todo: separate imageId and volumeId, for now just implementing the - // referenceImageId - throw new Error('Not implemented'); + throw new Error( + 'Non-acquisition plane measurement mapping not supported' + ); } - const { SeriesNumber } = displaySet; + const { SeriesNumber, SeriesInstanceUID } = displaySet; const { length } = targetStats; const unit = 'mm'; annotations.push({ SeriesInstanceUID, - SOPInstanceUID, SeriesNumber, unit, length, diff --git a/extensions/cornerstone-3d/src/utils/measurementServiceMappings/measurementServiceMappingsFactory.test.js b/extensions/cornerstone-3d/src/utils/measurementServiceMappings/measurementServiceMappingsFactory.test.js deleted file mode 100644 index 65bd22ae83c..00000000000 --- a/extensions/cornerstone-3d/src/utils/measurementServiceMappings/measurementServiceMappingsFactory.test.js +++ /dev/null @@ -1,102 +0,0 @@ -import measurementServiceMappingsFactory from './measurementServiceMappingsFactory'; - -jest.mock('cornerstone-core', () => ({ - ...jest.requireActual('cornerstone-core'), - getEnabledElement: () => ({ - image: { imageId: 123 }, - }), - metaData: { - ...jest.requireActual('cornerstone-core').metaData, - get: () => ({ - SOPInstanceUID: '123', - FrameOfReferenceUID: '123', - SeriesInstanceUID: '123', - StudyInstanceUID: '1234', - }), - }, -})); - -describe('measurementServiceMappings.js', () => { - let mappings; - let handles; - let points; - let csToolsAnnotation; - let measurement; - let measurementServiceMock; - let displaySetServiceMock; - let definition = 'Length'; - - beforeEach(() => { - measurementServiceMock = { - VALUE_TYPES: { - POLYLINE: 'value_type::polyline', - POINT: 'value_type::point', - ELLIPSE: 'value_type::ellipse', - MULTIPOINT: 'value_type::multipoint', - CIRCLE: 'value_type::circle', - }, - }; - displaySetServiceMock = { - getDisplaySetForSOPInstanceUID: (SOPInstanceUID, SeriesInstanceUID) => { - console.warn('SOPInstanceUID'); - - return { - displaySetInstanceUID: '1.2.3.4' - } - } - }; - mappings = measurementServiceMappingsFactory(measurementServiceMock, displaySetServiceMock); - handles = { start: { x: 1, y: 2 }, end: { x: 1, y: 2 } }; - points = [ - { x: 1, y: 2 }, - { x: 1, y: 2 }, - ]; - csToolsAnnotation = { - toolName: definition, - measurementData: { - id: 1, - label: 'Test', - SOPInstanceUID: '123', - FrameOfReferenceUID: '123', - SeriesInstanceUID: '123', - handles, - text: 'Test', - description: 'Test', - unit: 'mm', - length: undefined - }, - }; - measurement = { - id: 1, - SOPInstanceUID: '123', - FrameOfReferenceUID: '123', - referenceSeriesUID: '123', - referenceStudyUID: '1234', - displaySetInstanceUID: '1.2.3.4', - label: 'Test', - description: 'Test', - unit: 'mm', - type: measurementServiceMock.VALUE_TYPES.POLYLINE, - points, - }; - jest.clearAllMocks(); - }); - - /*describe('toAnnotation()', () => { - it('map measurement service format to annotation', async () => { - const mappedMeasurement = await mappings[csToolsAnnotation.toolName].toAnnotation( - measurement, - definition - ); - expect(mappedMeasurement).toEqual(csToolsAnnotation); - }); - });*/ - - describe('toMeasurement()', () => { - it('map annotation to measurement service format', async () => { - const getValueTypeFromToolType = (toolType) => 'valueType'; - const mappedAnnotation = await mappings[csToolsAnnotation.toolName].toMeasurement(csToolsAnnotation, displaySetServiceMock, getValueTypeFromToolType); - expect(mappedAnnotation).toEqual(measurement); - }); - }); -}); diff --git a/extensions/cornerstone-3d/src/utils/segmentationServiceMappings/Labelmap.js b/extensions/cornerstone-3d/src/utils/segmentationServiceMappings/Labelmap.js new file mode 100644 index 00000000000..cb058b06e62 --- /dev/null +++ b/extensions/cornerstone-3d/src/utils/segmentationServiceMappings/Labelmap.js @@ -0,0 +1,30 @@ +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; diff --git a/extensions/cornerstone-3d/src/utils/segmentationServiceMappings/segmentationServiceMappingsFactory.js b/extensions/cornerstone-3d/src/utils/segmentationServiceMappings/segmentationServiceMappingsFactory.js new file mode 100644 index 00000000000..ea248829abb --- /dev/null +++ b/extensions/cornerstone-3d/src/utils/segmentationServiceMappings/segmentationServiceMappingsFactory.js @@ -0,0 +1,16 @@ +import Labelmap from './Labelmap'; + +const segmentationServiceMappingsFactory = ( + SegmentationService, + DisplaySetService +) => { + return { + Labelmap: { + matchingCriteria: {}, + toSegmentation: csToolsSegmentation => + Labelmap.toSegmentation(csToolsSegmentation, DisplaySetService), + }, + }; +}; + +export default segmentationServiceMappingsFactory; diff --git a/extensions/cornerstone-dicom-sr/src/viewports/OHIFCornerstoneSRViewport.tsx b/extensions/cornerstone-dicom-sr/src/viewports/OHIFCornerstoneSRViewport.tsx index 11bc50f5deb..55d0ff5f956 100644 --- a/extensions/cornerstone-dicom-sr/src/viewports/OHIFCornerstoneSRViewport.tsx +++ b/extensions/cornerstone-dicom-sr/src/viewports/OHIFCornerstoneSRViewport.tsx @@ -27,7 +27,10 @@ function OHIFCornerstoneSRViewport(props) { extensionManager, } = props; - const { DisplaySetService } = servicesManager.services; + const { + DisplaySetService, + Cornerstone3DViewportService, + } = servicesManager.services; // SR viewport will always have a single display set if (displaySets.length > 1) { @@ -140,11 +143,6 @@ function OHIFCornerstoneSRViewport(props) { // imageIdIndex will handle it by updating the viewport, but if they // are the same we just need to use MeasurementService to jump to the // new measurement - const utilityModule = extensionManager.getModuleEntry( - '@ohif/extension-cornerstone-3d.utilityModule.common' - ); - - const { Cornerstone3DViewportService } = utilityModule.exports; const viewportInfo = Cornerstone3DViewportService.getViewportInfoByIndex( viewportIndex ); diff --git a/extensions/default/src/commandsModule.js b/extensions/default/src/commandsModule.js index a14c51e4552..d5d9aca35e2 100644 --- a/extensions/default/src/commandsModule.js +++ b/extensions/default/src/commandsModule.js @@ -2,9 +2,17 @@ const commandsModule = ({ servicesManager, commandsManager }) => { const { MeasurementService, HangingProtocolService, + UINotificationService, } = servicesManager.services; const actions = { + displayNotification: ({ text, title, type }) => { + UINotificationService.show({ + title: title, + message: text, + type: type, + }); + }, clearMeasurements: () => { MeasurementService.clear(); }, @@ -23,7 +31,11 @@ const commandsModule = ({ servicesManager, commandsManager }) => { storeContexts: [], options: {}, }, - + displayNotification: { + commandFn: actions.displayNotification, + storeContexts: [], + options: {}, + }, nextStage: { commandFn: actions.nextStage, storeContexts: [], diff --git a/extensions/default/src/getHangingProtocolModule.js b/extensions/default/src/getHangingProtocolModule.js index 468d14887ae..c85fac71a4d 100644 --- a/extensions/default/src/getHangingProtocolModule.js +++ b/extensions/default/src/getHangingProtocolModule.js @@ -32,6 +32,10 @@ const defaultProtocol = { { viewportOptions: { toolGroupId: 'default', + // initialImageOptions: { + // index: 180, + // preset: 'middle', // 'first', 'last', 'middle' + // }, }, displaySets: [ { diff --git a/extensions/default/src/getLayoutTemplateModule.js b/extensions/default/src/getLayoutTemplateModule.js index 793f002fa85..856d8d12ae1 100644 --- a/extensions/default/src/getLayoutTemplateModule.js +++ b/extensions/default/src/getLayoutTemplateModule.js @@ -5,11 +5,11 @@ import ViewerLayout from './ViewerLayout'; - Init layout based on the displaySets and the objects. */ -export default function ({ +export default function({ servicesManager, extensionManager, commandsManager, - hotkeysManager + hotkeysManager, }) { function ViewerLayoutWithServices(props) { return ViewerLayout({ diff --git a/extensions/default/src/index.js b/extensions/default/src/index.js index 2c5321989c2..0cf1773a8a1 100644 --- a/extensions/default/src/index.js +++ b/extensions/default/src/index.js @@ -5,6 +5,7 @@ import getSopClassHandlerModule from './getSopClassHandlerModule.js'; import getHangingProtocolModule from './getHangingProtocolModule.js'; import getToolbarModule from './getToolbarModule'; import commandsModule from './commandsModule'; +import getStudiesForPatientByStudyInstanceUID from './Panels/getStudiesForPatientByStudyInstanceUID'; import { id } from './id.js'; import init from './init'; @@ -25,6 +26,16 @@ const defaultExtension = { getCommandsModule({ servicesManager, commandsManager }) { return commandsModule({ servicesManager, commandsManager }); }, + getUtilityModule({ servicesManager }) { + return [ + { + name: 'common', + exports: { + getStudiesForPatientByStudyInstanceUID, + }, + }, + ]; + }, }; export default defaultExtension; diff --git a/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/index.tsx b/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/index.tsx index 2c2acaf147c..5ac9101946b 100644 --- a/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/index.tsx +++ b/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/index.tsx @@ -3,9 +3,17 @@ import PropTypes from 'prop-types'; // import PanelStudyBrowserTracking from './PanelStudyBrowserTracking'; import getImageSrcFromImageId from './getImageSrcFromImageId'; -import getStudiesForPatientByStudyInstanceUID from './getStudiesForPatientByStudyInstanceUID'; import requestDisplaySetCreationForStudy from './requestDisplaySetCreationForStudy'; +function _getStudyForPatientUtility(extensionManager) { + const utilityModule = extensionManager.getModuleEntry( + '@ohif/extension-default.utilityModule.common' + ); + + const { getStudiesForPatientByStudyInstanceUID } = utilityModule.exports; + return getStudiesForPatientByStudyInstanceUID; +} + /** * Wraps the PanelStudyBrowser and provides features afforded by managers/services * @@ -19,6 +27,10 @@ function WrappedPanelStudyBrowserTracking({ servicesManager, }) { const dataSource = extensionManager.getActiveDataSource()[0]; + + const getStudiesForPatientByStudyInstanceUID = _getStudyForPatientUtility( + extensionManager + ); const _getStudiesForPatientByStudyInstanceUID = getStudiesForPatientByStudyInstanceUID.bind( null, dataSource diff --git a/extensions/measurement-tracking/src/viewports/TrackedCornerstoneViewport.tsx b/extensions/measurement-tracking/src/viewports/TrackedCornerstoneViewport.tsx index 9f897b10015..0813272eddf 100644 --- a/extensions/measurement-tracking/src/viewports/TrackedCornerstoneViewport.tsx +++ b/extensions/measurement-tracking/src/viewports/TrackedCornerstoneViewport.tsx @@ -26,7 +26,10 @@ function TrackedCornerstoneViewport(props) { commandsManager, } = props; - const { MeasurementService } = servicesManager.services; + const { + MeasurementService, + Cornerstone3DViewportService, + } = servicesManager.services; // Todo: handling more than one displaySet on the same viewport const displaySet = displaySets[0]; @@ -41,11 +44,6 @@ function TrackedCornerstoneViewport(props) { const { trackedSeries } = trackedMeasurements.context; - const utilityModule = extensionManager.getModuleEntry( - '@ohif/extension-cornerstone-3d.utilityModule.common' - ); - - const { Cornerstone3DViewportService } = utilityModule.exports; const viewportId = Cornerstone3DViewportService.getViewportId(viewportIndex); const { diff --git a/extensions/tmtv/.webpack/webpack.dev.js b/extensions/tmtv/.webpack/webpack.dev.js new file mode 100644 index 00000000000..1ae30844802 --- /dev/null +++ b/extensions/tmtv/.webpack/webpack.dev.js @@ -0,0 +1,8 @@ +const path = require('path'); +const webpackCommon = require('./../../../.webpack/webpack.commonjs.js'); +const SRC_DIR = path.join(__dirname, '../src'); +const DIST_DIR = path.join(__dirname, '../dist'); + +module.exports = (env, argv) => { + return webpackCommon(env, argv, { SRC_DIR, DIST_DIR }); +}; diff --git a/extensions/tmtv/.webpack/webpack.prod.js b/extensions/tmtv/.webpack/webpack.prod.js new file mode 100644 index 00000000000..946363c5366 --- /dev/null +++ b/extensions/tmtv/.webpack/webpack.prod.js @@ -0,0 +1,43 @@ +const webpack = require('webpack'); +const merge = require('webpack-merge'); +const path = require('path'); +const webpackCommon = require('./../../../.webpack/webpack.base.js'); +const pkg = require('./../package.json'); + +const ROOT_DIR = path.join(__dirname, './..'); +const SRC_DIR = path.join(__dirname, '../src'); +const DIST_DIR = path.join(__dirname, '../dist'); + +module.exports = (env, argv) => { + const commonConfig = webpackCommon(env, argv, { SRC_DIR, DIST_DIR }); + + return merge(commonConfig, { + stats: { + colors: true, + hash: true, + timings: true, + assets: true, + chunks: false, + chunkModules: false, + modules: false, + children: false, + warnings: true, + }, + optimization: { + minimize: true, + sideEffects: true, + }, + output: { + path: ROOT_DIR, + library: 'OHIFExtDICOMSR', + libraryTarget: 'umd', + libraryExport: 'default', + filename: pkg.main, + }, + plugins: [ + new webpack.optimize.LimitChunkCountPlugin({ + maxChunks: 1, + }), + ], + }); +}; diff --git a/extensions/tmtv/LICENSE b/extensions/tmtv/LICENSE new file mode 100644 index 00000000000..19e20dd35ca --- /dev/null +++ b/extensions/tmtv/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018 Open Health Imaging Foundation + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/extensions/tmtv/README.md b/extensions/tmtv/README.md new file mode 100644 index 00000000000..f1753525b11 --- /dev/null +++ b/extensions/tmtv/README.md @@ -0,0 +1 @@ +# TMTV Extension diff --git a/extensions/tmtv/babel.config.js b/extensions/tmtv/babel.config.js new file mode 100644 index 00000000000..325ca2a8ee7 --- /dev/null +++ b/extensions/tmtv/babel.config.js @@ -0,0 +1 @@ +module.exports = require('../../babel.config.js'); diff --git a/extensions/tmtv/package.json b/extensions/tmtv/package.json new file mode 100644 index 00000000000..85bf6c78078 --- /dev/null +++ b/extensions/tmtv/package.json @@ -0,0 +1,43 @@ +{ + "name": "@ohif/extension-tmtv", + "version": "3.0.1", + "description": "OHIF extension for Total Metabolic Tumore Volume", + "author": "OHIF", + "license": "MIT", + "repository": "OHIF/Viewers", + "main": "dist/index.umd.js", + "module": "src/index.tsx", + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1.16.0" + }, + "files": [ + "dist", + "README.md" + ], + "publishConfig": { + "access": "public" + }, + "scripts": { + "dev": "cross-env NODE_ENV=development webpack --config .webpack/webpack.dev.js --watch --debug --output-pathinfo", + "build": "cross-env NODE_ENV=production webpack --config .webpack/webpack.prod.js", + "build:package": "yarn run build", + "start": "yarn run dev", + "test:unit": "jest --watchAll", + "test:unit:ci": "jest --ci --runInBand --collectCoverage --passWithNoTests" + }, + "peerDependencies": { + "@ohif/core": "^3.0.0", + "@ohif/ui": "^2.0.0", + "dcmjs": "0.22.0", + "dicom-parser": "^1.8.9", + "hammerjs": "^2.0.8", + "prop-types": "^15.6.2", + "react": "^17.0.2" + }, + "dependencies": { + "@babel/runtime": "7.7.6", + "classnames": "^2.2.6" + } +} diff --git a/extensions/tmtv/src/Panels/PanelPetSUV.tsx b/extensions/tmtv/src/Panels/PanelPetSUV.tsx new file mode 100644 index 00000000000..b7223122809 --- /dev/null +++ b/extensions/tmtv/src/Panels/PanelPetSUV.tsx @@ -0,0 +1,243 @@ +import React, { useEffect, useState, useCallback } from 'react'; +import PropTypes from 'prop-types'; +import { Input, Button } from '@ohif/ui'; +import { classes, DicomMetadataStore } from '@ohif/core'; +import { useTranslation } from 'react-i18next'; + +const DEFAULT_MEATADATA = { + PatientWeight: null, + PatientSex: null, + SeriesTime: null, + RadiopharmaceuticalInformationSequence: { + RadionuclideTotalDose: null, + RadionuclideHalfLife: null, + RadiopharmaceuticalStartTime: null, + }, +}; + +/* + * PETSUV panel enables the user to modify the patient related information, such as + * patient sex, patientWeight. This is allowed since + * sometimes these metadata are missing or wrong. By changing them + * @param param0 + * @returns + */ +export default function PanelPetSUV({ servicesManager, commandsManager }) { + const { t } = useTranslation('PanelSUV'); + const { DisplaySetService } = servicesManager.services; + const [metadata, setMetadata] = useState(DEFAULT_MEATADATA); + const [ptDisplaySet, setPtDisplaySet] = useState(null); + + const handleMetadataChange = useCallback( + metadata => { + setMetadata(prevState => { + const newState = { ...prevState }; + Object.keys(metadata).forEach(key => { + if (typeof metadata[key] === 'object') { + newState[key] = { + ...prevState[key], + ...metadata[key], + }; + } else { + newState[key] = metadata[key]; + } + }); + return newState; + }); + }, + [metadata] + ); + + const getMatchingPTDisplaySet = useCallback(() => { + const ptDisplaySet = commandsManager.runCommand('getMatchingPTDisplaySet'); + + if (!ptDisplaySet) { + return; + } + + const metadata = commandsManager.runCommand('getPTMetadata', { + ptDisplaySet, + }); + + return { + ptDisplaySet, + metadata, + }; + }, []); + + useEffect(() => { + const displaySets = DisplaySetService.activeDisplaySets; + + if (!displaySets.length) { + return; + } + + const displaySetInfo = getMatchingPTDisplaySet(); + + if (!displaySetInfo) { + return; + } + + const { ptDisplaySet, metadata } = displaySetInfo; + setPtDisplaySet(ptDisplaySet); + setMetadata(metadata); + }, []); + + // get the patientMetadata from the StudyInstanceUIDs and update the state + useEffect(() => { + const { unsubscribe } = DisplaySetService.subscribe( + DisplaySetService.EVENTS.DISPLAY_SETS_ADDED, + () => { + const displaySetInfo = getMatchingPTDisplaySet(); + + if (!displaySetInfo) { + return; + } + + const { ptDisplaySet, metadata } = displaySetInfo; + setPtDisplaySet(ptDisplaySet); + setMetadata(metadata); + } + ); + return () => { + unsubscribe(); + }; + }, []); + + function updateMetadata() { + if (!ptDisplaySet) { + throw new Error('No ptDisplaySet found'); + } + // metadata should be dcmjs naturalized + DicomMetadataStore.updateMetadataForSeries( + ptDisplaySet.StudyInstanceUID, + ptDisplaySet.SeriesInstanceUID, + metadata + ); + + // update the displaySets + DisplaySetService.setDisplaySetMetadataInvalidated( + ptDisplaySet.displaySetInstanceUID + ); + } + + return ( +
+ { +
+
+ { + handleMetadataChange({ + PatientSex: e.target.value, + }); + }} + /> + { + handleMetadataChange({ + PatientWeight: e.target.value, + }); + }} + /> + { + handleMetadataChange({ + RadiopharmaceuticalInformationSequence: { + RadionuclideTotalDose: e.target.value, + }, + }); + }} + /> + { + handleMetadataChange({ + RadiopharmaceuticalInformationSequence: { + RadionuclideHalfLife: e.target.value, + }, + }); + }} + /> + { + handleMetadataChange({ + RadiopharmaceuticalInformationSequence: { + RadiopharmaceuticalStartTime: e.target.value, + }, + }); + }} + /> + {}} + /> +
+ +
+ } +
+ ); +} + +PanelPetSUV.propTypes = { + servicesManager: PropTypes.shape({ + services: PropTypes.shape({ + MeasurementService: PropTypes.shape({ + getMeasurements: PropTypes.func.isRequired, + subscribe: PropTypes.func.isRequired, + EVENTS: PropTypes.object.isRequired, + VALUE_TYPES: PropTypes.object.isRequired, + }).isRequired, + }).isRequired, + }).isRequired, +}; diff --git a/extensions/tmtv/src/Panels/PanelROIThresholdSegmentation/ExportReports.tsx b/extensions/tmtv/src/Panels/PanelROIThresholdSegmentation/ExportReports.tsx new file mode 100644 index 00000000000..415b80015e4 --- /dev/null +++ b/extensions/tmtv/src/Panels/PanelROIThresholdSegmentation/ExportReports.tsx @@ -0,0 +1,44 @@ +import React from 'react'; +import { Button, ButtonGroup } from '@ohif/ui'; +import { useTranslation } from 'react-i18next'; + +function ExportReports({ segmentations, tmtvValue, config, commandsManager }) { + const { t } = useTranslation('PanelSUVExport'); + + return ( + <> + {segmentations?.length ? ( +
+ + + + + + +
+ ) : null} + + ); +} + +export default ExportReports; diff --git a/extensions/tmtv/src/Panels/PanelROIThresholdSegmentation/PanelROIThresholdSegmentation.tsx b/extensions/tmtv/src/Panels/PanelROIThresholdSegmentation/PanelROIThresholdSegmentation.tsx new file mode 100644 index 00000000000..128ccb83b20 --- /dev/null +++ b/extensions/tmtv/src/Panels/PanelROIThresholdSegmentation/PanelROIThresholdSegmentation.tsx @@ -0,0 +1,319 @@ +import React, { useEffect, useState, useCallback, useReducer } from 'react'; +import PropTypes from 'prop-types'; +import { SegmentationTable, Button, Icon } from '@ohif/ui'; +import { useTranslation } from 'react-i18next'; +import segmentationEditHandler from './segmentationEditHandler'; +import ExportReports from './ExportReports'; +import ROIThresholdConfiguration, { + ROI_STAT, +} from './ROIThresholdConfiguration'; + +const LOWER_THRESHOLD_DEFAULT = 0; +const UPPER_THRESHOLD_DEFAULT = 100; +const WEIGHT_DEFAULT = 0.41; // a default weight for suv max often used in the literature +const DEFAULT_STRATEGY = ROI_STAT; + +function reducer(state, action) { + const { payload } = action; + const { strategy, lower, upper, weight } = payload; + + switch (action.type) { + case 'setStrategy': + return { + ...state, + strategy, + }; + case 'setThreshold': + return { + ...state, + lower: lower ? lower : state.lower, + upper: upper ? upper : state.upper, + }; + case 'setWeight': + return { + ...state, + weight, + }; + default: + return state; + } +} + +export default function PanelRoiThresholdSegmentation({ + servicesManager, + commandsManager, +}) { + const { SegmentationService } = servicesManager.services; + + const { t } = useTranslation('PanelSUV'); + const [showConfig, setShowConfig] = useState(false); + const [labelmapLoading, setLabelmapLoading] = useState(false); + const [selectedSegmentationId, setSelectedSegmentationId] = useState(null); + const [segmentations, setSegmentations] = useState(() => + SegmentationService.getSegmentations() + ); + + const [config, dispatch] = useReducer(reducer, { + strategy: DEFAULT_STRATEGY, + lower: LOWER_THRESHOLD_DEFAULT, + upper: UPPER_THRESHOLD_DEFAULT, + weight: WEIGHT_DEFAULT, + }); + + const [tmtvValue, setTmtvValue] = useState(null); + + const runCommand = useCallback( + (commandName, commandOptions = {}) => { + return commandsManager.runCommand(commandName, commandOptions); + }, + [commandsManager] + ); + + const handleTMTVCalculation = useCallback(() => { + const tmtv = runCommand('calculateTMTV', { segmentations }); + + if (tmtv !== undefined) { + setTmtvValue(tmtv.toFixed(2)); + } + }, [segmentations, runCommand]); + + const handleROIThresholding = useCallback(() => { + const labelmap = runCommand('thresholdSegmentationByRectangleROITool', { + segmentationId: selectedSegmentationId, + config, + }); + + const lesionStats = runCommand('getLesionStats', { labelmap }); + const suvPeak = runCommand('calculateSuvPeak', { labelmap }); + const lesionGlyoclysisStats = lesionStats.volume * lesionStats.meanValue; + + // update segDetails with the suv peak for the active segmentation + const segmentation = SegmentationService.getSegmentation( + selectedSegmentationId + ); + + const data = { + lesionStats, + suvPeak, + lesionGlyoclysisStats, + }; + + const notYetUpdatedAtSource = true; + SegmentationService.addOrUpdateSegmentation( + selectedSegmentationId, + { + ...segmentation, + data: Object.assign(segmentation.data, data), + text: [`SUV Peak: ${suvPeak.suvPeak.toFixed(2)}`], + }, + notYetUpdatedAtSource + ); + + handleTMTVCalculation(); + }, [selectedSegmentationId, config]); + + /** + * Update UI based on segmentation changes (added, removed, updated) + */ + useEffect(() => { + // ~~ Subscription + const added = SegmentationService.EVENTS.SEGMENTATION_ADDED; + const updated = SegmentationService.EVENTS.SEGMENTATION_UPDATED; + const subscriptions = []; + + [added, updated].forEach(evt => { + const { unsubscribe } = SegmentationService.subscribe(evt, () => { + const segmentations = SegmentationService.getSegmentations(); + setSegmentations(segmentations); + }); + subscriptions.push(unsubscribe); + }); + + return () => { + subscriptions.forEach(unsub => { + unsub(); + }); + }; + }, []); + + useEffect(() => { + const { unsubscribe } = SegmentationService.subscribe( + SegmentationService.EVENTS.SEGMENTATION_REMOVED, + () => { + const segmentations = SegmentationService.getSegmentations(); + setSegmentations(segmentations); + + if (segmentations.length > 0) { + setSelectedSegmentationId(segmentations[0].id); + handleTMTVCalculation(); + } else { + setSelectedSegmentationId(null); + setTmtvValue(null); + } + } + ); + + return () => { + unsubscribe(); + }; + }, []); + + /** + * Toggle visibility of the segmentation + */ + useEffect(() => { + const subscription = SegmentationService.subscribe( + SegmentationService.EVENTS.SEGMENTATION_VISIBILITY_CHANGED, + ({ segmentation }) => { + runCommand('toggleSegmentationVisibility', { + segmentationId: segmentation.id, + }); + } + ); + return () => { + subscription.unsubscribe(); + }; + }, [SegmentationService]); + + /** + * Whenever the segmentations change, update the TMTV calculations + */ + useEffect(() => { + if (!selectedSegmentationId && segmentations.length > 0) { + setSelectedSegmentationId(segmentations[0].id); + } + + handleTMTVCalculation(); + }, [segmentations, selectedSegmentationId]); + + return ( +
+
+
+ + +
+
{ + setShowConfig(!showConfig); + }} + > +
+ {t('ROI Threshold Configuration')} +
+
+ {showConfig && ( + + )} + {labelmapLoading ? ( +
Loading Segmentation Panel ...
+ ) : null} + {/* show segmentation table */} +
+ {segmentations?.length ? ( + { + runCommand('setSegmentationActiveForToolGroups', { + segmentationId: id, + }); + setSelectedSegmentationId(id); + }} + onToggleVisibility={id => { + SegmentationService.toggleSegmentationsVisibility([id]); + }} + onToggleVisibilityAll={ids => { + SegmentationService.toggleSegmentationsVisibility(ids); + }} + onDelete={id => { + SegmentationService.remove(id); + }} + onEdit={id => { + segmentationEditHandler({ + id, + servicesManager, + }); + }} + /> + ) : null} +
+ {tmtvValue !== null ? ( +
+ + {'TMTV:'} + +
{`${tmtvValue} mL`}
+
+ ) : null} + +
+
{ + // navigate to a url in a new tab + window.open( + 'https://github.com/OHIF/Viewers/blob/feat/segmentation-service/modes/tmtv/README.md', + '_blank' + ); + }} + > + + {'About'} +
+
+ ); +} + +PanelRoiThresholdSegmentation.propTypes = { + commandsManager: PropTypes.shape({ + runCommand: PropTypes.func.isRequired, + }), + servicesManager: PropTypes.shape({ + services: PropTypes.shape({ + SegmentationService: PropTypes.shape({ + getSegmentation: PropTypes.func.isRequired, + getSegmentations: PropTypes.func.isRequired, + toggleSegmentationsVisibility: PropTypes.func.isRequired, + subscribe: PropTypes.func.isRequired, + EVENTS: PropTypes.object.isRequired, + VALUE_TYPES: PropTypes.object.isRequired, + }).isRequired, + }).isRequired, + }).isRequired, +}; diff --git a/extensions/tmtv/src/Panels/PanelROIThresholdSegmentation/ROIThresholdConfiguration.tsx b/extensions/tmtv/src/Panels/PanelROIThresholdSegmentation/ROIThresholdConfiguration.tsx new file mode 100644 index 00000000000..377e79a8401 --- /dev/null +++ b/extensions/tmtv/src/Panels/PanelROIThresholdSegmentation/ROIThresholdConfiguration.tsx @@ -0,0 +1,116 @@ +import React from 'react'; +import { Input, Select, Button, ButtonGroup } from '@ohif/ui'; +import { useTranslation } from 'react-i18next'; + +export const ROI_STAT = 'roi_stat'; +const RANGE = 'range'; + +const options = [ + { value: ROI_STAT, label: 'Max', placeHolder: 'Max' }, + { value: RANGE, label: 'Range', placeHolder: 'Range' }, +]; + +function ROIThresholdConfiguration({ config, dispatch, runCommand }) { + const { t } = useTranslation('ROIThresholdConfiguration'); + + return ( +
+
+
+ { + dispatch({ + type: 'setWeight', + payload: e.target.value, + }); + }} + /> + )} + {config.strategy !== ROI_STAT && ( +
+ { + dispatch({ + type: 'setThreshold', + payload: { + lower: e.target.value, + }, + }); + }} + /> + { + dispatch({ + type: 'setThreshold', + payload: { + upper: e.target.value, + }, + }); + }} + /> +
+ )} +
+ ); +} + +export default ROIThresholdConfiguration; diff --git a/extensions/tmtv/src/Panels/PanelROIThresholdSegmentation/index.ts b/extensions/tmtv/src/Panels/PanelROIThresholdSegmentation/index.ts new file mode 100644 index 00000000000..22404ee9861 --- /dev/null +++ b/extensions/tmtv/src/Panels/PanelROIThresholdSegmentation/index.ts @@ -0,0 +1,3 @@ +import PanelROIThresholdSegmentation from './PanelROIThresholdSegmentation'; + +export default PanelROIThresholdSegmentation; diff --git a/extensions/tmtv/src/Panels/PanelROIThresholdSegmentation/segmentationEditHandler.tsx b/extensions/tmtv/src/Panels/PanelROIThresholdSegmentation/segmentationEditHandler.tsx new file mode 100644 index 00000000000..56aeb74efe1 --- /dev/null +++ b/extensions/tmtv/src/Panels/PanelROIThresholdSegmentation/segmentationEditHandler.tsx @@ -0,0 +1,70 @@ +import React from 'react'; +import { Input, Dialog } from '@ohif/ui'; + +function segmentationItemEditHandler({ id, servicesManager }) { + const { SegmentationService, UIDialogService } = servicesManager.services; + + const segmentation = SegmentationService.getSegmentation(id); + + const onSubmitHandler = ({ action, value }) => { + switch (action.id) { + case 'save': { + SegmentationService.addOrUpdateSegmentation( + id, + { + ...segmentation, + ...value, + }, + true + ); + } + } + UIDialogService.dismiss({ id: 'enter-annotation' }); + }; + + UIDialogService.create({ + id: 'enter-annotation', + centralize: true, + isDraggable: false, + showOverlay: true, + content: Dialog, + contentProps: { + title: 'Enter your annotation', + noCloseButton: true, + value: { label: segmentation.label || '' }, + body: ({ value, setValue }) => { + const onChangeHandler = event => { + event.persist(); + setValue(value => ({ ...value, label: event.target.value })); + }; + + const onKeyPressHandler = event => { + if (event.key === 'Enter') { + onSubmitHandler({ value, action: { id: 'save' } }); + } + }; + return ( +
+ +
+ ); + }, + actions: [ + // temp: swap button types until colors are updated + { id: 'cancel', text: 'Cancel', type: 'primary' }, + { id: 'save', text: 'Save', type: 'secondary' }, + ], + onSubmit: onSubmitHandler, + }, + }); +} + +export default segmentationItemEditHandler; diff --git a/extensions/tmtv/src/Panels/index.tsx b/extensions/tmtv/src/Panels/index.tsx new file mode 100644 index 00000000000..0fa6418fdf4 --- /dev/null +++ b/extensions/tmtv/src/Panels/index.tsx @@ -0,0 +1,4 @@ +import PanelPetSUV from './PanelPetSUV'; +import PanelROIThresholdSegmentation from './PanelROIThresholdSegmentation'; + +export { PanelPetSUV, PanelROIThresholdSegmentation }; diff --git a/extensions/tmtv/src/commandsModule.js b/extensions/tmtv/src/commandsModule.js new file mode 100644 index 00000000000..ce671867235 --- /dev/null +++ b/extensions/tmtv/src/commandsModule.js @@ -0,0 +1,742 @@ +import { vec3 } from 'gl-matrix'; +import OHIF from '@ohif/core'; +import * as cs from '@cornerstonejs/core'; +import * as csTools from '@cornerstonejs/tools'; +import { classes } from '@ohif/core'; +import getThresholdValues from './utils/getThresholdValue'; +import calculateSuvPeak from './utils/calculateSUVPeak'; +import calculateTMTV from './utils/calculateTMTV'; +import createAndDownloadTMTVReport from './utils/createAndDownloadTMTVReport'; + +import dicomRTAnnotationExport from './utils/dicomRTAnnotationExport/RTStructureSet'; + +const metadataProvider = classes.MetadataProvider; +const RECTANGLE_ROI_THRESHOLD_MANUAL = 'RectangleROIStartEndThreshold'; + +const commandsModule = ({ + servicesManager, + commandsManager, + extensionManager, +}) => { + const { + ViewportGridService, + UINotificationService, + DisplaySetService, + HangingProtocolService, + ToolGroupService, + Cornerstone3DViewportService, + } = servicesManager.services; + + const utilityModule = extensionManager.getModuleEntry( + '@ohif/extension-cornerstone-3d.utilityModule.common' + ); + + const { getEnabledElement } = utilityModule.exports; + + function _getActiveViewportsEnabledElement() { + const { activeViewportIndex } = ViewportGridService.getState(); + const { element } = getEnabledElement(activeViewportIndex) || {}; + const enabledElement = cs.getEnabledElement(element); + return enabledElement; + } + + function _getMatchedViewportsToolGroupIds() { + const [matchedViewports] = HangingProtocolService.getState(); + const toolGroupIds = []; + matchedViewports.forEach(({ viewportOptions }) => { + const { toolGroupId } = viewportOptions; + if (toolGroupIds.indexOf(toolGroupId) === -1) { + toolGroupIds.push(toolGroupId); + } + }); + + return toolGroupIds; + } + + const actions = { + getMatchingPTDisplaySet: () => { + // Todo: this is assuming that the hanging protocol has successfully matched + // the correct PT. For future, we should have a way to filter out the PTs + // that are in the viewer layout (but then we have the problem of the attenuation + // corrected PT vs the non-attenuation correct PT) + const matches = HangingProtocolService.getDisplaySetsMatchDetails(); + + const matchedSeriesInstanceUIDs = Array.from(matches.values()).map( + ({ SeriesInstanceUID }) => SeriesInstanceUID + ); + + let ptDisplaySet = null; + for (const SeriesInstanceUID of matchedSeriesInstanceUIDs) { + const displaySets = DisplaySetService.getDisplaySetsForSeries( + SeriesInstanceUID + ); + + if (!displaySets || displaySets.length === 0) { + continue; + } + + const displaySet = displaySets[0]; + if (displaySet.Modality !== 'PT') { + continue; + } + + ptDisplaySet = displaySet; + } + + return ptDisplaySet; + }, + getPTMetadata: ({ ptDisplaySet }) => { + const dataSource = extensionManager.getDataSources()[0]; + const imageIds = dataSource.getImageIdsForDisplaySet(ptDisplaySet); + + const firstImageId = imageIds[0]; + const SeriesTime = metadataProvider.get('SeriesTime', firstImageId); + const metadata = {}; + + if (SeriesTime) { + metadata.SeriesTime = SeriesTime; + } + + // get metadata from the first image + const seriesModule = metadataProvider.get( + 'generalSeriesModule', + firstImageId + ); + + if (seriesModule && seriesModule.modality !== 'PT') { + return; + } + + // get metadata from the first image + const demographic = metadataProvider.get( + 'patientDemographicModule', + firstImageId + ); + + if (demographic) { + // naturalized dcmjs version + metadata.PatientSex = demographic.patientSex; + } + + // patientStudyModule + const studyModule = metadataProvider.get( + 'patientStudyModule', + firstImageId + ); + + if (studyModule) { + // naturalized dcmjs version + metadata.PatientWeight = studyModule.patientWeight; + } + + // total dose + const petSequenceModule = metadataProvider.get( + 'petIsotopeModule', + firstImageId + ); + const { radiopharmaceuticalInfo } = petSequenceModule; + + const { + radionuclideHalfLife, + radionuclideTotalDose, + radiopharmaceuticalStartTime, + } = radiopharmaceuticalInfo; + + const { + hours, + minutes, + seconds, + fractionalSeconds, + } = radiopharmaceuticalStartTime; + + // pad number with leading zero if less than 10 + const hoursString = hours < 10 ? `0${hours}` : hours; + const minutesString = minutes < 10 ? `0${minutes}` : minutes; + const secondsString = seconds < 10 ? `0${seconds}` : seconds; + + if (radiopharmaceuticalInfo) { + metadata.RadiopharmaceuticalInformationSequence = { + RadionuclideTotalDose: radionuclideTotalDose, + RadionuclideHalfLife: radionuclideHalfLife, + RadiopharmaceuticalStartTime: `${hoursString}${minutesString}${secondsString}.${fractionalSeconds}`, + }; + } + + return metadata; + }, + createNewLabelmapFromPT: async () => { + // Create a segmentation of the same resolution as the source data + // using volumeLoader.createAndCacheDerivedVolume. + const ptDisplaySet = actions.getMatchingPTDisplaySet(); + + if (!ptDisplaySet) { + UINotificationService.error('No matching PT display set found'); + return; + } + + const segmentationId = await commandsManager.runCommand( + 'createSegmentationForDisplaySet', + { + displaySetInstanceUID: ptDisplaySet.displaySetInstanceUID, + } + ); + + const toolGroupIds = _getMatchedViewportsToolGroupIds(); + + const representationType = + csTools.Enums.SegmentationRepresentations.Labelmap; + + for (const toolGroupId of toolGroupIds) { + await commandsManager.runCommand( + 'addSegmentationRepresentationToToolGroup', + { segmentationId, toolGroupId: toolGroupId, representationType } + ); + } + + return segmentationId; + }, + setSegmentationActiveForToolGroups: ({ segmentationId }) => { + const toolGroupIds = _getMatchedViewportsToolGroupIds(); + + toolGroupIds.forEach(toolGroupId => { + const segmentationRepresentations = csTools.segmentation.state.getSegmentationRepresentations( + toolGroupId + ); + + if (segmentationRepresentations.length === 0) { + return; + } + + // Todo: this finds the first segmentation representation that matches the segmentationId + // If there are two labelmap representations from the same segmentation, this will not work + const representation = segmentationRepresentations.find( + representation => representation.segmentationId === segmentationId + ); + + csTools.segmentation.activeSegmentation.setActiveSegmentationRepresentation( + toolGroupId, + representation.segmentationRepresentationUID + ); + }); + }, + thresholdSegmentationByRectangleROITool: ({ segmentationId, config }) => { + const segmentation = csTools.segmentation.state.getSegmentation( + segmentationId + ); + + const { representationData } = segmentation; + const { volumeId: segVolumeId } = representationData[ + csTools.Enums.SegmentationRepresentations.Labelmap + ]; + + const { referencedVolumeId } = cs.cache.getVolume(segVolumeId); + + const labelmapVolume = cs.cache.getVolume(segmentationId); + const referencedVolume = cs.cache.getVolume(referencedVolumeId); + + if (!referencedVolume) { + throw new Error('No Reference volume found'); + } + + if (!labelmapVolume) { + throw new Error('No Reference labelmap found'); + } + + const annotationUIDs = csTools.annotation.selection.getAnnotationsSelectedByToolName( + RECTANGLE_ROI_THRESHOLD_MANUAL + ); + + if (annotationUIDs.length === 0) { + UINotificationService.show({ + title: 'Commands Module', + message: 'No ROIThreshold Tool is Selected', + type: 'error', + }); + return; + } + + const { lower, higher } = getThresholdValues( + annotationUIDs, + referencedVolume, + config + ); + + const configToUse = { + lower, + higher, + overwrite: true, + }; + + return csTools.utilities.segmentation.rectangleROIThresholdVolumeByRange( + annotationUIDs, + labelmapVolume, + [referencedVolume], + configToUse + ); + }, + toggleSegmentationVisibility: ({ segmentationId }) => { + const toolGroupIds = _getMatchedViewportsToolGroupIds(); + + toolGroupIds.forEach(toolGroupId => { + const segmentationRepresentations = csTools.segmentation.state.getSegmentationRepresentations( + toolGroupId + ); + + if (segmentationRepresentations.length === 0) { + return; + } + + // Todo: this finds the first segmentation representation that matches the segmentationId + // If there are two labelmap representations from the same segmentation, this will not work + const representation = segmentationRepresentations.find( + representation => representation.segmentationId === segmentationId + ); + + const visibility = csTools.segmentation.config.visibility.getSegmentationVisibility( + toolGroupId, + representation.segmentationRepresentationUID + ); + + csTools.segmentation.config.visibility.setSegmentationVisibility( + toolGroupId, + representation.segmentationRepresentationUID, + !visibility + ); + }); + }, + calculateSuvPeak: ({ labelmap }) => { + const { referencedVolumeId } = labelmap; + + const referencedVolume = cs.cache.getVolume(referencedVolumeId); + + const annotationUIDs = csTools.annotation.selection.getAnnotationsSelectedByToolName( + RECTANGLE_ROI_THRESHOLD_MANUAL + ); + + const annotations = annotationUIDs.map(annotationUID => + csTools.annotation.state.getAnnotation(annotationUID) + ); + + const suvPeak = calculateSuvPeak(labelmap, referencedVolume, annotations); + return { + suvPeak: suvPeak.mean, + suvMax: suvPeak.max, + suvMaxIJK: suvPeak.maxIJK, + suvMaxLPS: suvPeak.maxLPS, + }; + }, + getLesionStats: ({ labelmap, segmentIndex = 1 }) => { + const { scalarData, spacing } = labelmap; + + const { scalarData: referencedScalarData } = cs.cache.getVolume( + labelmap.referencedVolumeId + ); + + let segmentationMax = -Infinity; + let segmentationMin = Infinity; + let segmentationValues = []; + + let voxelCount = 0; + for (let i = 0; i < scalarData.length; i++) { + if (scalarData[i] === segmentIndex) { + const value = referencedScalarData[i]; + segmentationValues.push(value); + if (value > segmentationMax) { + segmentationMax = value; + } + if (value < segmentationMin) { + segmentationMin = value; + } + voxelCount++; + } + } + + const stats = { + minValue: segmentationMin, + maxValue: segmentationMax, + meanValue: segmentationValues.reduce((a, b) => a + b, 0) / voxelCount, + stdValue: Math.sqrt( + segmentationValues.reduce((a, b) => a + b * b, 0) / voxelCount - + segmentationValues.reduce((a, b) => a + b, 0) / voxelCount ** 2 + ), + volume: voxelCount * spacing[0] * spacing[1] * spacing[2] * 1e-3, + }; + + return stats; + }, + calculateLesionGlycolysis: ({ lesionStats }) => { + const { meanValue, volume } = lesionStats; + + return { + lesionGlyoclysisStats: volume * meanValue, + }; + }, + calculateTMTV: ({ segmentations }) => { + const labelmaps = commandsManager.runCommand('getLabelmapVolumes', { + segmentations, + }); + + if (!labelmaps.length) { + return; + } + + return calculateTMTV(labelmaps); + }, + exportTMTVReportCSV: ({ segmentations, tmtv, config }) => { + const segReport = commandsManager.runCommand('getSegmentationCSVReport', { + segmentations, + }); + + const tlg = actions.getTotalLesionGlycolysis({ segmentations }); + const additionalReportRows = [ + { key: 'Total Metabolic Tumor Volume', value: { tmtv } }, + { key: 'Total Lesion Glycolysis', value: { tlg: tlg.toFixed(4) } }, + { key: 'Threshold Configuration', value: { ...config } }, + ]; + + createAndDownloadTMTVReport(segReport, additionalReportRows); + }, + getTotalLesionGlycolysis: ({ segmentations }) => { + const labelmapVolumes = commandsManager.runCommand('getLabelmapVolumes', { + segmentations, + }); + + let mergedLabelmap; + // merge labelmap will through an error if labels maps are not the same size + // or same direction or .... + try { + mergedLabelmap = csTools.utilities.segmentation.createMergedLabelmapForIndex( + labelmapVolumes + ); + } catch (e) { + console.error('commandsModule::getTotalLesionGlycolysis', e); + return; + } + + // grabbing the first labelmap referenceVolume since it will be the same for all + const { referencedVolumeId, spacing } = labelmapVolumes[0]; + + if (!referencedVolumeId) { + console.error( + 'commandsModule::getTotalLesionGlycolysis:No referencedVolumeId found' + ); + } + + const ptVolume = cs.cache.getVolume(referencedVolumeId); + const mergedLabelData = mergedLabelmap.scalarData; + + if (mergedLabelData.length !== ptVolume.scalarData.length) { + console.error( + 'commandsModule::getTotalLesionGlycolysis:Labelmap and ptVolume are not the same size' + ); + } + + let suv = 0; + let totalLesionVoxelCount = 0; + for (let i = 0; i < mergedLabelData.length; i++) { + // if not background + if (mergedLabelData[i] !== 0) { + suv += ptVolume.scalarData[i]; + totalLesionVoxelCount += 1; + } + } + + // Average SUV for the merged labelmap + const averageSuv = suv / totalLesionVoxelCount; + + // total Lesion Glycolysis [suv * ml] + return ( + averageSuv * + totalLesionVoxelCount * + spacing[0] * + spacing[1] * + spacing[2] * + 1e-3 + ); + }, + + setStartSliceForROIThresholdTool: () => { + const { viewport } = _getActiveViewportsEnabledElement(); + const { focalPoint, viewPlaneNormal } = viewport.getCamera(); + + const selectedAnnotationUIDs = csTools.annotation.selection.getAnnotationsSelectedByToolName( + RECTANGLE_ROI_THRESHOLD_MANUAL + ); + + const annotationUID = selectedAnnotationUIDs[0]; + + const annotation = csTools.annotation.state.getAnnotation(annotationUID); + + const { handles } = annotation.data; + const { points } = handles; + + // get the current slice Index + const sliceIndex = viewport.getCurrentImageIdIndex(); + annotation.data.startSlice = sliceIndex; + + // distance between camera focal point and each point on the rectangle + const newPoints = points.map(point => { + const distance = vec3.create(); + vec3.subtract(distance, focalPoint, point); + // distance in the direction of the viewPlaneNormal + const distanceInViewPlane = vec3.dot(distance, viewPlaneNormal); + // new point is current point minus distanceInViewPlane + const newPoint = vec3.create(); + vec3.scaleAndAdd(newPoint, point, viewPlaneNormal, distanceInViewPlane); + + return newPoint; + // + }); + + handles.points = newPoints; + // IMPORTANT: invalidate the toolData for the cached stat to get updated + // and re-calculate the projection points + annotation.invalidated = true; + viewport.render(); + }, + setEndSliceForROIThresholdTool: () => { + const { viewport } = _getActiveViewportsEnabledElement(); + + const selectedAnnotationUIDs = csTools.annotation.selection.getAnnotationsSelectedByToolName( + RECTANGLE_ROI_THRESHOLD_MANUAL + ); + + const annotationUID = selectedAnnotationUIDs[0]; + + const annotation = csTools.annotation.state.getAnnotation(annotationUID); + + // get the current slice Index + const sliceIndex = viewport.getCurrentImageIdIndex(); + annotation.data.endSlice = sliceIndex; + + // IMPORTANT: invalidate the toolData for the cached stat to get updated + // and re-calculate the projection points + annotation.invalidated = true; + + viewport.render(); + }, + createTMTVRTReport: () => { + // get all Rectangle ROI annotation + const stateManager = csTools.annotation.state.getDefaultAnnotationManager(); + + const annotations = []; + + Object.keys(stateManager.annotations).forEach(frameOfReferenceUID => { + const forAnnotations = stateManager.annotations[frameOfReferenceUID]; + const ROIAnnotations = forAnnotations[RECTANGLE_ROI_THRESHOLD_MANUAL]; + annotations.push(...ROIAnnotations); + }); + + commandsManager.runCommand('exportRTReportForAnnotations', { + annotations, + }); + }, + getSegmentationCSVReport: ({ segmentations }) => { + if (!segmentations || !segmentations.length) { + segmentations = SegmentationService.getSegmentations(); + } + + let report = {}; + + for (const segmentation of segmentations) { + const { id, label, data } = segmentation; + + const segReport = { id, label }; + + if (!data) { + report[id] = segReport; + continue; + } + + Object.keys(data).forEach(key => { + if (typeof data[key] !== 'object') { + segReport[key] = data[key]; + } else { + Object.keys(data[key]).forEach(subKey => { + const newKey = `${key}_${subKey}`; + segReport[newKey] = data[key][subKey]; + }); + } + }); + + const labelmapVolume = cornerstone.cache.getVolume(id); + + if (!labelmapVolume) { + report[id] = segReport; + continue; + } + + const referencedVolumeId = labelmapVolume.referencedVolumeId; + segReport.referencedVolumeId = referencedVolumeId; + + const referencedVolume = cornerstone.cache.getVolume( + referencedVolumeId + ); + + if (!referencedVolume) { + report[id] = segReport; + continue; + } + + if (!referencedVolume.imageIds || !referencedVolume.imageIds.length) { + report[id] = segReport; + continue; + } + + const firstImageId = referencedVolume.imageIds[0]; + const instance = OHIF.classes.MetadataProvider.get( + 'instance', + firstImageId + ); + + if (!instance) { + report[id] = segReport; + continue; + } + + report[id] = { + ...segReport, + PatientID: instance.PatientID, + PatientName: instance.PatientName.Alphabetic, + StudyInstanceUID: instance.StudyInstanceUID, + SeriesInstanceUID: instance.SeriesInstanceUID, + StudyDate: instance.StudyDate, + }; + } + + return report; + }, + exportRTReportForAnnotations: ({ annotations }) => { + dicomRTAnnotationExport(annotations); + }, + setFusionPTColormap: ({ toolGroupId, colormap }) => { + const toolGroup = ToolGroupService.getToolGroup(toolGroupId); + + const ptDisplaySet = actions.getMatchingPTDisplaySet(); + + if (!ptDisplaySet) { + return; + } + + const fusionViewportIds = toolGroup.getViewportIds(); + + let viewports = []; + fusionViewportIds.forEach(viewportId => { + const viewportInfo = Cornerstone3DViewportService.getViewportInfo( + viewportId + ); + + const viewportIndex = viewportInfo.getViewportIndex(); + commandsManager.runCommand('setViewportColormap', { + viewportIndex, + displaySetInstanceUID: ptDisplaySet.displaySetInstanceUID, + colormap, + }); + + viewports.push( + Cornerstone3DViewportService.getCornerstone3DViewport(viewportId) + ); + }); + + viewports.forEach(viewport => { + viewport.render(); + }); + }, + }; + + const definitions = { + setEndSliceForROIThresholdTool: { + commandFn: actions.setEndSliceForROIThresholdTool, + storeContexts: [], + options: {}, + }, + setStartSliceForROIThresholdTool: { + commandFn: actions.setStartSliceForROIThresholdTool, + storeContexts: [], + options: {}, + }, + getMatchingPTDisplaySet: { + commandFn: actions.getMatchingPTDisplaySet, + storeContexts: [], + options: {}, + }, + getPTMetadata: { + commandFn: actions.getPTMetadata, + storeContexts: [], + options: {}, + }, + createNewLabelmapFromPT: { + commandFn: actions.createNewLabelmapFromPT, + storeContexts: [], + options: {}, + }, + setSegmentationActiveForToolGroups: { + commandFn: actions.setSegmentationActiveForToolGroups, + storeContexts: [], + options: {}, + }, + thresholdSegmentationByRectangleROITool: { + commandFn: actions.thresholdSegmentationByRectangleROITool, + storeContexts: [], + options: {}, + }, + toggleSegmentationVisibility: { + commandFn: actions.toggleSegmentationVisibility, + storeContexts: [], + options: {}, + }, + getTotalLesionGlycolysis: { + commandFn: actions.getTotalLesionGlycolysis, + storeContexts: [], + options: {}, + }, + calculateSuvPeak: { + commandFn: actions.calculateSuvPeak, + storeContexts: [], + options: {}, + }, + getLesionStats: { + commandFn: actions.getLesionStats, + storeContexts: [], + options: {}, + }, + calculateTMTV: { + commandFn: actions.calculateTMTV, + storeContexts: [], + options: {}, + }, + exportTMTVReportCSV: { + commandFn: actions.exportTMTVReportCSV, + storeContexts: [], + options: {}, + }, + createTMTVRTReport: { + commandFn: actions.createTMTVRTReport, + storeContexts: [], + options: {}, + }, + getSegmentationCSVReport: { + commandFn: actions.getSegmentationCSVReport, + storeContexts: [], + options: {}, + }, + exportRTReportForAnnotations: { + commandFn: actions.exportRTReportForAnnotations, + storeContexts: [], + options: {}, + }, + setFusionPTColormap: { + commandFn: actions.setFusionPTColormap, + storeContexts: [], + options: {}, + }, + }; + + return { + actions, + definitions, + defaultContext: 'TMTV:CORNERSTONE3D', + }; +}; + +export default commandsModule; diff --git a/extensions/tmtv/src/getHangingProtocolModule.js b/extensions/tmtv/src/getHangingProtocolModule.js new file mode 100644 index 00000000000..ba36b99777d --- /dev/null +++ b/extensions/tmtv/src/getHangingProtocolModule.js @@ -0,0 +1,601 @@ +const ptCT = { + id: 'test', + locked: true, + hasUpdatedPriorsInformation: false, + name: 'Default', + createdDate: '2021-02-23T19:22:08.894Z', + modifiedDate: '2021-02-23T19:22:08.894Z', + availableTo: {}, + editableBy: {}, + toolGroupIds: [ + 'ctToolGroup', + 'ptToolGroup', + 'fusionToolGroup', + 'mipToolGroup', + ], + imageLoadStrategy: 'interleaveTopToBottom', // "default" , "interleaveTopToBottom", "interleaveCenter" + protocolMatchingRules: [ + { + id: 'wauZK2QNEfDPwcAQo', + weight: 1, + attribute: 'StudyInstanceUID', + constraint: { + contains: { + value: '1.3.6.1.4.', + }, + }, + required: false, + }, + { + id: 'wauZK2QNEfDPwcAQo', + weight: 1, + attribute: 'StudyDescription', + constraint: { + contains: { + value: 'PETCT', + }, + }, + required: false, + }, + { + id: 'wauZK2QNEfDPwcAQo', + weight: 1, + attribute: 'StudyDescription', + constraint: { + contains: { + value: 'PET/CT', + }, + }, + required: false, + }, + ], + stages: [ + { + id: 'hYbmMy3b7pz7GLiaT', + name: 'default', + 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, + }, + { + x: 2 / 4, + y: 0, + width: 1 / 4, + height: 1 / 3, + }, + { + x: 0, + y: 1 / 3, + width: 1 / 4, + height: 1 / 3, + }, + { + x: 1 / 4, + y: 1 / 3, + width: 1 / 4, + height: 1 / 3, + }, + { + x: 2 / 4, + y: 1 / 3, + width: 1 / 4, + height: 1 / 3, + }, + { + x: 0, + y: 2 / 3, + width: 1 / 4, + height: 1 / 3, + }, + { + x: 1 / 4, + y: 2 / 3, + width: 1 / 4, + height: 1 / 3, + }, + { + x: 2 / 4, + y: 2 / 3, + width: 1 / 4, + height: 1 / 3, + }, + { + x: 3 / 4, + y: 0, + width: 1 / 4, + height: 1, + }, + ], + }, + }, + displaySets: [ + { + id: 'ctDisplaySet', + imageMatchingRules: [], + seriesMatchingRules: [ + { + id: 'GPEYqFLv2dwzCM322', + weight: 1, + attribute: 'Modality', + constraint: { + equals: { + value: 'CT', + }, + }, + required: true, + }, + { + id: 'vSjk7NCYjtdS3XZAw', + weight: 1, + attribute: 'SeriesNumber', + constraint: { + equals: { + value: '4', + }, + }, + required: false, + }, + { + id: 'vSjk7NCYjtdS3XZAw', + weight: 1, + attribute: 'SeriesDescription', + constraint: { + contains: { + value: 'CT', + }, + }, + required: false, + }, + { + id: 'vSjk7NCYjtdS3XZAw', + weight: 1, + attribute: 'SeriesDescription', + constraint: { + contains: { + value: 'CT WB', + }, + }, + required: false, + }, + ], + studyMatchingRules: [], + }, + { + id: 'ptDisplaySet', + imageMatchingRules: [], + seriesMatchingRules: [ + { + id: 'GPEYqFLv2dwzCM322', + weight: 1, + attribute: 'Modality', + constraint: { + equals: { + value: 'PT', + }, + }, + required: true, + }, + { + id: 'GPEYqFLv2dwzCM322', + weight: 1, + attribute: 'SeriesDescription', + constraint: { + contains: { + value: 'Corrected', + }, + }, + required: false, + }, + { + id: 'GPEYqFLv2dwzCM322', + weight: 2, + attribute: 'SeriesDescription', + constraint: { + doesNotContain: { + value: 'Uncorrected', + }, + }, + required: false, + }, + ], + studyMatchingRules: [], + }, + ], + viewports: [ + { + viewportOptions: { + viewportId: 'ctAXIAL', + viewportType: 'volume', + orientation: 'axial', + toolGroupId: 'ctToolGroup', + initialImageOptions: { + // index: 5, + preset: 'first', // 'first', 'last', 'middle' + }, + syncGroups: [ + { + type: 'cameraPosition', + id: 'axialSync', + source: true, + target: true, + }, + { + type: 'voi', + id: 'ctWLSync', + source: true, + target: true, + }, + ], + }, + displaySets: [ + { + id: 'ctDisplaySet', + }, + ], + }, + { + viewportOptions: { + viewportId: 'ctSAGITTAL', + viewportType: 'volume', + orientation: 'sagittal', + toolGroupId: 'ctToolGroup', + syncGroups: [ + { + type: 'cameraPosition', + id: 'sagittalSync', + source: true, + target: true, + }, + { + type: 'voi', + id: 'ctWLSync', + source: true, + target: true, + }, + ], + }, + displaySets: [ + { + id: 'ctDisplaySet', + }, + ], + }, + { + viewportOptions: { + viewportId: 'ctCORONAL', + viewportType: 'volume', + orientation: 'coronal', + toolGroupId: 'ctToolGroup', + syncGroups: [ + { + type: 'cameraPosition', + id: 'coronalSync', + source: true, + target: true, + }, + { + type: 'voi', + id: 'ctWLSync', + source: true, + target: true, + }, + ], + }, + displaySets: [ + { + id: 'ctDisplaySet', + }, + ], + }, + { + viewportOptions: { + viewportId: 'ptAXIAL', + viewportType: 'volume', + background: [1, 1, 1], + orientation: 'axial', + toolGroupId: 'ptToolGroup', + initialImageOptions: { + // index: 5, + preset: 'first', // 'first', 'last', 'middle' + }, + syncGroups: [ + { + type: 'cameraPosition', + id: 'axialSync', + source: true, + target: true, + }, + { + type: 'voi', + id: 'ptWLSync', + source: true, + target: true, + }, + ], + }, + displaySets: [ + { + options: { + voi: { + windowWidth: 5, + windowCenter: 2.5, + }, + voiInverted: true, + }, + id: 'ptDisplaySet', + }, + ], + }, + { + viewportOptions: { + viewportId: 'ptSAGITTAL', + viewportType: 'volume', + orientation: 'sagittal', + background: [1, 1, 1], + toolGroupId: 'ptToolGroup', + syncGroups: [ + { + type: 'cameraPosition', + id: 'sagittalSync', + source: true, + target: true, + }, + { + type: 'voi', + id: 'ptWLSync', + source: true, + target: true, + }, + ], + }, + displaySets: [ + { + options: { + voi: { + windowWidth: 5, + windowCenter: 2.5, + }, + voiInverted: true, + }, + id: 'ptDisplaySet', + }, + ], + }, + { + viewportOptions: { + viewportId: 'ptCORONAL', + viewportType: 'volume', + orientation: 'coronal', + background: [1, 1, 1], + toolGroupId: 'ptToolGroup', + syncGroups: [ + { + type: 'cameraPosition', + id: 'coronalSync', + source: true, + target: true, + }, + { + type: 'voi', + id: 'ptWLSync', + source: true, + target: true, + }, + ], + }, + displaySets: [ + { + options: { + voi: { + windowWidth: 5, + windowCenter: 2.5, + }, + voiInverted: true, + }, + id: 'ptDisplaySet', + }, + ], + }, + { + viewportOptions: { + viewportId: 'fusionAXIAL', + viewportType: 'volume', + orientation: 'axial', + toolGroupId: 'fusionToolGroup', + initialImageOptions: { + // index: 5, + preset: 'first', // 'first', 'last', 'middle' + }, + syncGroups: [ + { + type: 'cameraPosition', + id: 'axialSync', + source: true, + target: true, + }, + { + type: 'voi', + id: 'ctWLSync', + source: false, + target: true, + }, + { + type: 'voi', + id: 'fusionWLSync', + source: true, + target: true, + }, + ], + }, + displaySets: [ + { + id: 'ctDisplaySet', + }, + { + options: { + colormap: 'hsv', + voi: { + windowWidth: 5, + windowCenter: 2.5, + }, + }, + id: 'ptDisplaySet', + }, + ], + }, + { + viewportOptions: { + viewportId: 'fusionSAGITTAL', + viewportType: 'volume', + orientation: 'sagittal', + toolGroupId: 'fusionToolGroup', + // initialImageOptions: { + // index: 180, + // preset: 'middle', // 'first', 'last', 'middle' + // }, + syncGroups: [ + { + type: 'cameraPosition', + id: 'sagittalSync', + source: true, + target: true, + }, + { + type: 'voi', + id: 'ctWLSync', + source: false, + target: true, + }, + { + type: 'voi', + id: 'fusionWLSync', + source: true, + target: true, + }, + ], + }, + displaySets: [ + { + id: 'ctDisplaySet', + }, + { + options: { + colormap: 'hsv', + voi: { + windowWidth: 5, + windowCenter: 2.5, + }, + }, + id: 'ptDisplaySet', + }, + ], + }, + { + viewportOptions: { + viewportId: 'fusionCoronal', + viewportType: 'volume', + orientation: 'coronal', + toolGroupId: 'fusionToolGroup', + // initialImageOptions: { + // index: 180, + // preset: 'middle', // 'first', 'last', 'middle' + // }, + syncGroups: [ + { + type: 'cameraPosition', + id: 'coronalSync', + source: true, + target: true, + }, + { + type: 'voi', + id: 'ctWLSync', + source: false, + target: true, + }, + { + type: 'voi', + id: 'fusionWLSync', + source: true, + target: true, + }, + ], + }, + displaySets: [ + { + id: 'ctDisplaySet', + }, + { + options: { + colormap: 'hsv', + voi: { + windowWidth: 5, + windowCenter: 2.5, + }, + }, + id: 'ptDisplaySet', + }, + ], + }, + { + viewportOptions: { + viewportId: 'mipSagittal', + viewportType: 'volume', + orientation: 'sagittal', + background: [1, 1, 1], + toolGroupId: 'mipToolGroup', + // Custom props can be used to set custom properties which extensions + // can react on. + customViewportOptions: { + // We use viewportDisplay to filter the viewports which are displayed + // in mip and we set the scrollbar according to their rotation index + // in the cornerstone3D extension. + hideOverlays: true, + }, + }, + displaySets: [ + { + options: { + blendMode: 'MIP', + slabThickness: 'fullVolume', + voi: { + windowWidth: 5, + windowCenter: 2.5, + }, + voiInverted: true, + }, + id: 'ptDisplaySet', + }, + ], + }, + ], + createdDate: '2021-02-23T18:32:42.850Z', + }, + ], + numberOfPriorsReferenced: -1, +}; + +function getHangingProtocolModule() { + return [ + { + name: 'ptCT', + protocols: [ptCT], + }, + ]; +} + +export default getHangingProtocolModule; diff --git a/extensions/tmtv/src/getPanelModule.tsx b/extensions/tmtv/src/getPanelModule.tsx new file mode 100644 index 00000000000..3a9dde3db3c --- /dev/null +++ b/extensions/tmtv/src/getPanelModule.tsx @@ -0,0 +1,52 @@ +import React from 'react'; +import { PanelPetSUV, PanelROIThresholdSegmentation } from './Panels'; + +// TODO: +// - No loading UI exists yet +// - cancel promises when component is destroyed +// - show errors in UI for thumbnails if promise fails + +function getPanelModule({ + commandsManager, + extensionManager, + servicesManager, +}) { + const wrappedPanelPetSuv = () => { + return ( + + ); + }; + + const wrappedROIThresholdSeg = () => { + return ( + + ); + }; + + return [ + { + name: 'petSUV', + iconName: 'circled-checkmark', + iconLabel: 'PET SUV', + label: 'PET-SUV', + component: wrappedPanelPetSuv, + }, + { + name: 'ROIThresholdSeg', + iconName: 'circled-checkmark', + iconLabel: 'Threshold Seg', + label: 'Threshold-Seg', + component: wrappedROIThresholdSeg, + }, + ]; +} + +export default getPanelModule; diff --git a/extensions/tmtv/src/id.js b/extensions/tmtv/src/id.js new file mode 100644 index 00000000000..ebe5acd98ae --- /dev/null +++ b/extensions/tmtv/src/id.js @@ -0,0 +1,5 @@ +import packageJson from '../package.json'; + +const id = packageJson.name; + +export { id }; diff --git a/extensions/tmtv/src/index.tsx b/extensions/tmtv/src/index.tsx new file mode 100644 index 00000000000..26628b505c9 --- /dev/null +++ b/extensions/tmtv/src/index.tsx @@ -0,0 +1,34 @@ +import { id } from './id'; +import getHangingProtocolModule from './getHangingProtocolModule'; +import getPanelModule from './getPanelModule'; +import init from './init'; +import commandsModule from './commandsModule'; + +/** + * + */ +const tmtvExtension = { + /** + * Only required property. Should be a unique value across all extensions. + */ + id, + preRegistration({ + servicesManager, + commandsManager, + extensionManager, + configuration = {}, + }) { + init({ servicesManager, commandsManager, extensionManager, configuration }); + }, + getPanelModule, + getHangingProtocolModule, + getCommandsModule({ servicesManager, commandsManager, extensionManager }) { + return commandsModule({ + servicesManager, + commandsManager, + extensionManager, + }); + }, +}; + +export default tmtvExtension; diff --git a/extensions/tmtv/src/init.js b/extensions/tmtv/src/init.js new file mode 100644 index 00000000000..cf362292306 --- /dev/null +++ b/extensions/tmtv/src/init.js @@ -0,0 +1,55 @@ +import * as cornerstone3DTools from '@cornerstonejs/tools'; + +import measurementServiceMappingsFactory from './utils/measurementServiceMappings/measurementServiceMappingsFactory'; +import colormaps from './utils/colormaps'; + +const CORNERSTONE_3D_TOOLS_SOURCE_NAME = 'Cornerstone3DTools'; +const CORNERSTONE_3D_TOOLS_SOURCE_VERSION = '0.1'; +/** + * + * @param {Object} servicesManager + * @param {Object} configuration + * @param {Object|Array} configuration.csToolsConfig + */ +export default function init({ servicesManager, extensionManager }) { + const { + MeasurementService, + DisplaySetService, + Cornerstone3DViewportService, + } = servicesManager.services; + + cornerstone3DTools.addTool( + cornerstone3DTools.RectangleROIStartEndThresholdTool + ); + + const { RectangleROIStartEndThreshold } = measurementServiceMappingsFactory( + MeasurementService, + DisplaySetService, + Cornerstone3DViewportService + ); + + const csTools3DVer1MeasurementSource = MeasurementService.getSource( + CORNERSTONE_3D_TOOLS_SOURCE_NAME, + CORNERSTONE_3D_TOOLS_SOURCE_VERSION + ); + + MeasurementService.addMapping( + csTools3DVer1MeasurementSource, + 'RectangleROIStartEndThreshold', + RectangleROIStartEndThreshold.matchingCriteria, + RectangleROIStartEndThreshold.toAnnotation, + RectangleROIStartEndThreshold.toMeasurement + ); + + initColormaps(extensionManager); +} + +function initColormaps(extensionManager) { + const utilityModule = extensionManager.getModuleEntry( + '@ohif/extension-cornerstone-3d.utilityModule.common' + ); + + const { registerColormap } = utilityModule.exports; + + colormaps.forEach(registerColormap); +} diff --git a/extensions/tmtv/src/utils/calculateSUVPeak.ts b/extensions/tmtv/src/utils/calculateSUVPeak.ts new file mode 100644 index 00000000000..982c4a18e4f --- /dev/null +++ b/extensions/tmtv/src/utils/calculateSUVPeak.ts @@ -0,0 +1,155 @@ +import { Types } from '@cornerstonejs/core'; +import { utilities } from '@cornerstonejs/tools'; +import { vec3 } from 'gl-matrix'; + +type AnnotationsForThresholding = { + data: { + handles: { + points: Types.Point3[]; + }; + cachedStats?: { + projectionPoints?: Types.Point3[][]; + }; + }; +}; + +/** + * This method calculates the SUV peak on a segmented ROI from a reference PET + * volume. If a rectangle annotation is provided, the peak is calculated within that + * rectangle. Otherwise, the calculation is performed on the entire volume which + * will be slower but same result. + * @param viewport Viewport to use for the calculation + * @param labelmap Labelmap from which the mask is taken + * @param referenceVolume PET volume to use for SUV calculation + * @param toolData [Optional] list of toolData to use for SUV calculation + * @param segmentIndex The index of the segment to use for masking + * @returns + */ +function calculateSuvPeak( + labelmap: Types.IImageVolume, + referenceVolume: Types.IImageVolume, + annotations?: AnnotationsForThresholding[], + segmentIndex = 1 +): { + max: number; + maxIJK: Types.Point3; + maxLPS: Types.Point3; + mean: number; +} { + if (referenceVolume.metadata.Modality !== 'PT') { + return; + } + + if (labelmap.scalarData.length !== referenceVolume.scalarData.length) { + throw new Error( + 'labelmap and referenceVolume must have the same number of pixels' + ); + } + + const { + scalarData: labelmapData, + dimensions, + imageData: labelmapImageData, + } = labelmap; + + const { + scalarData: referenceVolumeData, + imageData: referenceVolumeImageData, + } = referenceVolume; + + let boundsIJK; + // Todo: using the first annotation for now + if (annotations && annotations[0].data?.cachedStats) { + const { projectionPoints } = annotations[0].data.cachedStats; + const pointsToUse = [].concat(...projectionPoints); // cannot use flat() because of typescript compiler right now + + const rectangleCornersIJK = pointsToUse.map(world => { + const ijk = vec3.fromValues(0, 0, 0); + referenceVolumeImageData.worldToIndex(world, ijk); + return ijk as Types.Point3; + }); + + boundsIJK = utilities.boundingBox.getBoundingBoxAroundShape( + rectangleCornersIJK, + dimensions + ); + } + + let max = 0; + let maxIJK = [0, 0, 0]; + let maxLPS = [0, 0, 0]; + + const callback = ({ pointIJK, pointLPS }) => { + const offset = referenceVolumeImageData.computeOffsetIndex(pointIJK); + const value = labelmapData[offset]; + + if (value !== segmentIndex) { + return; + } + + const referenceValue = referenceVolumeData[offset]; + + if (referenceValue > max) { + max = referenceValue; + maxIJK = pointIJK; + maxLPS = pointLPS; + } + }; + + utilities.pointInShapeCallback( + labelmapImageData, + () => true, + callback, + boundsIJK + ); + + const direction = labelmapImageData + .getDirection() + .slice(0, 3) as Types.Point3; + + /** + * 2. Find the bottom and top of the great circle for the second sphere (1cc sphere) + * V = (4/3)Ï€r3 + */ + const radius = Math.pow(1 / ((4 / 3) * Math.PI), 1 / 3) * 10; + const diameter = radius * 2; + + const secondaryCircleWorld = vec3.create(); + const bottomWorld = vec3.create(); + const topWorld = vec3.create(); + referenceVolumeImageData.indexToWorld(maxIJK, secondaryCircleWorld); + vec3.scaleAndAdd(bottomWorld, secondaryCircleWorld, direction, -diameter / 2); + vec3.scaleAndAdd(topWorld, secondaryCircleWorld, direction, diameter / 2); + const suvPeakCirclePoints = [bottomWorld, topWorld] as [ + Types.Point3, + Types.Point3 + ]; + + /** + * 3. Find the Mean and Max of the 1cc sphere centered on the suv Max of the previous + * sphere + */ + let count = 0; + let acc = 0; + const suvPeakMeanCallback = ({ value }) => { + acc += value; + count += 1; + }; + + utilities.pointInSurroundingSphereCallback( + referenceVolumeImageData, + suvPeakCirclePoints, + suvPeakMeanCallback + ); + + const mean = acc / count; + + return { + max, + maxIJK, + maxLPS, + mean, + }; +} + +export default calculateSuvPeak; diff --git a/extensions/tmtv/src/utils/calculateTMTV.ts b/extensions/tmtv/src/utils/calculateTMTV.ts new file mode 100644 index 00000000000..f09375c346e --- /dev/null +++ b/extensions/tmtv/src/utils/calculateTMTV.ts @@ -0,0 +1,44 @@ +import { Types } from '@cornerstonejs/core'; +import { utilities } from '@cornerstonejs/tools'; + +/** + * Given a list of labelmaps (with the possibility of overlapping regions), + * and a referenceVolume, it calculates the total metabolic tumor volume (TMTV) + * by flattening and rasterizing each segment into a single labelmap and summing + * the total number of volume voxels. It should be noted that for this calculation + * we do not double count voxels that are part of multiple labelmaps. + * @param {} labelmaps + * @param {number} segmentIndex + * @returns {number} TMTV in ml + */ +function calculateTMTV( + labelmaps: Array, + segmentIndex = 1 +): number { + const volumeId = 'mergedLabelmap'; + + const mergedLabelmap = utilities.segmentation.createMergedLabelmapForIndex( + labelmaps, + segmentIndex, + volumeId + ); + + const { imageData, spacing } = mergedLabelmap; + const values = imageData + .getPointData() + .getScalars() + .getData(); + + // count non-zero values inside the outputData, this would + // consider the overlapping regions to be only counted once + const numVoxels = values.reduce((acc, curr) => { + if (curr > 0) { + return acc + 1; + } + return acc; + }, 0); + + return 1e-3 * numVoxels * spacing[0] * spacing[1] * spacing[2]; +} + +export default calculateTMTV; diff --git a/extensions/tmtv/src/utils/colormaps/index.js b/extensions/tmtv/src/utils/colormaps/index.js new file mode 100644 index 00000000000..34761d25963 --- /dev/null +++ b/extensions/tmtv/src/utils/colormaps/index.js @@ -0,0 +1,9272 @@ +export default [ + { + ColorSpace: 'RGB', + Name: 'hot_iron', + RGBPoints: [ + 0.0, + 0.0039215686, + 0.0039215686, + 0.0156862745, + 0.00392156862745098, + 0.0039215686, + 0.0039215686, + 0.0156862745, + 0.00784313725490196, + 0.0039215686, + 0.0039215686, + 0.031372549, + 0.011764705882352941, + 0.0039215686, + 0.0039215686, + 0.0470588235, + 0.01568627450980392, + 0.0039215686, + 0.0039215686, + 0.062745098, + 0.0196078431372549, + 0.0039215686, + 0.0039215686, + 0.0784313725, + 0.023529411764705882, + 0.0039215686, + 0.0039215686, + 0.0941176471, + 0.027450980392156862, + 0.0039215686, + 0.0039215686, + 0.1098039216, + 0.03137254901960784, + 0.0039215686, + 0.0039215686, + 0.1254901961, + 0.03529411764705882, + 0.0039215686, + 0.0039215686, + 0.1411764706, + 0.0392156862745098, + 0.0039215686, + 0.0039215686, + 0.1568627451, + 0.043137254901960784, + 0.0039215686, + 0.0039215686, + 0.1725490196, + 0.047058823529411764, + 0.0039215686, + 0.0039215686, + 0.1882352941, + 0.050980392156862744, + 0.0039215686, + 0.0039215686, + 0.2039215686, + 0.054901960784313725, + 0.0039215686, + 0.0039215686, + 0.2196078431, + 0.05882352941176471, + 0.0039215686, + 0.0039215686, + 0.2352941176, + 0.06274509803921569, + 0.0039215686, + 0.0039215686, + 0.2509803922, + 0.06666666666666667, + 0.0039215686, + 0.0039215686, + 0.262745098, + 0.07058823529411765, + 0.0039215686, + 0.0039215686, + 0.2784313725, + 0.07450980392156863, + 0.0039215686, + 0.0039215686, + 0.2941176471, + 0.0784313725490196, + 0.0039215686, + 0.0039215686, + 0.3098039216, + 0.08235294117647059, + 0.0039215686, + 0.0039215686, + 0.3254901961, + 0.08627450980392157, + 0.0039215686, + 0.0039215686, + 0.3411764706, + 0.09019607843137255, + 0.0039215686, + 0.0039215686, + 0.3568627451, + 0.09411764705882353, + 0.0039215686, + 0.0039215686, + 0.3725490196, + 0.09803921568627451, + 0.0039215686, + 0.0039215686, + 0.3882352941, + 0.10196078431372549, + 0.0039215686, + 0.0039215686, + 0.4039215686, + 0.10588235294117647, + 0.0039215686, + 0.0039215686, + 0.4196078431, + 0.10980392156862745, + 0.0039215686, + 0.0039215686, + 0.4352941176, + 0.11372549019607843, + 0.0039215686, + 0.0039215686, + 0.4509803922, + 0.11764705882352942, + 0.0039215686, + 0.0039215686, + 0.4666666667, + 0.12156862745098039, + 0.0039215686, + 0.0039215686, + 0.4823529412, + 0.12549019607843137, + 0.0039215686, + 0.0039215686, + 0.4980392157, + 0.12941176470588237, + 0.0039215686, + 0.0039215686, + 0.5137254902, + 0.13333333333333333, + 0.0039215686, + 0.0039215686, + 0.5294117647, + 0.13725490196078433, + 0.0039215686, + 0.0039215686, + 0.5450980392, + 0.1411764705882353, + 0.0039215686, + 0.0039215686, + 0.5607843137, + 0.1450980392156863, + 0.0039215686, + 0.0039215686, + 0.5764705882, + 0.14901960784313725, + 0.0039215686, + 0.0039215686, + 0.5921568627, + 0.15294117647058825, + 0.0039215686, + 0.0039215686, + 0.6078431373, + 0.1568627450980392, + 0.0039215686, + 0.0039215686, + 0.6235294118, + 0.1607843137254902, + 0.0039215686, + 0.0039215686, + 0.6392156863, + 0.16470588235294117, + 0.0039215686, + 0.0039215686, + 0.6549019608, + 0.16862745098039217, + 0.0039215686, + 0.0039215686, + 0.6705882353, + 0.17254901960784313, + 0.0039215686, + 0.0039215686, + 0.6862745098, + 0.17647058823529413, + 0.0039215686, + 0.0039215686, + 0.7019607843, + 0.1803921568627451, + 0.0039215686, + 0.0039215686, + 0.7176470588, + 0.1843137254901961, + 0.0039215686, + 0.0039215686, + 0.7333333333, + 0.18823529411764706, + 0.0039215686, + 0.0039215686, + 0.7490196078, + 0.19215686274509805, + 0.0039215686, + 0.0039215686, + 0.7607843137, + 0.19607843137254902, + 0.0039215686, + 0.0039215686, + 0.7764705882, + 0.2, + 0.0039215686, + 0.0039215686, + 0.7921568627, + 0.20392156862745098, + 0.0039215686, + 0.0039215686, + 0.8078431373, + 0.20784313725490197, + 0.0039215686, + 0.0039215686, + 0.8235294118, + 0.21176470588235294, + 0.0039215686, + 0.0039215686, + 0.8392156863, + 0.21568627450980393, + 0.0039215686, + 0.0039215686, + 0.8549019608, + 0.2196078431372549, + 0.0039215686, + 0.0039215686, + 0.8705882353, + 0.2235294117647059, + 0.0039215686, + 0.0039215686, + 0.8862745098, + 0.22745098039215686, + 0.0039215686, + 0.0039215686, + 0.9019607843, + 0.23137254901960785, + 0.0039215686, + 0.0039215686, + 0.9176470588, + 0.23529411764705885, + 0.0039215686, + 0.0039215686, + 0.9333333333, + 0.23921568627450984, + 0.0039215686, + 0.0039215686, + 0.9490196078, + 0.24313725490196078, + 0.0039215686, + 0.0039215686, + 0.9647058824, + 0.24705882352941178, + 0.0039215686, + 0.0039215686, + 0.9803921569, + 0.25098039215686274, + 0.0039215686, + 0.0039215686, + 0.9960784314, + 0.2549019607843137, + 0.0039215686, + 0.0039215686, + 0.9960784314, + 0.25882352941176473, + 0.0156862745, + 0.0039215686, + 0.9803921569, + 0.2627450980392157, + 0.031372549, + 0.0039215686, + 0.9647058824, + 0.26666666666666666, + 0.0470588235, + 0.0039215686, + 0.9490196078, + 0.27058823529411763, + 0.062745098, + 0.0039215686, + 0.9333333333, + 0.27450980392156865, + 0.0784313725, + 0.0039215686, + 0.9176470588, + 0.2784313725490196, + 0.0941176471, + 0.0039215686, + 0.9019607843, + 0.2823529411764706, + 0.1098039216, + 0.0039215686, + 0.8862745098, + 0.28627450980392155, + 0.1254901961, + 0.0039215686, + 0.8705882353, + 0.2901960784313726, + 0.1411764706, + 0.0039215686, + 0.8549019608, + 0.29411764705882354, + 0.1568627451, + 0.0039215686, + 0.8392156863, + 0.2980392156862745, + 0.1725490196, + 0.0039215686, + 0.8235294118, + 0.30196078431372547, + 0.1882352941, + 0.0039215686, + 0.8078431373, + 0.3058823529411765, + 0.2039215686, + 0.0039215686, + 0.7921568627, + 0.30980392156862746, + 0.2196078431, + 0.0039215686, + 0.7764705882, + 0.3137254901960784, + 0.2352941176, + 0.0039215686, + 0.7607843137, + 0.3176470588235294, + 0.2509803922, + 0.0039215686, + 0.7490196078, + 0.3215686274509804, + 0.262745098, + 0.0039215686, + 0.7333333333, + 0.3254901960784314, + 0.2784313725, + 0.0039215686, + 0.7176470588, + 0.32941176470588235, + 0.2941176471, + 0.0039215686, + 0.7019607843, + 0.3333333333333333, + 0.3098039216, + 0.0039215686, + 0.6862745098, + 0.33725490196078434, + 0.3254901961, + 0.0039215686, + 0.6705882353, + 0.3411764705882353, + 0.3411764706, + 0.0039215686, + 0.6549019608, + 0.34509803921568627, + 0.3568627451, + 0.0039215686, + 0.6392156863, + 0.34901960784313724, + 0.3725490196, + 0.0039215686, + 0.6235294118, + 0.35294117647058826, + 0.3882352941, + 0.0039215686, + 0.6078431373, + 0.3568627450980392, + 0.4039215686, + 0.0039215686, + 0.5921568627, + 0.3607843137254902, + 0.4196078431, + 0.0039215686, + 0.5764705882, + 0.36470588235294116, + 0.4352941176, + 0.0039215686, + 0.5607843137, + 0.3686274509803922, + 0.4509803922, + 0.0039215686, + 0.5450980392, + 0.37254901960784315, + 0.4666666667, + 0.0039215686, + 0.5294117647, + 0.3764705882352941, + 0.4823529412, + 0.0039215686, + 0.5137254902, + 0.3803921568627451, + 0.4980392157, + 0.0039215686, + 0.4980392157, + 0.3843137254901961, + 0.5137254902, + 0.0039215686, + 0.4823529412, + 0.38823529411764707, + 0.5294117647, + 0.0039215686, + 0.4666666667, + 0.39215686274509803, + 0.5450980392, + 0.0039215686, + 0.4509803922, + 0.396078431372549, + 0.5607843137, + 0.0039215686, + 0.4352941176, + 0.4, + 0.5764705882, + 0.0039215686, + 0.4196078431, + 0.403921568627451, + 0.5921568627, + 0.0039215686, + 0.4039215686, + 0.40784313725490196, + 0.6078431373, + 0.0039215686, + 0.3882352941, + 0.4117647058823529, + 0.6235294118, + 0.0039215686, + 0.3725490196, + 0.41568627450980394, + 0.6392156863, + 0.0039215686, + 0.3568627451, + 0.4196078431372549, + 0.6549019608, + 0.0039215686, + 0.3411764706, + 0.4235294117647059, + 0.6705882353, + 0.0039215686, + 0.3254901961, + 0.42745098039215684, + 0.6862745098, + 0.0039215686, + 0.3098039216, + 0.43137254901960786, + 0.7019607843, + 0.0039215686, + 0.2941176471, + 0.43529411764705883, + 0.7176470588, + 0.0039215686, + 0.2784313725, + 0.4392156862745098, + 0.7333333333, + 0.0039215686, + 0.262745098, + 0.44313725490196076, + 0.7490196078, + 0.0039215686, + 0.2509803922, + 0.4470588235294118, + 0.7607843137, + 0.0039215686, + 0.2352941176, + 0.45098039215686275, + 0.7764705882, + 0.0039215686, + 0.2196078431, + 0.4549019607843137, + 0.7921568627, + 0.0039215686, + 0.2039215686, + 0.4588235294117647, + 0.8078431373, + 0.0039215686, + 0.1882352941, + 0.4627450980392157, + 0.8235294118, + 0.0039215686, + 0.1725490196, + 0.4666666666666667, + 0.8392156863, + 0.0039215686, + 0.1568627451, + 0.4705882352941177, + 0.8549019608, + 0.0039215686, + 0.1411764706, + 0.4745098039215686, + 0.8705882353, + 0.0039215686, + 0.1254901961, + 0.4784313725490197, + 0.8862745098, + 0.0039215686, + 0.1098039216, + 0.48235294117647065, + 0.9019607843, + 0.0039215686, + 0.0941176471, + 0.48627450980392156, + 0.9176470588, + 0.0039215686, + 0.0784313725, + 0.49019607843137253, + 0.9333333333, + 0.0039215686, + 0.062745098, + 0.49411764705882355, + 0.9490196078, + 0.0039215686, + 0.0470588235, + 0.4980392156862745, + 0.9647058824, + 0.0039215686, + 0.031372549, + 0.5019607843137255, + 0.9803921569, + 0.0039215686, + 0.0156862745, + 0.5058823529411764, + 0.9960784314, + 0.0039215686, + 0.0039215686, + 0.5098039215686274, + 0.9960784314, + 0.0156862745, + 0.0039215686, + 0.5137254901960784, + 0.9960784314, + 0.031372549, + 0.0039215686, + 0.5176470588235295, + 0.9960784314, + 0.0470588235, + 0.0039215686, + 0.5215686274509804, + 0.9960784314, + 0.062745098, + 0.0039215686, + 0.5254901960784314, + 0.9960784314, + 0.0784313725, + 0.0039215686, + 0.5294117647058824, + 0.9960784314, + 0.0941176471, + 0.0039215686, + 0.5333333333333333, + 0.9960784314, + 0.1098039216, + 0.0039215686, + 0.5372549019607843, + 0.9960784314, + 0.1254901961, + 0.0039215686, + 0.5411764705882353, + 0.9960784314, + 0.1411764706, + 0.0039215686, + 0.5450980392156862, + 0.9960784314, + 0.1568627451, + 0.0039215686, + 0.5490196078431373, + 0.9960784314, + 0.1725490196, + 0.0039215686, + 0.5529411764705883, + 0.9960784314, + 0.1882352941, + 0.0039215686, + 0.5568627450980392, + 0.9960784314, + 0.2039215686, + 0.0039215686, + 0.5607843137254902, + 0.9960784314, + 0.2196078431, + 0.0039215686, + 0.5647058823529412, + 0.9960784314, + 0.2352941176, + 0.0039215686, + 0.5686274509803921, + 0.9960784314, + 0.2509803922, + 0.0039215686, + 0.5725490196078431, + 0.9960784314, + 0.262745098, + 0.0039215686, + 0.5764705882352941, + 0.9960784314, + 0.2784313725, + 0.0039215686, + 0.5803921568627451, + 0.9960784314, + 0.2941176471, + 0.0039215686, + 0.5843137254901961, + 0.9960784314, + 0.3098039216, + 0.0039215686, + 0.5882352941176471, + 0.9960784314, + 0.3254901961, + 0.0039215686, + 0.592156862745098, + 0.9960784314, + 0.3411764706, + 0.0039215686, + 0.596078431372549, + 0.9960784314, + 0.3568627451, + 0.0039215686, + 0.6, + 0.9960784314, + 0.3725490196, + 0.0039215686, + 0.6039215686274509, + 0.9960784314, + 0.3882352941, + 0.0039215686, + 0.6078431372549019, + 0.9960784314, + 0.4039215686, + 0.0039215686, + 0.611764705882353, + 0.9960784314, + 0.4196078431, + 0.0039215686, + 0.615686274509804, + 0.9960784314, + 0.4352941176, + 0.0039215686, + 0.6196078431372549, + 0.9960784314, + 0.4509803922, + 0.0039215686, + 0.6235294117647059, + 0.9960784314, + 0.4666666667, + 0.0039215686, + 0.6274509803921569, + 0.9960784314, + 0.4823529412, + 0.0039215686, + 0.6313725490196078, + 0.9960784314, + 0.4980392157, + 0.0039215686, + 0.6352941176470588, + 0.9960784314, + 0.5137254902, + 0.0039215686, + 0.6392156862745098, + 0.9960784314, + 0.5294117647, + 0.0039215686, + 0.6431372549019608, + 0.9960784314, + 0.5450980392, + 0.0039215686, + 0.6470588235294118, + 0.9960784314, + 0.5607843137, + 0.0039215686, + 0.6509803921568628, + 0.9960784314, + 0.5764705882, + 0.0039215686, + 0.6549019607843137, + 0.9960784314, + 0.5921568627, + 0.0039215686, + 0.6588235294117647, + 0.9960784314, + 0.6078431373, + 0.0039215686, + 0.6627450980392157, + 0.9960784314, + 0.6235294118, + 0.0039215686, + 0.6666666666666666, + 0.9960784314, + 0.6392156863, + 0.0039215686, + 0.6705882352941176, + 0.9960784314, + 0.6549019608, + 0.0039215686, + 0.6745098039215687, + 0.9960784314, + 0.6705882353, + 0.0039215686, + 0.6784313725490196, + 0.9960784314, + 0.6862745098, + 0.0039215686, + 0.6823529411764706, + 0.9960784314, + 0.7019607843, + 0.0039215686, + 0.6862745098039216, + 0.9960784314, + 0.7176470588, + 0.0039215686, + 0.6901960784313725, + 0.9960784314, + 0.7333333333, + 0.0039215686, + 0.6941176470588235, + 0.9960784314, + 0.7490196078, + 0.0039215686, + 0.6980392156862745, + 0.9960784314, + 0.7607843137, + 0.0039215686, + 0.7019607843137254, + 0.9960784314, + 0.7764705882, + 0.0039215686, + 0.7058823529411765, + 0.9960784314, + 0.7921568627, + 0.0039215686, + 0.7098039215686275, + 0.9960784314, + 0.8078431373, + 0.0039215686, + 0.7137254901960784, + 0.9960784314, + 0.8235294118, + 0.0039215686, + 0.7176470588235294, + 0.9960784314, + 0.8392156863, + 0.0039215686, + 0.7215686274509804, + 0.9960784314, + 0.8549019608, + 0.0039215686, + 0.7254901960784313, + 0.9960784314, + 0.8705882353, + 0.0039215686, + 0.7294117647058823, + 0.9960784314, + 0.8862745098, + 0.0039215686, + 0.7333333333333333, + 0.9960784314, + 0.9019607843, + 0.0039215686, + 0.7372549019607844, + 0.9960784314, + 0.9176470588, + 0.0039215686, + 0.7411764705882353, + 0.9960784314, + 0.9333333333, + 0.0039215686, + 0.7450980392156863, + 0.9960784314, + 0.9490196078, + 0.0039215686, + 0.7490196078431373, + 0.9960784314, + 0.9647058824, + 0.0039215686, + 0.7529411764705882, + 0.9960784314, + 0.9803921569, + 0.0039215686, + 0.7568627450980392, + 0.9960784314, + 0.9960784314, + 0.0039215686, + 0.7607843137254902, + 0.9960784314, + 0.9960784314, + 0.0196078431, + 0.7647058823529411, + 0.9960784314, + 0.9960784314, + 0.0352941176, + 0.7686274509803922, + 0.9960784314, + 0.9960784314, + 0.0509803922, + 0.7725490196078432, + 0.9960784314, + 0.9960784314, + 0.0666666667, + 0.7764705882352941, + 0.9960784314, + 0.9960784314, + 0.0823529412, + 0.7803921568627451, + 0.9960784314, + 0.9960784314, + 0.0980392157, + 0.7843137254901961, + 0.9960784314, + 0.9960784314, + 0.1137254902, + 0.788235294117647, + 0.9960784314, + 0.9960784314, + 0.1294117647, + 0.792156862745098, + 0.9960784314, + 0.9960784314, + 0.1450980392, + 0.796078431372549, + 0.9960784314, + 0.9960784314, + 0.1607843137, + 0.8, + 0.9960784314, + 0.9960784314, + 0.1764705882, + 0.803921568627451, + 0.9960784314, + 0.9960784314, + 0.1921568627, + 0.807843137254902, + 0.9960784314, + 0.9960784314, + 0.2078431373, + 0.8117647058823529, + 0.9960784314, + 0.9960784314, + 0.2235294118, + 0.8156862745098039, + 0.9960784314, + 0.9960784314, + 0.2392156863, + 0.8196078431372549, + 0.9960784314, + 0.9960784314, + 0.2509803922, + 0.8235294117647058, + 0.9960784314, + 0.9960784314, + 0.2666666667, + 0.8274509803921568, + 0.9960784314, + 0.9960784314, + 0.2823529412, + 0.8313725490196079, + 0.9960784314, + 0.9960784314, + 0.2980392157, + 0.8352941176470589, + 0.9960784314, + 0.9960784314, + 0.3137254902, + 0.8392156862745098, + 0.9960784314, + 0.9960784314, + 0.3333333333, + 0.8431372549019608, + 0.9960784314, + 0.9960784314, + 0.3490196078, + 0.8470588235294118, + 0.9960784314, + 0.9960784314, + 0.3647058824, + 0.8509803921568627, + 0.9960784314, + 0.9960784314, + 0.3803921569, + 0.8549019607843137, + 0.9960784314, + 0.9960784314, + 0.3960784314, + 0.8588235294117647, + 0.9960784314, + 0.9960784314, + 0.4117647059, + 0.8627450980392157, + 0.9960784314, + 0.9960784314, + 0.4274509804, + 0.8666666666666667, + 0.9960784314, + 0.9960784314, + 0.4431372549, + 0.8705882352941177, + 0.9960784314, + 0.9960784314, + 0.4588235294, + 0.8745098039215686, + 0.9960784314, + 0.9960784314, + 0.4745098039, + 0.8784313725490196, + 0.9960784314, + 0.9960784314, + 0.4901960784, + 0.8823529411764706, + 0.9960784314, + 0.9960784314, + 0.5058823529, + 0.8862745098039215, + 0.9960784314, + 0.9960784314, + 0.5215686275, + 0.8901960784313725, + 0.9960784314, + 0.9960784314, + 0.537254902, + 0.8941176470588236, + 0.9960784314, + 0.9960784314, + 0.5529411765, + 0.8980392156862745, + 0.9960784314, + 0.9960784314, + 0.568627451, + 0.9019607843137255, + 0.9960784314, + 0.9960784314, + 0.5843137255, + 0.9058823529411765, + 0.9960784314, + 0.9960784314, + 0.6, + 0.9098039215686274, + 0.9960784314, + 0.9960784314, + 0.6156862745, + 0.9137254901960784, + 0.9960784314, + 0.9960784314, + 0.631372549, + 0.9176470588235294, + 0.9960784314, + 0.9960784314, + 0.6470588235, + 0.9215686274509803, + 0.9960784314, + 0.9960784314, + 0.6666666667, + 0.9254901960784314, + 0.9960784314, + 0.9960784314, + 0.6823529412, + 0.9294117647058824, + 0.9960784314, + 0.9960784314, + 0.6980392157, + 0.9333333333333333, + 0.9960784314, + 0.9960784314, + 0.7137254902, + 0.9372549019607843, + 0.9960784314, + 0.9960784314, + 0.7294117647, + 0.9411764705882354, + 0.9960784314, + 0.9960784314, + 0.7450980392, + 0.9450980392156864, + 0.9960784314, + 0.9960784314, + 0.7568627451, + 0.9490196078431372, + 0.9960784314, + 0.9960784314, + 0.7725490196, + 0.9529411764705882, + 0.9960784314, + 0.9960784314, + 0.7882352941, + 0.9568627450980394, + 0.9960784314, + 0.9960784314, + 0.8039215686, + 0.9607843137254903, + 0.9960784314, + 0.9960784314, + 0.8196078431, + 0.9647058823529413, + 0.9960784314, + 0.9960784314, + 0.8352941176, + 0.9686274509803922, + 0.9960784314, + 0.9960784314, + 0.8509803922, + 0.9725490196078431, + 0.9960784314, + 0.9960784314, + 0.8666666667, + 0.9764705882352941, + 0.9960784314, + 0.9960784314, + 0.8823529412, + 0.9803921568627451, + 0.9960784314, + 0.9960784314, + 0.8980392157, + 0.984313725490196, + 0.9960784314, + 0.9960784314, + 0.9137254902, + 0.9882352941176471, + 0.9960784314, + 0.9960784314, + 0.9294117647, + 0.9921568627450981, + 0.9960784314, + 0.9960784314, + 0.9450980392, + 0.996078431372549, + 0.9960784314, + 0.9960784314, + 0.9607843137, + 1.0, + 0.9960784314, + 0.9960784314, + 0.9607843137, + ], + }, + { + ColorSpace: 'RGB', + Name: 'red_hot', + RGBPoints: [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.00392156862745098, + 0.0, + 0.0, + 0.0, + 0.00784313725490196, + 0.0, + 0.0, + 0.0, + 0.011764705882352941, + 0.0, + 0.0, + 0.0, + 0.01568627450980392, + 0.0039215686, + 0.0039215686, + 0.0039215686, + 0.0196078431372549, + 0.0039215686, + 0.0039215686, + 0.0039215686, + 0.023529411764705882, + 0.0039215686, + 0.0039215686, + 0.0039215686, + 0.027450980392156862, + 0.0039215686, + 0.0039215686, + 0.0039215686, + 0.03137254901960784, + 0.0039215686, + 0.0039215686, + 0.0039215686, + 0.03529411764705882, + 0.0156862745, + 0.0, + 0.0, + 0.0392156862745098, + 0.0274509804, + 0.0, + 0.0, + 0.043137254901960784, + 0.0392156863, + 0.0, + 0.0, + 0.047058823529411764, + 0.0509803922, + 0.0, + 0.0, + 0.050980392156862744, + 0.062745098, + 0.0, + 0.0, + 0.054901960784313725, + 0.0784313725, + 0.0, + 0.0, + 0.05882352941176471, + 0.0901960784, + 0.0, + 0.0, + 0.06274509803921569, + 0.1058823529, + 0.0, + 0.0, + 0.06666666666666667, + 0.1176470588, + 0.0, + 0.0, + 0.07058823529411765, + 0.1294117647, + 0.0, + 0.0, + 0.07450980392156863, + 0.1411764706, + 0.0, + 0.0, + 0.0784313725490196, + 0.1529411765, + 0.0, + 0.0, + 0.08235294117647059, + 0.1647058824, + 0.0, + 0.0, + 0.08627450980392157, + 0.1764705882, + 0.0, + 0.0, + 0.09019607843137255, + 0.1882352941, + 0.0, + 0.0, + 0.09411764705882353, + 0.2039215686, + 0.0, + 0.0, + 0.09803921568627451, + 0.2156862745, + 0.0, + 0.0, + 0.10196078431372549, + 0.2274509804, + 0.0, + 0.0, + 0.10588235294117647, + 0.2392156863, + 0.0, + 0.0, + 0.10980392156862745, + 0.2549019608, + 0.0, + 0.0, + 0.11372549019607843, + 0.2666666667, + 0.0, + 0.0, + 0.11764705882352942, + 0.2784313725, + 0.0, + 0.0, + 0.12156862745098039, + 0.2901960784, + 0.0, + 0.0, + 0.12549019607843137, + 0.3058823529, + 0.0, + 0.0, + 0.12941176470588237, + 0.3176470588, + 0.0, + 0.0, + 0.13333333333333333, + 0.3294117647, + 0.0, + 0.0, + 0.13725490196078433, + 0.3411764706, + 0.0, + 0.0, + 0.1411764705882353, + 0.3529411765, + 0.0, + 0.0, + 0.1450980392156863, + 0.3647058824, + 0.0, + 0.0, + 0.14901960784313725, + 0.3764705882, + 0.0, + 0.0, + 0.15294117647058825, + 0.3882352941, + 0.0, + 0.0, + 0.1568627450980392, + 0.4039215686, + 0.0, + 0.0, + 0.1607843137254902, + 0.4156862745, + 0.0, + 0.0, + 0.16470588235294117, + 0.431372549, + 0.0, + 0.0, + 0.16862745098039217, + 0.4431372549, + 0.0, + 0.0, + 0.17254901960784313, + 0.4588235294, + 0.0, + 0.0, + 0.17647058823529413, + 0.4705882353, + 0.0, + 0.0, + 0.1803921568627451, + 0.4823529412, + 0.0, + 0.0, + 0.1843137254901961, + 0.4941176471, + 0.0, + 0.0, + 0.18823529411764706, + 0.5098039216, + 0.0, + 0.0, + 0.19215686274509805, + 0.5215686275, + 0.0, + 0.0, + 0.19607843137254902, + 0.5333333333, + 0.0, + 0.0, + 0.2, + 0.5450980392, + 0.0, + 0.0, + 0.20392156862745098, + 0.5568627451, + 0.0, + 0.0, + 0.20784313725490197, + 0.568627451, + 0.0, + 0.0, + 0.21176470588235294, + 0.5803921569, + 0.0, + 0.0, + 0.21568627450980393, + 0.5921568627, + 0.0, + 0.0, + 0.2196078431372549, + 0.6078431373, + 0.0, + 0.0, + 0.2235294117647059, + 0.6196078431, + 0.0, + 0.0, + 0.22745098039215686, + 0.631372549, + 0.0, + 0.0, + 0.23137254901960785, + 0.6431372549, + 0.0, + 0.0, + 0.23529411764705885, + 0.6588235294, + 0.0, + 0.0, + 0.23921568627450984, + 0.6705882353, + 0.0, + 0.0, + 0.24313725490196078, + 0.6823529412, + 0.0, + 0.0, + 0.24705882352941178, + 0.6941176471, + 0.0, + 0.0, + 0.25098039215686274, + 0.7098039216, + 0.0, + 0.0, + 0.2549019607843137, + 0.7215686275, + 0.0, + 0.0, + 0.25882352941176473, + 0.7333333333, + 0.0, + 0.0, + 0.2627450980392157, + 0.7450980392, + 0.0, + 0.0, + 0.26666666666666666, + 0.7568627451, + 0.0, + 0.0, + 0.27058823529411763, + 0.768627451, + 0.0, + 0.0, + 0.27450980392156865, + 0.7843137255, + 0.0, + 0.0, + 0.2784313725490196, + 0.7960784314, + 0.0, + 0.0, + 0.2823529411764706, + 0.8117647059, + 0.0, + 0.0, + 0.28627450980392155, + 0.8235294118, + 0.0, + 0.0, + 0.2901960784313726, + 0.8352941176, + 0.0, + 0.0, + 0.29411764705882354, + 0.8470588235, + 0.0, + 0.0, + 0.2980392156862745, + 0.862745098, + 0.0, + 0.0, + 0.30196078431372547, + 0.8745098039, + 0.0, + 0.0, + 0.3058823529411765, + 0.8862745098, + 0.0, + 0.0, + 0.30980392156862746, + 0.8980392157, + 0.0, + 0.0, + 0.3137254901960784, + 0.9137254902, + 0.0, + 0.0, + 0.3176470588235294, + 0.9254901961, + 0.0, + 0.0, + 0.3215686274509804, + 0.937254902, + 0.0, + 0.0, + 0.3254901960784314, + 0.9490196078, + 0.0, + 0.0, + 0.32941176470588235, + 0.9607843137, + 0.0, + 0.0, + 0.3333333333333333, + 0.968627451, + 0.0, + 0.0, + 0.33725490196078434, + 0.9803921569, + 0.0039215686, + 0.0, + 0.3411764705882353, + 0.9882352941, + 0.0078431373, + 0.0, + 0.34509803921568627, + 1.0, + 0.0117647059, + 0.0, + 0.34901960784313724, + 1.0, + 0.0235294118, + 0.0, + 0.35294117647058826, + 1.0, + 0.0352941176, + 0.0, + 0.3568627450980392, + 1.0, + 0.0470588235, + 0.0, + 0.3607843137254902, + 1.0, + 0.062745098, + 0.0, + 0.36470588235294116, + 1.0, + 0.0745098039, + 0.0, + 0.3686274509803922, + 1.0, + 0.0862745098, + 0.0, + 0.37254901960784315, + 1.0, + 0.0980392157, + 0.0, + 0.3764705882352941, + 1.0, + 0.1137254902, + 0.0, + 0.3803921568627451, + 1.0, + 0.1254901961, + 0.0, + 0.3843137254901961, + 1.0, + 0.137254902, + 0.0, + 0.38823529411764707, + 1.0, + 0.1490196078, + 0.0, + 0.39215686274509803, + 1.0, + 0.1647058824, + 0.0, + 0.396078431372549, + 1.0, + 0.1764705882, + 0.0, + 0.4, + 1.0, + 0.1882352941, + 0.0, + 0.403921568627451, + 1.0, + 0.2, + 0.0, + 0.40784313725490196, + 1.0, + 0.2156862745, + 0.0, + 0.4117647058823529, + 1.0, + 0.2274509804, + 0.0, + 0.41568627450980394, + 1.0, + 0.2392156863, + 0.0, + 0.4196078431372549, + 1.0, + 0.2509803922, + 0.0, + 0.4235294117647059, + 1.0, + 0.2666666667, + 0.0, + 0.42745098039215684, + 1.0, + 0.2784313725, + 0.0, + 0.43137254901960786, + 1.0, + 0.2901960784, + 0.0, + 0.43529411764705883, + 1.0, + 0.3019607843, + 0.0, + 0.4392156862745098, + 1.0, + 0.3176470588, + 0.0, + 0.44313725490196076, + 1.0, + 0.3294117647, + 0.0, + 0.4470588235294118, + 1.0, + 0.3411764706, + 0.0, + 0.45098039215686275, + 1.0, + 0.3529411765, + 0.0, + 0.4549019607843137, + 1.0, + 0.368627451, + 0.0, + 0.4588235294117647, + 1.0, + 0.3803921569, + 0.0, + 0.4627450980392157, + 1.0, + 0.3921568627, + 0.0, + 0.4666666666666667, + 1.0, + 0.4039215686, + 0.0, + 0.4705882352941177, + 1.0, + 0.4156862745, + 0.0, + 0.4745098039215686, + 1.0, + 0.4274509804, + 0.0, + 0.4784313725490197, + 1.0, + 0.4392156863, + 0.0, + 0.48235294117647065, + 1.0, + 0.4509803922, + 0.0, + 0.48627450980392156, + 1.0, + 0.4666666667, + 0.0, + 0.49019607843137253, + 1.0, + 0.4784313725, + 0.0, + 0.49411764705882355, + 1.0, + 0.4941176471, + 0.0, + 0.4980392156862745, + 1.0, + 0.5058823529, + 0.0, + 0.5019607843137255, + 1.0, + 0.5215686275, + 0.0, + 0.5058823529411764, + 1.0, + 0.5333333333, + 0.0, + 0.5098039215686274, + 1.0, + 0.5450980392, + 0.0, + 0.5137254901960784, + 1.0, + 0.5568627451, + 0.0, + 0.5176470588235295, + 1.0, + 0.568627451, + 0.0, + 0.5215686274509804, + 1.0, + 0.5803921569, + 0.0, + 0.5254901960784314, + 1.0, + 0.5921568627, + 0.0, + 0.5294117647058824, + 1.0, + 0.6039215686, + 0.0, + 0.5333333333333333, + 1.0, + 0.6196078431, + 0.0, + 0.5372549019607843, + 1.0, + 0.631372549, + 0.0, + 0.5411764705882353, + 1.0, + 0.6431372549, + 0.0, + 0.5450980392156862, + 1.0, + 0.6549019608, + 0.0, + 0.5490196078431373, + 1.0, + 0.6705882353, + 0.0, + 0.5529411764705883, + 1.0, + 0.6823529412, + 0.0, + 0.5568627450980392, + 1.0, + 0.6941176471, + 0.0, + 0.5607843137254902, + 1.0, + 0.7058823529, + 0.0, + 0.5647058823529412, + 1.0, + 0.7215686275, + 0.0, + 0.5686274509803921, + 1.0, + 0.7333333333, + 0.0, + 0.5725490196078431, + 1.0, + 0.7450980392, + 0.0, + 0.5764705882352941, + 1.0, + 0.7568627451, + 0.0, + 0.5803921568627451, + 1.0, + 0.7725490196, + 0.0, + 0.5843137254901961, + 1.0, + 0.7843137255, + 0.0, + 0.5882352941176471, + 1.0, + 0.7960784314, + 0.0, + 0.592156862745098, + 1.0, + 0.8078431373, + 0.0, + 0.596078431372549, + 1.0, + 0.8196078431, + 0.0, + 0.6, + 1.0, + 0.831372549, + 0.0, + 0.6039215686274509, + 1.0, + 0.8470588235, + 0.0, + 0.6078431372549019, + 1.0, + 0.8588235294, + 0.0, + 0.611764705882353, + 1.0, + 0.8745098039, + 0.0, + 0.615686274509804, + 1.0, + 0.8862745098, + 0.0, + 0.6196078431372549, + 1.0, + 0.8980392157, + 0.0, + 0.6235294117647059, + 1.0, + 0.9098039216, + 0.0, + 0.6274509803921569, + 1.0, + 0.9254901961, + 0.0, + 0.6313725490196078, + 1.0, + 0.937254902, + 0.0, + 0.6352941176470588, + 1.0, + 0.9490196078, + 0.0, + 0.6392156862745098, + 1.0, + 0.9607843137, + 0.0, + 0.6431372549019608, + 1.0, + 0.9764705882, + 0.0, + 0.6470588235294118, + 1.0, + 0.9803921569, + 0.0039215686, + 0.6509803921568628, + 1.0, + 0.9882352941, + 0.0117647059, + 0.6549019607843137, + 1.0, + 0.9921568627, + 0.0156862745, + 0.6588235294117647, + 1.0, + 1.0, + 0.0235294118, + 0.6627450980392157, + 1.0, + 1.0, + 0.0352941176, + 0.6666666666666666, + 1.0, + 1.0, + 0.0470588235, + 0.6705882352941176, + 1.0, + 1.0, + 0.0588235294, + 0.6745098039215687, + 1.0, + 1.0, + 0.0745098039, + 0.6784313725490196, + 1.0, + 1.0, + 0.0862745098, + 0.6823529411764706, + 1.0, + 1.0, + 0.0980392157, + 0.6862745098039216, + 1.0, + 1.0, + 0.1098039216, + 0.6901960784313725, + 1.0, + 1.0, + 0.1254901961, + 0.6941176470588235, + 1.0, + 1.0, + 0.137254902, + 0.6980392156862745, + 1.0, + 1.0, + 0.1490196078, + 0.7019607843137254, + 1.0, + 1.0, + 0.1607843137, + 0.7058823529411765, + 1.0, + 1.0, + 0.1764705882, + 0.7098039215686275, + 1.0, + 1.0, + 0.1882352941, + 0.7137254901960784, + 1.0, + 1.0, + 0.2, + 0.7176470588235294, + 1.0, + 1.0, + 0.2117647059, + 0.7215686274509804, + 1.0, + 1.0, + 0.2274509804, + 0.7254901960784313, + 1.0, + 1.0, + 0.2392156863, + 0.7294117647058823, + 1.0, + 1.0, + 0.2509803922, + 0.7333333333333333, + 1.0, + 1.0, + 0.262745098, + 0.7372549019607844, + 1.0, + 1.0, + 0.2784313725, + 0.7411764705882353, + 1.0, + 1.0, + 0.2901960784, + 0.7450980392156863, + 1.0, + 1.0, + 0.3019607843, + 0.7490196078431373, + 1.0, + 1.0, + 0.3137254902, + 0.7529411764705882, + 1.0, + 1.0, + 0.3294117647, + 0.7568627450980392, + 1.0, + 1.0, + 0.3411764706, + 0.7607843137254902, + 1.0, + 1.0, + 0.3529411765, + 0.7647058823529411, + 1.0, + 1.0, + 0.3647058824, + 0.7686274509803922, + 1.0, + 1.0, + 0.3803921569, + 0.7725490196078432, + 1.0, + 1.0, + 0.3921568627, + 0.7764705882352941, + 1.0, + 1.0, + 0.4039215686, + 0.7803921568627451, + 1.0, + 1.0, + 0.4156862745, + 0.7843137254901961, + 1.0, + 1.0, + 0.431372549, + 0.788235294117647, + 1.0, + 1.0, + 0.4431372549, + 0.792156862745098, + 1.0, + 1.0, + 0.4549019608, + 0.796078431372549, + 1.0, + 1.0, + 0.4666666667, + 0.8, + 1.0, + 1.0, + 0.4784313725, + 0.803921568627451, + 1.0, + 1.0, + 0.4901960784, + 0.807843137254902, + 1.0, + 1.0, + 0.5019607843, + 0.8117647058823529, + 1.0, + 1.0, + 0.5137254902, + 0.8156862745098039, + 1.0, + 1.0, + 0.5294117647, + 0.8196078431372549, + 1.0, + 1.0, + 0.5411764706, + 0.8235294117647058, + 1.0, + 1.0, + 0.5568627451, + 0.8274509803921568, + 1.0, + 1.0, + 0.568627451, + 0.8313725490196079, + 1.0, + 1.0, + 0.5843137255, + 0.8352941176470589, + 1.0, + 1.0, + 0.5960784314, + 0.8392156862745098, + 1.0, + 1.0, + 0.6078431373, + 0.8431372549019608, + 1.0, + 1.0, + 0.6196078431, + 0.8470588235294118, + 1.0, + 1.0, + 0.631372549, + 0.8509803921568627, + 1.0, + 1.0, + 0.6431372549, + 0.8549019607843137, + 1.0, + 1.0, + 0.6549019608, + 0.8588235294117647, + 1.0, + 1.0, + 0.6666666667, + 0.8627450980392157, + 1.0, + 1.0, + 0.6823529412, + 0.8666666666666667, + 1.0, + 1.0, + 0.6941176471, + 0.8705882352941177, + 1.0, + 1.0, + 0.7058823529, + 0.8745098039215686, + 1.0, + 1.0, + 0.7176470588, + 0.8784313725490196, + 1.0, + 1.0, + 0.7333333333, + 0.8823529411764706, + 1.0, + 1.0, + 0.7450980392, + 0.8862745098039215, + 1.0, + 1.0, + 0.7568627451, + 0.8901960784313725, + 1.0, + 1.0, + 0.768627451, + 0.8941176470588236, + 1.0, + 1.0, + 0.7843137255, + 0.8980392156862745, + 1.0, + 1.0, + 0.7960784314, + 0.9019607843137255, + 1.0, + 1.0, + 0.8078431373, + 0.9058823529411765, + 1.0, + 1.0, + 0.8196078431, + 0.9098039215686274, + 1.0, + 1.0, + 0.8352941176, + 0.9137254901960784, + 1.0, + 1.0, + 0.8470588235, + 0.9176470588235294, + 1.0, + 1.0, + 0.8588235294, + 0.9215686274509803, + 1.0, + 1.0, + 0.8705882353, + 0.9254901960784314, + 1.0, + 1.0, + 0.8823529412, + 0.9294117647058824, + 1.0, + 1.0, + 0.8941176471, + 0.9333333333333333, + 1.0, + 1.0, + 0.9098039216, + 0.9372549019607843, + 1.0, + 1.0, + 0.9215686275, + 0.9411764705882354, + 1.0, + 1.0, + 0.937254902, + 0.9450980392156864, + 1.0, + 1.0, + 0.9490196078, + 0.9490196078431372, + 1.0, + 1.0, + 0.9607843137, + 0.9529411764705882, + 1.0, + 1.0, + 0.9725490196, + 0.9568627450980394, + 1.0, + 1.0, + 0.9882352941, + 0.9607843137254903, + 1.0, + 1.0, + 0.9882352941, + 0.9647058823529413, + 1.0, + 1.0, + 0.9921568627, + 0.9686274509803922, + 1.0, + 1.0, + 0.9960784314, + 0.9725490196078431, + 1.0, + 1.0, + 1.0, + 0.9764705882352941, + 1.0, + 1.0, + 1.0, + 0.9803921568627451, + 1.0, + 1.0, + 1.0, + 0.984313725490196, + 1.0, + 1.0, + 1.0, + 0.9882352941176471, + 1.0, + 1.0, + 1.0, + 0.9921568627450981, + 1.0, + 1.0, + 1.0, + 0.996078431372549, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + ], + }, + { + ColorSpace: 'RGB', + Name: 's_pet', + RGBPoints: [ + 0.0, + 0.0156862745, + 0.0039215686, + 0.0156862745, + 0.00392156862745098, + 0.0156862745, + 0.0039215686, + 0.0156862745, + 0.00784313725490196, + 0.0274509804, + 0.0039215686, + 0.031372549, + 0.011764705882352941, + 0.0352941176, + 0.0039215686, + 0.0509803922, + 0.01568627450980392, + 0.0392156863, + 0.0039215686, + 0.0666666667, + 0.0196078431372549, + 0.0509803922, + 0.0039215686, + 0.0823529412, + 0.023529411764705882, + 0.062745098, + 0.0039215686, + 0.0980392157, + 0.027450980392156862, + 0.0705882353, + 0.0039215686, + 0.1176470588, + 0.03137254901960784, + 0.0745098039, + 0.0039215686, + 0.1333333333, + 0.03529411764705882, + 0.0862745098, + 0.0039215686, + 0.1490196078, + 0.0392156862745098, + 0.0980392157, + 0.0039215686, + 0.1647058824, + 0.043137254901960784, + 0.1058823529, + 0.0039215686, + 0.1843137255, + 0.047058823529411764, + 0.1098039216, + 0.0039215686, + 0.2, + 0.050980392156862744, + 0.1215686275, + 0.0039215686, + 0.2156862745, + 0.054901960784313725, + 0.1333333333, + 0.0039215686, + 0.231372549, + 0.05882352941176471, + 0.137254902, + 0.0039215686, + 0.2509803922, + 0.06274509803921569, + 0.1490196078, + 0.0039215686, + 0.262745098, + 0.06666666666666667, + 0.1607843137, + 0.0039215686, + 0.2784313725, + 0.07058823529411765, + 0.168627451, + 0.0039215686, + 0.2941176471, + 0.07450980392156863, + 0.1725490196, + 0.0039215686, + 0.3137254902, + 0.0784313725490196, + 0.1843137255, + 0.0039215686, + 0.3294117647, + 0.08235294117647059, + 0.1960784314, + 0.0039215686, + 0.3450980392, + 0.08627450980392157, + 0.2039215686, + 0.0039215686, + 0.3607843137, + 0.09019607843137255, + 0.2078431373, + 0.0039215686, + 0.3803921569, + 0.09411764705882353, + 0.2196078431, + 0.0039215686, + 0.3960784314, + 0.09803921568627451, + 0.231372549, + 0.0039215686, + 0.4117647059, + 0.10196078431372549, + 0.2392156863, + 0.0039215686, + 0.4274509804, + 0.10588235294117647, + 0.2431372549, + 0.0039215686, + 0.4470588235, + 0.10980392156862745, + 0.2509803922, + 0.0039215686, + 0.462745098, + 0.11372549019607843, + 0.262745098, + 0.0039215686, + 0.4784313725, + 0.11764705882352942, + 0.2666666667, + 0.0039215686, + 0.4980392157, + 0.12156862745098039, + 0.2666666667, + 0.0039215686, + 0.4980392157, + 0.12549019607843137, + 0.262745098, + 0.0039215686, + 0.5137254902, + 0.12941176470588237, + 0.2509803922, + 0.0039215686, + 0.5294117647, + 0.13333333333333333, + 0.2431372549, + 0.0039215686, + 0.5450980392, + 0.13725490196078433, + 0.2392156863, + 0.0039215686, + 0.5607843137, + 0.1411764705882353, + 0.231372549, + 0.0039215686, + 0.5764705882, + 0.1450980392156863, + 0.2196078431, + 0.0039215686, + 0.5921568627, + 0.14901960784313725, + 0.2078431373, + 0.0039215686, + 0.6078431373, + 0.15294117647058825, + 0.2039215686, + 0.0039215686, + 0.6235294118, + 0.1568627450980392, + 0.1960784314, + 0.0039215686, + 0.6392156863, + 0.1607843137254902, + 0.1843137255, + 0.0039215686, + 0.6549019608, + 0.16470588235294117, + 0.1725490196, + 0.0039215686, + 0.6705882353, + 0.16862745098039217, + 0.168627451, + 0.0039215686, + 0.6862745098, + 0.17254901960784313, + 0.1607843137, + 0.0039215686, + 0.7019607843, + 0.17647058823529413, + 0.1490196078, + 0.0039215686, + 0.7176470588, + 0.1803921568627451, + 0.137254902, + 0.0039215686, + 0.7333333333, + 0.1843137254901961, + 0.1333333333, + 0.0039215686, + 0.7490196078, + 0.18823529411764706, + 0.1215686275, + 0.0039215686, + 0.7607843137, + 0.19215686274509805, + 0.1098039216, + 0.0039215686, + 0.7764705882, + 0.19607843137254902, + 0.1058823529, + 0.0039215686, + 0.7921568627, + 0.2, + 0.0980392157, + 0.0039215686, + 0.8078431373, + 0.20392156862745098, + 0.0862745098, + 0.0039215686, + 0.8235294118, + 0.20784313725490197, + 0.0745098039, + 0.0039215686, + 0.8392156863, + 0.21176470588235294, + 0.0705882353, + 0.0039215686, + 0.8549019608, + 0.21568627450980393, + 0.062745098, + 0.0039215686, + 0.8705882353, + 0.2196078431372549, + 0.0509803922, + 0.0039215686, + 0.8862745098, + 0.2235294117647059, + 0.0392156863, + 0.0039215686, + 0.9019607843, + 0.22745098039215686, + 0.0352941176, + 0.0039215686, + 0.9176470588, + 0.23137254901960785, + 0.0274509804, + 0.0039215686, + 0.9333333333, + 0.23529411764705885, + 0.0156862745, + 0.0039215686, + 0.9490196078, + 0.23921568627450984, + 0.0078431373, + 0.0039215686, + 0.9647058824, + 0.24313725490196078, + 0.0039215686, + 0.0039215686, + 0.9960784314, + 0.24705882352941178, + 0.0039215686, + 0.0039215686, + 0.9960784314, + 0.25098039215686274, + 0.0039215686, + 0.0196078431, + 0.9647058824, + 0.2549019607843137, + 0.0039215686, + 0.0392156863, + 0.9490196078, + 0.25882352941176473, + 0.0039215686, + 0.0549019608, + 0.9333333333, + 0.2627450980392157, + 0.0039215686, + 0.0745098039, + 0.9176470588, + 0.26666666666666666, + 0.0039215686, + 0.0901960784, + 0.9019607843, + 0.27058823529411763, + 0.0039215686, + 0.1098039216, + 0.8862745098, + 0.27450980392156865, + 0.0039215686, + 0.1254901961, + 0.8705882353, + 0.2784313725490196, + 0.0039215686, + 0.1450980392, + 0.8549019608, + 0.2823529411764706, + 0.0039215686, + 0.1607843137, + 0.8392156863, + 0.28627450980392155, + 0.0039215686, + 0.1803921569, + 0.8235294118, + 0.2901960784313726, + 0.0039215686, + 0.1960784314, + 0.8078431373, + 0.29411764705882354, + 0.0039215686, + 0.2156862745, + 0.7921568627, + 0.2980392156862745, + 0.0039215686, + 0.231372549, + 0.7764705882, + 0.30196078431372547, + 0.0039215686, + 0.2509803922, + 0.7607843137, + 0.3058823529411765, + 0.0039215686, + 0.262745098, + 0.7490196078, + 0.30980392156862746, + 0.0039215686, + 0.2823529412, + 0.7333333333, + 0.3137254901960784, + 0.0039215686, + 0.2980392157, + 0.7176470588, + 0.3176470588235294, + 0.0039215686, + 0.3176470588, + 0.7019607843, + 0.3215686274509804, + 0.0039215686, + 0.3333333333, + 0.6862745098, + 0.3254901960784314, + 0.0039215686, + 0.3529411765, + 0.6705882353, + 0.32941176470588235, + 0.0039215686, + 0.368627451, + 0.6549019608, + 0.3333333333333333, + 0.0039215686, + 0.3882352941, + 0.6392156863, + 0.33725490196078434, + 0.0039215686, + 0.4039215686, + 0.6235294118, + 0.3411764705882353, + 0.0039215686, + 0.4235294118, + 0.6078431373, + 0.34509803921568627, + 0.0039215686, + 0.4392156863, + 0.5921568627, + 0.34901960784313724, + 0.0039215686, + 0.4588235294, + 0.5764705882, + 0.35294117647058826, + 0.0039215686, + 0.4745098039, + 0.5607843137, + 0.3568627450980392, + 0.0039215686, + 0.4941176471, + 0.5450980392, + 0.3607843137254902, + 0.0039215686, + 0.5098039216, + 0.5294117647, + 0.36470588235294116, + 0.0039215686, + 0.5294117647, + 0.5137254902, + 0.3686274509803922, + 0.0039215686, + 0.5450980392, + 0.4980392157, + 0.37254901960784315, + 0.0039215686, + 0.5647058824, + 0.4784313725, + 0.3764705882352941, + 0.0039215686, + 0.5803921569, + 0.462745098, + 0.3803921568627451, + 0.0039215686, + 0.6, + 0.4470588235, + 0.3843137254901961, + 0.0039215686, + 0.6156862745, + 0.4274509804, + 0.38823529411764707, + 0.0039215686, + 0.6352941176, + 0.4117647059, + 0.39215686274509803, + 0.0039215686, + 0.6509803922, + 0.3960784314, + 0.396078431372549, + 0.0039215686, + 0.6705882353, + 0.3803921569, + 0.4, + 0.0039215686, + 0.6862745098, + 0.3607843137, + 0.403921568627451, + 0.0039215686, + 0.7058823529, + 0.3450980392, + 0.40784313725490196, + 0.0039215686, + 0.7215686275, + 0.3294117647, + 0.4117647058823529, + 0.0039215686, + 0.7411764706, + 0.3137254902, + 0.41568627450980394, + 0.0039215686, + 0.7529411765, + 0.2941176471, + 0.4196078431372549, + 0.0039215686, + 0.7960784314, + 0.2784313725, + 0.4235294117647059, + 0.0039215686, + 0.7960784314, + 0.262745098, + 0.42745098039215684, + 0.0392156863, + 0.8039215686, + 0.2509803922, + 0.43137254901960786, + 0.0745098039, + 0.8117647059, + 0.231372549, + 0.43529411764705883, + 0.1098039216, + 0.8196078431, + 0.2156862745, + 0.4392156862745098, + 0.1450980392, + 0.8274509804, + 0.2, + 0.44313725490196076, + 0.1803921569, + 0.8352941176, + 0.1843137255, + 0.4470588235294118, + 0.2156862745, + 0.8431372549, + 0.1647058824, + 0.45098039215686275, + 0.2509803922, + 0.8509803922, + 0.1490196078, + 0.4549019607843137, + 0.2823529412, + 0.8588235294, + 0.1333333333, + 0.4588235294117647, + 0.3176470588, + 0.8666666667, + 0.1176470588, + 0.4627450980392157, + 0.3529411765, + 0.8745098039, + 0.0980392157, + 0.4666666666666667, + 0.3882352941, + 0.8823529412, + 0.0823529412, + 0.4705882352941177, + 0.4235294118, + 0.8901960784, + 0.0666666667, + 0.4745098039215686, + 0.4588235294, + 0.8980392157, + 0.0509803922, + 0.4784313725490197, + 0.4941176471, + 0.9058823529, + 0.0431372549, + 0.48235294117647065, + 0.5294117647, + 0.9137254902, + 0.031372549, + 0.48627450980392156, + 0.5647058824, + 0.9215686275, + 0.0196078431, + 0.49019607843137253, + 0.6, + 0.9294117647, + 0.0078431373, + 0.49411764705882355, + 0.6352941176, + 0.937254902, + 0.0039215686, + 0.4980392156862745, + 0.6705882353, + 0.9450980392, + 0.0039215686, + 0.5019607843137255, + 0.7058823529, + 0.9490196078, + 0.0039215686, + 0.5058823529411764, + 0.7411764706, + 0.9568627451, + 0.0039215686, + 0.5098039215686274, + 0.7725490196, + 0.9607843137, + 0.0039215686, + 0.5137254901960784, + 0.8078431373, + 0.968627451, + 0.0039215686, + 0.5176470588235295, + 0.8431372549, + 0.9725490196, + 0.0039215686, + 0.5215686274509804, + 0.8784313725, + 0.9803921569, + 0.0039215686, + 0.5254901960784314, + 0.9137254902, + 0.9843137255, + 0.0039215686, + 0.5294117647058824, + 0.9490196078, + 0.9921568627, + 0.0039215686, + 0.5333333333333333, + 0.9960784314, + 0.9960784314, + 0.0039215686, + 0.5372549019607843, + 0.9960784314, + 0.9960784314, + 0.0039215686, + 0.5411764705882353, + 0.9960784314, + 0.9921568627, + 0.0039215686, + 0.5450980392156862, + 0.9960784314, + 0.9843137255, + 0.0039215686, + 0.5490196078431373, + 0.9960784314, + 0.9764705882, + 0.0039215686, + 0.5529411764705883, + 0.9960784314, + 0.968627451, + 0.0039215686, + 0.5568627450980392, + 0.9960784314, + 0.9607843137, + 0.0039215686, + 0.5607843137254902, + 0.9960784314, + 0.9529411765, + 0.0039215686, + 0.5647058823529412, + 0.9960784314, + 0.9450980392, + 0.0039215686, + 0.5686274509803921, + 0.9960784314, + 0.937254902, + 0.0039215686, + 0.5725490196078431, + 0.9960784314, + 0.9294117647, + 0.0039215686, + 0.5764705882352941, + 0.9960784314, + 0.9215686275, + 0.0039215686, + 0.5803921568627451, + 0.9960784314, + 0.9137254902, + 0.0039215686, + 0.5843137254901961, + 0.9960784314, + 0.9058823529, + 0.0039215686, + 0.5882352941176471, + 0.9960784314, + 0.8980392157, + 0.0039215686, + 0.592156862745098, + 0.9960784314, + 0.8901960784, + 0.0039215686, + 0.596078431372549, + 0.9960784314, + 0.8823529412, + 0.0039215686, + 0.6, + 0.9960784314, + 0.8745098039, + 0.0039215686, + 0.6039215686274509, + 0.9960784314, + 0.8666666667, + 0.0039215686, + 0.6078431372549019, + 0.9960784314, + 0.8588235294, + 0.0039215686, + 0.611764705882353, + 0.9960784314, + 0.8509803922, + 0.0039215686, + 0.615686274509804, + 0.9960784314, + 0.8431372549, + 0.0039215686, + 0.6196078431372549, + 0.9960784314, + 0.8352941176, + 0.0039215686, + 0.6235294117647059, + 0.9960784314, + 0.8274509804, + 0.0039215686, + 0.6274509803921569, + 0.9960784314, + 0.8196078431, + 0.0039215686, + 0.6313725490196078, + 0.9960784314, + 0.8117647059, + 0.0039215686, + 0.6352941176470588, + 0.9960784314, + 0.8039215686, + 0.0039215686, + 0.6392156862745098, + 0.9960784314, + 0.7960784314, + 0.0039215686, + 0.6431372549019608, + 0.9960784314, + 0.7882352941, + 0.0039215686, + 0.6470588235294118, + 0.9960784314, + 0.7803921569, + 0.0039215686, + 0.6509803921568628, + 0.9960784314, + 0.7725490196, + 0.0039215686, + 0.6549019607843137, + 0.9960784314, + 0.7647058824, + 0.0039215686, + 0.6588235294117647, + 0.9960784314, + 0.7568627451, + 0.0039215686, + 0.6627450980392157, + 0.9960784314, + 0.7490196078, + 0.0039215686, + 0.6666666666666666, + 0.9960784314, + 0.7450980392, + 0.0039215686, + 0.6705882352941176, + 0.9960784314, + 0.737254902, + 0.0039215686, + 0.6745098039215687, + 0.9960784314, + 0.7294117647, + 0.0039215686, + 0.6784313725490196, + 0.9960784314, + 0.7215686275, + 0.0039215686, + 0.6823529411764706, + 0.9960784314, + 0.7137254902, + 0.0039215686, + 0.6862745098039216, + 0.9960784314, + 0.7058823529, + 0.0039215686, + 0.6901960784313725, + 0.9960784314, + 0.6980392157, + 0.0039215686, + 0.6941176470588235, + 0.9960784314, + 0.6901960784, + 0.0039215686, + 0.6980392156862745, + 0.9960784314, + 0.6823529412, + 0.0039215686, + 0.7019607843137254, + 0.9960784314, + 0.6745098039, + 0.0039215686, + 0.7058823529411765, + 0.9960784314, + 0.6666666667, + 0.0039215686, + 0.7098039215686275, + 0.9960784314, + 0.6588235294, + 0.0039215686, + 0.7137254901960784, + 0.9960784314, + 0.6509803922, + 0.0039215686, + 0.7176470588235294, + 0.9960784314, + 0.6431372549, + 0.0039215686, + 0.7215686274509804, + 0.9960784314, + 0.6352941176, + 0.0039215686, + 0.7254901960784313, + 0.9960784314, + 0.6274509804, + 0.0039215686, + 0.7294117647058823, + 0.9960784314, + 0.6196078431, + 0.0039215686, + 0.7333333333333333, + 0.9960784314, + 0.6117647059, + 0.0039215686, + 0.7372549019607844, + 0.9960784314, + 0.6039215686, + 0.0039215686, + 0.7411764705882353, + 0.9960784314, + 0.5960784314, + 0.0039215686, + 0.7450980392156863, + 0.9960784314, + 0.5882352941, + 0.0039215686, + 0.7490196078431373, + 0.9960784314, + 0.5803921569, + 0.0039215686, + 0.7529411764705882, + 0.9960784314, + 0.5725490196, + 0.0039215686, + 0.7568627450980392, + 0.9960784314, + 0.5647058824, + 0.0039215686, + 0.7607843137254902, + 0.9960784314, + 0.5568627451, + 0.0039215686, + 0.7647058823529411, + 0.9960784314, + 0.5490196078, + 0.0039215686, + 0.7686274509803922, + 0.9960784314, + 0.5411764706, + 0.0039215686, + 0.7725490196078432, + 0.9960784314, + 0.5333333333, + 0.0039215686, + 0.7764705882352941, + 0.9960784314, + 0.5254901961, + 0.0039215686, + 0.7803921568627451, + 0.9960784314, + 0.5176470588, + 0.0039215686, + 0.7843137254901961, + 0.9960784314, + 0.5098039216, + 0.0039215686, + 0.788235294117647, + 0.9960784314, + 0.5019607843, + 0.0039215686, + 0.792156862745098, + 0.9960784314, + 0.4941176471, + 0.0039215686, + 0.796078431372549, + 0.9960784314, + 0.4862745098, + 0.0039215686, + 0.8, + 0.9960784314, + 0.4784313725, + 0.0039215686, + 0.803921568627451, + 0.9960784314, + 0.4705882353, + 0.0039215686, + 0.807843137254902, + 0.9960784314, + 0.462745098, + 0.0039215686, + 0.8117647058823529, + 0.9960784314, + 0.4549019608, + 0.0039215686, + 0.8156862745098039, + 0.9960784314, + 0.4470588235, + 0.0039215686, + 0.8196078431372549, + 0.9960784314, + 0.4392156863, + 0.0039215686, + 0.8235294117647058, + 0.9960784314, + 0.431372549, + 0.0039215686, + 0.8274509803921568, + 0.9960784314, + 0.4235294118, + 0.0039215686, + 0.8313725490196079, + 0.9960784314, + 0.4156862745, + 0.0039215686, + 0.8352941176470589, + 0.9960784314, + 0.4078431373, + 0.0039215686, + 0.8392156862745098, + 0.9960784314, + 0.4, + 0.0039215686, + 0.8431372549019608, + 0.9960784314, + 0.3921568627, + 0.0039215686, + 0.8470588235294118, + 0.9960784314, + 0.3843137255, + 0.0039215686, + 0.8509803921568627, + 0.9960784314, + 0.3764705882, + 0.0039215686, + 0.8549019607843137, + 0.9960784314, + 0.368627451, + 0.0039215686, + 0.8588235294117647, + 0.9960784314, + 0.3607843137, + 0.0039215686, + 0.8627450980392157, + 0.9960784314, + 0.3529411765, + 0.0039215686, + 0.8666666666666667, + 0.9960784314, + 0.3450980392, + 0.0039215686, + 0.8705882352941177, + 0.9960784314, + 0.337254902, + 0.0039215686, + 0.8745098039215686, + 0.9960784314, + 0.3294117647, + 0.0039215686, + 0.8784313725490196, + 0.9960784314, + 0.3215686275, + 0.0039215686, + 0.8823529411764706, + 0.9960784314, + 0.3137254902, + 0.0039215686, + 0.8862745098039215, + 0.9960784314, + 0.3058823529, + 0.0039215686, + 0.8901960784313725, + 0.9960784314, + 0.2980392157, + 0.0039215686, + 0.8941176470588236, + 0.9960784314, + 0.2901960784, + 0.0039215686, + 0.8980392156862745, + 0.9960784314, + 0.2823529412, + 0.0039215686, + 0.9019607843137255, + 0.9960784314, + 0.2705882353, + 0.0039215686, + 0.9058823529411765, + 0.9960784314, + 0.2588235294, + 0.0039215686, + 0.9098039215686274, + 0.9960784314, + 0.2509803922, + 0.0039215686, + 0.9137254901960784, + 0.9960784314, + 0.2431372549, + 0.0039215686, + 0.9176470588235294, + 0.9960784314, + 0.231372549, + 0.0039215686, + 0.9215686274509803, + 0.9960784314, + 0.2196078431, + 0.0039215686, + 0.9254901960784314, + 0.9960784314, + 0.2117647059, + 0.0039215686, + 0.9294117647058824, + 0.9960784314, + 0.2, + 0.0039215686, + 0.9333333333333333, + 0.9960784314, + 0.1882352941, + 0.0039215686, + 0.9372549019607843, + 0.9960784314, + 0.1764705882, + 0.0039215686, + 0.9411764705882354, + 0.9960784314, + 0.168627451, + 0.0039215686, + 0.9450980392156864, + 0.9960784314, + 0.1568627451, + 0.0039215686, + 0.9490196078431372, + 0.9960784314, + 0.1450980392, + 0.0039215686, + 0.9529411764705882, + 0.9960784314, + 0.1333333333, + 0.0039215686, + 0.9568627450980394, + 0.9960784314, + 0.1254901961, + 0.0039215686, + 0.9607843137254903, + 0.9960784314, + 0.1137254902, + 0.0039215686, + 0.9647058823529413, + 0.9960784314, + 0.1019607843, + 0.0039215686, + 0.9686274509803922, + 0.9960784314, + 0.0901960784, + 0.0039215686, + 0.9725490196078431, + 0.9960784314, + 0.0823529412, + 0.0039215686, + 0.9764705882352941, + 0.9960784314, + 0.0705882353, + 0.0039215686, + 0.9803921568627451, + 0.9960784314, + 0.0588235294, + 0.0039215686, + 0.984313725490196, + 0.9960784314, + 0.0470588235, + 0.0039215686, + 0.9882352941176471, + 0.9960784314, + 0.0392156863, + 0.0039215686, + 0.9921568627450981, + 0.9960784314, + 0.0274509804, + 0.0039215686, + 0.996078431372549, + 0.9960784314, + 0.0156862745, + 0.0039215686, + 1.0, + 0.9960784314, + 0.0156862745, + 0.0039215686, + ], + }, + { + ColorSpace: 'RGB', + Name: 'perfusion', + RGBPoints: [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.00392156862745098, + 0.0078431373, + 0.0235294118, + 0.0235294118, + 0.00784313725490196, + 0.0078431373, + 0.031372549, + 0.0470588235, + 0.011764705882352941, + 0.0078431373, + 0.0392156863, + 0.062745098, + 0.01568627450980392, + 0.0078431373, + 0.0470588235, + 0.0862745098, + 0.0196078431372549, + 0.0078431373, + 0.0549019608, + 0.1019607843, + 0.023529411764705882, + 0.0078431373, + 0.0549019608, + 0.1254901961, + 0.027450980392156862, + 0.0078431373, + 0.062745098, + 0.1411764706, + 0.03137254901960784, + 0.0078431373, + 0.0705882353, + 0.1647058824, + 0.03529411764705882, + 0.0078431373, + 0.0784313725, + 0.1803921569, + 0.0392156862745098, + 0.0078431373, + 0.0862745098, + 0.2039215686, + 0.043137254901960784, + 0.0078431373, + 0.0862745098, + 0.2196078431, + 0.047058823529411764, + 0.0078431373, + 0.0941176471, + 0.2431372549, + 0.050980392156862744, + 0.0078431373, + 0.1019607843, + 0.2666666667, + 0.054901960784313725, + 0.0078431373, + 0.1098039216, + 0.2823529412, + 0.05882352941176471, + 0.0078431373, + 0.1176470588, + 0.3058823529, + 0.06274509803921569, + 0.0078431373, + 0.1176470588, + 0.3215686275, + 0.06666666666666667, + 0.0078431373, + 0.1254901961, + 0.3450980392, + 0.07058823529411765, + 0.0078431373, + 0.1333333333, + 0.3607843137, + 0.07450980392156863, + 0.0078431373, + 0.1411764706, + 0.3843137255, + 0.0784313725490196, + 0.0078431373, + 0.1490196078, + 0.4, + 0.08235294117647059, + 0.0078431373, + 0.1490196078, + 0.4235294118, + 0.08627450980392157, + 0.0078431373, + 0.1568627451, + 0.4392156863, + 0.09019607843137255, + 0.0078431373, + 0.1647058824, + 0.462745098, + 0.09411764705882353, + 0.0078431373, + 0.1725490196, + 0.4784313725, + 0.09803921568627451, + 0.0078431373, + 0.1803921569, + 0.5019607843, + 0.10196078431372549, + 0.0078431373, + 0.1803921569, + 0.5254901961, + 0.10588235294117647, + 0.0078431373, + 0.1882352941, + 0.5411764706, + 0.10980392156862745, + 0.0078431373, + 0.1960784314, + 0.5647058824, + 0.11372549019607843, + 0.0078431373, + 0.2039215686, + 0.5803921569, + 0.11764705882352942, + 0.0078431373, + 0.2117647059, + 0.6039215686, + 0.12156862745098039, + 0.0078431373, + 0.2117647059, + 0.6196078431, + 0.12549019607843137, + 0.0078431373, + 0.2196078431, + 0.6431372549, + 0.12941176470588237, + 0.0078431373, + 0.2274509804, + 0.6588235294, + 0.13333333333333333, + 0.0078431373, + 0.2352941176, + 0.6823529412, + 0.13725490196078433, + 0.0078431373, + 0.2431372549, + 0.6980392157, + 0.1411764705882353, + 0.0078431373, + 0.2431372549, + 0.7215686275, + 0.1450980392156863, + 0.0078431373, + 0.2509803922, + 0.737254902, + 0.14901960784313725, + 0.0078431373, + 0.2588235294, + 0.7607843137, + 0.15294117647058825, + 0.0078431373, + 0.2666666667, + 0.7843137255, + 0.1568627450980392, + 0.0078431373, + 0.2745098039, + 0.8, + 0.1607843137254902, + 0.0078431373, + 0.2745098039, + 0.8235294118, + 0.16470588235294117, + 0.0078431373, + 0.2823529412, + 0.8392156863, + 0.16862745098039217, + 0.0078431373, + 0.2901960784, + 0.862745098, + 0.17254901960784313, + 0.0078431373, + 0.2980392157, + 0.8784313725, + 0.17647058823529413, + 0.0078431373, + 0.3058823529, + 0.9019607843, + 0.1803921568627451, + 0.0078431373, + 0.3058823529, + 0.9176470588, + 0.1843137254901961, + 0.0078431373, + 0.2980392157, + 0.9411764706, + 0.18823529411764706, + 0.0078431373, + 0.3058823529, + 0.9568627451, + 0.19215686274509805, + 0.0078431373, + 0.2980392157, + 0.9803921569, + 0.19607843137254902, + 0.0078431373, + 0.2980392157, + 0.9882352941, + 0.2, + 0.0078431373, + 0.2901960784, + 0.9803921569, + 0.20392156862745098, + 0.0078431373, + 0.2901960784, + 0.9647058824, + 0.20784313725490197, + 0.0078431373, + 0.2823529412, + 0.9568627451, + 0.21176470588235294, + 0.0078431373, + 0.2823529412, + 0.9411764706, + 0.21568627450980393, + 0.0078431373, + 0.2745098039, + 0.9333333333, + 0.2196078431372549, + 0.0078431373, + 0.2666666667, + 0.9176470588, + 0.2235294117647059, + 0.0078431373, + 0.2666666667, + 0.9098039216, + 0.22745098039215686, + 0.0078431373, + 0.2588235294, + 0.9019607843, + 0.23137254901960785, + 0.0078431373, + 0.2588235294, + 0.8862745098, + 0.23529411764705885, + 0.0078431373, + 0.2509803922, + 0.8784313725, + 0.23921568627450984, + 0.0078431373, + 0.2509803922, + 0.862745098, + 0.24313725490196078, + 0.0078431373, + 0.2431372549, + 0.8549019608, + 0.24705882352941178, + 0.0078431373, + 0.2352941176, + 0.8392156863, + 0.25098039215686274, + 0.0078431373, + 0.2352941176, + 0.831372549, + 0.2549019607843137, + 0.0078431373, + 0.2274509804, + 0.8235294118, + 0.25882352941176473, + 0.0078431373, + 0.2274509804, + 0.8078431373, + 0.2627450980392157, + 0.0078431373, + 0.2196078431, + 0.8, + 0.26666666666666666, + 0.0078431373, + 0.2196078431, + 0.7843137255, + 0.27058823529411763, + 0.0078431373, + 0.2117647059, + 0.7764705882, + 0.27450980392156865, + 0.0078431373, + 0.2039215686, + 0.7607843137, + 0.2784313725490196, + 0.0078431373, + 0.2039215686, + 0.7529411765, + 0.2823529411764706, + 0.0078431373, + 0.1960784314, + 0.7450980392, + 0.28627450980392155, + 0.0078431373, + 0.1960784314, + 0.7294117647, + 0.2901960784313726, + 0.0078431373, + 0.1882352941, + 0.7215686275, + 0.29411764705882354, + 0.0078431373, + 0.1882352941, + 0.7058823529, + 0.2980392156862745, + 0.0078431373, + 0.1803921569, + 0.6980392157, + 0.30196078431372547, + 0.0078431373, + 0.1803921569, + 0.6823529412, + 0.3058823529411765, + 0.0078431373, + 0.1725490196, + 0.6745098039, + 0.30980392156862746, + 0.0078431373, + 0.1647058824, + 0.6666666667, + 0.3137254901960784, + 0.0078431373, + 0.1647058824, + 0.6509803922, + 0.3176470588235294, + 0.0078431373, + 0.1568627451, + 0.6431372549, + 0.3215686274509804, + 0.0078431373, + 0.1568627451, + 0.6274509804, + 0.3254901960784314, + 0.0078431373, + 0.1490196078, + 0.6196078431, + 0.32941176470588235, + 0.0078431373, + 0.1490196078, + 0.6039215686, + 0.3333333333333333, + 0.0078431373, + 0.1411764706, + 0.5960784314, + 0.33725490196078434, + 0.0078431373, + 0.1333333333, + 0.5882352941, + 0.3411764705882353, + 0.0078431373, + 0.1333333333, + 0.5725490196, + 0.34509803921568627, + 0.0078431373, + 0.1254901961, + 0.5647058824, + 0.34901960784313724, + 0.0078431373, + 0.1254901961, + 0.5490196078, + 0.35294117647058826, + 0.0078431373, + 0.1176470588, + 0.5411764706, + 0.3568627450980392, + 0.0078431373, + 0.1176470588, + 0.5254901961, + 0.3607843137254902, + 0.0078431373, + 0.1098039216, + 0.5176470588, + 0.36470588235294116, + 0.0078431373, + 0.1019607843, + 0.5098039216, + 0.3686274509803922, + 0.0078431373, + 0.1019607843, + 0.4941176471, + 0.37254901960784315, + 0.0078431373, + 0.0941176471, + 0.4862745098, + 0.3764705882352941, + 0.0078431373, + 0.0941176471, + 0.4705882353, + 0.3803921568627451, + 0.0078431373, + 0.0862745098, + 0.462745098, + 0.3843137254901961, + 0.0078431373, + 0.0862745098, + 0.4470588235, + 0.38823529411764707, + 0.0078431373, + 0.0784313725, + 0.4392156863, + 0.39215686274509803, + 0.0078431373, + 0.0705882353, + 0.431372549, + 0.396078431372549, + 0.0078431373, + 0.0705882353, + 0.4156862745, + 0.4, + 0.0078431373, + 0.062745098, + 0.4078431373, + 0.403921568627451, + 0.0078431373, + 0.062745098, + 0.3921568627, + 0.40784313725490196, + 0.0078431373, + 0.0549019608, + 0.3843137255, + 0.4117647058823529, + 0.0078431373, + 0.0549019608, + 0.368627451, + 0.41568627450980394, + 0.0078431373, + 0.0470588235, + 0.3607843137, + 0.4196078431372549, + 0.0078431373, + 0.0470588235, + 0.3529411765, + 0.4235294117647059, + 0.0078431373, + 0.0392156863, + 0.337254902, + 0.42745098039215684, + 0.0078431373, + 0.031372549, + 0.3294117647, + 0.43137254901960786, + 0.0078431373, + 0.031372549, + 0.3137254902, + 0.43529411764705883, + 0.0078431373, + 0.0235294118, + 0.3058823529, + 0.4392156862745098, + 0.0078431373, + 0.0235294118, + 0.2901960784, + 0.44313725490196076, + 0.0078431373, + 0.0156862745, + 0.2823529412, + 0.4470588235294118, + 0.0078431373, + 0.0156862745, + 0.2745098039, + 0.45098039215686275, + 0.0078431373, + 0.0078431373, + 0.2588235294, + 0.4549019607843137, + 0.0235294118, + 0.0078431373, + 0.2509803922, + 0.4588235294117647, + 0.0078431373, + 0.0078431373, + 0.2352941176, + 0.4627450980392157, + 0.0078431373, + 0.0078431373, + 0.2274509804, + 0.4666666666666667, + 0.0078431373, + 0.0078431373, + 0.2117647059, + 0.4705882352941177, + 0.0078431373, + 0.0078431373, + 0.2039215686, + 0.4745098039215686, + 0.0078431373, + 0.0078431373, + 0.1960784314, + 0.4784313725490197, + 0.0078431373, + 0.0078431373, + 0.1803921569, + 0.48235294117647065, + 0.0078431373, + 0.0078431373, + 0.1725490196, + 0.48627450980392156, + 0.0078431373, + 0.0078431373, + 0.1568627451, + 0.49019607843137253, + 0.0078431373, + 0.0078431373, + 0.1490196078, + 0.49411764705882355, + 0.0078431373, + 0.0078431373, + 0.1333333333, + 0.4980392156862745, + 0.0078431373, + 0.0078431373, + 0.1254901961, + 0.5019607843137255, + 0.0078431373, + 0.0078431373, + 0.1176470588, + 0.5058823529411764, + 0.0078431373, + 0.0078431373, + 0.1019607843, + 0.5098039215686274, + 0.0078431373, + 0.0078431373, + 0.0941176471, + 0.5137254901960784, + 0.0078431373, + 0.0078431373, + 0.0784313725, + 0.5176470588235295, + 0.0078431373, + 0.0078431373, + 0.0705882353, + 0.5215686274509804, + 0.0078431373, + 0.0078431373, + 0.0549019608, + 0.5254901960784314, + 0.0078431373, + 0.0078431373, + 0.0470588235, + 0.5294117647058824, + 0.0235294118, + 0.0078431373, + 0.0392156863, + 0.5333333333333333, + 0.031372549, + 0.0078431373, + 0.0235294118, + 0.5372549019607843, + 0.0392156863, + 0.0078431373, + 0.0156862745, + 0.5411764705882353, + 0.0549019608, + 0.0078431373, + 0.0, + 0.5450980392156862, + 0.062745098, + 0.0078431373, + 0.0, + 0.5490196078431373, + 0.0705882353, + 0.0078431373, + 0.0, + 0.5529411764705883, + 0.0862745098, + 0.0078431373, + 0.0, + 0.5568627450980392, + 0.0941176471, + 0.0078431373, + 0.0, + 0.5607843137254902, + 0.1019607843, + 0.0078431373, + 0.0, + 0.5647058823529412, + 0.1098039216, + 0.0078431373, + 0.0, + 0.5686274509803921, + 0.1254901961, + 0.0078431373, + 0.0, + 0.5725490196078431, + 0.1333333333, + 0.0078431373, + 0.0, + 0.5764705882352941, + 0.1411764706, + 0.0078431373, + 0.0, + 0.5803921568627451, + 0.1568627451, + 0.0078431373, + 0.0, + 0.5843137254901961, + 0.1647058824, + 0.0078431373, + 0.0, + 0.5882352941176471, + 0.1725490196, + 0.0078431373, + 0.0, + 0.592156862745098, + 0.1882352941, + 0.0078431373, + 0.0, + 0.596078431372549, + 0.1960784314, + 0.0078431373, + 0.0, + 0.6, + 0.2039215686, + 0.0078431373, + 0.0, + 0.6039215686274509, + 0.2117647059, + 0.0078431373, + 0.0, + 0.6078431372549019, + 0.2274509804, + 0.0078431373, + 0.0, + 0.611764705882353, + 0.2352941176, + 0.0078431373, + 0.0, + 0.615686274509804, + 0.2431372549, + 0.0078431373, + 0.0, + 0.6196078431372549, + 0.2588235294, + 0.0078431373, + 0.0, + 0.6235294117647059, + 0.2666666667, + 0.0078431373, + 0.0, + 0.6274509803921569, + 0.2745098039, + 0.0, + 0.0, + 0.6313725490196078, + 0.2901960784, + 0.0156862745, + 0.0, + 0.6352941176470588, + 0.2980392157, + 0.0235294118, + 0.0, + 0.6392156862745098, + 0.3058823529, + 0.0392156863, + 0.0, + 0.6431372549019608, + 0.3137254902, + 0.0470588235, + 0.0, + 0.6470588235294118, + 0.3294117647, + 0.0549019608, + 0.0, + 0.6509803921568628, + 0.337254902, + 0.0705882353, + 0.0, + 0.6549019607843137, + 0.3450980392, + 0.0784313725, + 0.0, + 0.6588235294117647, + 0.3607843137, + 0.0862745098, + 0.0, + 0.6627450980392157, + 0.368627451, + 0.1019607843, + 0.0, + 0.6666666666666666, + 0.3764705882, + 0.1098039216, + 0.0, + 0.6705882352941176, + 0.3843137255, + 0.1176470588, + 0.0, + 0.6745098039215687, + 0.4, + 0.1333333333, + 0.0, + 0.6784313725490196, + 0.4078431373, + 0.1411764706, + 0.0, + 0.6823529411764706, + 0.4156862745, + 0.1490196078, + 0.0, + 0.6862745098039216, + 0.431372549, + 0.1647058824, + 0.0, + 0.6901960784313725, + 0.4392156863, + 0.1725490196, + 0.0, + 0.6941176470588235, + 0.4470588235, + 0.1803921569, + 0.0, + 0.6980392156862745, + 0.462745098, + 0.1960784314, + 0.0, + 0.7019607843137254, + 0.4705882353, + 0.2039215686, + 0.0, + 0.7058823529411765, + 0.4784313725, + 0.2117647059, + 0.0, + 0.7098039215686275, + 0.4862745098, + 0.2274509804, + 0.0, + 0.7137254901960784, + 0.5019607843, + 0.2352941176, + 0.0, + 0.7176470588235294, + 0.5098039216, + 0.2431372549, + 0.0, + 0.7215686274509804, + 0.5176470588, + 0.2588235294, + 0.0, + 0.7254901960784313, + 0.5333333333, + 0.2666666667, + 0.0, + 0.7294117647058823, + 0.5411764706, + 0.2745098039, + 0.0, + 0.7333333333333333, + 0.5490196078, + 0.2901960784, + 0.0, + 0.7372549019607844, + 0.5647058824, + 0.2980392157, + 0.0, + 0.7411764705882353, + 0.5725490196, + 0.3058823529, + 0.0, + 0.7450980392156863, + 0.5803921569, + 0.3215686275, + 0.0, + 0.7490196078431373, + 0.5882352941, + 0.3294117647, + 0.0, + 0.7529411764705882, + 0.6039215686, + 0.337254902, + 0.0, + 0.7568627450980392, + 0.6117647059, + 0.3529411765, + 0.0, + 0.7607843137254902, + 0.6196078431, + 0.3607843137, + 0.0, + 0.7647058823529411, + 0.6352941176, + 0.368627451, + 0.0, + 0.7686274509803922, + 0.6431372549, + 0.3843137255, + 0.0, + 0.7725490196078432, + 0.6509803922, + 0.3921568627, + 0.0, + 0.7764705882352941, + 0.6588235294, + 0.4, + 0.0, + 0.7803921568627451, + 0.6745098039, + 0.4156862745, + 0.0, + 0.7843137254901961, + 0.6823529412, + 0.4235294118, + 0.0, + 0.788235294117647, + 0.6901960784, + 0.431372549, + 0.0, + 0.792156862745098, + 0.7058823529, + 0.4470588235, + 0.0, + 0.796078431372549, + 0.7137254902, + 0.4549019608, + 0.0, + 0.8, + 0.7215686275, + 0.462745098, + 0.0, + 0.803921568627451, + 0.737254902, + 0.4784313725, + 0.0, + 0.807843137254902, + 0.7450980392, + 0.4862745098, + 0.0, + 0.8117647058823529, + 0.7529411765, + 0.4941176471, + 0.0, + 0.8156862745098039, + 0.7607843137, + 0.5098039216, + 0.0, + 0.8196078431372549, + 0.7764705882, + 0.5176470588, + 0.0, + 0.8235294117647058, + 0.7843137255, + 0.5254901961, + 0.0, + 0.8274509803921568, + 0.7921568627, + 0.5411764706, + 0.0, + 0.8313725490196079, + 0.8078431373, + 0.5490196078, + 0.0, + 0.8352941176470589, + 0.8156862745, + 0.5568627451, + 0.0, + 0.8392156862745098, + 0.8235294118, + 0.5725490196, + 0.0, + 0.8431372549019608, + 0.8392156863, + 0.5803921569, + 0.0, + 0.8470588235294118, + 0.8470588235, + 0.5882352941, + 0.0, + 0.8509803921568627, + 0.8549019608, + 0.6039215686, + 0.0, + 0.8549019607843137, + 0.862745098, + 0.6117647059, + 0.0, + 0.8588235294117647, + 0.8784313725, + 0.6196078431, + 0.0, + 0.8627450980392157, + 0.8862745098, + 0.6352941176, + 0.0, + 0.8666666666666667, + 0.8941176471, + 0.6431372549, + 0.0, + 0.8705882352941177, + 0.9098039216, + 0.6509803922, + 0.0, + 0.8745098039215686, + 0.9176470588, + 0.6666666667, + 0.0, + 0.8784313725490196, + 0.9254901961, + 0.6745098039, + 0.0, + 0.8823529411764706, + 0.9411764706, + 0.6823529412, + 0.0, + 0.8862745098039215, + 0.9490196078, + 0.6980392157, + 0.0, + 0.8901960784313725, + 0.9568627451, + 0.7058823529, + 0.0, + 0.8941176470588236, + 0.9647058824, + 0.7137254902, + 0.0, + 0.8980392156862745, + 0.9803921569, + 0.7294117647, + 0.0, + 0.9019607843137255, + 0.9882352941, + 0.737254902, + 0.0, + 0.9058823529411765, + 0.9960784314, + 0.7450980392, + 0.0, + 0.9098039215686274, + 0.9960784314, + 0.7607843137, + 0.0, + 0.9137254901960784, + 0.9960784314, + 0.768627451, + 0.0, + 0.9176470588235294, + 0.9960784314, + 0.7764705882, + 0.0, + 0.9215686274509803, + 0.9960784314, + 0.7921568627, + 0.0, + 0.9254901960784314, + 0.9960784314, + 0.8, + 0.0, + 0.9294117647058824, + 0.9960784314, + 0.8078431373, + 0.0, + 0.9333333333333333, + 0.9960784314, + 0.8235294118, + 0.0, + 0.9372549019607843, + 0.9960784314, + 0.831372549, + 0.0, + 0.9411764705882354, + 0.9960784314, + 0.8392156863, + 0.0, + 0.9450980392156864, + 0.9960784314, + 0.8549019608, + 0.0, + 0.9490196078431372, + 0.9960784314, + 0.862745098, + 0.0549019608, + 0.9529411764705882, + 0.9960784314, + 0.8705882353, + 0.1098039216, + 0.9568627450980394, + 0.9960784314, + 0.8862745098, + 0.1647058824, + 0.9607843137254903, + 0.9960784314, + 0.8941176471, + 0.2196078431, + 0.9647058823529413, + 0.9960784314, + 0.9019607843, + 0.2666666667, + 0.9686274509803922, + 0.9960784314, + 0.9176470588, + 0.3215686275, + 0.9725490196078431, + 0.9960784314, + 0.9254901961, + 0.3764705882, + 0.9764705882352941, + 0.9960784314, + 0.9333333333, + 0.431372549, + 0.9803921568627451, + 0.9960784314, + 0.9490196078, + 0.4862745098, + 0.984313725490196, + 0.9960784314, + 0.9568627451, + 0.5333333333, + 0.9882352941176471, + 0.9960784314, + 0.9647058824, + 0.5882352941, + 0.9921568627450981, + 0.9960784314, + 0.9803921569, + 0.6431372549, + 0.996078431372549, + 0.9960784314, + 0.9882352941, + 0.6980392157, + 1.0, + 0.9960784314, + 0.9960784314, + 0.7450980392, + ], + }, + { + ColorSpace: 'RGB', + Name: 'rainbow_2', + RGBPoints: [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.00392156862745098, + 0.0156862745, + 0.0, + 0.0117647059, + 0.00784313725490196, + 0.0352941176, + 0.0, + 0.0274509804, + 0.011764705882352941, + 0.0509803922, + 0.0, + 0.0392156863, + 0.01568627450980392, + 0.0705882353, + 0.0, + 0.0549019608, + 0.0196078431372549, + 0.0862745098, + 0.0, + 0.0745098039, + 0.023529411764705882, + 0.1058823529, + 0.0, + 0.0901960784, + 0.027450980392156862, + 0.1215686275, + 0.0, + 0.1098039216, + 0.03137254901960784, + 0.1411764706, + 0.0, + 0.1254901961, + 0.03529411764705882, + 0.1568627451, + 0.0, + 0.1490196078, + 0.0392156862745098, + 0.1764705882, + 0.0, + 0.168627451, + 0.043137254901960784, + 0.1960784314, + 0.0, + 0.1882352941, + 0.047058823529411764, + 0.2117647059, + 0.0, + 0.2078431373, + 0.050980392156862744, + 0.2274509804, + 0.0, + 0.231372549, + 0.054901960784313725, + 0.2392156863, + 0.0, + 0.2470588235, + 0.05882352941176471, + 0.2509803922, + 0.0, + 0.2666666667, + 0.06274509803921569, + 0.2666666667, + 0.0, + 0.2823529412, + 0.06666666666666667, + 0.2705882353, + 0.0, + 0.3019607843, + 0.07058823529411765, + 0.2823529412, + 0.0, + 0.3176470588, + 0.07450980392156863, + 0.2901960784, + 0.0, + 0.337254902, + 0.0784313725490196, + 0.3019607843, + 0.0, + 0.3568627451, + 0.08235294117647059, + 0.3098039216, + 0.0, + 0.3725490196, + 0.08627450980392157, + 0.3137254902, + 0.0, + 0.3921568627, + 0.09019607843137255, + 0.3215686275, + 0.0, + 0.4078431373, + 0.09411764705882353, + 0.3254901961, + 0.0, + 0.4274509804, + 0.09803921568627451, + 0.3333333333, + 0.0, + 0.4431372549, + 0.10196078431372549, + 0.3294117647, + 0.0, + 0.462745098, + 0.10588235294117647, + 0.337254902, + 0.0, + 0.4784313725, + 0.10980392156862745, + 0.3411764706, + 0.0, + 0.4980392157, + 0.11372549019607843, + 0.3450980392, + 0.0, + 0.5176470588, + 0.11764705882352942, + 0.337254902, + 0.0, + 0.5333333333, + 0.12156862745098039, + 0.3411764706, + 0.0, + 0.5529411765, + 0.12549019607843137, + 0.3411764706, + 0.0, + 0.568627451, + 0.12941176470588237, + 0.3411764706, + 0.0, + 0.5882352941, + 0.13333333333333333, + 0.3333333333, + 0.0, + 0.6039215686, + 0.13725490196078433, + 0.3294117647, + 0.0, + 0.6235294118, + 0.1411764705882353, + 0.3294117647, + 0.0, + 0.6392156863, + 0.1450980392156863, + 0.3294117647, + 0.0, + 0.6588235294, + 0.14901960784313725, + 0.3254901961, + 0.0, + 0.6784313725, + 0.15294117647058825, + 0.3098039216, + 0.0, + 0.6941176471, + 0.1568627450980392, + 0.3058823529, + 0.0, + 0.7137254902, + 0.1607843137254902, + 0.3019607843, + 0.0, + 0.7294117647, + 0.16470588235294117, + 0.2980392157, + 0.0, + 0.7490196078, + 0.16862745098039217, + 0.2784313725, + 0.0, + 0.7647058824, + 0.17254901960784313, + 0.2745098039, + 0.0, + 0.7843137255, + 0.17647058823529413, + 0.2666666667, + 0.0, + 0.8, + 0.1803921568627451, + 0.2588235294, + 0.0, + 0.8196078431, + 0.1843137254901961, + 0.2352941176, + 0.0, + 0.8392156863, + 0.18823529411764706, + 0.2274509804, + 0.0, + 0.8549019608, + 0.19215686274509805, + 0.2156862745, + 0.0, + 0.8745098039, + 0.19607843137254902, + 0.2078431373, + 0.0, + 0.8901960784, + 0.2, + 0.1803921569, + 0.0, + 0.9098039216, + 0.20392156862745098, + 0.168627451, + 0.0, + 0.9254901961, + 0.20784313725490197, + 0.1568627451, + 0.0, + 0.9450980392, + 0.21176470588235294, + 0.1411764706, + 0.0, + 0.9607843137, + 0.21568627450980393, + 0.1294117647, + 0.0, + 0.9803921569, + 0.2196078431372549, + 0.0980392157, + 0.0, + 1.0, + 0.2235294117647059, + 0.0823529412, + 0.0, + 1.0, + 0.22745098039215686, + 0.062745098, + 0.0, + 1.0, + 0.23137254901960785, + 0.0470588235, + 0.0, + 1.0, + 0.23529411764705885, + 0.0156862745, + 0.0, + 1.0, + 0.23921568627450984, + 0.0, + 0.0, + 1.0, + 0.24313725490196078, + 0.0, + 0.0156862745, + 1.0, + 0.24705882352941178, + 0.0, + 0.031372549, + 1.0, + 0.25098039215686274, + 0.0, + 0.062745098, + 1.0, + 0.2549019607843137, + 0.0, + 0.0823529412, + 1.0, + 0.25882352941176473, + 0.0, + 0.0980392157, + 1.0, + 0.2627450980392157, + 0.0, + 0.1137254902, + 1.0, + 0.26666666666666666, + 0.0, + 0.1490196078, + 1.0, + 0.27058823529411763, + 0.0, + 0.1647058824, + 1.0, + 0.27450980392156865, + 0.0, + 0.1803921569, + 1.0, + 0.2784313725490196, + 0.0, + 0.2, + 1.0, + 0.2823529411764706, + 0.0, + 0.2156862745, + 1.0, + 0.28627450980392155, + 0.0, + 0.2470588235, + 1.0, + 0.2901960784313726, + 0.0, + 0.262745098, + 1.0, + 0.29411764705882354, + 0.0, + 0.2823529412, + 1.0, + 0.2980392156862745, + 0.0, + 0.2980392157, + 1.0, + 0.30196078431372547, + 0.0, + 0.3294117647, + 1.0, + 0.3058823529411765, + 0.0, + 0.3490196078, + 1.0, + 0.30980392156862746, + 0.0, + 0.3647058824, + 1.0, + 0.3137254901960784, + 0.0, + 0.3803921569, + 1.0, + 0.3176470588235294, + 0.0, + 0.4156862745, + 1.0, + 0.3215686274509804, + 0.0, + 0.431372549, + 1.0, + 0.3254901960784314, + 0.0, + 0.4470588235, + 1.0, + 0.32941176470588235, + 0.0, + 0.4666666667, + 1.0, + 0.3333333333333333, + 0.0, + 0.4980392157, + 1.0, + 0.33725490196078434, + 0.0, + 0.5137254902, + 1.0, + 0.3411764705882353, + 0.0, + 0.5294117647, + 1.0, + 0.34509803921568627, + 0.0, + 0.5490196078, + 1.0, + 0.34901960784313724, + 0.0, + 0.5647058824, + 1.0, + 0.35294117647058826, + 0.0, + 0.5960784314, + 1.0, + 0.3568627450980392, + 0.0, + 0.6156862745, + 1.0, + 0.3607843137254902, + 0.0, + 0.631372549, + 1.0, + 0.36470588235294116, + 0.0, + 0.6470588235, + 1.0, + 0.3686274509803922, + 0.0, + 0.6823529412, + 1.0, + 0.37254901960784315, + 0.0, + 0.6980392157, + 1.0, + 0.3764705882352941, + 0.0, + 0.7137254902, + 1.0, + 0.3803921568627451, + 0.0, + 0.7333333333, + 1.0, + 0.3843137254901961, + 0.0, + 0.7647058824, + 1.0, + 0.38823529411764707, + 0.0, + 0.7803921569, + 1.0, + 0.39215686274509803, + 0.0, + 0.7960784314, + 1.0, + 0.396078431372549, + 0.0, + 0.8156862745, + 1.0, + 0.4, + 0.0, + 0.8470588235, + 1.0, + 0.403921568627451, + 0.0, + 0.862745098, + 1.0, + 0.40784313725490196, + 0.0, + 0.8823529412, + 1.0, + 0.4117647058823529, + 0.0, + 0.8980392157, + 1.0, + 0.41568627450980394, + 0.0, + 0.9137254902, + 1.0, + 0.4196078431372549, + 0.0, + 0.9490196078, + 1.0, + 0.4235294117647059, + 0.0, + 0.9647058824, + 1.0, + 0.42745098039215684, + 0.0, + 0.9803921569, + 1.0, + 0.43137254901960786, + 0.0, + 1.0, + 1.0, + 0.43529411764705883, + 0.0, + 1.0, + 0.9647058824, + 0.4392156862745098, + 0.0, + 1.0, + 0.9490196078, + 0.44313725490196076, + 0.0, + 1.0, + 0.9333333333, + 0.4470588235294118, + 0.0, + 1.0, + 0.9137254902, + 0.45098039215686275, + 0.0, + 1.0, + 0.8823529412, + 0.4549019607843137, + 0.0, + 1.0, + 0.862745098, + 0.4588235294117647, + 0.0, + 1.0, + 0.8470588235, + 0.4627450980392157, + 0.0, + 1.0, + 0.831372549, + 0.4666666666666667, + 0.0, + 1.0, + 0.7960784314, + 0.4705882352941177, + 0.0, + 1.0, + 0.7803921569, + 0.4745098039215686, + 0.0, + 1.0, + 0.7647058824, + 0.4784313725490197, + 0.0, + 1.0, + 0.7490196078, + 0.48235294117647065, + 0.0, + 1.0, + 0.7333333333, + 0.48627450980392156, + 0.0, + 1.0, + 0.6980392157, + 0.49019607843137253, + 0.0, + 1.0, + 0.6823529412, + 0.49411764705882355, + 0.0, + 1.0, + 0.6666666667, + 0.4980392156862745, + 0.0, + 1.0, + 0.6470588235, + 0.5019607843137255, + 0.0, + 1.0, + 0.6156862745, + 0.5058823529411764, + 0.0, + 1.0, + 0.5960784314, + 0.5098039215686274, + 0.0, + 1.0, + 0.5803921569, + 0.5137254901960784, + 0.0, + 1.0, + 0.5647058824, + 0.5176470588235295, + 0.0, + 1.0, + 0.5294117647, + 0.5215686274509804, + 0.0, + 1.0, + 0.5137254902, + 0.5254901960784314, + 0.0, + 1.0, + 0.4980392157, + 0.5294117647058824, + 0.0, + 1.0, + 0.4823529412, + 0.5333333333333333, + 0.0, + 1.0, + 0.4470588235, + 0.5372549019607843, + 0.0, + 1.0, + 0.431372549, + 0.5411764705882353, + 0.0, + 1.0, + 0.4156862745, + 0.5450980392156862, + 0.0, + 1.0, + 0.4, + 0.5490196078431373, + 0.0, + 1.0, + 0.3803921569, + 0.5529411764705883, + 0.0, + 1.0, + 0.3490196078, + 0.5568627450980392, + 0.0, + 1.0, + 0.3294117647, + 0.5607843137254902, + 0.0, + 1.0, + 0.3137254902, + 0.5647058823529412, + 0.0, + 1.0, + 0.2980392157, + 0.5686274509803921, + 0.0, + 1.0, + 0.262745098, + 0.5725490196078431, + 0.0, + 1.0, + 0.2470588235, + 0.5764705882352941, + 0.0, + 1.0, + 0.231372549, + 0.5803921568627451, + 0.0, + 1.0, + 0.2156862745, + 0.5843137254901961, + 0.0, + 1.0, + 0.1803921569, + 0.5882352941176471, + 0.0, + 1.0, + 0.1647058824, + 0.592156862745098, + 0.0, + 1.0, + 0.1490196078, + 0.596078431372549, + 0.0, + 1.0, + 0.1333333333, + 0.6, + 0.0, + 1.0, + 0.0980392157, + 0.6039215686274509, + 0.0, + 1.0, + 0.0823529412, + 0.6078431372549019, + 0.0, + 1.0, + 0.062745098, + 0.611764705882353, + 0.0, + 1.0, + 0.0470588235, + 0.615686274509804, + 0.0, + 1.0, + 0.031372549, + 0.6196078431372549, + 0.0, + 1.0, + 0.0, + 0.6235294117647059, + 0.0156862745, + 1.0, + 0.0, + 0.6274509803921569, + 0.031372549, + 1.0, + 0.0, + 0.6313725490196078, + 0.0470588235, + 1.0, + 0.0, + 0.6352941176470588, + 0.0823529412, + 1.0, + 0.0, + 0.6392156862745098, + 0.0980392157, + 1.0, + 0.0, + 0.6431372549019608, + 0.1137254902, + 1.0, + 0.0, + 0.6470588235294118, + 0.1294117647, + 1.0, + 0.0, + 0.6509803921568628, + 0.1647058824, + 1.0, + 0.0, + 0.6549019607843137, + 0.1803921569, + 1.0, + 0.0, + 0.6588235294117647, + 0.2, + 1.0, + 0.0, + 0.6627450980392157, + 0.2156862745, + 1.0, + 0.0, + 0.6666666666666666, + 0.2470588235, + 1.0, + 0.0, + 0.6705882352941176, + 0.262745098, + 1.0, + 0.0, + 0.6745098039215687, + 0.2823529412, + 1.0, + 0.0, + 0.6784313725490196, + 0.2980392157, + 1.0, + 0.0, + 0.6823529411764706, + 0.3137254902, + 1.0, + 0.0, + 0.6862745098039216, + 0.3490196078, + 1.0, + 0.0, + 0.6901960784313725, + 0.3647058824, + 1.0, + 0.0, + 0.6941176470588235, + 0.3803921569, + 1.0, + 0.0, + 0.6980392156862745, + 0.3960784314, + 1.0, + 0.0, + 0.7019607843137254, + 0.431372549, + 1.0, + 0.0, + 0.7058823529411765, + 0.4470588235, + 1.0, + 0.0, + 0.7098039215686275, + 0.4666666667, + 1.0, + 0.0, + 0.7137254901960784, + 0.4823529412, + 1.0, + 0.0, + 0.7176470588235294, + 0.5137254902, + 1.0, + 0.0, + 0.7215686274509804, + 0.5294117647, + 1.0, + 0.0, + 0.7254901960784313, + 0.5490196078, + 1.0, + 0.0, + 0.7294117647058823, + 0.5647058824, + 1.0, + 0.0, + 0.7333333333333333, + 0.6, + 1.0, + 0.0, + 0.7372549019607844, + 0.6156862745, + 1.0, + 0.0, + 0.7411764705882353, + 0.631372549, + 1.0, + 0.0, + 0.7450980392156863, + 0.6470588235, + 1.0, + 0.0, + 0.7490196078431373, + 0.662745098, + 1.0, + 0.0, + 0.7529411764705882, + 0.6980392157, + 1.0, + 0.0, + 0.7568627450980392, + 0.7137254902, + 1.0, + 0.0, + 0.7607843137254902, + 0.7333333333, + 1.0, + 0.0, + 0.7647058823529411, + 0.7490196078, + 1.0, + 0.0, + 0.7686274509803922, + 0.7803921569, + 1.0, + 0.0, + 0.7725490196078432, + 0.7960784314, + 1.0, + 0.0, + 0.7764705882352941, + 0.8156862745, + 1.0, + 0.0, + 0.7803921568627451, + 0.831372549, + 1.0, + 0.0, + 0.7843137254901961, + 0.8666666667, + 1.0, + 0.0, + 0.788235294117647, + 0.8823529412, + 1.0, + 0.0, + 0.792156862745098, + 0.8980392157, + 1.0, + 0.0, + 0.796078431372549, + 0.9137254902, + 1.0, + 0.0, + 0.8, + 0.9490196078, + 1.0, + 0.0, + 0.803921568627451, + 0.9647058824, + 1.0, + 0.0, + 0.807843137254902, + 0.9803921569, + 1.0, + 0.0, + 0.8117647058823529, + 1.0, + 1.0, + 0.0, + 0.8156862745098039, + 1.0, + 0.9803921569, + 0.0, + 0.8196078431372549, + 1.0, + 0.9490196078, + 0.0, + 0.8235294117647058, + 1.0, + 0.9333333333, + 0.0, + 0.8274509803921568, + 1.0, + 0.9137254902, + 0.0, + 0.8313725490196079, + 1.0, + 0.8980392157, + 0.0, + 0.8352941176470589, + 1.0, + 0.8666666667, + 0.0, + 0.8392156862745098, + 1.0, + 0.8470588235, + 0.0, + 0.8431372549019608, + 1.0, + 0.831372549, + 0.0, + 0.8470588235294118, + 1.0, + 0.8156862745, + 0.0, + 0.8509803921568627, + 1.0, + 0.7803921569, + 0.0, + 0.8549019607843137, + 1.0, + 0.7647058824, + 0.0, + 0.8588235294117647, + 1.0, + 0.7490196078, + 0.0, + 0.8627450980392157, + 1.0, + 0.7333333333, + 0.0, + 0.8666666666666667, + 1.0, + 0.6980392157, + 0.0, + 0.8705882352941177, + 1.0, + 0.6823529412, + 0.0, + 0.8745098039215686, + 1.0, + 0.6666666667, + 0.0, + 0.8784313725490196, + 1.0, + 0.6470588235, + 0.0, + 0.8823529411764706, + 1.0, + 0.631372549, + 0.0, + 0.8862745098039215, + 1.0, + 0.6, + 0.0, + 0.8901960784313725, + 1.0, + 0.5803921569, + 0.0, + 0.8941176470588236, + 1.0, + 0.5647058824, + 0.0, + 0.8980392156862745, + 1.0, + 0.5490196078, + 0.0, + 0.9019607843137255, + 1.0, + 0.5137254902, + 0.0, + 0.9058823529411765, + 1.0, + 0.4980392157, + 0.0, + 0.9098039215686274, + 1.0, + 0.4823529412, + 0.0, + 0.9137254901960784, + 1.0, + 0.4666666667, + 0.0, + 0.9176470588235294, + 1.0, + 0.431372549, + 0.0, + 0.9215686274509803, + 1.0, + 0.4156862745, + 0.0, + 0.9254901960784314, + 1.0, + 0.4, + 0.0, + 0.9294117647058824, + 1.0, + 0.3803921569, + 0.0, + 0.9333333333333333, + 1.0, + 0.3490196078, + 0.0, + 0.9372549019607843, + 1.0, + 0.3333333333, + 0.0, + 0.9411764705882354, + 1.0, + 0.3137254902, + 0.0, + 0.9450980392156864, + 1.0, + 0.2980392157, + 0.0, + 0.9490196078431372, + 1.0, + 0.2823529412, + 0.0, + 0.9529411764705882, + 1.0, + 0.2470588235, + 0.0, + 0.9568627450980394, + 1.0, + 0.231372549, + 0.0, + 0.9607843137254903, + 1.0, + 0.2156862745, + 0.0, + 0.9647058823529413, + 1.0, + 0.2, + 0.0, + 0.9686274509803922, + 1.0, + 0.1647058824, + 0.0, + 0.9725490196078431, + 1.0, + 0.1490196078, + 0.0, + 0.9764705882352941, + 1.0, + 0.1333333333, + 0.0, + 0.9803921568627451, + 1.0, + 0.1137254902, + 0.0, + 0.984313725490196, + 1.0, + 0.0823529412, + 0.0, + 0.9882352941176471, + 1.0, + 0.0666666667, + 0.0, + 0.9921568627450981, + 1.0, + 0.0470588235, + 0.0, + 0.996078431372549, + 1.0, + 0.031372549, + 0.0, + 1.0, + 1.0, + 0.0, + 0.0, + ], + }, + { + ColorSpace: 'RGB', + Name: 'suv', + RGBPoints: [ + 0.0, + 1.0, + 1.0, + 1.0, + 0.00392156862745098, + 1.0, + 1.0, + 1.0, + 0.00784313725490196, + 1.0, + 1.0, + 1.0, + 0.011764705882352941, + 1.0, + 1.0, + 1.0, + 0.01568627450980392, + 1.0, + 1.0, + 1.0, + 0.0196078431372549, + 1.0, + 1.0, + 1.0, + 0.023529411764705882, + 1.0, + 1.0, + 1.0, + 0.027450980392156862, + 1.0, + 1.0, + 1.0, + 0.03137254901960784, + 1.0, + 1.0, + 1.0, + 0.03529411764705882, + 1.0, + 1.0, + 1.0, + 0.0392156862745098, + 1.0, + 1.0, + 1.0, + 0.043137254901960784, + 1.0, + 1.0, + 1.0, + 0.047058823529411764, + 1.0, + 1.0, + 1.0, + 0.050980392156862744, + 1.0, + 1.0, + 1.0, + 0.054901960784313725, + 1.0, + 1.0, + 1.0, + 0.05882352941176471, + 1.0, + 1.0, + 1.0, + 0.06274509803921569, + 1.0, + 1.0, + 1.0, + 0.06666666666666667, + 1.0, + 1.0, + 1.0, + 0.07058823529411765, + 1.0, + 1.0, + 1.0, + 0.07450980392156863, + 1.0, + 1.0, + 1.0, + 0.0784313725490196, + 1.0, + 1.0, + 1.0, + 0.08235294117647059, + 1.0, + 1.0, + 1.0, + 0.08627450980392157, + 1.0, + 1.0, + 1.0, + 0.09019607843137255, + 1.0, + 1.0, + 1.0, + 0.09411764705882353, + 1.0, + 1.0, + 1.0, + 0.09803921568627451, + 1.0, + 1.0, + 1.0, + 0.10196078431372549, + 0.737254902, + 0.737254902, + 0.737254902, + 0.10588235294117647, + 0.737254902, + 0.737254902, + 0.737254902, + 0.10980392156862745, + 0.737254902, + 0.737254902, + 0.737254902, + 0.11372549019607843, + 0.737254902, + 0.737254902, + 0.737254902, + 0.11764705882352942, + 0.737254902, + 0.737254902, + 0.737254902, + 0.12156862745098039, + 0.737254902, + 0.737254902, + 0.737254902, + 0.12549019607843137, + 0.737254902, + 0.737254902, + 0.737254902, + 0.12941176470588237, + 0.737254902, + 0.737254902, + 0.737254902, + 0.13333333333333333, + 0.737254902, + 0.737254902, + 0.737254902, + 0.13725490196078433, + 0.737254902, + 0.737254902, + 0.737254902, + 0.1411764705882353, + 0.737254902, + 0.737254902, + 0.737254902, + 0.1450980392156863, + 0.737254902, + 0.737254902, + 0.737254902, + 0.14901960784313725, + 0.737254902, + 0.737254902, + 0.737254902, + 0.15294117647058825, + 0.737254902, + 0.737254902, + 0.737254902, + 0.1568627450980392, + 0.737254902, + 0.737254902, + 0.737254902, + 0.1607843137254902, + 0.737254902, + 0.737254902, + 0.737254902, + 0.16470588235294117, + 0.737254902, + 0.737254902, + 0.737254902, + 0.16862745098039217, + 0.737254902, + 0.737254902, + 0.737254902, + 0.17254901960784313, + 0.737254902, + 0.737254902, + 0.737254902, + 0.17647058823529413, + 0.737254902, + 0.737254902, + 0.737254902, + 0.1803921568627451, + 0.737254902, + 0.737254902, + 0.737254902, + 0.1843137254901961, + 0.737254902, + 0.737254902, + 0.737254902, + 0.18823529411764706, + 0.737254902, + 0.737254902, + 0.737254902, + 0.19215686274509805, + 0.737254902, + 0.737254902, + 0.737254902, + 0.19607843137254902, + 0.737254902, + 0.737254902, + 0.737254902, + 0.2, + 0.737254902, + 0.737254902, + 0.737254902, + 0.20392156862745098, + 0.431372549, + 0.0, + 0.568627451, + 0.20784313725490197, + 0.431372549, + 0.0, + 0.568627451, + 0.21176470588235294, + 0.431372549, + 0.0, + 0.568627451, + 0.21568627450980393, + 0.431372549, + 0.0, + 0.568627451, + 0.2196078431372549, + 0.431372549, + 0.0, + 0.568627451, + 0.2235294117647059, + 0.431372549, + 0.0, + 0.568627451, + 0.22745098039215686, + 0.431372549, + 0.0, + 0.568627451, + 0.23137254901960785, + 0.431372549, + 0.0, + 0.568627451, + 0.23529411764705885, + 0.431372549, + 0.0, + 0.568627451, + 0.23921568627450984, + 0.431372549, + 0.0, + 0.568627451, + 0.24313725490196078, + 0.431372549, + 0.0, + 0.568627451, + 0.24705882352941178, + 0.431372549, + 0.0, + 0.568627451, + 0.25098039215686274, + 0.431372549, + 0.0, + 0.568627451, + 0.2549019607843137, + 0.431372549, + 0.0, + 0.568627451, + 0.25882352941176473, + 0.431372549, + 0.0, + 0.568627451, + 0.2627450980392157, + 0.431372549, + 0.0, + 0.568627451, + 0.26666666666666666, + 0.431372549, + 0.0, + 0.568627451, + 0.27058823529411763, + 0.431372549, + 0.0, + 0.568627451, + 0.27450980392156865, + 0.431372549, + 0.0, + 0.568627451, + 0.2784313725490196, + 0.431372549, + 0.0, + 0.568627451, + 0.2823529411764706, + 0.431372549, + 0.0, + 0.568627451, + 0.28627450980392155, + 0.431372549, + 0.0, + 0.568627451, + 0.2901960784313726, + 0.431372549, + 0.0, + 0.568627451, + 0.29411764705882354, + 0.431372549, + 0.0, + 0.568627451, + 0.2980392156862745, + 0.431372549, + 0.0, + 0.568627451, + 0.30196078431372547, + 0.431372549, + 0.0, + 0.568627451, + 0.3058823529411765, + 0.2509803922, + 0.3333333333, + 0.6509803922, + 0.30980392156862746, + 0.2509803922, + 0.3333333333, + 0.6509803922, + 0.3137254901960784, + 0.2509803922, + 0.3333333333, + 0.6509803922, + 0.3176470588235294, + 0.2509803922, + 0.3333333333, + 0.6509803922, + 0.3215686274509804, + 0.2509803922, + 0.3333333333, + 0.6509803922, + 0.3254901960784314, + 0.2509803922, + 0.3333333333, + 0.6509803922, + 0.32941176470588235, + 0.2509803922, + 0.3333333333, + 0.6509803922, + 0.3333333333333333, + 0.2509803922, + 0.3333333333, + 0.6509803922, + 0.33725490196078434, + 0.2509803922, + 0.3333333333, + 0.6509803922, + 0.3411764705882353, + 0.2509803922, + 0.3333333333, + 0.6509803922, + 0.34509803921568627, + 0.2509803922, + 0.3333333333, + 0.6509803922, + 0.34901960784313724, + 0.2509803922, + 0.3333333333, + 0.6509803922, + 0.35294117647058826, + 0.2509803922, + 0.3333333333, + 0.6509803922, + 0.3568627450980392, + 0.2509803922, + 0.3333333333, + 0.6509803922, + 0.3607843137254902, + 0.2509803922, + 0.3333333333, + 0.6509803922, + 0.36470588235294116, + 0.2509803922, + 0.3333333333, + 0.6509803922, + 0.3686274509803922, + 0.2509803922, + 0.3333333333, + 0.6509803922, + 0.37254901960784315, + 0.2509803922, + 0.3333333333, + 0.6509803922, + 0.3764705882352941, + 0.2509803922, + 0.3333333333, + 0.6509803922, + 0.3803921568627451, + 0.2509803922, + 0.3333333333, + 0.6509803922, + 0.3843137254901961, + 0.2509803922, + 0.3333333333, + 0.6509803922, + 0.38823529411764707, + 0.2509803922, + 0.3333333333, + 0.6509803922, + 0.39215686274509803, + 0.2509803922, + 0.3333333333, + 0.6509803922, + 0.396078431372549, + 0.2509803922, + 0.3333333333, + 0.6509803922, + 0.4, + 0.2509803922, + 0.3333333333, + 0.6509803922, + 0.403921568627451, + 0.2509803922, + 0.3333333333, + 0.6509803922, + 0.40784313725490196, + 0.0, + 0.8, + 1.0, + 0.4117647058823529, + 0.0, + 0.8, + 1.0, + 0.41568627450980394, + 0.0, + 0.8, + 1.0, + 0.4196078431372549, + 0.0, + 0.8, + 1.0, + 0.4235294117647059, + 0.0, + 0.8, + 1.0, + 0.42745098039215684, + 0.0, + 0.8, + 1.0, + 0.43137254901960786, + 0.0, + 0.8, + 1.0, + 0.43529411764705883, + 0.0, + 0.8, + 1.0, + 0.4392156862745098, + 0.0, + 0.8, + 1.0, + 0.44313725490196076, + 0.0, + 0.8, + 1.0, + 0.4470588235294118, + 0.0, + 0.8, + 1.0, + 0.45098039215686275, + 0.0, + 0.8, + 1.0, + 0.4549019607843137, + 0.0, + 0.8, + 1.0, + 0.4588235294117647, + 0.0, + 0.8, + 1.0, + 0.4627450980392157, + 0.0, + 0.8, + 1.0, + 0.4666666666666667, + 0.0, + 0.8, + 1.0, + 0.4705882352941177, + 0.0, + 0.8, + 1.0, + 0.4745098039215686, + 0.0, + 0.8, + 1.0, + 0.4784313725490197, + 0.0, + 0.8, + 1.0, + 0.48235294117647065, + 0.0, + 0.8, + 1.0, + 0.48627450980392156, + 0.0, + 0.8, + 1.0, + 0.49019607843137253, + 0.0, + 0.8, + 1.0, + 0.49411764705882355, + 0.0, + 0.8, + 1.0, + 0.4980392156862745, + 0.0, + 0.8, + 1.0, + 0.5019607843137255, + 0.0, + 0.8, + 1.0, + 0.5058823529411764, + 0.0, + 0.6666666667, + 0.5333333333, + 0.5098039215686274, + 0.0, + 0.6666666667, + 0.5333333333, + 0.5137254901960784, + 0.0, + 0.6666666667, + 0.5333333333, + 0.5176470588235295, + 0.0, + 0.6666666667, + 0.5333333333, + 0.5215686274509804, + 0.0, + 0.6666666667, + 0.5333333333, + 0.5254901960784314, + 0.0, + 0.6666666667, + 0.5333333333, + 0.5294117647058824, + 0.0, + 0.6666666667, + 0.5333333333, + 0.5333333333333333, + 0.0, + 0.6666666667, + 0.5333333333, + 0.5372549019607843, + 0.0, + 0.6666666667, + 0.5333333333, + 0.5411764705882353, + 0.0, + 0.6666666667, + 0.5333333333, + 0.5450980392156862, + 0.0, + 0.6666666667, + 0.5333333333, + 0.5490196078431373, + 0.0, + 0.6666666667, + 0.5333333333, + 0.5529411764705883, + 0.0, + 0.6666666667, + 0.5333333333, + 0.5568627450980392, + 0.0, + 0.6666666667, + 0.5333333333, + 0.5607843137254902, + 0.0, + 0.6666666667, + 0.5333333333, + 0.5647058823529412, + 0.0, + 0.6666666667, + 0.5333333333, + 0.5686274509803921, + 0.0, + 0.6666666667, + 0.5333333333, + 0.5725490196078431, + 0.0, + 0.6666666667, + 0.5333333333, + 0.5764705882352941, + 0.0, + 0.6666666667, + 0.5333333333, + 0.5803921568627451, + 0.0, + 0.6666666667, + 0.5333333333, + 0.5843137254901961, + 0.0, + 0.6666666667, + 0.5333333333, + 0.5882352941176471, + 0.0, + 0.6666666667, + 0.5333333333, + 0.592156862745098, + 0.0, + 0.6666666667, + 0.5333333333, + 0.596078431372549, + 0.0, + 0.6666666667, + 0.5333333333, + 0.6, + 0.0, + 0.6666666667, + 0.5333333333, + 0.6039215686274509, + 0.0, + 0.6666666667, + 0.5333333333, + 0.6078431372549019, + 0.4, + 1.0, + 0.4, + 0.611764705882353, + 0.4, + 1.0, + 0.4, + 0.615686274509804, + 0.4, + 1.0, + 0.4, + 0.6196078431372549, + 0.4, + 1.0, + 0.4, + 0.6235294117647059, + 0.4, + 1.0, + 0.4, + 0.6274509803921569, + 0.4, + 1.0, + 0.4, + 0.6313725490196078, + 0.4, + 1.0, + 0.4, + 0.6352941176470588, + 0.4, + 1.0, + 0.4, + 0.6392156862745098, + 0.4, + 1.0, + 0.4, + 0.6431372549019608, + 0.4, + 1.0, + 0.4, + 0.6470588235294118, + 0.4, + 1.0, + 0.4, + 0.6509803921568628, + 0.4, + 1.0, + 0.4, + 0.6549019607843137, + 0.4, + 1.0, + 0.4, + 0.6588235294117647, + 0.4, + 1.0, + 0.4, + 0.6627450980392157, + 0.4, + 1.0, + 0.4, + 0.6666666666666666, + 0.4, + 1.0, + 0.4, + 0.6705882352941176, + 0.4, + 1.0, + 0.4, + 0.6745098039215687, + 0.4, + 1.0, + 0.4, + 0.6784313725490196, + 0.4, + 1.0, + 0.4, + 0.6823529411764706, + 0.4, + 1.0, + 0.4, + 0.6862745098039216, + 0.4, + 1.0, + 0.4, + 0.6901960784313725, + 0.4, + 1.0, + 0.4, + 0.6941176470588235, + 0.4, + 1.0, + 0.4, + 0.6980392156862745, + 0.4, + 1.0, + 0.4, + 0.7019607843137254, + 0.4, + 1.0, + 0.4, + 0.7058823529411765, + 1.0, + 0.9490196078, + 0.0, + 0.7098039215686275, + 1.0, + 0.9490196078, + 0.0, + 0.7137254901960784, + 1.0, + 0.9490196078, + 0.0, + 0.7176470588235294, + 1.0, + 0.9490196078, + 0.0, + 0.7215686274509804, + 1.0, + 0.9490196078, + 0.0, + 0.7254901960784313, + 1.0, + 0.9490196078, + 0.0, + 0.7294117647058823, + 1.0, + 0.9490196078, + 0.0, + 0.7333333333333333, + 1.0, + 0.9490196078, + 0.0, + 0.7372549019607844, + 1.0, + 0.9490196078, + 0.0, + 0.7411764705882353, + 1.0, + 0.9490196078, + 0.0, + 0.7450980392156863, + 1.0, + 0.9490196078, + 0.0, + 0.7490196078431373, + 1.0, + 0.9490196078, + 0.0, + 0.7529411764705882, + 1.0, + 0.9490196078, + 0.0, + 0.7568627450980392, + 1.0, + 0.9490196078, + 0.0, + 0.7607843137254902, + 1.0, + 0.9490196078, + 0.0, + 0.7647058823529411, + 1.0, + 0.9490196078, + 0.0, + 0.7686274509803922, + 1.0, + 0.9490196078, + 0.0, + 0.7725490196078432, + 1.0, + 0.9490196078, + 0.0, + 0.7764705882352941, + 1.0, + 0.9490196078, + 0.0, + 0.7803921568627451, + 1.0, + 0.9490196078, + 0.0, + 0.7843137254901961, + 1.0, + 0.9490196078, + 0.0, + 0.788235294117647, + 1.0, + 0.9490196078, + 0.0, + 0.792156862745098, + 1.0, + 0.9490196078, + 0.0, + 0.796078431372549, + 1.0, + 0.9490196078, + 0.0, + 0.8, + 1.0, + 0.9490196078, + 0.0, + 0.803921568627451, + 1.0, + 0.9490196078, + 0.0, + 0.807843137254902, + 0.9490196078, + 0.6509803922, + 0.2509803922, + 0.8117647058823529, + 0.9490196078, + 0.6509803922, + 0.2509803922, + 0.8156862745098039, + 0.9490196078, + 0.6509803922, + 0.2509803922, + 0.8196078431372549, + 0.9490196078, + 0.6509803922, + 0.2509803922, + 0.8235294117647058, + 0.9490196078, + 0.6509803922, + 0.2509803922, + 0.8274509803921568, + 0.9490196078, + 0.6509803922, + 0.2509803922, + 0.8313725490196079, + 0.9490196078, + 0.6509803922, + 0.2509803922, + 0.8352941176470589, + 0.9490196078, + 0.6509803922, + 0.2509803922, + 0.8392156862745098, + 0.9490196078, + 0.6509803922, + 0.2509803922, + 0.8431372549019608, + 0.9490196078, + 0.6509803922, + 0.2509803922, + 0.8470588235294118, + 0.9490196078, + 0.6509803922, + 0.2509803922, + 0.8509803921568627, + 0.9490196078, + 0.6509803922, + 0.2509803922, + 0.8549019607843137, + 0.9490196078, + 0.6509803922, + 0.2509803922, + 0.8588235294117647, + 0.9490196078, + 0.6509803922, + 0.2509803922, + 0.8627450980392157, + 0.9490196078, + 0.6509803922, + 0.2509803922, + 0.8666666666666667, + 0.9490196078, + 0.6509803922, + 0.2509803922, + 0.8705882352941177, + 0.9490196078, + 0.6509803922, + 0.2509803922, + 0.8745098039215686, + 0.9490196078, + 0.6509803922, + 0.2509803922, + 0.8784313725490196, + 0.9490196078, + 0.6509803922, + 0.2509803922, + 0.8823529411764706, + 0.9490196078, + 0.6509803922, + 0.2509803922, + 0.8862745098039215, + 0.9490196078, + 0.6509803922, + 0.2509803922, + 0.8901960784313725, + 0.9490196078, + 0.6509803922, + 0.2509803922, + 0.8941176470588236, + 0.9490196078, + 0.6509803922, + 0.2509803922, + 0.8980392156862745, + 0.9490196078, + 0.6509803922, + 0.2509803922, + 0.9019607843137255, + 0.9490196078, + 0.6509803922, + 0.2509803922, + 0.9058823529411765, + 0.9490196078, + 0.6509803922, + 0.2509803922, + 0.9098039215686274, + 1.0, + 0.0, + 0.0, + 0.9137254901960784, + 1.0, + 0.0, + 0.0, + 0.9176470588235294, + 1.0, + 0.0, + 0.0, + 0.9215686274509803, + 1.0, + 0.0, + 0.0, + 0.9254901960784314, + 1.0, + 0.0, + 0.0, + 0.9294117647058824, + 1.0, + 0.0, + 0.0, + 0.9333333333333333, + 1.0, + 0.0, + 0.0, + 0.9372549019607843, + 1.0, + 0.0, + 0.0, + 0.9411764705882354, + 1.0, + 0.0, + 0.0, + 0.9450980392156864, + 1.0, + 0.0, + 0.0, + 0.9490196078431372, + 1.0, + 0.0, + 0.0, + 0.9529411764705882, + 1.0, + 0.0, + 0.0, + 0.9568627450980394, + 1.0, + 0.0, + 0.0, + 0.9607843137254903, + 1.0, + 0.0, + 0.0, + 0.9647058823529413, + 1.0, + 0.0, + 0.0, + 0.9686274509803922, + 1.0, + 0.0, + 0.0, + 0.9725490196078431, + 1.0, + 0.0, + 0.0, + 0.9764705882352941, + 1.0, + 0.0, + 0.0, + 0.9803921568627451, + 1.0, + 0.0, + 0.0, + 0.984313725490196, + 1.0, + 0.0, + 0.0, + 0.9882352941176471, + 1.0, + 0.0, + 0.0, + 0.9921568627450981, + 1.0, + 0.0, + 0.0, + 0.996078431372549, + 1.0, + 0.0, + 0.0, + 1.0, + 1.0, + 0.0, + 0.0, + ], + }, + { + ColorSpace: 'RGB', + Name: 'ge_256', + RGBPoints: [ + 0.0, + 0.0039215686, + 0.0078431373, + 0.0078431373, + 0.00392156862745098, + 0.0039215686, + 0.0078431373, + 0.0078431373, + 0.00784313725490196, + 0.0039215686, + 0.0078431373, + 0.0117647059, + 0.011764705882352941, + 0.0039215686, + 0.0117647059, + 0.0156862745, + 0.01568627450980392, + 0.0039215686, + 0.0117647059, + 0.0196078431, + 0.0196078431372549, + 0.0039215686, + 0.0156862745, + 0.0235294118, + 0.023529411764705882, + 0.0039215686, + 0.0156862745, + 0.0274509804, + 0.027450980392156862, + 0.0039215686, + 0.0196078431, + 0.031372549, + 0.03137254901960784, + 0.0039215686, + 0.0196078431, + 0.0352941176, + 0.03529411764705882, + 0.0039215686, + 0.0235294118, + 0.0392156863, + 0.0392156862745098, + 0.0039215686, + 0.0235294118, + 0.0431372549, + 0.043137254901960784, + 0.0039215686, + 0.0274509804, + 0.0470588235, + 0.047058823529411764, + 0.0039215686, + 0.0274509804, + 0.0509803922, + 0.050980392156862744, + 0.0039215686, + 0.031372549, + 0.0549019608, + 0.054901960784313725, + 0.0039215686, + 0.031372549, + 0.0588235294, + 0.05882352941176471, + 0.0039215686, + 0.0352941176, + 0.062745098, + 0.06274509803921569, + 0.0039215686, + 0.0352941176, + 0.0666666667, + 0.06666666666666667, + 0.0039215686, + 0.0392156863, + 0.0705882353, + 0.07058823529411765, + 0.0039215686, + 0.0392156863, + 0.0745098039, + 0.07450980392156863, + 0.0039215686, + 0.0431372549, + 0.0784313725, + 0.0784313725490196, + 0.0039215686, + 0.0431372549, + 0.0823529412, + 0.08235294117647059, + 0.0039215686, + 0.0470588235, + 0.0862745098, + 0.08627450980392157, + 0.0039215686, + 0.0470588235, + 0.0901960784, + 0.09019607843137255, + 0.0039215686, + 0.0509803922, + 0.0941176471, + 0.09411764705882353, + 0.0039215686, + 0.0509803922, + 0.0980392157, + 0.09803921568627451, + 0.0039215686, + 0.0549019608, + 0.1019607843, + 0.10196078431372549, + 0.0039215686, + 0.0549019608, + 0.1058823529, + 0.10588235294117647, + 0.0039215686, + 0.0588235294, + 0.1098039216, + 0.10980392156862745, + 0.0039215686, + 0.0588235294, + 0.1137254902, + 0.11372549019607843, + 0.0039215686, + 0.062745098, + 0.1176470588, + 0.11764705882352942, + 0.0039215686, + 0.062745098, + 0.1215686275, + 0.12156862745098039, + 0.0039215686, + 0.0666666667, + 0.1254901961, + 0.12549019607843137, + 0.0039215686, + 0.0666666667, + 0.1294117647, + 0.12941176470588237, + 0.0039215686, + 0.0705882353, + 0.1333333333, + 0.13333333333333333, + 0.0039215686, + 0.0705882353, + 0.137254902, + 0.13725490196078433, + 0.0039215686, + 0.0745098039, + 0.1411764706, + 0.1411764705882353, + 0.0039215686, + 0.0745098039, + 0.1450980392, + 0.1450980392156863, + 0.0039215686, + 0.0784313725, + 0.1490196078, + 0.14901960784313725, + 0.0039215686, + 0.0784313725, + 0.1529411765, + 0.15294117647058825, + 0.0039215686, + 0.0823529412, + 0.1568627451, + 0.1568627450980392, + 0.0039215686, + 0.0823529412, + 0.1607843137, + 0.1607843137254902, + 0.0039215686, + 0.0862745098, + 0.1647058824, + 0.16470588235294117, + 0.0039215686, + 0.0862745098, + 0.168627451, + 0.16862745098039217, + 0.0039215686, + 0.0901960784, + 0.1725490196, + 0.17254901960784313, + 0.0039215686, + 0.0901960784, + 0.1764705882, + 0.17647058823529413, + 0.0039215686, + 0.0941176471, + 0.1803921569, + 0.1803921568627451, + 0.0039215686, + 0.0941176471, + 0.1843137255, + 0.1843137254901961, + 0.0039215686, + 0.0980392157, + 0.1882352941, + 0.18823529411764706, + 0.0039215686, + 0.0980392157, + 0.1921568627, + 0.19215686274509805, + 0.0039215686, + 0.1019607843, + 0.1960784314, + 0.19607843137254902, + 0.0039215686, + 0.1019607843, + 0.2, + 0.2, + 0.0039215686, + 0.1058823529, + 0.2039215686, + 0.20392156862745098, + 0.0039215686, + 0.1058823529, + 0.2078431373, + 0.20784313725490197, + 0.0039215686, + 0.1098039216, + 0.2117647059, + 0.21176470588235294, + 0.0039215686, + 0.1098039216, + 0.2156862745, + 0.21568627450980393, + 0.0039215686, + 0.1137254902, + 0.2196078431, + 0.2196078431372549, + 0.0039215686, + 0.1137254902, + 0.2235294118, + 0.2235294117647059, + 0.0039215686, + 0.1176470588, + 0.2274509804, + 0.22745098039215686, + 0.0039215686, + 0.1176470588, + 0.231372549, + 0.23137254901960785, + 0.0039215686, + 0.1215686275, + 0.2352941176, + 0.23529411764705885, + 0.0039215686, + 0.1215686275, + 0.2392156863, + 0.23921568627450984, + 0.0039215686, + 0.1254901961, + 0.2431372549, + 0.24313725490196078, + 0.0039215686, + 0.1254901961, + 0.2470588235, + 0.24705882352941178, + 0.0039215686, + 0.1294117647, + 0.2509803922, + 0.25098039215686274, + 0.0039215686, + 0.1294117647, + 0.2509803922, + 0.2549019607843137, + 0.0078431373, + 0.1254901961, + 0.2549019608, + 0.25882352941176473, + 0.0156862745, + 0.1254901961, + 0.2588235294, + 0.2627450980392157, + 0.0235294118, + 0.1215686275, + 0.262745098, + 0.26666666666666666, + 0.031372549, + 0.1215686275, + 0.2666666667, + 0.27058823529411763, + 0.0392156863, + 0.1176470588, + 0.2705882353, + 0.27450980392156865, + 0.0470588235, + 0.1176470588, + 0.2745098039, + 0.2784313725490196, + 0.0549019608, + 0.1137254902, + 0.2784313725, + 0.2823529411764706, + 0.062745098, + 0.1137254902, + 0.2823529412, + 0.28627450980392155, + 0.0705882353, + 0.1098039216, + 0.2862745098, + 0.2901960784313726, + 0.0784313725, + 0.1098039216, + 0.2901960784, + 0.29411764705882354, + 0.0862745098, + 0.1058823529, + 0.2941176471, + 0.2980392156862745, + 0.0941176471, + 0.1058823529, + 0.2980392157, + 0.30196078431372547, + 0.1019607843, + 0.1019607843, + 0.3019607843, + 0.3058823529411765, + 0.1098039216, + 0.1019607843, + 0.3058823529, + 0.30980392156862746, + 0.1176470588, + 0.0980392157, + 0.3098039216, + 0.3137254901960784, + 0.1254901961, + 0.0980392157, + 0.3137254902, + 0.3176470588235294, + 0.1333333333, + 0.0941176471, + 0.3176470588, + 0.3215686274509804, + 0.1411764706, + 0.0941176471, + 0.3215686275, + 0.3254901960784314, + 0.1490196078, + 0.0901960784, + 0.3254901961, + 0.32941176470588235, + 0.1568627451, + 0.0901960784, + 0.3294117647, + 0.3333333333333333, + 0.1647058824, + 0.0862745098, + 0.3333333333, + 0.33725490196078434, + 0.1725490196, + 0.0862745098, + 0.337254902, + 0.3411764705882353, + 0.1803921569, + 0.0823529412, + 0.3411764706, + 0.34509803921568627, + 0.1882352941, + 0.0823529412, + 0.3450980392, + 0.34901960784313724, + 0.1960784314, + 0.0784313725, + 0.3490196078, + 0.35294117647058826, + 0.2039215686, + 0.0784313725, + 0.3529411765, + 0.3568627450980392, + 0.2117647059, + 0.0745098039, + 0.3568627451, + 0.3607843137254902, + 0.2196078431, + 0.0745098039, + 0.3607843137, + 0.36470588235294116, + 0.2274509804, + 0.0705882353, + 0.3647058824, + 0.3686274509803922, + 0.2352941176, + 0.0705882353, + 0.368627451, + 0.37254901960784315, + 0.2431372549, + 0.0666666667, + 0.3725490196, + 0.3764705882352941, + 0.2509803922, + 0.0666666667, + 0.3764705882, + 0.3803921568627451, + 0.2549019608, + 0.062745098, + 0.3803921569, + 0.3843137254901961, + 0.262745098, + 0.062745098, + 0.3843137255, + 0.38823529411764707, + 0.2705882353, + 0.0588235294, + 0.3882352941, + 0.39215686274509803, + 0.2784313725, + 0.0588235294, + 0.3921568627, + 0.396078431372549, + 0.2862745098, + 0.0549019608, + 0.3960784314, + 0.4, + 0.2941176471, + 0.0549019608, + 0.4, + 0.403921568627451, + 0.3019607843, + 0.0509803922, + 0.4039215686, + 0.40784313725490196, + 0.3098039216, + 0.0509803922, + 0.4078431373, + 0.4117647058823529, + 0.3176470588, + 0.0470588235, + 0.4117647059, + 0.41568627450980394, + 0.3254901961, + 0.0470588235, + 0.4156862745, + 0.4196078431372549, + 0.3333333333, + 0.0431372549, + 0.4196078431, + 0.4235294117647059, + 0.3411764706, + 0.0431372549, + 0.4235294118, + 0.42745098039215684, + 0.3490196078, + 0.0392156863, + 0.4274509804, + 0.43137254901960786, + 0.3568627451, + 0.0392156863, + 0.431372549, + 0.43529411764705883, + 0.3647058824, + 0.0352941176, + 0.4352941176, + 0.4392156862745098, + 0.3725490196, + 0.0352941176, + 0.4392156863, + 0.44313725490196076, + 0.3803921569, + 0.031372549, + 0.4431372549, + 0.4470588235294118, + 0.3882352941, + 0.031372549, + 0.4470588235, + 0.45098039215686275, + 0.3960784314, + 0.0274509804, + 0.4509803922, + 0.4549019607843137, + 0.4039215686, + 0.0274509804, + 0.4549019608, + 0.4588235294117647, + 0.4117647059, + 0.0235294118, + 0.4588235294, + 0.4627450980392157, + 0.4196078431, + 0.0235294118, + 0.462745098, + 0.4666666666666667, + 0.4274509804, + 0.0196078431, + 0.4666666667, + 0.4705882352941177, + 0.4352941176, + 0.0196078431, + 0.4705882353, + 0.4745098039215686, + 0.4431372549, + 0.0156862745, + 0.4745098039, + 0.4784313725490197, + 0.4509803922, + 0.0156862745, + 0.4784313725, + 0.48235294117647065, + 0.4588235294, + 0.0117647059, + 0.4823529412, + 0.48627450980392156, + 0.4666666667, + 0.0117647059, + 0.4862745098, + 0.49019607843137253, + 0.4745098039, + 0.0078431373, + 0.4901960784, + 0.49411764705882355, + 0.4823529412, + 0.0078431373, + 0.4941176471, + 0.4980392156862745, + 0.4901960784, + 0.0039215686, + 0.4980392157, + 0.5019607843137255, + 0.4980392157, + 0.0117647059, + 0.4980392157, + 0.5058823529411764, + 0.5058823529, + 0.0156862745, + 0.4901960784, + 0.5098039215686274, + 0.5137254902, + 0.0235294118, + 0.4823529412, + 0.5137254901960784, + 0.5215686275, + 0.0274509804, + 0.4745098039, + 0.5176470588235295, + 0.5294117647, + 0.0352941176, + 0.4666666667, + 0.5215686274509804, + 0.537254902, + 0.0392156863, + 0.4588235294, + 0.5254901960784314, + 0.5450980392, + 0.0470588235, + 0.4509803922, + 0.5294117647058824, + 0.5529411765, + 0.0509803922, + 0.4431372549, + 0.5333333333333333, + 0.5607843137, + 0.0588235294, + 0.4352941176, + 0.5372549019607843, + 0.568627451, + 0.062745098, + 0.4274509804, + 0.5411764705882353, + 0.5764705882, + 0.0705882353, + 0.4196078431, + 0.5450980392156862, + 0.5843137255, + 0.0745098039, + 0.4117647059, + 0.5490196078431373, + 0.5921568627, + 0.0823529412, + 0.4039215686, + 0.5529411764705883, + 0.6, + 0.0862745098, + 0.3960784314, + 0.5568627450980392, + 0.6078431373, + 0.0941176471, + 0.3882352941, + 0.5607843137254902, + 0.6156862745, + 0.0980392157, + 0.3803921569, + 0.5647058823529412, + 0.6235294118, + 0.1058823529, + 0.3725490196, + 0.5686274509803921, + 0.631372549, + 0.1098039216, + 0.3647058824, + 0.5725490196078431, + 0.6392156863, + 0.1176470588, + 0.3568627451, + 0.5764705882352941, + 0.6470588235, + 0.1215686275, + 0.3490196078, + 0.5803921568627451, + 0.6549019608, + 0.1294117647, + 0.3411764706, + 0.5843137254901961, + 0.662745098, + 0.1333333333, + 0.3333333333, + 0.5882352941176471, + 0.6705882353, + 0.1411764706, + 0.3254901961, + 0.592156862745098, + 0.6784313725, + 0.1450980392, + 0.3176470588, + 0.596078431372549, + 0.6862745098, + 0.1529411765, + 0.3098039216, + 0.6, + 0.6941176471, + 0.1568627451, + 0.3019607843, + 0.6039215686274509, + 0.7019607843, + 0.1647058824, + 0.2941176471, + 0.6078431372549019, + 0.7098039216, + 0.168627451, + 0.2862745098, + 0.611764705882353, + 0.7176470588, + 0.1764705882, + 0.2784313725, + 0.615686274509804, + 0.7254901961, + 0.1803921569, + 0.2705882353, + 0.6196078431372549, + 0.7333333333, + 0.1882352941, + 0.262745098, + 0.6235294117647059, + 0.7411764706, + 0.1921568627, + 0.2549019608, + 0.6274509803921569, + 0.7490196078, + 0.2, + 0.2509803922, + 0.6313725490196078, + 0.7529411765, + 0.2039215686, + 0.2431372549, + 0.6352941176470588, + 0.7607843137, + 0.2117647059, + 0.2352941176, + 0.6392156862745098, + 0.768627451, + 0.2156862745, + 0.2274509804, + 0.6431372549019608, + 0.7764705882, + 0.2235294118, + 0.2196078431, + 0.6470588235294118, + 0.7843137255, + 0.2274509804, + 0.2117647059, + 0.6509803921568628, + 0.7921568627, + 0.2352941176, + 0.2039215686, + 0.6549019607843137, + 0.8, + 0.2392156863, + 0.1960784314, + 0.6588235294117647, + 0.8078431373, + 0.2470588235, + 0.1882352941, + 0.6627450980392157, + 0.8156862745, + 0.2509803922, + 0.1803921569, + 0.6666666666666666, + 0.8235294118, + 0.2549019608, + 0.1725490196, + 0.6705882352941176, + 0.831372549, + 0.2588235294, + 0.1647058824, + 0.6745098039215687, + 0.8392156863, + 0.2666666667, + 0.1568627451, + 0.6784313725490196, + 0.8470588235, + 0.2705882353, + 0.1490196078, + 0.6823529411764706, + 0.8549019608, + 0.2784313725, + 0.1411764706, + 0.6862745098039216, + 0.862745098, + 0.2823529412, + 0.1333333333, + 0.6901960784313725, + 0.8705882353, + 0.2901960784, + 0.1254901961, + 0.6941176470588235, + 0.8784313725, + 0.2941176471, + 0.1176470588, + 0.6980392156862745, + 0.8862745098, + 0.3019607843, + 0.1098039216, + 0.7019607843137254, + 0.8941176471, + 0.3058823529, + 0.1019607843, + 0.7058823529411765, + 0.9019607843, + 0.3137254902, + 0.0941176471, + 0.7098039215686275, + 0.9098039216, + 0.3176470588, + 0.0862745098, + 0.7137254901960784, + 0.9176470588, + 0.3254901961, + 0.0784313725, + 0.7176470588235294, + 0.9254901961, + 0.3294117647, + 0.0705882353, + 0.7215686274509804, + 0.9333333333, + 0.337254902, + 0.062745098, + 0.7254901960784313, + 0.9411764706, + 0.3411764706, + 0.0549019608, + 0.7294117647058823, + 0.9490196078, + 0.3490196078, + 0.0470588235, + 0.7333333333333333, + 0.9568627451, + 0.3529411765, + 0.0392156863, + 0.7372549019607844, + 0.9647058824, + 0.3607843137, + 0.031372549, + 0.7411764705882353, + 0.9725490196, + 0.3647058824, + 0.0235294118, + 0.7450980392156863, + 0.9803921569, + 0.3725490196, + 0.0156862745, + 0.7490196078431373, + 0.9882352941, + 0.3725490196, + 0.0039215686, + 0.7529411764705882, + 0.9960784314, + 0.3843137255, + 0.0156862745, + 0.7568627450980392, + 0.9960784314, + 0.3921568627, + 0.031372549, + 0.7607843137254902, + 0.9960784314, + 0.4039215686, + 0.0470588235, + 0.7647058823529411, + 0.9960784314, + 0.4117647059, + 0.062745098, + 0.7686274509803922, + 0.9960784314, + 0.4235294118, + 0.0784313725, + 0.7725490196078432, + 0.9960784314, + 0.431372549, + 0.0941176471, + 0.7764705882352941, + 0.9960784314, + 0.4431372549, + 0.1098039216, + 0.7803921568627451, + 0.9960784314, + 0.4509803922, + 0.1254901961, + 0.7843137254901961, + 0.9960784314, + 0.462745098, + 0.1411764706, + 0.788235294117647, + 0.9960784314, + 0.4705882353, + 0.1568627451, + 0.792156862745098, + 0.9960784314, + 0.4823529412, + 0.1725490196, + 0.796078431372549, + 0.9960784314, + 0.4901960784, + 0.1882352941, + 0.8, + 0.9960784314, + 0.5019607843, + 0.2039215686, + 0.803921568627451, + 0.9960784314, + 0.5098039216, + 0.2196078431, + 0.807843137254902, + 0.9960784314, + 0.5215686275, + 0.2352941176, + 0.8117647058823529, + 0.9960784314, + 0.5294117647, + 0.2509803922, + 0.8156862745098039, + 0.9960784314, + 0.5411764706, + 0.262745098, + 0.8196078431372549, + 0.9960784314, + 0.5490196078, + 0.2784313725, + 0.8235294117647058, + 0.9960784314, + 0.5607843137, + 0.2941176471, + 0.8274509803921568, + 0.9960784314, + 0.568627451, + 0.3098039216, + 0.8313725490196079, + 0.9960784314, + 0.5803921569, + 0.3254901961, + 0.8352941176470589, + 0.9960784314, + 0.5882352941, + 0.3411764706, + 0.8392156862745098, + 0.9960784314, + 0.6, + 0.3568627451, + 0.8431372549019608, + 0.9960784314, + 0.6078431373, + 0.3725490196, + 0.8470588235294118, + 0.9960784314, + 0.6196078431, + 0.3882352941, + 0.8509803921568627, + 0.9960784314, + 0.6274509804, + 0.4039215686, + 0.8549019607843137, + 0.9960784314, + 0.6392156863, + 0.4196078431, + 0.8588235294117647, + 0.9960784314, + 0.6470588235, + 0.4352941176, + 0.8627450980392157, + 0.9960784314, + 0.6588235294, + 0.4509803922, + 0.8666666666666667, + 0.9960784314, + 0.6666666667, + 0.4666666667, + 0.8705882352941177, + 0.9960784314, + 0.6784313725, + 0.4823529412, + 0.8745098039215686, + 0.9960784314, + 0.6862745098, + 0.4980392157, + 0.8784313725490196, + 0.9960784314, + 0.6980392157, + 0.5137254902, + 0.8823529411764706, + 0.9960784314, + 0.7058823529, + 0.5294117647, + 0.8862745098039215, + 0.9960784314, + 0.7176470588, + 0.5450980392, + 0.8901960784313725, + 0.9960784314, + 0.7254901961, + 0.5607843137, + 0.8941176470588236, + 0.9960784314, + 0.737254902, + 0.5764705882, + 0.8980392156862745, + 0.9960784314, + 0.7450980392, + 0.5921568627, + 0.9019607843137255, + 0.9960784314, + 0.7529411765, + 0.6078431373, + 0.9058823529411765, + 0.9960784314, + 0.7607843137, + 0.6235294118, + 0.9098039215686274, + 0.9960784314, + 0.7725490196, + 0.6392156863, + 0.9137254901960784, + 0.9960784314, + 0.7803921569, + 0.6549019608, + 0.9176470588235294, + 0.9960784314, + 0.7921568627, + 0.6705882353, + 0.9215686274509803, + 0.9960784314, + 0.8, + 0.6862745098, + 0.9254901960784314, + 0.9960784314, + 0.8117647059, + 0.7019607843, + 0.9294117647058824, + 0.9960784314, + 0.8196078431, + 0.7176470588, + 0.9333333333333333, + 0.9960784314, + 0.831372549, + 0.7333333333, + 0.9372549019607843, + 0.9960784314, + 0.8392156863, + 0.7490196078, + 0.9411764705882354, + 0.9960784314, + 0.8509803922, + 0.7607843137, + 0.9450980392156864, + 0.9960784314, + 0.8588235294, + 0.7764705882, + 0.9490196078431372, + 0.9960784314, + 0.8705882353, + 0.7921568627, + 0.9529411764705882, + 0.9960784314, + 0.8784313725, + 0.8078431373, + 0.9568627450980394, + 0.9960784314, + 0.8901960784, + 0.8235294118, + 0.9607843137254903, + 0.9960784314, + 0.8980392157, + 0.8392156863, + 0.9647058823529413, + 0.9960784314, + 0.9098039216, + 0.8549019608, + 0.9686274509803922, + 0.9960784314, + 0.9176470588, + 0.8705882353, + 0.9725490196078431, + 0.9960784314, + 0.9294117647, + 0.8862745098, + 0.9764705882352941, + 0.9960784314, + 0.937254902, + 0.9019607843, + 0.9803921568627451, + 0.9960784314, + 0.9490196078, + 0.9176470588, + 0.984313725490196, + 0.9960784314, + 0.9568627451, + 0.9333333333, + 0.9882352941176471, + 0.9960784314, + 0.968627451, + 0.9490196078, + 0.9921568627450981, + 0.9960784314, + 0.9764705882, + 0.9647058824, + 0.996078431372549, + 0.9960784314, + 0.9882352941, + 0.9803921569, + 1.0, + 0.9960784314, + 0.9882352941, + 0.9803921569, + ], + }, + { + ColorSpace: 'RGB', + Name: 'ge', + RGBPoints: [ + 0.0, + 0.0078431373, + 0.0078431373, + 0.0078431373, + 0.00392156862745098, + 0.0078431373, + 0.0078431373, + 0.0078431373, + 0.00784313725490196, + 0.0078431373, + 0.0078431373, + 0.0078431373, + 0.011764705882352941, + 0.0078431373, + 0.0078431373, + 0.0078431373, + 0.01568627450980392, + 0.0078431373, + 0.0078431373, + 0.0078431373, + 0.0196078431372549, + 0.0078431373, + 0.0078431373, + 0.0078431373, + 0.023529411764705882, + 0.0078431373, + 0.0078431373, + 0.0078431373, + 0.027450980392156862, + 0.0078431373, + 0.0078431373, + 0.0078431373, + 0.03137254901960784, + 0.0078431373, + 0.0078431373, + 0.0078431373, + 0.03529411764705882, + 0.0078431373, + 0.0078431373, + 0.0078431373, + 0.0392156862745098, + 0.0078431373, + 0.0078431373, + 0.0078431373, + 0.043137254901960784, + 0.0078431373, + 0.0078431373, + 0.0078431373, + 0.047058823529411764, + 0.0078431373, + 0.0078431373, + 0.0078431373, + 0.050980392156862744, + 0.0078431373, + 0.0078431373, + 0.0078431373, + 0.054901960784313725, + 0.0078431373, + 0.0078431373, + 0.0078431373, + 0.05882352941176471, + 0.0117647059, + 0.0078431373, + 0.0078431373, + 0.06274509803921569, + 0.0078431373, + 0.0156862745, + 0.0156862745, + 0.06666666666666667, + 0.0078431373, + 0.0235294118, + 0.0235294118, + 0.07058823529411765, + 0.0078431373, + 0.031372549, + 0.031372549, + 0.07450980392156863, + 0.0078431373, + 0.0392156863, + 0.0392156863, + 0.0784313725490196, + 0.0078431373, + 0.0470588235, + 0.0470588235, + 0.08235294117647059, + 0.0078431373, + 0.0549019608, + 0.0549019608, + 0.08627450980392157, + 0.0078431373, + 0.062745098, + 0.062745098, + 0.09019607843137255, + 0.0078431373, + 0.0705882353, + 0.0705882353, + 0.09411764705882353, + 0.0078431373, + 0.0784313725, + 0.0784313725, + 0.09803921568627451, + 0.0078431373, + 0.0901960784, + 0.0862745098, + 0.10196078431372549, + 0.0078431373, + 0.0980392157, + 0.0941176471, + 0.10588235294117647, + 0.0078431373, + 0.1058823529, + 0.1019607843, + 0.10980392156862745, + 0.0078431373, + 0.1137254902, + 0.1098039216, + 0.11372549019607843, + 0.0078431373, + 0.1215686275, + 0.1176470588, + 0.11764705882352942, + 0.0078431373, + 0.1294117647, + 0.1254901961, + 0.12156862745098039, + 0.0078431373, + 0.137254902, + 0.1333333333, + 0.12549019607843137, + 0.0078431373, + 0.1450980392, + 0.1411764706, + 0.12941176470588237, + 0.0078431373, + 0.1529411765, + 0.1490196078, + 0.13333333333333333, + 0.0078431373, + 0.1647058824, + 0.1568627451, + 0.13725490196078433, + 0.0078431373, + 0.1725490196, + 0.1647058824, + 0.1411764705882353, + 0.0078431373, + 0.1803921569, + 0.1725490196, + 0.1450980392156863, + 0.0078431373, + 0.1882352941, + 0.1803921569, + 0.14901960784313725, + 0.0078431373, + 0.1960784314, + 0.1882352941, + 0.15294117647058825, + 0.0078431373, + 0.2039215686, + 0.1960784314, + 0.1568627450980392, + 0.0078431373, + 0.2117647059, + 0.2039215686, + 0.1607843137254902, + 0.0078431373, + 0.2196078431, + 0.2117647059, + 0.16470588235294117, + 0.0078431373, + 0.2274509804, + 0.2196078431, + 0.16862745098039217, + 0.0078431373, + 0.2352941176, + 0.2274509804, + 0.17254901960784313, + 0.0078431373, + 0.2470588235, + 0.2352941176, + 0.17647058823529413, + 0.0078431373, + 0.2509803922, + 0.2431372549, + 0.1803921568627451, + 0.0078431373, + 0.2549019608, + 0.2509803922, + 0.1843137254901961, + 0.0078431373, + 0.262745098, + 0.2509803922, + 0.18823529411764706, + 0.0078431373, + 0.2705882353, + 0.2588235294, + 0.19215686274509805, + 0.0078431373, + 0.2784313725, + 0.2666666667, + 0.19607843137254902, + 0.0078431373, + 0.2862745098, + 0.2745098039, + 0.2, + 0.0078431373, + 0.2941176471, + 0.2823529412, + 0.20392156862745098, + 0.0078431373, + 0.3019607843, + 0.2901960784, + 0.20784313725490197, + 0.0078431373, + 0.3137254902, + 0.2980392157, + 0.21176470588235294, + 0.0078431373, + 0.3215686275, + 0.3058823529, + 0.21568627450980393, + 0.0078431373, + 0.3294117647, + 0.3137254902, + 0.2196078431372549, + 0.0078431373, + 0.337254902, + 0.3215686275, + 0.2235294117647059, + 0.0078431373, + 0.3450980392, + 0.3294117647, + 0.22745098039215686, + 0.0078431373, + 0.3529411765, + 0.337254902, + 0.23137254901960785, + 0.0078431373, + 0.3607843137, + 0.3450980392, + 0.23529411764705885, + 0.0078431373, + 0.368627451, + 0.3529411765, + 0.23921568627450984, + 0.0078431373, + 0.3764705882, + 0.3607843137, + 0.24313725490196078, + 0.0078431373, + 0.3843137255, + 0.368627451, + 0.24705882352941178, + 0.0078431373, + 0.3960784314, + 0.3764705882, + 0.25098039215686274, + 0.0078431373, + 0.4039215686, + 0.3843137255, + 0.2549019607843137, + 0.0078431373, + 0.4117647059, + 0.3921568627, + 0.25882352941176473, + 0.0078431373, + 0.4196078431, + 0.4, + 0.2627450980392157, + 0.0078431373, + 0.4274509804, + 0.4078431373, + 0.26666666666666666, + 0.0078431373, + 0.4352941176, + 0.4156862745, + 0.27058823529411763, + 0.0078431373, + 0.4431372549, + 0.4235294118, + 0.27450980392156865, + 0.0078431373, + 0.4509803922, + 0.431372549, + 0.2784313725490196, + 0.0078431373, + 0.4588235294, + 0.4392156863, + 0.2823529411764706, + 0.0078431373, + 0.4705882353, + 0.4470588235, + 0.28627450980392155, + 0.0078431373, + 0.4784313725, + 0.4549019608, + 0.2901960784313726, + 0.0078431373, + 0.4862745098, + 0.462745098, + 0.29411764705882354, + 0.0078431373, + 0.4941176471, + 0.4705882353, + 0.2980392156862745, + 0.0078431373, + 0.5019607843, + 0.4784313725, + 0.30196078431372547, + 0.0117647059, + 0.5098039216, + 0.4862745098, + 0.3058823529411765, + 0.0196078431, + 0.5019607843, + 0.4941176471, + 0.30980392156862746, + 0.0274509804, + 0.4941176471, + 0.5058823529, + 0.3137254901960784, + 0.0352941176, + 0.4862745098, + 0.5137254902, + 0.3176470588235294, + 0.0431372549, + 0.4784313725, + 0.5215686275, + 0.3215686274509804, + 0.0509803922, + 0.4705882353, + 0.5294117647, + 0.3254901960784314, + 0.0588235294, + 0.462745098, + 0.537254902, + 0.32941176470588235, + 0.0666666667, + 0.4549019608, + 0.5450980392, + 0.3333333333333333, + 0.0745098039, + 0.4470588235, + 0.5529411765, + 0.33725490196078434, + 0.0823529412, + 0.4392156863, + 0.5607843137, + 0.3411764705882353, + 0.0901960784, + 0.431372549, + 0.568627451, + 0.34509803921568627, + 0.0980392157, + 0.4235294118, + 0.5764705882, + 0.34901960784313724, + 0.1058823529, + 0.4156862745, + 0.5843137255, + 0.35294117647058826, + 0.1137254902, + 0.4078431373, + 0.5921568627, + 0.3568627450980392, + 0.1215686275, + 0.4, + 0.6, + 0.3607843137254902, + 0.1294117647, + 0.3921568627, + 0.6078431373, + 0.36470588235294116, + 0.137254902, + 0.3843137255, + 0.6156862745, + 0.3686274509803922, + 0.1450980392, + 0.3764705882, + 0.6235294118, + 0.37254901960784315, + 0.1529411765, + 0.368627451, + 0.631372549, + 0.3764705882352941, + 0.1607843137, + 0.3607843137, + 0.6392156863, + 0.3803921568627451, + 0.168627451, + 0.3529411765, + 0.6470588235, + 0.3843137254901961, + 0.1764705882, + 0.3450980392, + 0.6549019608, + 0.38823529411764707, + 0.1843137255, + 0.337254902, + 0.662745098, + 0.39215686274509803, + 0.1921568627, + 0.3294117647, + 0.6705882353, + 0.396078431372549, + 0.2, + 0.3215686275, + 0.6784313725, + 0.4, + 0.2078431373, + 0.3137254902, + 0.6862745098, + 0.403921568627451, + 0.2156862745, + 0.3058823529, + 0.6941176471, + 0.40784313725490196, + 0.2235294118, + 0.2980392157, + 0.7019607843, + 0.4117647058823529, + 0.231372549, + 0.2901960784, + 0.7098039216, + 0.41568627450980394, + 0.2392156863, + 0.2823529412, + 0.7176470588, + 0.4196078431372549, + 0.2470588235, + 0.2745098039, + 0.7254901961, + 0.4235294117647059, + 0.2509803922, + 0.2666666667, + 0.7333333333, + 0.42745098039215684, + 0.2509803922, + 0.2588235294, + 0.7411764706, + 0.43137254901960786, + 0.2588235294, + 0.2509803922, + 0.7490196078, + 0.43529411764705883, + 0.2666666667, + 0.2509803922, + 0.7490196078, + 0.4392156862745098, + 0.2745098039, + 0.2431372549, + 0.7568627451, + 0.44313725490196076, + 0.2823529412, + 0.2352941176, + 0.7647058824, + 0.4470588235294118, + 0.2901960784, + 0.2274509804, + 0.7725490196, + 0.45098039215686275, + 0.2980392157, + 0.2196078431, + 0.7803921569, + 0.4549019607843137, + 0.3058823529, + 0.2117647059, + 0.7882352941, + 0.4588235294117647, + 0.3137254902, + 0.2039215686, + 0.7960784314, + 0.4627450980392157, + 0.3215686275, + 0.1960784314, + 0.8039215686, + 0.4666666666666667, + 0.3294117647, + 0.1882352941, + 0.8117647059, + 0.4705882352941177, + 0.337254902, + 0.1803921569, + 0.8196078431, + 0.4745098039215686, + 0.3450980392, + 0.1725490196, + 0.8274509804, + 0.4784313725490197, + 0.3529411765, + 0.1647058824, + 0.8352941176, + 0.48235294117647065, + 0.3607843137, + 0.1568627451, + 0.8431372549, + 0.48627450980392156, + 0.368627451, + 0.1490196078, + 0.8509803922, + 0.49019607843137253, + 0.3764705882, + 0.1411764706, + 0.8588235294, + 0.49411764705882355, + 0.3843137255, + 0.1333333333, + 0.8666666667, + 0.4980392156862745, + 0.3921568627, + 0.1254901961, + 0.8745098039, + 0.5019607843137255, + 0.4, + 0.1176470588, + 0.8823529412, + 0.5058823529411764, + 0.4078431373, + 0.1098039216, + 0.8901960784, + 0.5098039215686274, + 0.4156862745, + 0.1019607843, + 0.8980392157, + 0.5137254901960784, + 0.4235294118, + 0.0941176471, + 0.9058823529, + 0.5176470588235295, + 0.431372549, + 0.0862745098, + 0.9137254902, + 0.5215686274509804, + 0.4392156863, + 0.0784313725, + 0.9215686275, + 0.5254901960784314, + 0.4470588235, + 0.0705882353, + 0.9294117647, + 0.5294117647058824, + 0.4549019608, + 0.062745098, + 0.937254902, + 0.5333333333333333, + 0.462745098, + 0.0549019608, + 0.9450980392, + 0.5372549019607843, + 0.4705882353, + 0.0470588235, + 0.9529411765, + 0.5411764705882353, + 0.4784313725, + 0.0392156863, + 0.9607843137, + 0.5450980392156862, + 0.4862745098, + 0.031372549, + 0.968627451, + 0.5490196078431373, + 0.4941176471, + 0.0235294118, + 0.9764705882, + 0.5529411764705883, + 0.4980392157, + 0.0156862745, + 0.9843137255, + 0.5568627450980392, + 0.5058823529, + 0.0078431373, + 0.9921568627, + 0.5607843137254902, + 0.5137254902, + 0.0156862745, + 0.9803921569, + 0.5647058823529412, + 0.5215686275, + 0.0235294118, + 0.9647058824, + 0.5686274509803921, + 0.5294117647, + 0.0352941176, + 0.9490196078, + 0.5725490196078431, + 0.537254902, + 0.0431372549, + 0.9333333333, + 0.5764705882352941, + 0.5450980392, + 0.0509803922, + 0.9176470588, + 0.5803921568627451, + 0.5529411765, + 0.062745098, + 0.9019607843, + 0.5843137254901961, + 0.5607843137, + 0.0705882353, + 0.8862745098, + 0.5882352941176471, + 0.568627451, + 0.0784313725, + 0.8705882353, + 0.592156862745098, + 0.5764705882, + 0.0901960784, + 0.8549019608, + 0.596078431372549, + 0.5843137255, + 0.0980392157, + 0.8392156863, + 0.6, + 0.5921568627, + 0.1098039216, + 0.8235294118, + 0.6039215686274509, + 0.6, + 0.1176470588, + 0.8078431373, + 0.6078431372549019, + 0.6078431373, + 0.1254901961, + 0.7921568627, + 0.611764705882353, + 0.6156862745, + 0.137254902, + 0.7764705882, + 0.615686274509804, + 0.6235294118, + 0.1450980392, + 0.7607843137, + 0.6196078431372549, + 0.631372549, + 0.1529411765, + 0.7490196078, + 0.6235294117647059, + 0.6392156863, + 0.1647058824, + 0.737254902, + 0.6274509803921569, + 0.6470588235, + 0.1725490196, + 0.7215686275, + 0.6313725490196078, + 0.6549019608, + 0.1843137255, + 0.7058823529, + 0.6352941176470588, + 0.662745098, + 0.1921568627, + 0.6901960784, + 0.6392156862745098, + 0.6705882353, + 0.2, + 0.6745098039, + 0.6431372549019608, + 0.6784313725, + 0.2117647059, + 0.6588235294, + 0.6470588235294118, + 0.6862745098, + 0.2196078431, + 0.6431372549, + 0.6509803921568628, + 0.6941176471, + 0.2274509804, + 0.6274509804, + 0.6549019607843137, + 0.7019607843, + 0.2392156863, + 0.6117647059, + 0.6588235294117647, + 0.7098039216, + 0.2470588235, + 0.5960784314, + 0.6627450980392157, + 0.7176470588, + 0.2509803922, + 0.5803921569, + 0.6666666666666666, + 0.7254901961, + 0.2588235294, + 0.5647058824, + 0.6705882352941176, + 0.7333333333, + 0.2666666667, + 0.5490196078, + 0.6745098039215687, + 0.7411764706, + 0.2784313725, + 0.5333333333, + 0.6784313725490196, + 0.7490196078, + 0.2862745098, + 0.5176470588, + 0.6823529411764706, + 0.7490196078, + 0.2941176471, + 0.5019607843, + 0.6862745098039216, + 0.7529411765, + 0.3058823529, + 0.4862745098, + 0.6901960784313725, + 0.7607843137, + 0.3137254902, + 0.4705882353, + 0.6941176470588235, + 0.768627451, + 0.3215686275, + 0.4549019608, + 0.6980392156862745, + 0.7764705882, + 0.3333333333, + 0.4392156863, + 0.7019607843137254, + 0.7843137255, + 0.3411764706, + 0.4235294118, + 0.7058823529411765, + 0.7921568627, + 0.3529411765, + 0.4078431373, + 0.7098039215686275, + 0.8, + 0.3607843137, + 0.3921568627, + 0.7137254901960784, + 0.8078431373, + 0.368627451, + 0.3764705882, + 0.7176470588235294, + 0.8156862745, + 0.3803921569, + 0.3607843137, + 0.7215686274509804, + 0.8235294118, + 0.3882352941, + 0.3450980392, + 0.7254901960784313, + 0.831372549, + 0.3960784314, + 0.3294117647, + 0.7294117647058823, + 0.8392156863, + 0.4078431373, + 0.3137254902, + 0.7333333333333333, + 0.8470588235, + 0.4156862745, + 0.2980392157, + 0.7372549019607844, + 0.8549019608, + 0.4274509804, + 0.2823529412, + 0.7411764705882353, + 0.862745098, + 0.4352941176, + 0.2666666667, + 0.7450980392156863, + 0.8705882353, + 0.4431372549, + 0.2509803922, + 0.7490196078431373, + 0.8784313725, + 0.4549019608, + 0.2431372549, + 0.7529411764705882, + 0.8862745098, + 0.462745098, + 0.2274509804, + 0.7568627450980392, + 0.8941176471, + 0.4705882353, + 0.2117647059, + 0.7607843137254902, + 0.9019607843, + 0.4823529412, + 0.1960784314, + 0.7647058823529411, + 0.9098039216, + 0.4901960784, + 0.1803921569, + 0.7686274509803922, + 0.9176470588, + 0.4980392157, + 0.1647058824, + 0.7725490196078432, + 0.9254901961, + 0.5098039216, + 0.1490196078, + 0.7764705882352941, + 0.9333333333, + 0.5176470588, + 0.1333333333, + 0.7803921568627451, + 0.9411764706, + 0.5294117647, + 0.1176470588, + 0.7843137254901961, + 0.9490196078, + 0.537254902, + 0.1019607843, + 0.788235294117647, + 0.9568627451, + 0.5450980392, + 0.0862745098, + 0.792156862745098, + 0.9647058824, + 0.5568627451, + 0.0705882353, + 0.796078431372549, + 0.9725490196, + 0.5647058824, + 0.0549019608, + 0.8, + 0.9803921569, + 0.5725490196, + 0.0392156863, + 0.803921568627451, + 0.9882352941, + 0.5843137255, + 0.0235294118, + 0.807843137254902, + 0.9921568627, + 0.5921568627, + 0.0078431373, + 0.8117647058823529, + 0.9921568627, + 0.6039215686, + 0.0274509804, + 0.8156862745098039, + 0.9921568627, + 0.6117647059, + 0.0509803922, + 0.8196078431372549, + 0.9921568627, + 0.6196078431, + 0.0745098039, + 0.8235294117647058, + 0.9921568627, + 0.631372549, + 0.0980392157, + 0.8274509803921568, + 0.9921568627, + 0.6392156863, + 0.1215686275, + 0.8313725490196079, + 0.9921568627, + 0.6470588235, + 0.1411764706, + 0.8352941176470589, + 0.9921568627, + 0.6588235294, + 0.1647058824, + 0.8392156862745098, + 0.9921568627, + 0.6666666667, + 0.1882352941, + 0.8431372549019608, + 0.9921568627, + 0.6784313725, + 0.2117647059, + 0.8470588235294118, + 0.9921568627, + 0.6862745098, + 0.2352941176, + 0.8509803921568627, + 0.9921568627, + 0.6941176471, + 0.2509803922, + 0.8549019607843137, + 0.9921568627, + 0.7058823529, + 0.2705882353, + 0.8588235294117647, + 0.9921568627, + 0.7137254902, + 0.2941176471, + 0.8627450980392157, + 0.9921568627, + 0.7215686275, + 0.3176470588, + 0.8666666666666667, + 0.9921568627, + 0.7333333333, + 0.3411764706, + 0.8705882352941177, + 0.9921568627, + 0.7411764706, + 0.3647058824, + 0.8745098039215686, + 0.9921568627, + 0.7490196078, + 0.3843137255, + 0.8784313725490196, + 0.9921568627, + 0.7529411765, + 0.4078431373, + 0.8823529411764706, + 0.9921568627, + 0.7607843137, + 0.431372549, + 0.8862745098039215, + 0.9921568627, + 0.7725490196, + 0.4549019608, + 0.8901960784313725, + 0.9921568627, + 0.7803921569, + 0.4784313725, + 0.8941176470588236, + 0.9921568627, + 0.7882352941, + 0.4980392157, + 0.8980392156862745, + 0.9921568627, + 0.8, + 0.5215686275, + 0.9019607843137255, + 0.9921568627, + 0.8078431373, + 0.5450980392, + 0.9058823529411765, + 0.9921568627, + 0.8156862745, + 0.568627451, + 0.9098039215686274, + 0.9921568627, + 0.8274509804, + 0.5921568627, + 0.9137254901960784, + 0.9921568627, + 0.8352941176, + 0.6156862745, + 0.9176470588235294, + 0.9921568627, + 0.8470588235, + 0.6352941176, + 0.9215686274509803, + 0.9921568627, + 0.8549019608, + 0.6588235294, + 0.9254901960784314, + 0.9921568627, + 0.862745098, + 0.6823529412, + 0.9294117647058824, + 0.9921568627, + 0.8745098039, + 0.7058823529, + 0.9333333333333333, + 0.9921568627, + 0.8823529412, + 0.7294117647, + 0.9372549019607843, + 0.9921568627, + 0.8901960784, + 0.7490196078, + 0.9411764705882354, + 0.9921568627, + 0.9019607843, + 0.7647058824, + 0.9450980392156864, + 0.9921568627, + 0.9098039216, + 0.7882352941, + 0.9490196078431372, + 0.9921568627, + 0.9215686275, + 0.8117647059, + 0.9529411764705882, + 0.9921568627, + 0.9294117647, + 0.8352941176, + 0.9568627450980394, + 0.9921568627, + 0.937254902, + 0.8588235294, + 0.9607843137254903, + 0.9921568627, + 0.9490196078, + 0.8784313725, + 0.9647058823529413, + 0.9921568627, + 0.9568627451, + 0.9019607843, + 0.9686274509803922, + 0.9921568627, + 0.9647058824, + 0.9254901961, + 0.9725490196078431, + 0.9921568627, + 0.9764705882, + 0.9490196078, + 0.9764705882352941, + 0.9921568627, + 0.9843137255, + 0.9725490196, + 0.9803921568627451, + 0.9921568627, + 0.9921568627, + 0.9921568627, + 0.984313725490196, + 0.9921568627, + 0.9921568627, + 0.9921568627, + 0.9882352941176471, + 0.9921568627, + 0.9921568627, + 0.9921568627, + 0.9921568627450981, + 0.9921568627, + 0.9921568627, + 0.9921568627, + 0.996078431372549, + 0.9921568627, + 0.9921568627, + 0.9921568627, + 1.0, + 0.9921568627, + 0.9921568627, + 0.9921568627, + ], + }, + { + ColorSpace: 'RGB', + Name: 'siemens', + RGBPoints: [ + 0.0, + 0.0078431373, + 0.0039215686, + 0.1254901961, + 0.00392156862745098, + 0.0078431373, + 0.0039215686, + 0.1254901961, + 0.00784313725490196, + 0.0078431373, + 0.0039215686, + 0.1882352941, + 0.011764705882352941, + 0.0117647059, + 0.0039215686, + 0.2509803922, + 0.01568627450980392, + 0.0117647059, + 0.0039215686, + 0.3098039216, + 0.0196078431372549, + 0.0156862745, + 0.0039215686, + 0.3725490196, + 0.023529411764705882, + 0.0156862745, + 0.0039215686, + 0.3725490196, + 0.027450980392156862, + 0.0156862745, + 0.0039215686, + 0.3725490196, + 0.03137254901960784, + 0.0156862745, + 0.0039215686, + 0.3725490196, + 0.03529411764705882, + 0.0156862745, + 0.0039215686, + 0.3725490196, + 0.0392156862745098, + 0.0156862745, + 0.0039215686, + 0.3725490196, + 0.043137254901960784, + 0.0156862745, + 0.0039215686, + 0.3725490196, + 0.047058823529411764, + 0.0156862745, + 0.0039215686, + 0.3725490196, + 0.050980392156862744, + 0.0156862745, + 0.0039215686, + 0.3725490196, + 0.054901960784313725, + 0.0156862745, + 0.0039215686, + 0.3725490196, + 0.05882352941176471, + 0.0156862745, + 0.0039215686, + 0.3725490196, + 0.06274509803921569, + 0.0156862745, + 0.0039215686, + 0.3882352941, + 0.06666666666666667, + 0.0156862745, + 0.0039215686, + 0.4078431373, + 0.07058823529411765, + 0.0156862745, + 0.0039215686, + 0.4235294118, + 0.07450980392156863, + 0.0156862745, + 0.0039215686, + 0.4431372549, + 0.0784313725490196, + 0.0156862745, + 0.0039215686, + 0.462745098, + 0.08235294117647059, + 0.0156862745, + 0.0039215686, + 0.4784313725, + 0.08627450980392157, + 0.0156862745, + 0.0039215686, + 0.4980392157, + 0.09019607843137255, + 0.0196078431, + 0.0039215686, + 0.5137254902, + 0.09411764705882353, + 0.0196078431, + 0.0039215686, + 0.5333333333, + 0.09803921568627451, + 0.0196078431, + 0.0039215686, + 0.5529411765, + 0.10196078431372549, + 0.0196078431, + 0.0039215686, + 0.568627451, + 0.10588235294117647, + 0.0196078431, + 0.0039215686, + 0.5882352941, + 0.10980392156862745, + 0.0196078431, + 0.0039215686, + 0.6039215686, + 0.11372549019607843, + 0.0196078431, + 0.0039215686, + 0.6235294118, + 0.11764705882352942, + 0.0196078431, + 0.0039215686, + 0.6431372549, + 0.12156862745098039, + 0.0235294118, + 0.0039215686, + 0.6588235294, + 0.12549019607843137, + 0.0235294118, + 0.0039215686, + 0.6784313725, + 0.12941176470588237, + 0.0235294118, + 0.0039215686, + 0.6980392157, + 0.13333333333333333, + 0.0235294118, + 0.0039215686, + 0.7137254902, + 0.13725490196078433, + 0.0235294118, + 0.0039215686, + 0.7333333333, + 0.1411764705882353, + 0.0235294118, + 0.0039215686, + 0.7490196078, + 0.1450980392156863, + 0.0235294118, + 0.0039215686, + 0.7647058824, + 0.14901960784313725, + 0.0235294118, + 0.0039215686, + 0.7843137255, + 0.15294117647058825, + 0.0274509804, + 0.0039215686, + 0.8, + 0.1568627450980392, + 0.0274509804, + 0.0039215686, + 0.8196078431, + 0.1607843137254902, + 0.0274509804, + 0.0039215686, + 0.8352941176, + 0.16470588235294117, + 0.0274509804, + 0.0039215686, + 0.8549019608, + 0.16862745098039217, + 0.0274509804, + 0.0039215686, + 0.8745098039, + 0.17254901960784313, + 0.0274509804, + 0.0039215686, + 0.8901960784, + 0.17647058823529413, + 0.0274509804, + 0.0039215686, + 0.9098039216, + 0.1803921568627451, + 0.031372549, + 0.0039215686, + 0.9294117647, + 0.1843137254901961, + 0.031372549, + 0.0039215686, + 0.9254901961, + 0.18823529411764706, + 0.0509803922, + 0.0039215686, + 0.9098039216, + 0.19215686274509805, + 0.0705882353, + 0.0039215686, + 0.8901960784, + 0.19607843137254902, + 0.0901960784, + 0.0039215686, + 0.8705882353, + 0.2, + 0.1137254902, + 0.0039215686, + 0.8509803922, + 0.20392156862745098, + 0.1333333333, + 0.0039215686, + 0.831372549, + 0.20784313725490197, + 0.1529411765, + 0.0039215686, + 0.8117647059, + 0.21176470588235294, + 0.1725490196, + 0.0039215686, + 0.7921568627, + 0.21568627450980393, + 0.1960784314, + 0.0039215686, + 0.7725490196, + 0.2196078431372549, + 0.2156862745, + 0.0039215686, + 0.7529411765, + 0.2235294117647059, + 0.2352941176, + 0.0039215686, + 0.737254902, + 0.22745098039215686, + 0.2509803922, + 0.0039215686, + 0.7176470588, + 0.23137254901960785, + 0.2745098039, + 0.0039215686, + 0.6980392157, + 0.23529411764705885, + 0.2941176471, + 0.0039215686, + 0.6784313725, + 0.23921568627450984, + 0.3137254902, + 0.0039215686, + 0.6588235294, + 0.24313725490196078, + 0.3333333333, + 0.0039215686, + 0.6392156863, + 0.24705882352941178, + 0.3568627451, + 0.0039215686, + 0.6196078431, + 0.25098039215686274, + 0.3764705882, + 0.0039215686, + 0.6, + 0.2549019607843137, + 0.3960784314, + 0.0039215686, + 0.5803921569, + 0.25882352941176473, + 0.4156862745, + 0.0039215686, + 0.5607843137, + 0.2627450980392157, + 0.4392156863, + 0.0039215686, + 0.5411764706, + 0.26666666666666666, + 0.4588235294, + 0.0039215686, + 0.5215686275, + 0.27058823529411763, + 0.4784313725, + 0.0039215686, + 0.5019607843, + 0.27450980392156865, + 0.4980392157, + 0.0039215686, + 0.4823529412, + 0.2784313725490196, + 0.5215686275, + 0.0039215686, + 0.4666666667, + 0.2823529411764706, + 0.5411764706, + 0.0039215686, + 0.4470588235, + 0.28627450980392155, + 0.5607843137, + 0.0039215686, + 0.4274509804, + 0.2901960784313726, + 0.5803921569, + 0.0039215686, + 0.4078431373, + 0.29411764705882354, + 0.6039215686, + 0.0039215686, + 0.3882352941, + 0.2980392156862745, + 0.6235294118, + 0.0039215686, + 0.368627451, + 0.30196078431372547, + 0.6431372549, + 0.0039215686, + 0.3490196078, + 0.3058823529411765, + 0.662745098, + 0.0039215686, + 0.3294117647, + 0.30980392156862746, + 0.6862745098, + 0.0039215686, + 0.3098039216, + 0.3137254901960784, + 0.7058823529, + 0.0039215686, + 0.2901960784, + 0.3176470588235294, + 0.7254901961, + 0.0039215686, + 0.2705882353, + 0.3215686274509804, + 0.7450980392, + 0.0039215686, + 0.2509803922, + 0.3254901960784314, + 0.7647058824, + 0.0039215686, + 0.2352941176, + 0.32941176470588235, + 0.7843137255, + 0.0039215686, + 0.2156862745, + 0.3333333333333333, + 0.8039215686, + 0.0039215686, + 0.1960784314, + 0.33725490196078434, + 0.8235294118, + 0.0039215686, + 0.1764705882, + 0.3411764705882353, + 0.8470588235, + 0.0039215686, + 0.1568627451, + 0.34509803921568627, + 0.8666666667, + 0.0039215686, + 0.137254902, + 0.34901960784313724, + 0.8862745098, + 0.0039215686, + 0.1176470588, + 0.35294117647058826, + 0.9058823529, + 0.0039215686, + 0.0980392157, + 0.3568627450980392, + 0.9294117647, + 0.0039215686, + 0.0784313725, + 0.3607843137254902, + 0.9490196078, + 0.0039215686, + 0.0588235294, + 0.36470588235294116, + 0.968627451, + 0.0039215686, + 0.0392156863, + 0.3686274509803922, + 0.9921568627, + 0.0039215686, + 0.0235294118, + 0.37254901960784315, + 0.9529411765, + 0.0039215686, + 0.0588235294, + 0.3764705882352941, + 0.9529411765, + 0.0078431373, + 0.0549019608, + 0.3803921568627451, + 0.9529411765, + 0.0156862745, + 0.0549019608, + 0.3843137254901961, + 0.9529411765, + 0.0235294118, + 0.0549019608, + 0.38823529411764707, + 0.9529411765, + 0.031372549, + 0.0549019608, + 0.39215686274509803, + 0.9529411765, + 0.0352941176, + 0.0549019608, + 0.396078431372549, + 0.9529411765, + 0.0431372549, + 0.0549019608, + 0.4, + 0.9529411765, + 0.0509803922, + 0.0549019608, + 0.403921568627451, + 0.9529411765, + 0.0588235294, + 0.0549019608, + 0.40784313725490196, + 0.9529411765, + 0.062745098, + 0.0549019608, + 0.4117647058823529, + 0.9529411765, + 0.0705882353, + 0.0549019608, + 0.41568627450980394, + 0.9529411765, + 0.0784313725, + 0.0509803922, + 0.4196078431372549, + 0.9529411765, + 0.0862745098, + 0.0509803922, + 0.4235294117647059, + 0.9568627451, + 0.0941176471, + 0.0509803922, + 0.42745098039215684, + 0.9568627451, + 0.0980392157, + 0.0509803922, + 0.43137254901960786, + 0.9568627451, + 0.1058823529, + 0.0509803922, + 0.43529411764705883, + 0.9568627451, + 0.1137254902, + 0.0509803922, + 0.4392156862745098, + 0.9568627451, + 0.1215686275, + 0.0509803922, + 0.44313725490196076, + 0.9568627451, + 0.1254901961, + 0.0509803922, + 0.4470588235294118, + 0.9568627451, + 0.1333333333, + 0.0509803922, + 0.45098039215686275, + 0.9568627451, + 0.1411764706, + 0.0509803922, + 0.4549019607843137, + 0.9568627451, + 0.1490196078, + 0.0470588235, + 0.4588235294117647, + 0.9568627451, + 0.1568627451, + 0.0470588235, + 0.4627450980392157, + 0.9568627451, + 0.1607843137, + 0.0470588235, + 0.4666666666666667, + 0.9568627451, + 0.168627451, + 0.0470588235, + 0.4705882352941177, + 0.9607843137, + 0.1764705882, + 0.0470588235, + 0.4745098039215686, + 0.9607843137, + 0.1843137255, + 0.0470588235, + 0.4784313725490197, + 0.9607843137, + 0.1882352941, + 0.0470588235, + 0.48235294117647065, + 0.9607843137, + 0.1960784314, + 0.0470588235, + 0.48627450980392156, + 0.9607843137, + 0.2039215686, + 0.0470588235, + 0.49019607843137253, + 0.9607843137, + 0.2117647059, + 0.0470588235, + 0.49411764705882355, + 0.9607843137, + 0.2196078431, + 0.0431372549, + 0.4980392156862745, + 0.9607843137, + 0.2235294118, + 0.0431372549, + 0.5019607843137255, + 0.9607843137, + 0.231372549, + 0.0431372549, + 0.5058823529411764, + 0.9607843137, + 0.2392156863, + 0.0431372549, + 0.5098039215686274, + 0.9607843137, + 0.2470588235, + 0.0431372549, + 0.5137254901960784, + 0.9607843137, + 0.2509803922, + 0.0431372549, + 0.5176470588235295, + 0.9647058824, + 0.2549019608, + 0.0431372549, + 0.5215686274509804, + 0.9647058824, + 0.262745098, + 0.0431372549, + 0.5254901960784314, + 0.9647058824, + 0.2705882353, + 0.0431372549, + 0.5294117647058824, + 0.9647058824, + 0.2745098039, + 0.0431372549, + 0.5333333333333333, + 0.9647058824, + 0.2823529412, + 0.0392156863, + 0.5372549019607843, + 0.9647058824, + 0.2901960784, + 0.0392156863, + 0.5411764705882353, + 0.9647058824, + 0.2980392157, + 0.0392156863, + 0.5450980392156862, + 0.9647058824, + 0.3058823529, + 0.0392156863, + 0.5490196078431373, + 0.9647058824, + 0.3098039216, + 0.0392156863, + 0.5529411764705883, + 0.9647058824, + 0.3176470588, + 0.0392156863, + 0.5568627450980392, + 0.9647058824, + 0.3254901961, + 0.0392156863, + 0.5607843137254902, + 0.9647058824, + 0.3333333333, + 0.0392156863, + 0.5647058823529412, + 0.9647058824, + 0.337254902, + 0.0392156863, + 0.5686274509803921, + 0.968627451, + 0.3450980392, + 0.0392156863, + 0.5725490196078431, + 0.968627451, + 0.3529411765, + 0.0352941176, + 0.5764705882352941, + 0.968627451, + 0.3607843137, + 0.0352941176, + 0.5803921568627451, + 0.968627451, + 0.368627451, + 0.0352941176, + 0.5843137254901961, + 0.968627451, + 0.3725490196, + 0.0352941176, + 0.5882352941176471, + 0.968627451, + 0.3803921569, + 0.0352941176, + 0.592156862745098, + 0.968627451, + 0.3882352941, + 0.0352941176, + 0.596078431372549, + 0.968627451, + 0.3960784314, + 0.0352941176, + 0.6, + 0.968627451, + 0.4, + 0.0352941176, + 0.6039215686274509, + 0.968627451, + 0.4078431373, + 0.0352941176, + 0.6078431372549019, + 0.968627451, + 0.4156862745, + 0.0352941176, + 0.611764705882353, + 0.968627451, + 0.4235294118, + 0.031372549, + 0.615686274509804, + 0.9725490196, + 0.431372549, + 0.031372549, + 0.6196078431372549, + 0.9725490196, + 0.4352941176, + 0.031372549, + 0.6235294117647059, + 0.9725490196, + 0.4431372549, + 0.031372549, + 0.6274509803921569, + 0.9725490196, + 0.4509803922, + 0.031372549, + 0.6313725490196078, + 0.9725490196, + 0.4588235294, + 0.031372549, + 0.6352941176470588, + 0.9725490196, + 0.462745098, + 0.031372549, + 0.6392156862745098, + 0.9725490196, + 0.4705882353, + 0.031372549, + 0.6431372549019608, + 0.9725490196, + 0.4784313725, + 0.031372549, + 0.6470588235294118, + 0.9725490196, + 0.4862745098, + 0.031372549, + 0.6509803921568628, + 0.9725490196, + 0.4941176471, + 0.0274509804, + 0.6549019607843137, + 0.9725490196, + 0.4980392157, + 0.0274509804, + 0.6588235294117647, + 0.9725490196, + 0.5058823529, + 0.0274509804, + 0.6627450980392157, + 0.9764705882, + 0.5137254902, + 0.0274509804, + 0.6666666666666666, + 0.9764705882, + 0.5215686275, + 0.0274509804, + 0.6705882352941176, + 0.9764705882, + 0.5254901961, + 0.0274509804, + 0.6745098039215687, + 0.9764705882, + 0.5333333333, + 0.0274509804, + 0.6784313725490196, + 0.9764705882, + 0.5411764706, + 0.0274509804, + 0.6823529411764706, + 0.9764705882, + 0.5490196078, + 0.0274509804, + 0.6862745098039216, + 0.9764705882, + 0.5529411765, + 0.0274509804, + 0.6901960784313725, + 0.9764705882, + 0.5607843137, + 0.0235294118, + 0.6941176470588235, + 0.9764705882, + 0.568627451, + 0.0235294118, + 0.6980392156862745, + 0.9764705882, + 0.5764705882, + 0.0235294118, + 0.7019607843137254, + 0.9764705882, + 0.5843137255, + 0.0235294118, + 0.7058823529411765, + 0.9764705882, + 0.5882352941, + 0.0235294118, + 0.7098039215686275, + 0.9764705882, + 0.5960784314, + 0.0235294118, + 0.7137254901960784, + 0.9803921569, + 0.6039215686, + 0.0235294118, + 0.7176470588235294, + 0.9803921569, + 0.6117647059, + 0.0235294118, + 0.7215686274509804, + 0.9803921569, + 0.6156862745, + 0.0235294118, + 0.7254901960784313, + 0.9803921569, + 0.6235294118, + 0.0235294118, + 0.7294117647058823, + 0.9803921569, + 0.631372549, + 0.0196078431, + 0.7333333333333333, + 0.9803921569, + 0.6392156863, + 0.0196078431, + 0.7372549019607844, + 0.9803921569, + 0.6470588235, + 0.0196078431, + 0.7411764705882353, + 0.9803921569, + 0.6509803922, + 0.0196078431, + 0.7450980392156863, + 0.9803921569, + 0.6588235294, + 0.0196078431, + 0.7490196078431373, + 0.9803921569, + 0.6666666667, + 0.0196078431, + 0.7529411764705882, + 0.9803921569, + 0.6745098039, + 0.0196078431, + 0.7568627450980392, + 0.9803921569, + 0.6784313725, + 0.0196078431, + 0.7607843137254902, + 0.9843137255, + 0.6862745098, + 0.0196078431, + 0.7647058823529411, + 0.9843137255, + 0.6941176471, + 0.0196078431, + 0.7686274509803922, + 0.9843137255, + 0.7019607843, + 0.0156862745, + 0.7725490196078432, + 0.9843137255, + 0.7098039216, + 0.0156862745, + 0.7764705882352941, + 0.9843137255, + 0.7137254902, + 0.0156862745, + 0.7803921568627451, + 0.9843137255, + 0.7215686275, + 0.0156862745, + 0.7843137254901961, + 0.9843137255, + 0.7294117647, + 0.0156862745, + 0.788235294117647, + 0.9843137255, + 0.737254902, + 0.0156862745, + 0.792156862745098, + 0.9843137255, + 0.7411764706, + 0.0156862745, + 0.796078431372549, + 0.9843137255, + 0.7490196078, + 0.0156862745, + 0.8, + 0.9843137255, + 0.7529411765, + 0.0156862745, + 0.803921568627451, + 0.9843137255, + 0.7607843137, + 0.0156862745, + 0.807843137254902, + 0.9882352941, + 0.768627451, + 0.0156862745, + 0.8117647058823529, + 0.9882352941, + 0.768627451, + 0.0156862745, + 0.8156862745098039, + 0.9843137255, + 0.7843137255, + 0.0117647059, + 0.8196078431372549, + 0.9843137255, + 0.8, + 0.0117647059, + 0.8235294117647058, + 0.9843137255, + 0.8156862745, + 0.0117647059, + 0.8274509803921568, + 0.9803921569, + 0.831372549, + 0.0117647059, + 0.8313725490196079, + 0.9803921569, + 0.8431372549, + 0.0117647059, + 0.8352941176470589, + 0.9803921569, + 0.8588235294, + 0.0078431373, + 0.8392156862745098, + 0.9803921569, + 0.8745098039, + 0.0078431373, + 0.8431372549019608, + 0.9764705882, + 0.8901960784, + 0.0078431373, + 0.8470588235294118, + 0.9764705882, + 0.9058823529, + 0.0078431373, + 0.8509803921568627, + 0.9764705882, + 0.9176470588, + 0.0078431373, + 0.8549019607843137, + 0.9764705882, + 0.9333333333, + 0.0039215686, + 0.8588235294117647, + 0.9725490196, + 0.9490196078, + 0.0039215686, + 0.8627450980392157, + 0.9725490196, + 0.9647058824, + 0.0039215686, + 0.8666666666666667, + 0.9725490196, + 0.9803921569, + 0.0039215686, + 0.8705882352941177, + 0.9725490196, + 0.9960784314, + 0.0039215686, + 0.8745098039215686, + 0.9725490196, + 0.9960784314, + 0.0039215686, + 0.8784313725490196, + 0.9725490196, + 0.9960784314, + 0.0352941176, + 0.8823529411764706, + 0.9725490196, + 0.9960784314, + 0.0666666667, + 0.8862745098039215, + 0.9725490196, + 0.9960784314, + 0.0980392157, + 0.8901960784313725, + 0.9725490196, + 0.9960784314, + 0.1294117647, + 0.8941176470588236, + 0.9725490196, + 0.9960784314, + 0.1647058824, + 0.8980392156862745, + 0.9764705882, + 0.9960784314, + 0.1960784314, + 0.9019607843137255, + 0.9764705882, + 0.9960784314, + 0.2274509804, + 0.9058823529411765, + 0.9764705882, + 0.9960784314, + 0.2549019608, + 0.9098039215686274, + 0.9764705882, + 0.9960784314, + 0.2901960784, + 0.9137254901960784, + 0.9764705882, + 0.9960784314, + 0.3215686275, + 0.9176470588235294, + 0.9803921569, + 0.9960784314, + 0.3529411765, + 0.9215686274509803, + 0.9803921569, + 0.9960784314, + 0.3843137255, + 0.9254901960784314, + 0.9803921569, + 0.9960784314, + 0.4156862745, + 0.9294117647058824, + 0.9803921569, + 0.9960784314, + 0.4509803922, + 0.9333333333333333, + 0.9803921569, + 0.9960784314, + 0.4823529412, + 0.9372549019607843, + 0.9843137255, + 0.9960784314, + 0.5137254902, + 0.9411764705882354, + 0.9843137255, + 0.9960784314, + 0.5450980392, + 0.9450980392156864, + 0.9843137255, + 0.9960784314, + 0.5803921569, + 0.9490196078431372, + 0.9843137255, + 0.9960784314, + 0.6117647059, + 0.9529411764705882, + 0.9843137255, + 0.9960784314, + 0.6431372549, + 0.9568627450980394, + 0.9882352941, + 0.9960784314, + 0.6745098039, + 0.9607843137254903, + 0.9882352941, + 0.9960784314, + 0.7058823529, + 0.9647058823529413, + 0.9882352941, + 0.9960784314, + 0.7411764706, + 0.9686274509803922, + 0.9882352941, + 0.9960784314, + 0.768627451, + 0.9725490196078431, + 0.9882352941, + 0.9960784314, + 0.8, + 0.9764705882352941, + 0.9921568627, + 0.9960784314, + 0.831372549, + 0.9803921568627451, + 0.9921568627, + 0.9960784314, + 0.8666666667, + 0.984313725490196, + 0.9921568627, + 0.9960784314, + 0.8980392157, + 0.9882352941176471, + 0.9921568627, + 0.9960784314, + 0.9294117647, + 0.9921568627450981, + 0.9921568627, + 0.9960784314, + 0.9607843137, + 0.996078431372549, + 0.9960784314, + 0.9960784314, + 0.9607843137, + 1.0, + 0.9960784314, + 0.9960784314, + 0.9607843137, + ], + }, +]; diff --git a/extensions/tmtv/src/utils/createAndDownloadTMTVReport.js b/extensions/tmtv/src/utils/createAndDownloadTMTVReport.js new file mode 100644 index 00000000000..996ebd0f50f --- /dev/null +++ b/extensions/tmtv/src/utils/createAndDownloadTMTVReport.js @@ -0,0 +1,50 @@ +export default function createAndDownloadTMTVReport( + segReport, + additionalReportRows +) { + const firstReport = segReport[Object.keys(segReport)[0]]; + const columns = Object.keys(firstReport); + const csv = [columns.join(',')]; + + Object.values(segReport).forEach(segmentation => { + const row = []; + columns.forEach(column => { + // if it is array then we need to replace , with space to avoid csv parsing error + row.push( + Array.isArray(segmentation[column]) + ? segmentation[column].join(' ') + : segmentation[column] + ); + }); + csv.push(row.join(',')); + }); + + csv.push(''); + csv.push(''); + csv.push(''); + + csv.push(`Patient ID,${firstReport.PatientID}`); + csv.push(`Study Date,${firstReport.StudyDate}`); + csv.push(''); + additionalReportRows.forEach(({ key, value: values }) => { + const temp = []; + temp.push(`${key}`); + Object.keys(values).forEach(k => { + temp.push(`${k}`); + temp.push(`${values[k]}`); + }); + + csv.push(temp.join(',')); + }); + + const blob = new Blob([csv.join('\n')], { + type: 'text/csv;charset=utf-8', + }); + + const url = URL.createObjectURL(blob); + + const a = document.createElement('a'); + a.href = url; + a.download = `${firstReport.PatientID}_tmtv.csv`; + a.click(); +} diff --git a/extensions/tmtv/src/utils/dicomRTAnnotationExport/RTStructureSet/RTSSReport.js b/extensions/tmtv/src/utils/dicomRTAnnotationExport/RTStructureSet/RTSSReport.js new file mode 100644 index 00000000000..a8be7de18b6 --- /dev/null +++ b/extensions/tmtv/src/utils/dicomRTAnnotationExport/RTStructureSet/RTSSReport.js @@ -0,0 +1,262 @@ +import AnnotationToPointData from './measurements/AnnotationToPointData'; +import dcmjs from 'dcmjs'; +import { DicomMetadataStore } from '@ohif/core'; + +const { DicomMetaDictionary } = dcmjs.data; + +export default class RTSSReport { + constructor() {} + + /** + * Convert handles to RTSSReport report object containing the dcmjs dicom dataset. + * + * Note: The tool data needs to be formatted in a specific way, and currently + * it is limited to the RectangleROIStartEndTool in the Cornerstone3D. + * + * @param annotations Array of Cornerstone tool annotation data + * @param metadataProvider Metadata provider + * @param options report generation options + * @returns Report object containing the dataset + */ + static generateReport(annotations, metadataProvider, options) { + let dataset = initializeDataset(annotations, metadataProvider); + + annotations.forEach((annotation, index) => { + const ContourSequence = AnnotationToPointData.convert( + annotation, + index, + metadataProvider, + options + ); + + dataset.StructureSetROISequence.push( + getStructureSetModule(annotation, index, metadataProvider) + ); + + dataset.ROIContourSequence.push(ContourSequence); + dataset.RTROIObservationsSequence.push( + getRTROIObservationsSequence(annotation, index, metadataProvider) + ); + + // ReferencedSeriesSequence + // Todo: handle more than one series + dataset.ReferencedSeriesSequence = getReferencedSeriesSequence( + annotation, + index, + metadataProvider + ); + + // ReferencedFrameOfReferenceSequence + dataset.ReferencedFrameOfReferenceSequence = getReferencedFrameOfReferenceSequence( + annotation, + metadataProvider, + dataset + ); + }); + + const fileMetaInformationVersionArray = new Uint8Array(2); + fileMetaInformationVersionArray[1] = 1; + + const _meta = { + FileMetaInformationVersion: { + Value: [fileMetaInformationVersionArray.buffer], + vr: 'OB', + }, + TransferSyntaxUID: { + Value: ['1.2.840.10008.1.2.1'], + vr: 'UI', + }, + ImplementationClassUID: { + Value: [DicomMetaDictionary.uid()], // TODO: could be git hash or other valid id + vr: 'UI', + }, + ImplementationVersionName: { + Value: ['dcmjs'], + vr: 'SH', + }, + }; + + dataset._meta = _meta; + + return dataset; + } + + /** + * Generate Cornerstone tool state from dataset + * @param {object} dataset dataset + * @param {object} hooks + * @param {function} hooks.getToolClass Function to map dataset to a tool class + * @returns + */ + static generateToolState(dataset, hooks = {}) { + // Todo + console.warn('RTSSReport.generateToolState not implemented'); + } +} + +function initializeDataset(annotations, metadataProvider) { + const rtSOPInstanceUID = DicomMetaDictionary.uid(); + + // get the first annotation data + const { + referencedImageId: imageId, + FrameOfReferenceUID, + } = annotations[0].metadata; + + const { studyInstanceUID } = metadataProvider.get( + 'generalSeriesModule', + imageId + ); + + const patientModule = getPatientModule(imageId, metadataProvider); + const rtSeriesModule = getRTSeriesModule(imageId, metadataProvider); + + return { + StructureSetROISequence: [], + ROIContourSequence: [], + RTROIObservationsSequence: [], + ReferencedSeriesSequence: [], + ReferencedFrameOfReferenceSequence: [], + ...patientModule, + ...rtSeriesModule, + StudyInstanceUID: studyInstanceUID, + SOPClassUID: '1.2.840.10008.5.1.4.1.1.481.3', // RT Structure Set Storage + SOPInstanceUID: rtSOPInstanceUID, + Manufacturer: 'dcmjs', + Modality: 'RTSTRUCT', + FrameOfReferenceUID, + PositionReferenceIndicator: '', + StructureSetLabel: '', + StructureSetName: '', + ReferringPhysicianName: '', + OperatorsName: '', + StructureSetDate: DicomMetaDictionary.date(), + StructureSetTime: DicomMetaDictionary.time(), + }; +} + +function getPatientModule(imageId, metadataProvider) { + const generalSeriesModule = metadataProvider.get( + 'generalSeriesModule', + imageId + ); + const generalStudyModule = metadataProvider.get( + 'generalStudyModule', + imageId + ); + const patientStudyModule = metadataProvider.get( + 'patientStudyModule', + imageId + ); + const patientModule = metadataProvider.get('patientModule', imageId); + const patientDemographicModule = metadataProvider.get( + 'patientDemographicModule', + imageId + ); + + return { + Modality: generalSeriesModule.modality, + PatientID: patientModule.patientId, + PatientName: patientModule.patientName, + PatientBirthDate: '', + PatientAge: patientStudyModule.patientAge, + PatientSex: patientDemographicModule.patientSex, + PatientWeight: patientStudyModule.patientWeight, + StudyDate: generalStudyModule.studyDate, + StudyTime: generalStudyModule.studyTime, + StudyID: 'ToDo', + AccessionNumber: generalStudyModule.accessionNumber, + }; +} + +function getReferencedFrameOfReferenceSequence( + toolData, + metadataProvider, + dataset +) { + const { referencedImageId: imageId, FrameOfReferenceUID } = toolData.metadata; + const instance = metadataProvider.get('instance', imageId); + const { SeriesInstanceUID } = instance; + + const { ReferencedSeriesSequence } = dataset; + + return [ + { + FrameOfReferenceUID, + RTReferencedStudySequence: [ + { + ReferencedSOPClassUID: dataset.SOPClassUID, + ReferencedSOPInstanceUID: dataset.SOPInstanceUID, + RTReferencedSeriesSequence: [ + { + SeriesInstanceUID, + ContourImageSequence: [ + ...ReferencedSeriesSequence[0].ReferencedInstanceSequence, + ], + }, + ], + }, + ], + }, + ]; +} + +function getReferencedSeriesSequence(toolData, index, metadataProvider) { + // grab imageId from toolData + const { referencedImageId: imageId } = toolData.metadata; + const instance = metadataProvider.get('instance', imageId); + const { SeriesInstanceUID, StudyInstanceUID } = instance; + + const ReferencedSeriesSequence = []; + if (SeriesInstanceUID) { + const series = DicomMetadataStore.getSeries( + StudyInstanceUID, + SeriesInstanceUID + ); + + const ReferencedSeries = { + SeriesInstanceUID, + ReferencedInstanceSequence: [], + }; + + series.instances.forEach(instance => { + const { SOPInstanceUID, SOPClassUID } = instance; + ReferencedSeries.ReferencedInstanceSequence.push({ + ReferencedSOPClassUID: SOPClassUID, + ReferencedSOPInstanceUID: SOPInstanceUID, + }); + }); + + ReferencedSeriesSequence.push(ReferencedSeries); + } + + return ReferencedSeriesSequence; +} + +function getRTSeriesModule(imageId, metadataProvider) { + return { + SeriesInstanceUID: DicomMetaDictionary.uid(), // generate a new series instance uid + SeriesNumber: '99', // Todo:: what should be the series number? + }; +} + +function getStructureSetModule(toolData, index, metadataProvider) { + const { FrameOfReferenceUID } = toolData.metadata; + + return { + ROINumber: index + 1, + ROIName: `Todo: name ${index + 1}`, + ROIDescription: `Todo: description ${index + 1}`, + ROIGenerationAlgorithm: 'Todo: algorithm', + ReferencedFrameOfReferenceUID: FrameOfReferenceUID, + }; +} + +function getRTROIObservationsSequence(toolData, index, metadataProvider) { + return { + ObservationNumber: index + 1, + ReferencedROINumber: index + 1, + RTROIInterpretedType: 'Todo: type', + ROIInterpreter: 'Todo: interpreter', + }; +} diff --git a/extensions/tmtv/src/utils/dicomRTAnnotationExport/RTStructureSet/dicomRTAnnotationExport.js b/extensions/tmtv/src/utils/dicomRTAnnotationExport/RTStructureSet/dicomRTAnnotationExport.js new file mode 100644 index 00000000000..999b37addaf --- /dev/null +++ b/extensions/tmtv/src/utils/dicomRTAnnotationExport/RTStructureSet/dicomRTAnnotationExport.js @@ -0,0 +1,15 @@ +import RTSSReport from './RTSSReport'; +import dcmjs from 'dcmjs'; +import { classes } from '@ohif/core'; + +const { datasetToBlob } = dcmjs.data; +const metadataProvider = classes.MetadataProvider; + +export default function dicomRTAnnotationExport(annotations) { + const dataset = RTSSReport.generateReport(annotations, metadataProvider); + const reportBlob = datasetToBlob(dataset); + + //Create a URL for the binary. + var objectUrl = URL.createObjectURL(reportBlob); + window.location.assign(objectUrl); +} diff --git a/extensions/tmtv/src/utils/dicomRTAnnotationExport/RTStructureSet/index.js b/extensions/tmtv/src/utils/dicomRTAnnotationExport/RTStructureSet/index.js new file mode 100644 index 00000000000..7a915da2ce9 --- /dev/null +++ b/extensions/tmtv/src/utils/dicomRTAnnotationExport/RTStructureSet/index.js @@ -0,0 +1,3 @@ +import dicomRTAnnotationExport from './dicomRTAnnotationExport'; + +export default dicomRTAnnotationExport; diff --git a/extensions/tmtv/src/utils/dicomRTAnnotationExport/RTStructureSet/measurements/AnnotationToPointData.js b/extensions/tmtv/src/utils/dicomRTAnnotationExport/RTStructureSet/measurements/AnnotationToPointData.js new file mode 100644 index 00000000000..f74fdc72ae7 --- /dev/null +++ b/extensions/tmtv/src/utils/dicomRTAnnotationExport/RTStructureSet/measurements/AnnotationToPointData.js @@ -0,0 +1,58 @@ +import RectangleROIStartEndThreshold from './RectangleROIStartEndThreshold'; + +function validateAnnotation(annotation) { + if (!annotation?.data) { + throw new Error('Tool data is empty'); + } + + if (!annotation.metadata || annotation.metadata.referenceImageId) { + throw new Error('Tool data is not associated with any imageId'); + } +} + +class AnnotationToPointData { + constructor() {} + + static convert(annotation, index, metadataProvider) { + validateAnnotation(annotation); + + const { toolName } = annotation.metadata; + const toolClass = AnnotationToPointData.TOOL_NAMES[toolName]; + + if (!toolClass) { + throw new Error( + `Unknown tool type: ${toolName}, cannot convert to RTSSReport` + ); + } + + // Each toolData should become a list of contours, ContourSequence + // contains a list of contours with their pointData, their geometry + // type and their length. + const ContourSequence = toolClass.getContourSequence( + annotation, + metadataProvider + ); + + // Todo: random rgb color for now, options should be passed in + const color = [ + Math.floor(Math.random() * 255), + Math.floor(Math.random() * 255), + Math.floor(Math.random() * 255), + ]; + + return { + ReferencedROINumber: index + 1, + ROIDisplayColor: color, + ContourSequence, + }; + } + + static register(toolClass) { + AnnotationToPointData.TOOL_NAMES[toolClass.toolName] = toolClass; + } +} + +AnnotationToPointData.TOOL_NAMES = {}; +AnnotationToPointData.register(RectangleROIStartEndThreshold); + +export default AnnotationToPointData; diff --git a/extensions/tmtv/src/utils/dicomRTAnnotationExport/RTStructureSet/measurements/RectangleROIStartEndThreshold.js b/extensions/tmtv/src/utils/dicomRTAnnotationExport/RTStructureSet/measurements/RectangleROIStartEndThreshold.js new file mode 100644 index 00000000000..803bef365d0 --- /dev/null +++ b/extensions/tmtv/src/utils/dicomRTAnnotationExport/RTStructureSet/measurements/RectangleROIStartEndThreshold.js @@ -0,0 +1,56 @@ +// comment +class RectangleROIStartEndThreshold { + constructor() {} + + static getContourSequence(toolData, metadataProvider) { + const { data } = toolData; + const { projectionPoints, projectionPointsImageIds } = data.cachedStats; + + return projectionPoints.map((point, index) => { + const ContourData = getPointData(point); + const ContourImageSequence = getContourImageSequence( + projectionPointsImageIds[index], + metadataProvider + ); + + return { + NumberOfContourPoints: ContourData.length / 3, + ContourImageSequence, + ContourGeometricType: 'CLOSED_PLANAR', + ContourData, + }; + }); + } +} + +RectangleROIStartEndThreshold.toolName = 'RectangleROIStartEndThreshold'; + +function getPointData(points) { + // Since this is a closed contour, the order of the points is important. + // re-order the points to be in the correct order clockwise + // Spread to make sure Float32Arrays are converted to arrays + const orderedPoints = [ + ...points[0], + ...points[1], + ...points[3], + ...points[2], + ]; + const pointsArray = orderedPoints.flat(); + + // reduce the precision of the points to 2 decimal places + const pointsArrayWithPrecision = pointsArray.map(point => { + return point.toFixed(2); + }); + + return pointsArrayWithPrecision; +} + +function getContourImageSequence(imageId, metadataProvider) { + const sopCommon = metadataProvider.get('sopCommonModule', imageId); + + return { + ReferencedSOPClassUID: sopCommon.sopClassUID, + ReferencedSOPInstanceUID: sopCommon.sopInstanceUID, + }; +} +export default RectangleROIStartEndThreshold; diff --git a/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/getStudiesForPatientByStudyInstanceUID.js b/extensions/tmtv/src/utils/getStudiesForPatientByStudyInstanceUID.js similarity index 100% rename from extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/getStudiesForPatientByStudyInstanceUID.js rename to extensions/tmtv/src/utils/getStudiesForPatientByStudyInstanceUID.js diff --git a/extensions/tmtv/src/utils/getThresholdValue.ts b/extensions/tmtv/src/utils/getThresholdValue.ts new file mode 100644 index 00000000000..d613dd1598d --- /dev/null +++ b/extensions/tmtv/src/utils/getThresholdValue.ts @@ -0,0 +1,66 @@ +import * as csTools from '@cornerstonejs/tools'; + +function getThresholdValues( + annotationUIDs, + referencedVolume, + config +): { lower: number; upper: number } { + if (config.strategy === 'range') { + return { + lower: config.minValue, + upper: config.maxValue, + }; + } + + // roiStats + const { weight } = config; + const { imageData } = referencedVolume; + const values = imageData + .getPointData() + .getScalars() + .getData(); + + // Todo: add support for other strategies + const { fn, baseValue } = _getStrategyFn('max'); + let value = baseValue; + + const annotations = annotationUIDs.map(annotationUID => + csTools.annotation.state.getAnnotation(annotationUID) + ); + + const boundsIJK = csTools.utilities.rectangleROITool.getBoundsIJKFromRectangleAnnotations( + annotations, + referencedVolume + ); + + const [[iMin, iMax], [jMin, jMax], [kMin, kMax]] = boundsIJK; + + for (let i = iMin; i <= iMax; i++) { + for (let j = jMin; j <= jMax; j++) { + for (let k = kMin; k <= kMax; k++) { + const offset = imageData.computeOffsetIndex([i, j, k]); + value = fn(values[offset], value); + } + } + } + + return { + lower: weight * value, + upper: +Infinity, + }; +} + +function _getStrategyFn( + statistic +): { fn: (a: number, b: number) => number; baseValue: number } { + const baseValue = -Infinity; + const fn = (number, maxValue) => { + if (number > maxValue) { + maxValue = number; + } + return maxValue; + }; + return { fn, baseValue }; +} + +export default getThresholdValues; diff --git a/extensions/tmtv/src/utils/measurementServiceMappings/RectangleRoiStartEndThreshold.js b/extensions/tmtv/src/utils/measurementServiceMappings/RectangleRoiStartEndThreshold.js new file mode 100644 index 00000000000..9de0d31e502 --- /dev/null +++ b/extensions/tmtv/src/utils/measurementServiceMappings/RectangleRoiStartEndThreshold.js @@ -0,0 +1,75 @@ +import SUPPORTED_TOOLS from './constants/supportedTools'; +import getSOPInstanceAttributes from './utils/getSOPInstanceAttributes'; + +const RectangleROIStartEndThreshold = { + toAnnotation: (measurement, definition) => {}, + + /** + * Maps cornerstone annotation event data to measurement service format. + * + * @param {Object} cornerstone Cornerstone event data + * @return {Measurement} Measurement instance + */ + toMeasurement: ( + csToolsEventDetail, + DisplaySetService, + Cornerstone3DViewportService + ) => { + const { annotation, viewportId } = csToolsEventDetail; + const { metadata, data, annotationUID } = annotation; + + if (!metadata || !data) { + console.warn('Length tool: Missing metadata or data'); + return null; + } + + const { toolName, referencedImageId, FrameOfReferenceUID } = metadata; + const validToolType = SUPPORTED_TOOLS.includes(toolName); + + if (!validToolType) { + throw new Error('Tool not supported'); + } + + const { + SOPInstanceUID, + SeriesInstanceUID, + StudyInstanceUID, + } = getSOPInstanceAttributes( + referencedImageId, + Cornerstone3DViewportService, + viewportId + ); + + let displaySet; + + if (SOPInstanceUID) { + displaySet = DisplaySetService.getDisplaySetForSOPInstanceUID( + SOPInstanceUID, + SeriesInstanceUID + ); + } else { + displaySet = DisplaySetService.getDisplaySetsForSeries(SeriesInstanceUID); + } + + const { cachedStats } = data; + + return { + uid: annotationUID, + SOPInstanceUID, + FrameOfReferenceUID, + // points, + metadata, + referenceSeriesUID: SeriesInstanceUID, + referenceStudyUID: StudyInstanceUID, + toolName: metadata.toolName, + displaySetInstanceUID: displaySet.displaySetInstanceUID, + label: metadata.label, + // displayText: displayText, + data: data.cachedStats, + type: 'RectangleROIStartEndThreshold', + // getReport, + }; + }, +}; + +export default RectangleROIStartEndThreshold; diff --git a/extensions/tmtv/src/utils/measurementServiceMappings/constants/supportedTools.js b/extensions/tmtv/src/utils/measurementServiceMappings/constants/supportedTools.js new file mode 100644 index 00000000000..31cc1059ef7 --- /dev/null +++ b/extensions/tmtv/src/utils/measurementServiceMappings/constants/supportedTools.js @@ -0,0 +1 @@ +export default ['RectangleROIStartEndThreshold']; diff --git a/extensions/tmtv/src/utils/measurementServiceMappings/measurementServiceMappingsFactory.js b/extensions/tmtv/src/utils/measurementServiceMappings/measurementServiceMappingsFactory.js new file mode 100644 index 00000000000..53d373d3cf8 --- /dev/null +++ b/extensions/tmtv/src/utils/measurementServiceMappings/measurementServiceMappingsFactory.js @@ -0,0 +1,26 @@ +import RectangleROIStartEndThreshold from './RectangleROIStartEndThreshold'; + +const measurementServiceMappingsFactory = ( + MeasurementService, + DisplaySetService, + Cornerstone3DViewportService +) => { + return { + RectangleROIStartEndThreshold: { + toAnnotation: RectangleROIStartEndThreshold.toAnnotation, + toMeasurement: csToolsAnnotation => + RectangleROIStartEndThreshold.toMeasurement( + csToolsAnnotation, + DisplaySetService, + Cornerstone3DViewportService + ), + matchingCriteria: [ + { + valueType: MeasurementService.VALUE_TYPES.ROI_THRESHOLD_MANUAL, + }, + ], + }, + }; +}; + +export default measurementServiceMappingsFactory; diff --git a/extensions/tmtv/src/utils/measurementServiceMappings/utils/getSOPInstanceAttributes.js b/extensions/tmtv/src/utils/measurementServiceMappings/utils/getSOPInstanceAttributes.js new file mode 100644 index 00000000000..4f412417822 --- /dev/null +++ b/extensions/tmtv/src/utils/measurementServiceMappings/utils/getSOPInstanceAttributes.js @@ -0,0 +1,17 @@ +import { metaData } from '@cornerstonejs/core'; + +export default function getSOPInstanceAttributes(imageId) { + if (imageId) { + return _getUIDFromImageID(imageId); + } +} + +function _getUIDFromImageID(imageId) { + const instance = metaData.get('instance', imageId); + + return { + SOPInstanceUID: instance.SOPInstanceUID, + SeriesInstanceUID: instance.SeriesInstanceUID, + StudyInstanceUID: instance.StudyInstanceUID, + }; +} diff --git a/modes/longitudinal/src/index.js b/modes/longitudinal/src/index.js index 6891f8d6e4f..1e471d5bcdf 100644 --- a/modes/longitudinal/src/index.js +++ b/modes/longitudinal/src/index.js @@ -105,6 +105,7 @@ function modeFactory({ modeConfiguration }) { onModeExit: ({ servicesManager }) => { const { ToolGroupService, + SyncGroupService, MeasurementService, ToolBarService, } = servicesManager.services; @@ -112,6 +113,7 @@ function modeFactory({ modeConfiguration }) { ToolBarService.reset(); MeasurementService.clearMeasurements(); ToolGroupService.destroy(); + SyncGroupService.destroy(); }, validationTags: { study: [], diff --git a/modes/tmtv/.webpack/webpack.dev.js b/modes/tmtv/.webpack/webpack.dev.js new file mode 100644 index 00000000000..db7c206b134 --- /dev/null +++ b/modes/tmtv/.webpack/webpack.dev.js @@ -0,0 +1,8 @@ +const path = require('path'); +const webpackCommon = require('./../../../.webpack/webpack.base.js'); +const SRC_DIR = path.join(__dirname, '../src'); +const DIST_DIR = path.join(__dirname, '../dist'); + +module.exports = (env, argv) => { + return webpackCommon(env, argv, { SRC_DIR, DIST_DIR }); +}; diff --git a/modes/tmtv/.webpack/webpack.prod.js b/modes/tmtv/.webpack/webpack.prod.js new file mode 100644 index 00000000000..ecd8874190d --- /dev/null +++ b/modes/tmtv/.webpack/webpack.prod.js @@ -0,0 +1,50 @@ +const webpack = require('webpack'); +const { merge } = require('webpack-merge'); +const path = require('path'); +const MiniCssExtractPlugin = require('mini-css-extract-plugin'); + +const pkg = require('./../package.json'); +const webpackCommon = require('./../../../.webpack/webpack.base.js'); + +const ROOT_DIR = path.join(__dirname, './../'); +const SRC_DIR = path.join(__dirname, '../src'); +const DIST_DIR = path.join(__dirname, '../dist'); + +const fileName = 'index.umd.js'; +module.exports = (env, argv) => { + const commonConfig = webpackCommon(env, argv, { SRC_DIR, DIST_DIR }); + + return merge(commonConfig, { + stats: { + colors: true, + hash: true, + timings: true, + assets: true, + chunks: false, + chunkModules: false, + modules: false, + children: false, + warnings: true, + }, + optimization: { + minimize: true, + sideEffects: true, + }, + output: { + path: ROOT_DIR, + library: 'OHIFExtCornerstone', + libraryTarget: 'umd', + libraryExport: 'default', + filename: pkg.main, + }, + plugins: [ + new webpack.optimize.LimitChunkCountPlugin({ + maxChunks: 1, + }), + new MiniCssExtractPlugin({ + filename: './dist/[name].css', + chunkFilename: './dist/[id].css', + }), + ], + }); +}; diff --git a/modes/tmtv/LICENSE b/modes/tmtv/LICENSE new file mode 100644 index 00000000000..19e20dd35ca --- /dev/null +++ b/modes/tmtv/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018 Open Health Imaging Foundation + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/modes/tmtv/README.md b/modes/tmtv/README.md new file mode 100644 index 00000000000..7b36205e413 --- /dev/null +++ b/modes/tmtv/README.md @@ -0,0 +1,80 @@ +# Total Metabolic Tumor Volume + + +## Introduction + +Total Metabolic Tumor Volume (TMTV) workflow mode enables quantitatively measurement of a tumor volume in a patient's body. +This mode is accessible in any study that has a PT and CT image series as you can see below + + +![modeValid](https://user-images.githubusercontent.com/7490180/171256138-7a948654-6836-460c-817a-fa9a1929926b.png) + +Note: If the study does not have a PT and CT image series, the TMTV workflow mode will not be available +and will become grayed out. + +## Layout +The designed layout for the viewports follows a predefined hanging protocol which will place +10 viewports containing CT, PT, Fusion and Maximum Intensity Projection (MIP) PT scenes. + +The hanging protocol will match the CT and PT displaySets based on series description. In terms +of PT displaySets, the hanging protocol will match the PT displaySet that has attenuated +corrected PET image data. + +As seen in the image below, the first row contains CT volume in 3 different views of Axial, +Sagittal and Coronal. The second row contains PT volume in the same views as the first row. +The last row contains the fusion volume and the viewport to the right is a MIP of the PT +Volume in the Sagittal view. + + + +![modeLayout](https://user-images.githubusercontent.com/7490180/171256159-1e94edac-985f-4de3-8759-27a077541f8f.png) + +## Synchronization + +The viewports in the 3 rows are synchronized both for the Camera and WindowLevel. +It means that when you interact with the CT viewport (pan, zoom, scroll), +the PT and Fusion viewports will be synchronized to the same view. In addition +to camera synchronization, the window level of the CT viewport will be synchronized +with the fusion viewport. + + +### MIP +The tools that are activated on each viewport is unique to its data. For instance, +the mouse scroll tool for PT, CT and Fusion viewports are scrolling through the image data +(in different directions); however, the mouse scroll tool for the MIP viewport will +rotate the camera to match the usecase for the MIP. + + +## Panels +There are two panels that are available in the TMTV workflow mode and we will +discuss them in detail below. + +### SUV Panel +This panel shows the PT metadata derived from the matched PT displaySet. The user +can edit/change the metadata if needed, and by reloading the data the new +metadata will be applied to the PT volume. + + +## ROI Threshold Panel +The ROI Threshold panel is a panel that allows the user to use the `RectangleROIStartEnd` +tool from Cornerstone to define and edit a region of interest. Then, the user can +apply a threshold to the pixels in the ROI and save the result as a segmentation volume. + +By applying each threshold to the ROI, the Total Metabolic Tumor Volume (TMTV), and +the SUV Peak values will get calculated for the labelmap segments and shown in the +panel. + + +## Export Report + +Finally, the results can be saved in the CSV format. The RectangleROI annotations +can also be extracted as a dicom RT Structure Set and saved as a DICOM file. + + +## Video Tutorial + +Below you can see a video tutorial on how to use the TMTV workflow mode. + + +https://user-images.githubusercontent.com/7490180/171065443-35369fba-e955-48ac-94da-d262e0fccb6b.mp4 + diff --git a/modes/tmtv/assets/modeLayout.png b/modes/tmtv/assets/modeLayout.png new file mode 100644 index 00000000000..028b579b468 Binary files /dev/null and b/modes/tmtv/assets/modeLayout.png differ diff --git a/modes/tmtv/assets/modeValid.png b/modes/tmtv/assets/modeValid.png new file mode 100644 index 00000000000..69ea706267b Binary files /dev/null and b/modes/tmtv/assets/modeValid.png differ diff --git a/modes/tmtv/babel.config.js b/modes/tmtv/babel.config.js new file mode 100644 index 00000000000..325ca2a8ee7 --- /dev/null +++ b/modes/tmtv/babel.config.js @@ -0,0 +1 @@ +module.exports = require('../../babel.config.js'); diff --git a/modes/tmtv/package.json b/modes/tmtv/package.json new file mode 100644 index 00000000000..b94d3c59236 --- /dev/null +++ b/modes/tmtv/package.json @@ -0,0 +1,50 @@ +{ + "name": "@ohif/mode-tmtv", + "version": "3.0.0", + "description": "Total Metabolic Tumor Volume Workflow", + "author": "OHIF", + "license": "MIT", + "repository": "OHIF/Viewers", + "main": "dist/index.umd.js", + "module": "src/index.js", + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1.16.0" + }, + "files": [ + "dist", + "README.md" + ], + "publishConfig": { + "access": "public" + }, + "keywords": [ + "ohif-mode" + ], + "scripts": { + "dev": "cross-env NODE_ENV=development webpack --config .webpack/webpack.dev.js --watch --debug --output-pathinfo", + "dev:cornerstone": "yarn run dev", + "build": "cross-env NODE_ENV=production webpack --config .webpack/webpack.prod.js", + "build:package": "yarn run build", + "start": "yarn run dev", + "test:unit": "jest --watchAll", + "test:unit:ci": "jest --ci --runInBand --collectCoverage --passWithNoTests" + }, + "peerDependencies": { + "@ohif/core": "^3.0.0", + "@ohif/extension-default": "^3.0.0", + "@ohif/extension-cornerstone-3d": "^3.0.0", + "@ohif/extension-cornerstone-dicom-sr": "^3.0.0", + "@ohif/extension-dicom-pdf": "^3.0.1", + "@ohif/extension-dicom-video": "^3.0.1", + "@ohif/extension-measurement-tracking": "^3.0.0" + }, + "dependencies": { + "@babel/runtime": "7.16.3" + }, + "devDependencies": { + "webpack": "^5.50.0", + "webpack-merge": "^5.7.3" + } +} diff --git a/modes/tmtv/src/id.js b/modes/tmtv/src/id.js new file mode 100644 index 00000000000..ebe5acd98ae --- /dev/null +++ b/modes/tmtv/src/id.js @@ -0,0 +1,5 @@ +import packageJson from '../package.json'; + +const id = packageJson.name; + +export { id }; diff --git a/modes/tmtv/src/index.js b/modes/tmtv/src/index.js new file mode 100644 index 00000000000..e6cd723f9ec --- /dev/null +++ b/modes/tmtv/src/index.js @@ -0,0 +1,200 @@ +import { hotkeys } from '@ohif/core'; +import toolbarButtons from './toolbarButtons.js'; +import { id } from './id.js'; +import initToolGroups, { toolGroupIds } from './initToolGroups.js'; +import setCrosshairsConfiguration from './utils/setCrosshairsConfiguration.js'; +import setEllipticalROIConfiguration from './utils/setEllipticalROIConfiguration.js'; + +const ohif = { + layout: '@ohif/extension-default.layoutTemplateModule.viewerLayout', + sopClassHandler: '@ohif/extension-default.sopClassHandlerModule.stack', + hangingProtocols: '@ohif/extension-default.hangingProtocolModule.default', + measurements: '@ohif/extension-default.panelModule.measure', + thumbnailList: '@ohif/extension-default.panelModule.seriesList', +}; + +const cs3d = { + viewport: '@ohif/extension-cornerstone-3d.viewportModule.cornerstone-3d', +}; + +const tmtv = { + hangingProtocols: '@ohif/extension-tmtv.hangingProtocolModule.ptCT', + petSUV: '@ohif/extension-tmtv.panelModule.petSUV', + ROIThresholdPanel: '@ohif/extension-tmtv.panelModule.ROIThresholdSeg', +}; + +const extensionDependencies = { + // Can derive the versions at least process.env.from npm_package_version + '@ohif/extension-default': '^3.0.0', + '@ohif/extension-cornerstone-3d': '^3.0.0', + '@ohif/extension-tmtv': '^3.0.0', +}; + +let unsubscriptions = []; +function modeFactory({ modeConfiguration }) { + return { + // TODO: We're using this as a route segment + // We should not be. + id, + routeName: 'tmtv', + displayName: 'Total Metabolic Tumor Volume', + /** + * Lifecycle hooks + */ + onModeEnter: ({ servicesManager, extensionManager, commandsManager }) => { + const { + ToolBarService, + ToolGroupService, + HangingProtocolService, + DisplaySetService, + } = servicesManager.services; + + const utilityModule = extensionManager.getModuleEntry( + '@ohif/extension-cornerstone-3d.utilityModule.tools' + ); + + const { toolNames, Enums } = utilityModule.exports; + + // Init Default and SR ToolGroups + initToolGroups(toolNames, Enums, ToolGroupService, commandsManager); + + const activateWindowLevel = () => { + ToolBarService.recordInteraction({ + groupId: 'WindowLevel', + itemId: 'WindowLevel', + interactionType: 'tool', + commands: [ + { + commandName: 'setToolActive', + commandOptions: { + toolName: toolNames.WindowLevel, + toolGroupId: toolGroupIds.CT, + }, + context: 'CORNERSTONE3D', + }, + { + commandName: 'setToolActive', + commandOptions: { + toolName: toolNames.WindowLevel, + toolGroupId: toolGroupIds.PT, + }, + context: 'CORNERSTONE3D', + }, + { + commandName: 'setToolActive', + commandOptions: { + toolName: toolNames.WindowLevel, + toolGroupId: toolGroupIds.Fusion, + }, + context: 'CORNERSTONE3D', + }, + ], + }); + }; + + // Since we only have one viewport for the basic cs3d mode and it has + // only one hanging protocol, we can just use the first viewport + const { unsubscribe } = ToolGroupService.subscribe( + ToolGroupService.EVENTS.VIEWPORT_ADDED, + () => { + activateWindowLevel(); + // For fusion toolGroup we need to add the volumeIds for the crosshairs + // since in the fusion viewport we don't want both PT and CT to render MIP + // when slabThickness is modified + const matches = HangingProtocolService.getDisplaySetsMatchDetails(); + + setCrosshairsConfiguration( + matches, + toolNames, + ToolGroupService, + DisplaySetService + ); + + setEllipticalROIConfiguration( + matches, + toolNames, + ToolGroupService, + DisplaySetService + ); + } + ); + + unsubscriptions.push(unsubscribe); + ToolBarService.init(extensionManager); + ToolBarService.addButtons(toolbarButtons); + ToolBarService.createButtonSection('primary', [ + 'MeasurementTools', + 'Zoom', + 'WindowLevel', + 'Crosshairs', + 'Pan', + 'RectangleROIStartEndThreshold', + 'fusionPTColormap', + ]); + }, + onModeExit: ({ servicesManager }) => { + const { + ToolGroupService, + SyncGroupService, + MeasurementService, + ToolBarService, + } = servicesManager.services; + + unsubscriptions.forEach(unsubscribe => unsubscribe()); + ToolBarService.reset(); + MeasurementService.clearMeasurements(); + ToolGroupService.destroy(); + SyncGroupService.destroy(); + }, + validationTags: { + study: [], + series: [], + }, + isValidMode: ({ modalities }) => { + const modalities_list = modalities.split('\\'); + const invalidModalities = ['SM']; + + // there should be both CT and PT modalities and the modality should not be SM + return ( + modalities_list.includes('CT') && + modalities_list.includes('PT') && + !invalidModalities.some(modality => modalities_list.includes(modality)) + ); + }, + routes: [ + { + path: 'tmtv', + /*init: ({ servicesManager, extensionManager }) => { + //defaultViewerRouteInit + },*/ + layoutTemplate: ({ location, servicesManager }) => { + return { + id: ohif.layout, + props: { + leftPanels: [], + rightPanels: [tmtv.ROIThresholdPanel, tmtv.petSUV], + viewports: [ + { + namespace: cs3d.viewport, + displaySetsToDisplay: [ohif.sopClassHandler], + }, + ], + }, + }; + }, + }, + ], + extensions: extensionDependencies, + hangingProtocols: [ohif.hangingProtocols, tmtv.hangingProtocols], + sopClassHandlers: [ohif.sopClassHandler], + hotkeys: [...hotkeys.defaults.hotkeyBindings], + }; +} + +const mode = { + id, + modeFactory, + extensionDependencies, +}; + +export default mode; diff --git a/modes/tmtv/src/initToolGroups.js b/modes/tmtv/src/initToolGroups.js new file mode 100644 index 00000000000..1ae73946a20 --- /dev/null +++ b/modes/tmtv/src/initToolGroups.js @@ -0,0 +1,124 @@ +export const toolGroupIds = { + CT: 'ctToolGroup', + PT: 'ptToolGroup', + Fusion: 'fusionToolGroup', + MIP: 'mipToolGroup', + default: 'default', +}; + +function initToolGroups(toolNames, Enums, ToolGroupService, commandsManager) { + const tools = { + active: [ + { + toolName: toolNames.WindowLevel, + bindings: [{ mouseButton: Enums.MouseBindings.Primary }], + }, + { + toolName: toolNames.Pan, + bindings: [{ mouseButton: Enums.MouseBindings.Auxiliary }], + }, + { + toolName: toolNames.Zoom, + bindings: [{ mouseButton: Enums.MouseBindings.Secondary }], + }, + { toolName: toolNames.StackScrollMouseWheel, bindings: [] }, + ], + passive: [ + { toolName: toolNames.Length }, + { toolName: toolNames.ArrowAnnotate }, + { toolName: toolNames.Bidirectional }, + { toolName: toolNames.DragProbe }, + { toolName: toolNames.EllipticalROI }, + { toolName: toolNames.RectangleROI }, + { toolName: toolNames.StackScroll }, + { toolName: toolNames.Angle }, + { toolName: toolNames.Magnify }, + ], + enabled: [{ toolName: toolNames.SegmentationDisplay }], + disabled: [{ toolName: toolNames.Crosshairs }], + }; + + const toolsConfig = { + [toolNames.Crosshairs]: { + viewportIndicators: false, + autoPan: { + enabled: true, + panSize: 10, + }, + }, + [toolNames.ArrowAnnotate]: { + getTextCallback: (callback, eventDetails) => { + commandsManager.runCommand('arrowTextCallback', { + callback, + eventDetails, + }); + }, + + changeTextCallback: (data, eventDetails, callback) => + commandsManager.runCommand('arrowTextCallback', { + callback, + data, + eventDetails, + }), + }, + }; + + ToolGroupService.createToolGroupAndAddTools( + toolGroupIds.CT, + tools, + toolsConfig + ); + ToolGroupService.createToolGroupAndAddTools( + toolGroupIds.PT, + { + active: tools.active, + passive: [ + ...tools.passive, + { toolName: 'RectangleROIStartEndThreshold' }, + ], + enabled: tools.enabled, + disabled: tools.disabled, + }, + toolsConfig + ); + ToolGroupService.createToolGroupAndAddTools( + toolGroupIds.Fusion, + tools, + toolsConfig + ); + ToolGroupService.createToolGroupAndAddTools( + toolGroupIds.default, + tools, + toolsConfig + ); + + const mipTools = { + active: [ + { + toolName: toolNames.VolumeRotateMouseWheel, + }, + { + toolName: toolNames.MipJumpToClick, + bindings: [{ mouseButton: Enums.MouseBindings.Primary }], + }, + ], + enabled: [{ toolName: toolNames.SegmentationDisplay }], + }; + + const mipToolsConfig = { + [toolNames.VolumeRotateMouseWheel]: { + rotateIncrementDegrees: 0.1, + }, + [toolNames.MipJumpToClick]: { + targetViewportIds: ['ptAXIAL', 'ptCORONAL', 'ptSAGITTAL'], + }, + }; + + ToolGroupService.createToolGroupAndAddTools( + toolGroupIds.MIP, + mipTools, + mipToolsConfig + ); +} + +export default initToolGroups; diff --git a/modes/tmtv/src/toolbarButtons.js b/modes/tmtv/src/toolbarButtons.js new file mode 100644 index 00000000000..a292343a784 --- /dev/null +++ b/modes/tmtv/src/toolbarButtons.js @@ -0,0 +1,324 @@ +// TODO: torn, can either bake this here; or have to create a whole new button type +// Only ways that you can pass in a custom React component for render :l +import { WindowLevelMenuItem } from '@ohif/ui'; +import { defaults } from '@ohif/core'; +import { toolGroupIds } from './initToolGroups'; +const { windowLevelPresets } = defaults; +/** + * + * @param {*} type - 'tool' | 'action' | 'toggle' + * @param {*} id + * @param {*} icon + * @param {*} label + */ +function _createButton(type, id, icon, label, commands, tooltip) { + return { + id, + icon, + label, + type, + commands, + tooltip, + }; +} + +function _createColormap(label, colormap) { + return { + id: label.toString(), + title: label, + subtitle: label, + type: 'action', + commands: [ + { + commandName: 'setFusionPTColormap', + commandOptions: { + toolGroupId: toolGroupIds.Fusion, + colormap, + }, + }, + { + commandName: 'setFusionPTColormap', + commandOptions: { + toolGroupId: toolGroupIds.Fusion, + colormap, + }, + }, + ], + }; +} + +const _createActionButton = _createButton.bind(null, 'action'); +const _createToggleButton = _createButton.bind(null, 'toggle'); +const _createToolButton = _createButton.bind(null, 'tool'); + +/** + * + * @param {*} preset - preset number (from above import) + * @param {*} title + * @param {*} subtitle + */ +function _createWwwcPreset(preset, title, subtitle) { + return { + id: preset.toString(), + title, + subtitle, + type: 'action', + commands: [ + { + commandName: 'setWindowLevel', + commandOptions: { + windowLevel: windowLevelPresets[preset], + }, + context: 'CORNERSTONE3D', + }, + ], + }; +} + +function _createCommands(commandName, toolName, toolGroupIds) { + return toolGroupIds.map(toolGroupId => ({ + /* It's a command that is being run when the button is clicked. */ + commandName, + commandOptions: { + toolName, + toolGroupId, + }, + context: 'CORNERSTONE3D', + })); +} + +const toolbarButtons = [ + // Measurement + { + id: 'MeasurementTools', + type: 'ohif.splitButton', + props: { + groupId: 'MeasurementTools', + isRadio: true, // ? + // Switch? + primary: _createToolButton( + 'Length', + 'tool-length', + 'Length', + [ + ..._createCommands('setToolActive', 'Length', [ + toolGroupIds.CT, + toolGroupIds.PT, + toolGroupIds.Fusion, + ]), + ], + 'Length' + ), + secondary: { + icon: 'chevron-down', + label: '', + isActive: true, + tooltip: 'More Measure Tools', + }, + items: [ + _createToolButton( + 'Length', + 'tool-length', + 'Length', + [ + ..._createCommands('setToolActive', 'Length', [ + toolGroupIds.CT, + toolGroupIds.PT, + toolGroupIds.Fusion, + ]), + ], + 'Length Tool' + ), + _createToolButton( + 'Bidirectional', + 'tool-bidirectional', + 'Bidirectional', + [ + ..._createCommands('setToolActive', 'Bidirectional', [ + toolGroupIds.CT, + toolGroupIds.PT, + toolGroupIds.Fusion, + ]), + ], + 'Bidirectional Tool' + ), + _createToolButton( + 'ArrowAnnotate', + 'tool-annotate', + 'Annotation', + [ + ..._createCommands('setToolActive', 'ArrowAnnotate', [ + toolGroupIds.CT, + toolGroupIds.PT, + toolGroupIds.Fusion, + ]), + ], + 'Arrow Annotate' + ), + _createToolButton( + 'EllipticalROI', + 'tool-elipse', + 'Ellipse', + [ + ..._createCommands('setToolActive', 'EllipticalROI', [ + toolGroupIds.CT, + toolGroupIds.PT, + toolGroupIds.Fusion, + ]), + ], + 'Ellipse Tool' + ), + ], + }, + }, + // Zoom.. + { + id: 'Zoom', + type: 'ohif.radioGroup', + props: { + type: 'tool', + icon: 'tool-zoom', + label: 'Zoom', + commands: [ + ..._createCommands('setToolActive', 'Zoom', [ + toolGroupIds.CT, + toolGroupIds.PT, + toolGroupIds.Fusion, + ]), + ], + }, + }, + // Window Level + Presets... + { + id: 'WindowLevel', + type: 'ohif.splitButton', + props: { + groupId: 'WindowLevel', + primary: _createToolButton( + 'WindowLevel', + 'tool-window-level', + 'Window Level', + [ + ..._createCommands('setToolActive', 'WindowLevel', [ + toolGroupIds.CT, + toolGroupIds.PT, + toolGroupIds.Fusion, + ]), + ], + 'Window Level' + ), + secondary: { + icon: 'chevron-down', + label: 'W/L Manual', + isActive: true, + tooltip: 'W/L Presets', + }, + isAction: true, // ? + renderer: WindowLevelMenuItem, + items: [ + _createWwwcPreset(1, 'Soft tissue', '400 / 40'), + _createWwwcPreset(2, 'Lung', '1500 / -600'), + _createWwwcPreset(3, 'Liver', '150 / 90'), + _createWwwcPreset(4, 'Bone', '80 / 40'), + _createWwwcPreset(5, 'Brain', '2500 / 480'), + ], + }, + }, + { + id: 'Crosshairs', + type: 'ohif.radioGroup', + props: { + type: 'toggle', + icon: 'tool-crosshair', + label: 'Crosshairs', + commands: [ + ..._createCommands('toggleCrosshairs', 'Crosshairs', [ + toolGroupIds.CT, + toolGroupIds.PT, + toolGroupIds.Fusion, + ]), + ], + }, + }, + // Pan... + { + id: 'Pan', + type: 'ohif.radioGroup', + props: { + type: 'tool', + icon: 'tool-move', + label: 'Pan', + commands: [ + ..._createCommands('setToolActive', 'Pan', [ + toolGroupIds.CT, + toolGroupIds.PT, + toolGroupIds.Fusion, + ]), + ], + }, + }, + { + id: 'RectangleROIStartEndThreshold', + type: 'ohif.radioGroup', + props: { + type: 'tool', + icon: 'pencil', + label: 'Rectangle ROI Threshold', + commands: [ + ..._createCommands('setToolActive', 'RectangleROIStartEndThreshold', [ + toolGroupIds.PT, + ]), + { + commandName: 'displayNotification', + commandOptions: { + title: 'RectangleROI Threshold Tip', + text: + 'RectangleROI Threshold tool should be used on PT Axial Viewport', + type: 'info', + }, + }, + { + commandName: 'setViewportActive', + commandOptions: { + viewportId: 'ptAXIAL', + }, + }, + ], + }, + }, + { + id: 'fusionPTColormap', + type: 'ohif.splitButton', + props: { + groupId: 'fusionPTColormap', + primary: _createToolButton( + 'fusionPTColormap', + 'tool-window-level', + 'Fusion PT Colormap', + [], + 'Fusion PT Colormap' + ), + secondary: { + icon: 'chevron-down', + label: 'PT Colormap', + isActive: true, + tooltip: 'PET Image Colormap', + }, + isAction: true, // ? + renderer: WindowLevelMenuItem, + items: [ + _createColormap('Hot Iron', 'hot_iron'), + _createColormap('S PET', 's_pet'), + _createColormap('Ret Hot', 'red_hot'), + _createColormap('Perfusion', 'perfusion'), + _createColormap('Rainbow', 'rainbow_2'), + _createColormap('SUV', 'suv'), + _createColormap('GE 256', 'ge_256'), + _createColormap('GE', 'ge'), + _createColormap('Siemens', 'siemens'), + ], + }, + }, +]; + +export default toolbarButtons; diff --git a/modes/tmtv/src/utils/setCrosshairsConfiguration.js b/modes/tmtv/src/utils/setCrosshairsConfiguration.js new file mode 100644 index 00000000000..3d83fd14a7a --- /dev/null +++ b/modes/tmtv/src/utils/setCrosshairsConfiguration.js @@ -0,0 +1,35 @@ +import { toolGroupIds } from '../initToolGroups'; + +export default function setCrosshairsConfiguration( + matches, + toolNames, + ToolGroupService, + DisplaySetService +) { + const matchDetails = matches.get('ctDisplaySet'); + + if (!matchDetails) { + return; + } + + const { SeriesInstanceUID } = matchDetails; + const displaySets = DisplaySetService.getDisplaySetsForSeries( + SeriesInstanceUID + ); + + const toolConfig = ToolGroupService.getToolConfiguration( + toolGroupIds.Fusion, + toolNames.Crosshairs + ); + + const crosshairsConfig = { + ...toolConfig, + filterActorUIDsToSetSlabThickness: [displaySets[0].displaySetInstanceUID], + }; + + ToolGroupService.setToolConfiguration( + toolGroupIds.Fusion, + toolNames.Crosshairs, + crosshairsConfig + ); +} diff --git a/modes/tmtv/src/utils/setEllipticalROIConfiguration.js b/modes/tmtv/src/utils/setEllipticalROIConfiguration.js new file mode 100644 index 00000000000..31c37bd6b12 --- /dev/null +++ b/modes/tmtv/src/utils/setEllipticalROIConfiguration.js @@ -0,0 +1,36 @@ +import { toolGroupIds } from '../initToolGroups'; + +export default function setEllipticalROIConfiguration( + matches, + toolNames, + ToolGroupService, + DisplaySetService +) { + const matchDetails = matches.get('ptDisplaySet'); + + if (!matchDetails) { + return; + } + + const { SeriesInstanceUID } = matchDetails; + + const displaySets = DisplaySetService.getDisplaySetsForSeries( + SeriesInstanceUID + ); + + const toolConfig = ToolGroupService.getToolConfiguration( + toolGroupIds.Fusion, + toolNames.EllipticalROI + ); + + const ellipticalROIConfig = { + ...toolConfig, + volumeId: displaySets[0].displaySetInstanceUID, + }; + + ToolGroupService.setToolConfiguration( + toolGroupIds.Fusion, + toolNames.EllipticalROI, + ellipticalROIConfig + ); +} diff --git a/platform/core/src/classes/CommandsManager.js b/platform/core/src/classes/CommandsManager.js index 88823f95e99..2ff961230ad 100644 --- a/platform/core/src/classes/CommandsManager.js +++ b/platform/core/src/classes/CommandsManager.js @@ -18,16 +18,8 @@ import log from '../log.js'; * to extend this class, please check it's source before adding new methods. */ export class CommandsManager { - constructor({ getActiveContexts } = {}) { + constructor({} = {}) { this.contexts = {}; - - if (!getActiveContexts) { - throw new Error( - 'CommandsManager was instantiated without getActiveContexts()' - ); - } - - this._getActiveContexts = getActiveContexts; } /** @@ -122,12 +114,8 @@ export class CommandsManager { contexts.push(context); } } else { - const activeContexts = this._getActiveContexts(); - activeContexts.forEach(activeContext => { - const context = this.getContext(activeContext); - if (context) { - contexts.push(context); - } + Object.keys(this.contexts).forEach(contextName => { + contexts.push(this.getContext(contextName)); }); } diff --git a/platform/core/src/classes/CommandsManager.test.js b/platform/core/src/classes/CommandsManager.test.js index da260281f0f..69d190e3189 100644 --- a/platform/core/src/classes/CommandsManager.test.js +++ b/platform/core/src/classes/CommandsManager.test.js @@ -12,7 +12,11 @@ describe('CommandsManager', () => { options: { passMeToCommandFn: ':wave:' }, }, commandsManagerConfig = { - getActiveContexts: () => ['VIEWER', 'ACTIVE_VIEWER::CORNERSTONE'], + getAppState: () => { + return { + viewers: 'Test', + }; + }, }; beforeEach(() => { @@ -29,12 +33,6 @@ describe('CommandsManager', () => { expect(localCommandsManager.contexts).toEqual({}); }); - it('throws Error if instantiated without getActiveContexts', () => { - expect(() => { - new CommandsManager(); - }).toThrow(new Error('CommandsManager was instantiated without getActiveContexts()')); - }); - describe('createContext()', () => { it('creates a context', () => { commandsManager.createContext(contextName); diff --git a/platform/core/src/extensions/ExtensionManager.js b/platform/core/src/extensions/ExtensionManager.js index 3a483cfea39..38583497d72 100644 --- a/platform/core/src/extensions/ExtensionManager.js +++ b/platform/core/src/extensions/ExtensionManager.js @@ -100,28 +100,27 @@ export default class ExtensionManager { * @param {Object[]} extensions - Array of extensions */ registerExtensions = async (extensions, dataSources = []) => { - const promises = extensions.map(extension => { + // Todo: we ideally should be able to run registrations in parallel + // but currently since some extensions need to be registered before + // others, we need to run them sequentially. We need a postInit hook + // to avoid this sequential async registration + for (const extension of extensions) { const hasConfiguration = Array.isArray(extension); - let promise; try { if (hasConfiguration) { const [ohifExtension, configuration] = extension; - promise = this.registerExtension( + await this.registerExtension( ohifExtension, configuration, dataSources ); } else { - promise = this.registerExtension(extension, {}, dataSources); + await this.registerExtension(extension, {}, dataSources); } } catch (error) { console.error(error); } - - return promise; - }); - - await Promise.all(promises); + } }; /** @@ -160,6 +159,7 @@ export default class ExtensionManager { servicesManager: this._servicesManager, commandsManager: this._commandsManager, hotkeysManager: this._hotkeysManager, + extensionManager: this, appConfig: this._appConfig, configuration, }); diff --git a/platform/core/src/extensions/ExtensionManager.test.js b/platform/core/src/extensions/ExtensionManager.test.js index 940c0cec3b8..8bc41e75523 100644 --- a/platform/core/src/extensions/ExtensionManager.test.js +++ b/platform/core/src/extensions/ExtensionManager.test.js @@ -40,18 +40,18 @@ describe('ExtensionManager.js', () => { }); describe('registerExtensions()', () => { - it('calls registerExtension() for each extension', () => { + it('calls registerExtension() for each extension', async () => { extensionManager.registerExtension = jest.fn(); // SUT const fakeExtensions = [{ one: '1' }, { two: '2' }, { three: '3 ' }]; - extensionManager.registerExtensions(fakeExtensions); + await extensionManager.registerExtensions(fakeExtensions); // Assert expect(extensionManager.registerExtension.mock.calls.length).toBe(3); }); - it('calls registerExtension() for each extension passing its configuration if tuple', () => { + it('calls registerExtension() for each extension passing its configuration if tuple', async () => { const fakeConfiguration = { testing: true }; extensionManager.registerExtension = jest.fn(); @@ -61,7 +61,7 @@ describe('ExtensionManager.js', () => { [{ two: '2' }, fakeConfiguration], { three: '3 ' }, ]; - extensionManager.registerExtensions(fakeExtensions); + await extensionManager.registerExtensions(fakeExtensions); // Assert expect(extensionManager.registerExtension.mock.calls[1][1]).toEqual( @@ -91,6 +91,7 @@ describe('ExtensionManager.js', () => { expect(extension.preRegistration.mock.calls[0][0]).toEqual({ servicesManager, commandsManager, + extensionManager, appConfig, configuration: extensionConfiguration, }); diff --git a/platform/core/src/index.js b/platform/core/src/index.js index a56e2e2adb5..c45e3c6f25e 100644 --- a/platform/core/src/index.js +++ b/platform/core/src/index.js @@ -26,6 +26,7 @@ import { HangingProtocolService, pubSubServiceInterface, UserAuthenticationService, + SegmentationService, } from './services'; import IWebApiDataSource from './DataSources/IWebApiDataSource'; @@ -66,6 +67,7 @@ const OHIF = { ViewportGridService, HangingProtocolService, UserAuthenticationService, + SegmentationService, IWebApiDataSource, DicomMetadataStore, pubSubServiceInterface, @@ -101,6 +103,7 @@ export { ViewportGridService, HangingProtocolService, UserAuthenticationService, + SegmentationService, IWebApiDataSource, DicomMetadataStore, pubSubServiceInterface, diff --git a/platform/core/src/index.test.js b/platform/core/src/index.test.js index a6b8b046a43..955c599e2a9 100644 --- a/platform/core/src/index.test.js +++ b/platform/core/src/index.test.js @@ -32,6 +32,7 @@ describe('Top level exports', () => { 'MeasurementService', 'ToolBarService', 'ViewportGridService', + 'SegmentationService', 'HangingProtocolService', 'UserAuthenticationService', 'IWebApiDataSource', diff --git a/platform/core/src/services/DicomMetadataStore/DicomMetadataStore.js b/platform/core/src/services/DicomMetadataStore/DicomMetadataStore.js index 100a2910e4a..5380dd726ff 100644 --- a/platform/core/src/services/DicomMetadataStore/DicomMetadataStore.js +++ b/platform/core/src/services/DicomMetadataStore/DicomMetadataStore.js @@ -102,37 +102,43 @@ function _getInstanceByImageId(imageId) { * @param {*} metadata metadata inform of key value pairs * @returns */ -function _updateMetadataForStudy(StudyInstanceUID, metadata) { +function _updateMetadataForSeries( + StudyInstanceUID, + SeriesInstanceUID, + metadata +) { const study = _getStudy(StudyInstanceUID); if (!study) { return; } - for (let series of study.series) { - const { SeriesInstanceUID, instances } = series; - // update all instances metadata for this series with the new metadata - instances.forEach(instance => { - Object.keys(metadata).forEach(key => { - // if metadata[key] is an object, we need to merge it with the existing - // metadata of the instance - if (typeof metadata[key] === 'object') { - instance[key] = { ...instance[key], ...metadata[key] }; - } - // otherwise, we just replace the existing metadata with the new one - else { - instance[key] = metadata[key]; - } - }); - }); + const series = study.series.find( + aSeries => aSeries.SeriesInstanceUID === SeriesInstanceUID + ); - // broadcast the series updated event - this._broadcastEvent(EVENTS.SERIES_UPDATED, { - SeriesInstanceUID, - StudyInstanceUID, - madeInClient: true, + const { instances } = series; + // update all instances metadata for this series with the new metadata + instances.forEach(instance => { + Object.keys(metadata).forEach(key => { + // if metadata[key] is an object, we need to merge it with the existing + // metadata of the instance + if (typeof metadata[key] === 'object') { + instance[key] = { ...instance[key], ...metadata[key] }; + } + // otherwise, we just replace the existing metadata with the new one + else { + instance[key] = metadata[key]; + } }); - } + }); + + // broadcast the series updated event + this._broadcastEvent(EVENTS.SERIES_UPDATED, { + SeriesInstanceUID, + StudyInstanceUID, + madeInClient: true, + }); } const BaseImplementation = { @@ -246,7 +252,7 @@ const BaseImplementation = { getSeries: _getSeries, getInstance: _getInstance, getInstanceByImageId: _getInstanceByImageId, - updateMetadataForStudy: _updateMetadataForStudy, + updateMetadataForSeries: _updateMetadataForSeries, }; const DicomMetadataStore = Object.assign( // get study @@ -258,7 +264,5 @@ const DicomMetadataStore = Object.assign( pubSubServiceInterface ); -window._model = _model; - export { DicomMetadataStore }; export default DicomMetadataStore; diff --git a/platform/core/src/services/DisplaySetService/DisplaySetService.js b/platform/core/src/services/DisplaySetService/DisplaySetService.js index 64fcfd31613..7a5ec81584e 100644 --- a/platform/core/src/services/DisplaySetService/DisplaySetService.js +++ b/platform/core/src/services/DisplaySetService/DisplaySetService.js @@ -89,19 +89,18 @@ export default class DisplaySetService { return displaySet; } - setDisplaySetsMetadataUpdated(displaySetUIDs) { - displaySetUIDs.forEach(displaySetUID => { - const displaySet = this.getDisplaySetByUID(displaySetUID); + setDisplaySetMetadataInvalidated(displaySetInstanceUID) { + const displaySet = this.getDisplaySetByUID(displaySetInstanceUID); - if (!displaySet) { - return; - } - - displaySet.needsRerendering = true; - }); + if (!displaySet) { + return; + } - // boradcast event to update listeners with the new displaySets - this._broadcastEvent(EVENTS.DISPLAY_SETS_METADATA_UPDATED, displaySetUIDs); + // broadcast event to update listeners with the new displaySets + this._broadcastEvent( + EVENTS.DISPLAY_SET_SERIES_METADATA_INVALIDATED, + displaySetInstanceUID + ); } deleteDisplaySet(displaySetInstanceUID) { diff --git a/platform/core/src/services/DisplaySetService/EVENTS.js b/platform/core/src/services/DisplaySetService/EVENTS.js index 791e8e9e48d..445736a1230 100644 --- a/platform/core/src/services/DisplaySetService/EVENTS.js +++ b/platform/core/src/services/DisplaySetService/EVENTS.js @@ -2,8 +2,8 @@ const EVENTS = { DISPLAY_SETS_ADDED: 'event::displaySetService:displaySetsAdded', DISPLAY_SETS_CHANGED: 'event::displaySetService:displaySetsChanged', DISPLAY_SETS_REMOVED: 'event::displaySetService:displaySetsRemoved', - DISPLAY_SETS_METADATA_UPDATED: - 'event::displaySetService:displaySetsMetadataUpdated', + DISPLAY_SET_SERIES_METADATA_INVALIDATED: + 'event::displaySetService:displaySetSeriesMetadataInvalidated', }; export default EVENTS; diff --git a/platform/core/src/services/HangingProtocolService/HangingProtocolService.js b/platform/core/src/services/HangingProtocolService/HangingProtocolService.js index 4cc0191aa06..56e5b40e450 100644 --- a/platform/core/src/services/HangingProtocolService/HangingProtocolService.js +++ b/platform/core/src/services/HangingProtocolService/HangingProtocolService.js @@ -45,8 +45,8 @@ class HangingProtocolService { this.customViewportSettings = []; this.customAttributeRetrievalCallbacks = {}; this.listeners = {}; - this.imageLoadStrategies = {}; - this.activeImageLoadStrategy = null; + this.registeredImageLoadStrategies = {}; + this.activeImageLoadStrategyName = null; this.customImageLoadPerformed = false; Object.defineProperty(this, 'EVENTS', { value: EVENTS, @@ -114,8 +114,10 @@ class HangingProtocolService { */ hasCustomImageLoadStrategy() { return ( - this.activeImageLoadStrategy !== null && - this.imageLoadStrategies[this.activeImageLoadStrategy] instanceof Function + this.activeImageLoadStrategyName !== null && + this.registeredImageLoadStrategies[ + this.activeImageLoadStrategyName + ] instanceof Function ); } @@ -123,45 +125,6 @@ class HangingProtocolService { return this.customImageLoadPerformed; } - /** - * Executes the callback function for the custom loading strategy for the images - * if no strategy is set, the default strategy is used - * @param {Object} props Properties to be passed to the loaders - * @param {string} strategyName The name of the strategy to be used - */ - runImageLoadStrategy(props, strategyName) { - if (strategyName) { - this.activeImageLoadStrategy = strategyName; - } - - const displaySetsMatchDetails = this.getDisplaySetsMatchDetails(); - if (this.activeImageLoadStrategy) { - const loader = this.imageLoadStrategies[this.activeImageLoadStrategy]; - const loadedProps = loader({ ...props, displaySetsMatchDetails }); - - // if loader successfully re-arranged the data with the custom strategy - // and returned the new props, then broadcast them - if (!loadedProps) { - return; - } - - this.customImageLoadPerformed = true; - - this._broadcastChange( - this.EVENTS.CUSTOM_IMAGE_LOAD_PERFORMED, - loadedProps - ); - } - } - - /** - * Set the name of the active imageLoadStrategy - * @param {string} name strategy name - */ - setActiveImageLoadStrategy(name) { - this.activeImageLoadStrategy = name; - } - /** * Set the strategy callback for loading images to the HangingProtocolService * @param {string} name strategy name @@ -169,8 +132,7 @@ class HangingProtocolService { */ registerImageLoadStrategy(name, callback) { if (callback instanceof Function && name) { - this.imageLoadStrategies[name] = callback; - this.activeImageLoadStrategy = name; + this.registeredImageLoadStrategies[name] = callback; } } @@ -227,6 +189,30 @@ class HangingProtocolService { } } + /** + * Executes the callback function for the custom loading strategy for the images + * if no strategy is set, the default strategy is used + */ + runImageLoadStrategy(data) { + const loader = this.registeredImageLoadStrategies[ + this.activeImageLoadStrategyName + ]; + const loadedData = loader({ + data, + displaySetsMatchDetails: this.getDisplaySetsMatchDetails(), + matchDetails: this.matchDetails, + }); + + // if loader successfully re-arranged the data with the custom strategy + // and returned the new props, then broadcast them + if (!loadedData) { + return; + } + + this.customImageLoadPerformed = true; + this._broadcastChange(this.EVENTS.CUSTOM_IMAGE_LOAD_PERFORMED, loadedData); + } + _validateProtocol(protocol) { protocol.id = protocol.id || protocol.name; // Automatically compute some number of attributes if they @@ -268,10 +254,17 @@ class HangingProtocolService { // which are entered manually this.stage = 0; this.protocol = protocol; - if (protocol.imageLoadStrategy) { - this.activeImageLoadStrategy = protocol.imageLoadStrategy; + const { imageLoadStrategy } = protocol; + if (imageLoadStrategy) { + // check if the imageLoadStrategy is a valid strategy + if ( + this.registeredImageLoadStrategies[imageLoadStrategy] instanceof + Function + ) { + this.activeImageLoadStrategyName = imageLoadStrategy; + } } - this._updateViewports(protocol); + this._updateViewports(); } /** @@ -333,17 +326,13 @@ class HangingProtocolService { return; } - const { - columns: numCols, - rows: numRows, - viewportOptions = [], - } = layoutProps; + const { columns: numCols, rows: numRows, layoutOptions = [] } = layoutProps; this._broadcastChange(this.EVENTS.NEW_LAYOUT, { layoutType, numRows, numCols, - viewportOptions, + layoutOptions, }); // Matching the displaySets diff --git a/platform/core/src/services/SegmentationService/SegmentationService.js b/platform/core/src/services/SegmentationService/SegmentationService.js new file mode 100644 index 00000000000..b0daa6fc073 --- /dev/null +++ b/platform/core/src/services/SegmentationService/SegmentationService.js @@ -0,0 +1,136 @@ +import pubSubServiceInterface from '../_shared/pubSubServiceInterface'; + +const EVENTS = { + SEGMENTATION_UPDATED: 'event::segmentation_updated', + SEGMENTATION_ADDED: 'event::segmentation_added', + SEGMENTATION_REMOVED: 'event::segmentation_removed', + SEGMENTATION_VISIBILITY_CHANGED: 'event::SEGMENTATION_VISIBILITY_CHANGED', +}; + +const VALUE_TYPES = {}; + +class SegmentationService { + constructor() { + this.segmentations = {}; + this.listeners = {}; + Object.defineProperty(this, 'EVENTS', { + value: EVENTS, + writable: false, + enumerable: true, + configurable: false, + }); + + Object.assign(this, pubSubServiceInterface); + } + + /** + * Get all segmentations. + * + * @return Array of segmentations + */ + getSegmentations() { + const segmentations = this._arrayOfObjects(this.segmentations); + return ( + segmentations && + segmentations.map(m => this.segmentations[Object.keys(m)[0]]) + ); + } + + /** + * Get specific segmentation by its id. + * + * @param id If of the segmentation + * @return segmentation instance + */ + getSegmentation(id) { + return this.segmentations[id]; + } + + addOrUpdateSegmentation( + id, + segmentationSchema, + notYetUpdatedAtSource = false + ) { + const segmentation = this.segmentations[id]; + + if (segmentation) { + Object.assign(segmentation, segmentationSchema); + + this._broadcastEvent(this.EVENTS.SEGMENTATION_UPDATED, { + id, + segmentation, + notYetUpdatedAtSource: notYetUpdatedAtSource, + }); + + return; + } + + this.segmentations[id] = { + ...segmentationSchema, + visible: true, + }; + + this._broadcastEvent(this.EVENTS.SEGMENTATION_ADDED, { + id, + segmentation, + }); + } + + /** + * Toggles the visibility of a segmentation in the state, and broadcasts the event. + * Note: this method does not update the segmentation state in the source. It only + * updates the state, and there should be separate listeners for that. + * @param ids segmentation ids + */ + toggleSegmentationsVisibility(ids) { + ids.forEach(id => { + const segmentation = this.segmentations[id]; + if (!segmentation) { + throw new Error(`Segmentation with id ${id} not found.`); + } + segmentation.visible = !segmentation.visible; + this._broadcastEvent(this.EVENTS.SEGMENTATION_VISIBILITY_CHANGED, { + segmentation, + }); + }); + } + + /** + * Removes a segmentation and broadcasts the removed event. + * + * @param {string} id The segmentation id + */ + remove(id) { + if (!id || !this.segmentations[id]) { + console.warn(`No id provided, or unable to find segmentation by id.`); + return; + } + delete this.segmentations[id]; + this._broadcastEvent(this.EVENTS.SEGMENTATION_REMOVED, { + id, + }); + } + + /** + * Clear all segmentations and broadcasts cleared event. + */ + clear() { + Object.keys(this.segmentations).forEach(id => { + this.remove(id); + }); + + this.segmentations = {}; + } + + /** + * Converts object of objects to array. + * + * @return {Array} Array of objects + */ + _arrayOfObjects = obj => { + return Object.entries(obj).map(e => ({ [e[0]]: e[1] })); + }; +} + +export default SegmentationService; +export { EVENTS, VALUE_TYPES }; diff --git a/platform/core/src/services/SegmentationService/index.js b/platform/core/src/services/SegmentationService/index.js new file mode 100644 index 00000000000..8cda4de8488 --- /dev/null +++ b/platform/core/src/services/SegmentationService/index.js @@ -0,0 +1,8 @@ +import SegmentationService from './SegmentationService'; + +export default { + name: 'SegmentationService', + create: ({ configuration = {} }) => { + return new SegmentationService(); + }, +}; diff --git a/platform/core/src/services/ToolBarService/ToolBarService.js b/platform/core/src/services/ToolBarService/ToolBarService.js index b0a8d30666e..f9e5b951b93 100644 --- a/platform/core/src/services/ToolBarService/ToolBarService.js +++ b/platform/core/src/services/ToolBarService/ToolBarService.js @@ -71,9 +71,11 @@ export default class ToolBarService { } case 'tool': { this.state.primaryToolId = itemId; - commands.forEach(({ commandOptions, context }) => { - commandsManager.runCommand('setToolActive', commandOptions, context); - }); + commands.forEach( + ({ commandName = 'setToolActive', commandOptions, context }) => { + commandsManager.runCommand(commandName, commandOptions, context); + } + ); break; } case 'toggle': { @@ -81,6 +83,9 @@ export default class ToolBarService { this.state.toggles[itemId] === undefined ? true : !this.state.toggles[itemId]; + + const { commands } = interaction; + commands.forEach(({ commandName, commandOptions, context }) => { if (!commandOptions) { commandOptions = {}; diff --git a/platform/core/src/services/index.js b/platform/core/src/services/index.js index 87f714815c2..3ddd0834fc0 100644 --- a/platform/core/src/services/index.js +++ b/platform/core/src/services/index.js @@ -12,6 +12,7 @@ import CineService from './CineService'; import HangingProtocolService from './HangingProtocolService'; import pubSubServiceInterface from './_shared/pubSubServiceInterface'; import UserAuthenticationService from './UserAuthenticationService'; +import SegmentationService from './SegmentationService'; export { MeasurementService, @@ -28,4 +29,5 @@ export { CineService, pubSubServiceInterface, UserAuthenticationService, + SegmentationService, }; diff --git a/platform/core/src/utils/StackManager.js b/platform/core/src/utils/StackManager.js deleted file mode 100644 index 8a46c5d2b81..00000000000 --- a/platform/core/src/utils/StackManager.js +++ /dev/null @@ -1,136 +0,0 @@ -let stackMap = {}; -let configuration = {}; -const stackUpdatedCallbacks = []; - -/** - * Loop through the current series and add metadata to the - * Cornerstone meta data provider. This will be used to fill information - * into the viewport overlays, and to calculate reference lines and orientation markers - * @param {Object} stackMap stackMap object - * @param {Object} displaySet The set of images to make the stack from - * @return {Array} Array with image IDs - */ -function createAndAddStack( - stackMap, - displaySet, - dataSource, - stackUpdatedCallbacks -) { - const { - images, - displaySetInstanceUID, - StudyInstanceUID, - frameRate, - isClip, - initialImageIdIndex, - } = displaySet; - if (!images) { - return; - } - - const imageIds = dataSource.getImageIdsForDisplaySet(displaySet); - - const stack = { - StudyInstanceUID, - displaySetInstanceUID, - imageIds, - frameRate, - isClip, - initialImageIdIndex, - }; - - stackMap[displaySetInstanceUID] = stack; - - return stack; -} - -configuration = { - createAndAddStack, -}; - -/** - * This object contains all the functions needed for interacting with the stack manager. - * Generally, findStack is the only function used. If you want to know when new stacks - * come in, you can register a callback with addStackUpdatedCallback. - */ -const StackManager = { - /** - * Removes all current stacks - */ - clearStacks() { - stackMap = {}; - }, - /** - * Create a stack from an image set, as well as add in the metadata on a per image bases. - * @param displaySet The set of images to make the stack from - * @return {Array} Array with image IDs - */ - makeAndAddStack(displaySet, dataSource) { - return configuration.createAndAddStack( - stackMap, - displaySet, - dataSource, - stackUpdatedCallbacks - ); - }, - /** - * Find a stack from the currently created stacks. - * @param displaySetInstanceUID The UID of the stack to find. - * @returns {*} undefined if not found, otherwise the stack object is returned. - */ - findStack(displaySetInstanceUID) { - return stackMap[displaySetInstanceUID]; - }, - /** - * Find a stack or create one if it has not been created yet - * @param displaySet The set of images to make the stack from - * @return {Array} Array with image IDs - */ - findOrCreateStack(displaySet, dataSource) { - let stack = this.findStack(displaySet.displaySetInstanceUID); - - if (!stack || !stack.imageIds) { - stack = this.makeAndAddStack(displaySet, dataSource); - } - - return stack; - }, - /** - * Gets the underlying map of displaySetInstanceUID to stack object. - * WARNING: Do not change this object. It directly affects the manager. - * @returns {{}} map of displaySetInstanceUID -> stack. - */ - getAllStacks() { - return stackMap; - }, - /** - * Adds in a callback to be called on a stack being added / updated. - * @param callback must accept at minimum one argument, - * which is the stack that was added / updated. - */ - addStackUpdatedCallback(callback) { - if (typeof callback !== 'function') { - throw new Error('callback must be provided as a function'); - } - stackUpdatedCallbacks.push(callback); - }, - /** - * Return configuration - */ - getConfiguration() { - return configuration; - }, - /** - * Set configuration, in order to provide compatibility - * with other systems by overriding this functions - * @param {Object} config object with functions to be overrided - * - * For now, only makeAndAddStack can be overrided - */ - setConfiguration(config) { - configuration = config; - }, -}; - -export { StackManager }; -export default StackManager; diff --git a/platform/core/src/utils/index.js b/platform/core/src/utils/index.js index a59bf0a20a6..923b28c402c 100644 --- a/platform/core/src/utils/index.js +++ b/platform/core/src/utils/index.js @@ -1,5 +1,4 @@ import ObjectPath from './objectPath'; -import StackManager from './StackManager.js'; import absoluteUrl from './absoluteUrl'; import guid from './guid'; import sortBy from './sortBy.js'; @@ -39,7 +38,6 @@ const utils = { formatDate, formatPN, b64toBlob, - StackManager, urlUtil, imageIdToURI, //loadAndCacheDerivedDisplaySets, @@ -67,7 +65,6 @@ export { formatDate, writeScript, b64toBlob, - StackManager, urlUtil, //loadAndCacheDerivedDisplaySets, makeDeferred, diff --git a/platform/core/src/utils/index.test.js b/platform/core/src/utils/index.test.js index fe9d03dedcc..d85a1d86cdf 100644 --- a/platform/core/src/utils/index.test.js +++ b/platform/core/src/utils/index.test.js @@ -15,7 +15,6 @@ describe('Top level exports', () => { 'imageIdToURI', 'roundNumber', 'b64toBlob', - 'StackManager', 'formatDate', 'formatPN', //'loadAndCacheDerivedDisplaySets', diff --git a/platform/ui/src/components/SegmentationTable/SegmentationItem.tsx b/platform/ui/src/components/SegmentationTable/SegmentationItem.tsx index 934a0c3c226..1c1020ae5a3 100644 --- a/platform/ui/src/components/SegmentationTable/SegmentationItem.tsx +++ b/platform/ui/src/components/SegmentationTable/SegmentationItem.tsx @@ -70,7 +70,7 @@ const SegmentationItem = ({ {index} )}
-
+
{ - // filter segmentaion ids that are hidden + // filter segmentation ids that are hidden const visibleSegmentationsIds = segmentations .filter(segmentation => !hiddenSegmentationIds.includes(segmentation.id)) .map(segmentation => segmentation.id); @@ -63,14 +63,14 @@ const SegmentationTable = ({
{!!segmentations.length && - segmentations.map((segData, i) => { - const { id, label, displayText } = segData; + segmentations.map((segmentation, i) => { + const { id, label, displayText = [] } = segmentation; return ( = numPanes ? 0 : state.activeViewportIndex; @@ -91,8 +86,8 @@ export function ViewportGridProvider({ children, service }) { for (let i = 0; i < numPanes; i++) { let xPos, yPos, w, h; - if (viewportOptions && viewportOptions[i]) { - ({ x: xPos, y: yPos, width: w, height: h } = viewportOptions[i]); + if (layoutOptions && layoutOptions[i]) { + ({ x: xPos, y: yPos, width: w, height: h } = layoutOptions[i]); } else { const { row, col } = unravelIndex(i, numRows, numCols); w = 1 / numCols; @@ -172,7 +167,7 @@ export function ViewportGridProvider({ children, service }) { viewportIndex, displaySetInstanceUIDs, viewportOptions = {}, - displaySetOptions = [], + displaySetOptions = [{}], }) => dispatch({ type: 'SET_DISPLAYSET_FOR_VIEWPORT', @@ -187,14 +182,14 @@ export function ViewportGridProvider({ children, service }) { ); const setLayout = useCallback( - ({ layoutType, numRows, numCols, viewportOptions = [] }) => + ({ layoutType, numRows, numCols, layoutOptions = [] }) => dispatch({ type: 'SET_LAYOUT', payload: { layoutType, numRows, numCols, - viewportOptions, + layoutOptions, }, }), [dispatch] diff --git a/platform/viewer/pluginConfig.json b/platform/viewer/pluginConfig.json index 648196e0ccd..761f6077d9a 100644 --- a/platform/viewer/pluginConfig.json +++ b/platform/viewer/pluginConfig.json @@ -23,12 +23,20 @@ { "packageName": "@ohif/extension-dicom-video", "version": "3.0.1" + }, + { + "packageName": "@ohif/extension-tmtv", + "version": "3.0.0" } ], "modes": [ { "packageName": "@ohif/mode-longitudinal", "version": "3.0.0" + }, + { + "packageName": "@ohif/mode-tmtv", + "version": "3.0.0" } ] } diff --git a/platform/viewer/src/App.tsx b/platform/viewer/src/App.tsx index 9b483af2326..dd754780dce 100644 --- a/platform/viewer/src/App.tsx +++ b/platform/viewer/src/App.tsx @@ -61,6 +61,7 @@ function App({ config, defaultExtensions, defaultModes }) { servicesManager, commandsManager, hotkeysManager, + commandsManager, routerBasename, }); const { diff --git a/platform/viewer/src/appInit.js b/platform/viewer/src/appInit.js index 477bc1348b6..47589ffa739 100644 --- a/platform/viewer/src/appInit.js +++ b/platform/viewer/src/appInit.js @@ -12,6 +12,7 @@ import { ToolBarService, ViewportGridService, HangingProtocolService, + SegmentationService, CineService, UserAuthenticationService, errorHandler, @@ -31,13 +32,6 @@ async function appInit(appConfigOrFunc, defaultExtensions, defaultModes) { const commandsManagerConfig = { getAppState: () => {}, - /** Used by commands to determine active context */ - getActiveContexts: () => [ - 'VIEWER', - 'DEFAULT', - 'CORNERSTONE', - 'CORNERSTONE3D', - ], }; const commandsManager = new CommandsManager(commandsManagerConfig); @@ -60,6 +54,7 @@ async function appInit(appConfigOrFunc, defaultExtensions, defaultModes) { ToolBarService, ViewportGridService, HangingProtocolService, + SegmentationService, CineService, UserAuthenticationService, ]); diff --git a/platform/viewer/src/components/ViewportGrid.tsx b/platform/viewer/src/components/ViewportGrid.tsx index eeac0775cda..7ef54cede5e 100644 --- a/platform/viewer/src/components/ViewportGrid.tsx +++ b/platform/viewer/src/components/ViewportGrid.tsx @@ -1,7 +1,4 @@ -/** - * CSS Grid Reference: http://grid.malven.co/ - */ -import React, { useEffect, useCallback, useState } from 'react'; +import React, { useEffect, useCallback } from 'react'; import PropTypes from 'prop-types'; import { ViewportGrid, ViewportPane, useViewportGrid } from '@ohif/ui'; import EmptyViewport from './EmptyViewport'; @@ -99,12 +96,12 @@ function ViewerViewportGrid(props) { useEffect(() => { const { unsubscribe } = HangingProtocolService.subscribe( HangingProtocolService.EVENTS.NEW_LAYOUT, - ({ layoutType, numRows, numCols, viewportOptions }) => { + ({ layoutType, numRows, numCols, layoutOptions }) => { viewportGridService.setLayout({ numRows, numCols, layoutType, - viewportOptions, + layoutOptions, }); } ); @@ -159,13 +156,6 @@ function ViewerViewportGrid(props) { }; }, [viewports]); - /** - * Loading indicator until numCols and numRows are gotten from the HangingProtocolService - */ - if (!numRows || !numCols) { - return null; - } - /** //Changing the Hanging protocol while viewing useEffect(() => { @@ -232,6 +222,7 @@ function ViewerViewportGrid(props) { }; */ + const onDropHandler = (viewportIndex, { displaySetInstanceUID }) => { viewportGridService.setDisplaySetsForViewport({ viewportIndex, @@ -239,11 +230,10 @@ function ViewerViewportGrid(props) { }); }; - const getViewportPanes = () => { + const getViewportPanes = useCallback(() => { const viewportPanes = []; - const numViewportPanes = numCols * numRows; - for (let i = 0; i < numViewportPanes; i++) { + for (let i = 0; i < viewports.length; i++) { const viewportIndex = i; const isActive = activeViewportIndex === viewportIndex; const paneMetadata = viewports[i] || {}; @@ -328,7 +318,14 @@ function ViewerViewportGrid(props) { } return viewportPanes; - }; + }, [viewports, activeViewportIndex, viewportComponents, dataSource]); + + /** + * Loading indicator until numCols and numRows are gotten from the HangingProtocolService + */ + if (!numRows || !numCols) { + return null; + } return ( diff --git a/platform/viewer/src/routes/Mode/Mode.tsx b/platform/viewer/src/routes/Mode/Mode.tsx index 97e8f4f0010..f2e164bbfa3 100644 --- a/platform/viewer/src/routes/Mode/Mode.tsx +++ b/platform/viewer/src/routes/Mode/Mode.tsx @@ -206,7 +206,11 @@ export default function ModeRoute({ // Add SOPClassHandlers to a new SOPClassManager. DisplaySetService.init(extensionManager, sopClassHandlers); - extensionManager.onModeEnter(); + extensionManager.onModeEnter({ + servicesManager, + extensionManager, + commandsManager, + }); mode?.onModeEnter({ servicesManager, extensionManager, commandsManager }); // Adding hanging protocols of extensions after onModeEnter since diff --git a/platform/viewer/src/routes/buildModeRoutes.tsx b/platform/viewer/src/routes/buildModeRoutes.tsx index 1447dff8990..88c5b7b1279 100644 --- a/platform/viewer/src/routes/buildModeRoutes.tsx +++ b/platform/viewer/src/routes/buildModeRoutes.tsx @@ -61,6 +61,7 @@ export default function buildModeRoutes({ servicesManager={servicesManager} commandsManager={commandsManager} hotkeysManager={hotkeysManager} + commandsManager={commandsManager} /> ); @@ -84,6 +85,7 @@ export default function buildModeRoutes({ servicesManager={servicesManager} commandsManager={commandsManager} hotkeysManager={hotkeysManager} + commandsManager={commandsManager} /> ); diff --git a/platform/viewer/src/routes/index.tsx b/platform/viewer/src/routes/index.tsx index 2b1570f673a..4ffb212827b 100644 --- a/platform/viewer/src/routes/index.tsx +++ b/platform/viewer/src/routes/index.tsx @@ -35,6 +35,7 @@ const createRoutes = ({ servicesManager, commandsManager, hotkeysManager, + commandsManager, routerBasename, }) => { const routes =