OHIF with Cornerstone3D 2.0 (#4347)
This commit is contained in:
1 parent
8981a61b8c
commit
7ce7127d7c
803 files changed
+32760
-15893
No files matched your search
+22
-25
@@ -110,7 +110,12 @@ function _getTimePointsData(volume) {
|
||||
return { timePoints, timePointsUnit };
|
||||
}
|
||||
|
||||
function _getSegmentationData(segmentation, volumesTimePointsCache, displaySetService) {
|
||||
function _getSegmentationData(
|
||||
segmentation,
|
||||
volumesTimePointsCache,
|
||||
{ servicesManager }: { servicesManager: AppTypes.ServicesManager }
|
||||
) {
|
||||
const { displaySetService, segmentationService, viewportGridService } = servicesManager.services;
|
||||
const displaySets = displaySetService.getActiveDisplaySets();
|
||||
|
||||
const dynamic4DDisplaySet = displaySets.find(displaySet => {
|
||||
@@ -145,8 +150,11 @@ function _getSegmentationData(segmentation, volumesTimePointsCache, displaySetSe
|
||||
referencedDynamicVolume.imageIds[0]
|
||||
);
|
||||
|
||||
const segmentationVolume = segmentationService.getLabelmapVolume(segmentation.segmentationId);
|
||||
const maskVolumeId = segmentationVolume?.volumeId;
|
||||
|
||||
const [timeData, _] = csToolsUtils.dynamicVolume.getDataInTime(referencedDynamicVolume, {
|
||||
maskVolumeId: segmentation.id,
|
||||
maskVolumeId,
|
||||
}) as number[][];
|
||||
|
||||
const pixelCount = timeData.length;
|
||||
@@ -155,27 +163,15 @@ function _getSegmentationData(segmentation, volumesTimePointsCache, displaySetSe
|
||||
return [];
|
||||
}
|
||||
|
||||
// since we only use one segmentation representation per segmentationId
|
||||
// it is fine to pick the first one
|
||||
const segmentationRepresentations = csTools.segmentation.state.getSegmentationIdRepresentations(
|
||||
segmentation.id
|
||||
);
|
||||
|
||||
const segmentationRepresentationUID =
|
||||
segmentationRepresentations[0].segmentationRepresentationUID;
|
||||
|
||||
const toolGroupId = csTools.segmentation.state.getToolGroupIdFromSegmentationRepresentationUID(
|
||||
segmentationRepresentationUID
|
||||
);
|
||||
|
||||
// Todo: this is useless we should be able to grab color with just segRepUID and segmentIndex
|
||||
const color = csTools.segmentation.config.color.getColorForSegmentIndex(
|
||||
toolGroupId,
|
||||
segmentationRepresentationUID,
|
||||
1 // segmentIndex
|
||||
);
|
||||
// const color = csTools.segmentation.config.color.getSegmentIndexColor(
|
||||
// segmentationRepresentationUID,
|
||||
// 1 // segmentIndex
|
||||
// );
|
||||
const viewportId = viewportGridService.getActiveViewportId();
|
||||
const color = segmentationService.getSegmentColor(viewportId, segmentation.segmentationId, 1);
|
||||
|
||||
const hexColor = cs.utilities.color.rgbToHex(...color);
|
||||
const hexColor = cs.utilities.color.rgbToHex(color[0], color[1], color[2]);
|
||||
let timePointsData = volumesTimePointsCache.get(referencedDynamicVolume);
|
||||
|
||||
if (!timePointsData) {
|
||||
@@ -219,14 +215,14 @@ function _getSegmentationData(segmentation, volumesTimePointsCache, displaySetSe
|
||||
};
|
||||
}
|
||||
|
||||
function _getInstanceFromSegmentations(segmentations, displaySetService) {
|
||||
function _getInstanceFromSegmentations(segmentations, { servicesManager }) {
|
||||
if (!segmentations.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const volumesTimePointsCache = new WeakMap();
|
||||
const segmentationsData = segmentations.map(segmentation =>
|
||||
_getSegmentationData(segmentation, volumesTimePointsCache, displaySetService)
|
||||
_getSegmentationData(segmentation, volumesTimePointsCache, { servicesManager })
|
||||
);
|
||||
|
||||
const { date: seriesDate, time: seriesTime } = _getDateTimeStr();
|
||||
@@ -266,10 +262,11 @@ function _getInstanceFromSegmentations(segmentations, displaySetService) {
|
||||
}
|
||||
|
||||
function updateSegmentationsChartDisplaySet({ servicesManager }: withAppTypes): void {
|
||||
const { segmentationService, displaySetService } = servicesManager.services;
|
||||
debugger;
|
||||
const { segmentationService } = servicesManager.services;
|
||||
const segmentations = segmentationService.getSegmentations();
|
||||
const { seriesMetadata, instance } =
|
||||
_getInstanceFromSegmentations(segmentations, displaySetService) ?? {};
|
||||
_getInstanceFromSegmentations(segmentations, { servicesManager }) ?? {};
|
||||
|
||||
if (seriesMetadata && instance) {
|
||||
// An event is triggered after adding the instance and the displaySet is created
|
||||
|
||||
@@ -84,11 +84,11 @@ const commandsModule = ({ commandsManager, servicesManager }: withAppTypes) => {
|
||||
const stdDeviation = Math.sqrt(variance);
|
||||
return stdDeviation;
|
||||
}
|
||||
|
||||
// Iterate through each segmentation to get the timeData and ijkCoords
|
||||
segmentations.forEach((segmentation, segmentationIndex) => {
|
||||
segmentations.forEach(segmentation => {
|
||||
const volume = segmentationService.getLabelmapVolume(segmentation.segmentationId);
|
||||
const [timeData, ijkCoords] = utilities.dynamicVolume.getDataInTime(dynamicVolume, {
|
||||
maskVolumeId: segmentation.id,
|
||||
maskVolumeId: volume.volumeId,
|
||||
}) as number[][];
|
||||
|
||||
if (summaryStats) {
|
||||
@@ -113,17 +113,33 @@ const commandsModule = ({ commandsManager, servicesManager }: withAppTypes) => {
|
||||
for (let timeIndex = 0; timeIndex < maxLength; timeIndex++) {
|
||||
// for each voxel in the ROI, get the value at the current time point
|
||||
const voxelValues = [];
|
||||
let sum = 0;
|
||||
let minValue = Infinity;
|
||||
let maxValue = -Infinity;
|
||||
let minIndex = 0;
|
||||
let maxIndex = 0;
|
||||
|
||||
// Single pass through the data to collect all needed values
|
||||
for (let voxelIndex = 0; voxelIndex < numVoxels; voxelIndex++) {
|
||||
voxelValues.push(timeData[voxelIndex][timeIndex]);
|
||||
const value = timeData[voxelIndex][timeIndex];
|
||||
voxelValues.push(value);
|
||||
sum += value;
|
||||
|
||||
if (value < minValue) {
|
||||
minValue = value;
|
||||
minIndex = voxelIndex;
|
||||
}
|
||||
if (value > maxValue) {
|
||||
maxValue = value;
|
||||
maxIndex = voxelIndex;
|
||||
}
|
||||
}
|
||||
|
||||
mean.push(voxelValues.reduce((acc, value) => acc + value, 0) / numVoxels);
|
||||
const minimum = Math.min(...voxelValues);
|
||||
min.push(minimum);
|
||||
minIJK.push(ijkCoords[voxelValues.indexOf(minimum)]);
|
||||
const maximum = Math.max(...voxelValues);
|
||||
max.push(maximum);
|
||||
maxIJK.push(ijkCoords[voxelValues.indexOf(maximum)]);
|
||||
mean.push(sum / numVoxels);
|
||||
min.push(minValue);
|
||||
minIJK.push(ijkCoords[minIndex]);
|
||||
max.push(maxValue);
|
||||
maxIJK.push(ijkCoords[maxIndex]);
|
||||
std.push(calculateStandardDeviation(voxelValues));
|
||||
}
|
||||
|
||||
@@ -340,30 +356,19 @@ const commandsModule = ({ commandsManager, servicesManager }: withAppTypes) => {
|
||||
throw new Error('No reference display set found based on the dynamic data');
|
||||
}
|
||||
|
||||
const segmentationId = await segmentationService.createSegmentationForDisplaySet(
|
||||
referenceDisplaySet.displaySetInstanceUID,
|
||||
{ label }
|
||||
const displaySet = displaySetService.getDisplaySetByUID(
|
||||
referenceDisplaySet.displaySetInstanceUID
|
||||
);
|
||||
|
||||
// Add Segmentation to all toolGroupIds in the viewer
|
||||
const toolGroupIds = Array.from(
|
||||
viewports.values(),
|
||||
viewport => viewport.viewportOptions.toolGroupId
|
||||
);
|
||||
const segmentationId = await segmentationService.createLabelmapForDisplaySet(displaySet, {
|
||||
label,
|
||||
});
|
||||
|
||||
const representationType = LABELMAP;
|
||||
const firstViewport = viewports.values().next().value;
|
||||
|
||||
for (const toolGroupId of toolGroupIds) {
|
||||
const hydrateSegmentation = true;
|
||||
await segmentationService.addSegmentationRepresentationToToolGroup(
|
||||
toolGroupId,
|
||||
segmentationId,
|
||||
hydrateSegmentation,
|
||||
representationType
|
||||
);
|
||||
|
||||
segmentationService.setActiveSegmentationForToolGroup(segmentationId, toolGroupId);
|
||||
}
|
||||
await segmentationService.addSegmentationRepresentation(firstViewport.viewportId, {
|
||||
segmentationId,
|
||||
});
|
||||
|
||||
return segmentationId;
|
||||
},
|
||||
|
||||
@@ -147,8 +147,8 @@ function getFusionViewports() {
|
||||
name: DEFAULT_COLORMAP,
|
||||
opacity: [
|
||||
{ value: 0, opacity: 0 },
|
||||
{ value: 0.1, opacity: 0.3 },
|
||||
{ value: 1, opacity: 0.3 },
|
||||
{ value: 0.1, opacity: 0.8 },
|
||||
{ value: 1, opacity: 0.8 },
|
||||
],
|
||||
},
|
||||
};
|
||||
@@ -191,6 +191,15 @@ function getFusionViewports() {
|
||||
syncInvertState: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'hydrateseg',
|
||||
id: 'sameFORId',
|
||||
source: true,
|
||||
target: true,
|
||||
options: {
|
||||
matchingRules: ['sameFOR'],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
displaySets: [
|
||||
@@ -240,6 +249,15 @@ function getFusionViewports() {
|
||||
syncInvertState: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'hydrateseg',
|
||||
id: 'sameFORId',
|
||||
source: true,
|
||||
target: true,
|
||||
options: {
|
||||
matchingRules: ['sameFOR'],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
displaySets: [
|
||||
@@ -289,6 +307,15 @@ function getFusionViewports() {
|
||||
syncInvertState: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'hydrateseg',
|
||||
id: 'sameFORId',
|
||||
source: true,
|
||||
target: true,
|
||||
options: {
|
||||
matchingRules: ['sameFOR'],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
displaySets: [
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import React from 'react';
|
||||
import { DynamicDataPanel } from './panels';
|
||||
import { Toolbox } from '@ohif/ui-next';
|
||||
import { PanelSegmentation } from '@ohif/extension-cornerstone';
|
||||
import DynamicExport from './panels/DynamicExport';
|
||||
|
||||
function getPanelModule({ commandsManager, extensionManager, servicesManager }) {
|
||||
const wrappedDynamicDataPanel = ({}) => {
|
||||
function getPanelModule({ commandsManager, extensionManager, servicesManager, configuration }) {
|
||||
const wrappedDynamicDataPanel = () => {
|
||||
return (
|
||||
<DynamicDataPanel
|
||||
commandsManager={commandsManager}
|
||||
@@ -14,7 +15,7 @@ function getPanelModule({ commandsManager, extensionManager, servicesManager })
|
||||
);
|
||||
};
|
||||
|
||||
const wrappedDynamicToolbox = ({}) => {
|
||||
const wrappedDynamicSegmentation = () => {
|
||||
return (
|
||||
<>
|
||||
<Toolbox
|
||||
@@ -24,18 +25,17 @@ function getPanelModule({ commandsManager, extensionManager, servicesManager })
|
||||
buttonSectionId="dynamic-toolbox"
|
||||
title="Threshold Tools"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const wrappedDynamicExport = () => {
|
||||
return (
|
||||
<>
|
||||
<DynamicExport
|
||||
commandsManager={commandsManager}
|
||||
<PanelSegmentation
|
||||
servicesManager={servicesManager}
|
||||
commandsManager={commandsManager}
|
||||
extensionManager={extensionManager}
|
||||
/>
|
||||
configuration={configuration}
|
||||
>
|
||||
<DynamicExport
|
||||
servicesManager={servicesManager}
|
||||
commandsManager={commandsManager}
|
||||
/>
|
||||
</PanelSegmentation>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -49,18 +49,11 @@ function getPanelModule({ commandsManager, extensionManager, servicesManager })
|
||||
component: wrappedDynamicDataPanel,
|
||||
},
|
||||
{
|
||||
name: 'dynamic-toolbox',
|
||||
iconName: 'tab-4d',
|
||||
iconLabel: '4D Workflow',
|
||||
label: 'Dynamic Toolbox',
|
||||
component: wrappedDynamicToolbox,
|
||||
},
|
||||
{
|
||||
name: 'dynamic-export',
|
||||
iconName: 'tab-4d',
|
||||
iconLabel: '4D Workflow',
|
||||
label: '4D Workflow',
|
||||
component: wrappedDynamicExport,
|
||||
name: 'dynamic-segmentation',
|
||||
iconName: 'tab-segmentation',
|
||||
iconLabel: 'Segmentation',
|
||||
label: 'Segmentation',
|
||||
component: wrappedDynamicSegmentation,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import React from 'react';
|
||||
import PanelGenerateImage from './PanelGenerateImage';
|
||||
import { Separator } from '@ohif/ui-next';
|
||||
|
||||
function DynamicDataPanel({ servicesManager, commandsManager, tab }: withAppTypes) {
|
||||
return (
|
||||
|
||||
@@ -1,73 +1,52 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { ActionButtons } from '@ohif/ui';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import React from 'react';
|
||||
import { Button, Icons } from '@ohif/ui-next';
|
||||
import { useSegmentations } from '@ohif/extension-cornerstone';
|
||||
|
||||
function DynamicExport({ commandsManager, servicesManager, extensionManager }: withAppTypes) {
|
||||
const { segmentationService } = servicesManager.services;
|
||||
const { t } = useTranslation('dynamicExport');
|
||||
function DynamicExport({ commandsManager, servicesManager }: withAppTypes) {
|
||||
const segmentations = useSegmentations({ servicesManager });
|
||||
|
||||
const [segmentations, setSegmentations] = useState(() => segmentationService.getSegmentations());
|
||||
|
||||
const actions = [
|
||||
{
|
||||
label: 'Export Time Data',
|
||||
onClick: () => {
|
||||
commandsManager.runCommand('exportTimeReportCSV', {
|
||||
segmentations,
|
||||
options: {
|
||||
filename: 'TimeData.csv',
|
||||
},
|
||||
});
|
||||
},
|
||||
disabled: !segmentations?.length,
|
||||
},
|
||||
{
|
||||
label: 'Export ROI Stats',
|
||||
onClick: () => {
|
||||
commandsManager.runCommand('exportTimeReportCSV', {
|
||||
segmentations,
|
||||
summaryStats: true,
|
||||
options: {
|
||||
filename: 'ROIStats.csv',
|
||||
},
|
||||
});
|
||||
},
|
||||
disabled: !segmentations?.length,
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* Update UI based on segmentation changes (added, removed, updated)
|
||||
*/
|
||||
useEffect(() => {
|
||||
// ~~ Subscription
|
||||
const added = segmentationService.EVENTS.SEGMENTATION_ADDED;
|
||||
const updated = segmentationService.EVENTS.SEGMENTATION_UPDATED;
|
||||
const removed = segmentationService.EVENTS.SEGMENTATION_REMOVED;
|
||||
const subscriptions = [];
|
||||
|
||||
[added, updated, removed].forEach(evt => {
|
||||
const { unsubscribe } = segmentationService.subscribe(evt, () => {
|
||||
const segmentations = segmentationService.getSegmentations();
|
||||
setSegmentations(segmentations);
|
||||
});
|
||||
subscriptions.push(unsubscribe);
|
||||
});
|
||||
|
||||
return () => {
|
||||
subscriptions.forEach(unsub => {
|
||||
unsub();
|
||||
});
|
||||
};
|
||||
}, []);
|
||||
if (!segmentations?.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="mt-3 flex justify-center px-2">
|
||||
<ActionButtons
|
||||
actions={actions}
|
||||
t={t}
|
||||
/>
|
||||
<div className="flex gap-2">
|
||||
<div className="flex h-8 w-full items-center rounded pr-0.5">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="pl-1.5"
|
||||
onClick={() => {
|
||||
commandsManager.runCommand('exportTimeReportCSV', {
|
||||
segmentations,
|
||||
options: {
|
||||
filename: 'TimeData.csv',
|
||||
},
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Icons.Export />
|
||||
<span className="pl-1">Time Data</span>
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex h-8 w-full items-center rounded pr-0.5">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="pl-1.5"
|
||||
onClick={() => {
|
||||
commandsManager.runCommand('exportTimeReportCSV', {
|
||||
segmentations,
|
||||
summaryStats: true,
|
||||
options: {
|
||||
filename: 'ROIStats.csv',
|
||||
},
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Icons.Export />
|
||||
<span className="pl-1">ROI Stats</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import {
|
||||
InputDoubleRange,
|
||||
Button,
|
||||
PanelSection,
|
||||
ButtonGroup,
|
||||
@@ -10,6 +9,8 @@ import {
|
||||
Tooltip,
|
||||
} from '@ohif/ui';
|
||||
|
||||
import { DoubleSlider } from '@ohif/ui-next';
|
||||
|
||||
import { Enums } from '@cornerstonejs/core';
|
||||
|
||||
const controlClassNames = {
|
||||
@@ -55,21 +56,15 @@ const DynamicVolumeControls = ({
|
||||
|
||||
const [computeViewMode, setComputeViewMode] = useState(Enums.DynamicOperatorType.SUM);
|
||||
|
||||
const [sliderRangeValues, setSliderRangeValues] = useState([framesLength / 4, framesLength / 2]);
|
||||
|
||||
useEffect(() => {
|
||||
setSliderRangeValues([framesLength / 4, framesLength / 2]);
|
||||
}, [framesLength]);
|
||||
const [sliderRangeValues, setSliderRangeValues] = useState([0, framesLength - 1]);
|
||||
|
||||
const handleSliderChange = newValues => {
|
||||
onDoubleRangeChange(newValues);
|
||||
|
||||
if (newValues[0] === sliderRangeValues[0] && newValues[1] === sliderRangeValues[1]) {
|
||||
return;
|
||||
}
|
||||
setSliderRangeValues(newValues);
|
||||
};
|
||||
|
||||
const formatLabel = value => Math.round(value);
|
||||
|
||||
return (
|
||||
<div className="flex select-none flex-col">
|
||||
<PanelSection
|
||||
@@ -134,7 +129,7 @@ const DynamicVolumeControls = ({
|
||||
}
|
||||
/>
|
||||
<ButtonGroup
|
||||
className={`mt-2 w-full `}
|
||||
className={`mt-2 w-full`}
|
||||
separated={true}
|
||||
>
|
||||
<button
|
||||
@@ -156,15 +151,15 @@ const DynamicVolumeControls = ({
|
||||
{Enums.DynamicOperatorType.SUBTRACT.toString().toUpperCase()}
|
||||
</button>
|
||||
</ButtonGroup>
|
||||
<div className="w-ful mt-2">
|
||||
<InputDoubleRange
|
||||
values={sliderRangeValues}
|
||||
onChange={handleSliderChange}
|
||||
minValue={0}
|
||||
showLabel={true}
|
||||
allowNumberEdit={true}
|
||||
maxValue={framesLength}
|
||||
<div className="mt-2 w-full">
|
||||
<DoubleSlider
|
||||
min={0}
|
||||
max={framesLength - 1}
|
||||
step={1}
|
||||
defaultValue={sliderRangeValues}
|
||||
onValueChange={handleSliderChange}
|
||||
formatLabel={formatLabel}
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
@@ -216,14 +211,14 @@ function FrameControls({
|
||||
>
|
||||
<Icon
|
||||
name={getPlayPauseIconName()}
|
||||
className=" active:text-primary-light hover:bg-customblue-300 h-[24px] w-[24px] cursor-pointer text-white"
|
||||
className="active:text-primary-light hover:bg-customblue-300 h-[24px] w-[24px] cursor-pointer text-white"
|
||||
/>
|
||||
</IconButton>
|
||||
<InputNumber
|
||||
value={currentFrameIndex}
|
||||
onChange={onFrameChange}
|
||||
minValue={0}
|
||||
maxValue={framesLength}
|
||||
maxValue={framesLength - 1}
|
||||
label="Frame"
|
||||
{...controlClassNames}
|
||||
/>
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import React, { useEffect, useState, useRef } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useCine, useViewportGrid } from '@ohif/ui';
|
||||
import { cache, utilities as csUtils, volumeLoader, eventTarget } from '@cornerstonejs/core';
|
||||
import { Enums } from '@cornerstonejs/streaming-image-volume-loader';
|
||||
import { utilities as csUtils, volumeLoader, eventTarget, Enums, cache } from '@cornerstonejs/core';
|
||||
import { utilities as cstUtils } from '@cornerstonejs/tools';
|
||||
import DynamicVolumeControls from './DynamicVolumeControls';
|
||||
|
||||
@@ -16,8 +14,8 @@ export default function PanelGenerateImage({ servicesManager, commandsManager }:
|
||||
const [{ activeViewportId }] = useViewportGrid();
|
||||
|
||||
//
|
||||
const [timePointsRange, setTimePointsRange] = useState([]);
|
||||
const [timePointsRangeToUseForGenerate, setTimePointsRangeToUseForGenerate] = useState([]);
|
||||
const [timePointsRange, setTimePointsRange] = useState([0, 0]);
|
||||
const [timePointsRangeToUseForGenerate, setTimePointsRangeToUseForGenerate] = useState([0, 0]);
|
||||
const [computedDisplaySet, setComputedDisplaySet] = useState(null);
|
||||
const [dynamicVolume, setDynamicVolume] = useState(null);
|
||||
const [frameRate, setFrameRate] = useState(20);
|
||||
@@ -31,47 +29,62 @@ export default function PanelGenerateImage({ servicesManager, commandsManager }:
|
||||
const computedVolumeId = `cornerstoneStreamingImageVolume:${uuidComputedVolume.current}`;
|
||||
|
||||
useEffect(() => {
|
||||
const evt = cornerstoneViewportService.EVENTS.VIEWPORT_DATA_CHANGED;
|
||||
const viewportDataChangedEvt = cornerstoneViewportService.EVENTS.VIEWPORT_DATA_CHANGED;
|
||||
const cineStateChangedEvt = servicesManager.services.cineService.EVENTS.CINE_STATE_CHANGED;
|
||||
|
||||
const { unsubscribe } = cornerstoneViewportService.subscribe(evt, evtDetails => {
|
||||
const viewportDataChangedCallback = evtDetails => {
|
||||
evtDetails.viewportData.data.forEach(volumeData => {
|
||||
if (volumeData.volume.isDynamicVolume()) {
|
||||
if (volumeData.volume?.isDynamicVolume()) {
|
||||
setDynamicVolume(volumeData.volume);
|
||||
uuidDynamicVolume.current = volumeData.displaySetInstanceUID;
|
||||
setTimePointsRange([1, volumeData.volume.numTimePoints]);
|
||||
const newRange = [1, volumeData.volume.numTimePoints];
|
||||
setTimePointsRange(newRange);
|
||||
setTimePointsRangeToUseForGenerate(newRange);
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const cineStateChangedCallback = evt => {
|
||||
setIsPlaying(evt.isPlaying);
|
||||
};
|
||||
|
||||
const { unsubscribe: unsubscribeViewportData } = cornerstoneViewportService.subscribe(
|
||||
viewportDataChangedEvt,
|
||||
viewportDataChangedCallback
|
||||
);
|
||||
const { unsubscribe: unsubscribeCineState } = servicesManager.services.cineService.subscribe(
|
||||
cineStateChangedEvt,
|
||||
cineStateChangedCallback
|
||||
);
|
||||
|
||||
return () => {
|
||||
unsubscribe();
|
||||
unsubscribeViewportData();
|
||||
unsubscribeCineState();
|
||||
};
|
||||
}, [cornerstoneViewportService, cineService, servicesManager.services.cineService]);
|
||||
|
||||
useEffect(() => {
|
||||
const evt = Enums.Events.DYNAMIC_VOLUME_TIME_POINT_INDEX_CHANGED;
|
||||
|
||||
const callback = evt => {
|
||||
setTimePointRendered(evt.detail.timePointIndex);
|
||||
};
|
||||
|
||||
eventTarget.addEventListener(evt, callback);
|
||||
|
||||
return () => {
|
||||
eventTarget.removeEventListener(evt, callback);
|
||||
};
|
||||
}, [cornerstoneViewportService]);
|
||||
|
||||
useEffect(() => {
|
||||
const { unsubscribe } = servicesManager.services.cineService.subscribe(
|
||||
servicesManager.services.cineService.EVENTS.CINE_STATE_CHANGED,
|
||||
evt => {
|
||||
setIsPlaying(evt.isPlaying);
|
||||
}
|
||||
);
|
||||
|
||||
return () => {
|
||||
unsubscribe();
|
||||
};
|
||||
}, [cineService]);
|
||||
|
||||
useEffect(() => {
|
||||
const displaySetUIDs = viewportGridService.getDisplaySetsUIDsForViewport(activeViewportId);
|
||||
|
||||
if (!displaySetUIDs || displaySetUIDs.length === 0) {
|
||||
if (!displaySetUIDs?.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const displaySets = displaySetUIDs.map(displaySetUID =>
|
||||
displaySetService.getDisplaySetByUID(displaySetUID)
|
||||
);
|
||||
|
||||
const displaySets = displaySetUIDs.map(displaySetService.getDisplaySetByUID);
|
||||
const dynamicVolumeDisplaySet = displaySets.find(displaySet => displaySet.isDynamicVolume);
|
||||
|
||||
if (!dynamicVolumeDisplaySet) {
|
||||
@@ -88,23 +101,16 @@ export default function PanelGenerateImage({ servicesManager, commandsManager }:
|
||||
|
||||
setDynamicVolume(dynamicVolume);
|
||||
uuidDynamicVolume.current = dynamicVolumeDisplaySet.displaySetInstanceUID;
|
||||
setTimePointsRange([1, dynamicVolume.numTimePoints]);
|
||||
}, [activeViewportId, cornerstoneViewportService]);
|
||||
|
||||
useEffect(() => {
|
||||
// ~~ Subscription
|
||||
const evt = Enums.Events.DYNAMIC_VOLUME_TIME_POINT_INDEX_CHANGED;
|
||||
|
||||
const callback = evt => {
|
||||
setTimePointRendered(evt.detail.timePointIndex);
|
||||
};
|
||||
|
||||
eventTarget.addEventListener(evt, callback);
|
||||
|
||||
return () => {
|
||||
eventTarget.removeEventListener(evt, callback);
|
||||
};
|
||||
}, [cornerstoneViewportService]);
|
||||
const newRange = [1, dynamicVolume.numTimePoints];
|
||||
setTimePointsRange(newRange);
|
||||
setTimePointsRangeToUseForGenerate(newRange);
|
||||
}, [
|
||||
activeViewportId,
|
||||
viewportGridService,
|
||||
displaySetService,
|
||||
cornerstoneViewportService,
|
||||
cineService,
|
||||
]);
|
||||
|
||||
function renderGeneratedImage(displaySet) {
|
||||
commandsManager.runCommand('swapDynamicWithComputedDisplaySet', {
|
||||
@@ -130,26 +136,19 @@ export default function PanelGenerateImage({ servicesManager, commandsManager }:
|
||||
let computedVolume = cache.getVolume(computedVolumeId);
|
||||
|
||||
if (!computedVolume) {
|
||||
await createComputedVolume(dynamicVolumeId, computedVolumeId);
|
||||
computedVolume = cache.getVolume(computedVolumeId);
|
||||
computedVolume = await volumeLoader.createAndCacheDerivedVolume(dynamicVolumeId, {
|
||||
volumeId: computedVolumeId,
|
||||
});
|
||||
}
|
||||
const [start, end] = timePointsRangeToUseForGenerate;
|
||||
const frameNumbers = Array.from({ length: end - start + 1 }, (_, i) => i + start - 1);
|
||||
|
||||
const vals = timePointsRangeToUseForGenerate;
|
||||
const options = {
|
||||
frameNumbers: operationName === 'SUBTRACT' ? [start, end - 1] : frameNumbers,
|
||||
targetVolume: computedVolume,
|
||||
};
|
||||
|
||||
const targets = Array.from({ length: vals[1] - vals[0] + 1 }, (_, i) => i + vals[0]);
|
||||
|
||||
const dataInTime = cstUtils.dynamicVolume.generateImageFromTimeData(
|
||||
dynamicVolume,
|
||||
operationName,
|
||||
operationName === 'SUBTRACT' ? vals : targets
|
||||
);
|
||||
|
||||
// Add loadStatus.loaded to computed volume and set to true
|
||||
computedVolume.loadStatus = {};
|
||||
computedVolume.loadStatus.loaded = true;
|
||||
// Set computed scalar data to volume
|
||||
const scalarData = computedVolume.getScalarData();
|
||||
scalarData.set(dataInTime);
|
||||
cstUtils.dynamicVolume.updateVolumeFromTimeData(dynamicVolume, operationName, options);
|
||||
|
||||
// If computed display set does not exist, create an object to be used as
|
||||
// the displaySet. If it does exist, update the image data and vtkTexture
|
||||
@@ -166,6 +165,7 @@ export default function PanelGenerateImage({ servicesManager, commandsManager }:
|
||||
madeInClient: true,
|
||||
FrameOfReferenceUID: dynamicVolume.metadata.FrameOfReferenceUID,
|
||||
isDerived: true,
|
||||
imageIds: computedVolume.imageIds,
|
||||
};
|
||||
setComputedDisplaySet(displaySet);
|
||||
renderGeneratedImage(displaySet);
|
||||
@@ -173,19 +173,8 @@ export default function PanelGenerateImage({ servicesManager, commandsManager }:
|
||||
commandsManager.runCommand('updateVolumeData', {
|
||||
volume: computedVolume,
|
||||
});
|
||||
// Check if viewport is currently displaying the computed volume, if so,
|
||||
// call render on the viewports to update the image, if not, call
|
||||
// renderGeneratedImage
|
||||
// if (!cache.getVolume(dynamicVolumeId)) {
|
||||
// for (const viewportId of viewports.keys()) {
|
||||
// const viewportForRendering =
|
||||
// cornerstoneViewportService.getCornerstoneViewport(viewportId);
|
||||
// viewportForRendering.render();
|
||||
// }
|
||||
// } else {
|
||||
cornerstoneViewportService.getRenderingEngine().render();
|
||||
renderGeneratedImage(computedDisplaySet);
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -248,28 +237,7 @@ export default function PanelGenerateImage({ servicesManager, commandsManager }:
|
||||
onGenerate={onGenerateImage}
|
||||
onDynamicClick={displayingComputed ? () => renderDynamicImage(computedDisplaySet) : null}
|
||||
onDoubleRangeChange={handleSliderChange}
|
||||
initialRangeValues={timePointsRangeToUseForGenerate}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
async function createComputedVolume(dynamicVolumeId, computedVolumeId) {
|
||||
if (!cache.getVolume(computedVolumeId)) {
|
||||
const computedVolume = await volumeLoader.createAndCacheDerivedVolume(dynamicVolumeId, {
|
||||
volumeId: computedVolumeId,
|
||||
});
|
||||
return computedVolume;
|
||||
}
|
||||
}
|
||||
|
||||
PanelGenerateImage.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,
|
||||
};
|
||||
Reference in new issue
Block a user