Files
ohif-viewer/extensions/cornerstone-dynamic-volume/src/panels/PanelGenerateImage.tsx
2025-05-27 11:03:57 +07:00

244 lines
7.9 KiB
TypeScript

import React, { useEffect, useState, useRef } from 'react';
import { useCine, useViewportGrid } from '@ohif/ui';
import { utilities as csUtils, volumeLoader, eventTarget, Enums, cache } from '@cornerstonejs/core';
import { utilities as cstUtils } from '@cornerstonejs/tools';
import DynamicVolumeControls from './DynamicVolumeControls';
const SOPClassHandlerId = '@ohif/extension-default.sopClassHandlerModule.stack';
export default function PanelGenerateImage({ servicesManager, commandsManager }: withAppTypes) {
const { cornerstoneViewportService, viewportGridService, displaySetService } =
servicesManager.services;
const [{ isCineEnabled }, cineService] = useCine();
const [{ activeViewportId }] = useViewportGrid();
//
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);
const [isPlaying, setIsPlaying] = useState(isCineEnabled);
const [timePointRendered, setTimePointRendered] = useState(null);
const [displayingComputed, setDisplayingComputed] = useState(false);
//
const uuidComputedVolume = useRef(csUtils.uuidv4());
const uuidDynamicVolume = useRef(null);
const computedVolumeId = `cornerstoneStreamingImageVolume:${uuidComputedVolume.current}`;
useEffect(() => {
const viewportDataChangedEvt = cornerstoneViewportService.EVENTS.VIEWPORT_DATA_CHANGED;
const cineStateChangedEvt = servicesManager.services.cineService.EVENTS.CINE_STATE_CHANGED;
const viewportDataChangedCallback = evtDetails => {
evtDetails.viewportData.data.forEach(volumeData => {
if (volumeData.volume?.isDynamicVolume()) {
setDynamicVolume(volumeData.volume);
uuidDynamicVolume.current = volumeData.displaySetInstanceUID;
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 () => {
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 displaySetUIDs = viewportGridService.getDisplaySetsUIDsForViewport(activeViewportId);
if (!displaySetUIDs?.length) {
return;
}
const displaySets = displaySetUIDs.map(displaySetService.getDisplaySetByUID);
const dynamicVolumeDisplaySet = displaySets.find(displaySet => displaySet.isDynamicVolume);
if (!dynamicVolumeDisplaySet) {
return;
}
const dynamicVolume = cache
.getVolumes()
.find(volume => volume.volumeId.includes(dynamicVolumeDisplaySet.displaySetInstanceUID));
if (!dynamicVolume) {
return;
}
setDynamicVolume(dynamicVolume);
uuidDynamicVolume.current = dynamicVolumeDisplaySet.displaySetInstanceUID;
const newRange = [1, dynamicVolume.numTimePoints];
setTimePointsRange(newRange);
setTimePointsRangeToUseForGenerate(newRange);
}, [
activeViewportId,
viewportGridService,
displaySetService,
cornerstoneViewportService,
cineService,
]);
function renderGeneratedImage(displaySet) {
commandsManager.runCommand('swapDynamicWithComputedDisplaySet', {
displaySet,
});
setDisplayingComputed(true);
}
function renderDynamicImage(displaySet) {
commandsManager.runCommand('swapComputedWithDynamicDisplaySet');
}
// Get computed volume from cache, calculate the data across the time frames,
// set the scalar data to the computedVolume, and create displaySet
async function onGenerateImage(operationName) {
const dynamicVolumeId = dynamicVolume.volumeId;
if (!dynamicVolumeId) {
return;
}
let computedVolume = cache.getVolume(computedVolumeId);
if (!computedVolume) {
computedVolume = await volumeLoader.createAndCacheDerivedVolume(dynamicVolumeId, {
volumeId: computedVolumeId,
});
}
const [start, end] = timePointsRangeToUseForGenerate;
const frameNumbers = Array.from({ length: end - start + 1 }, (_, i) => i + start - 1);
const options = {
frameNumbers: operationName === 'SUBTRACT' ? [start, end - 1] : frameNumbers,
targetVolume: computedVolume,
};
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
if (!computedDisplaySet) {
const displaySet = {
volumeLoaderSchema: computedVolume.volumeId.split(':')[0],
displaySetInstanceUID: uuidComputedVolume.current,
SOPClassHandlerId: SOPClassHandlerId,
Modality: dynamicVolume.metadata.Modality,
isMultiFrame: false,
numImageFrames: 1,
uid: uuidComputedVolume.current,
referenceDisplaySetUID: dynamicVolume.volumeId.split(':')[1],
madeInClient: true,
FrameOfReferenceUID: dynamicVolume.metadata.FrameOfReferenceUID,
isDerived: true,
imageIds: computedVolume.imageIds,
};
setComputedDisplaySet(displaySet);
renderGeneratedImage(displaySet);
} else {
commandsManager.runCommand('updateVolumeData', {
volume: computedVolume,
});
cornerstoneViewportService.getRenderingEngine().render();
renderGeneratedImage(computedDisplaySet);
}
}
const onPlayPauseChange = isPlaying => {
isPlaying ? handlePlay() : handleStop();
};
const handlePlay = () => {
setIsPlaying(true);
const viewportInfo = cornerstoneViewportService.getViewportInfo(activeViewportId);
if (!viewportInfo) {
return;
}
const { element } = viewportInfo;
cineService.playClip(element, { framesPerSecond: frameRate, viewportId: activeViewportId });
};
const handleStop = () => {
setIsPlaying(false);
const { element } = cornerstoneViewportService.getViewportInfo(activeViewportId);
cineService.stopClip(element);
};
const handleSetFrameRate = newFrameRate => {
setFrameRate(newFrameRate);
handleStop();
handlePlay();
};
function handleSliderChange(newValues) {
if (
newValues[0] === timePointsRangeToUseForGenerate[0] &&
newValues[1] === timePointsRangeToUseForGenerate[1]
) {
return;
}
setTimePointsRangeToUseForGenerate(newValues);
}
if (!dynamicVolume || timePointsRange.length === 0) {
return null;
}
return (
<DynamicVolumeControls
fps={frameRate}
isPlaying={isPlaying}
onPlayPauseChange={onPlayPauseChange}
minFps={1}
maxFps={50}
currentFrameIndex={timePointRendered}
onFpsChange={handleSetFrameRate}
framesLength={timePointsRange[1]}
onFrameChange={timePointIndex => {
dynamicVolume.timePointIndex = timePointIndex;
}}
onGenerate={onGenerateImage}
onDynamicClick={displayingComputed ? () => renderDynamicImage(computedDisplaySet) : null}
onDoubleRangeChange={handleSliderChange}
initialRangeValues={timePointsRangeToUseForGenerate}
/>
);
}