413 lines
15 KiB
TypeScript
413 lines
15 KiB
TypeScript
import * as importedActions from './actions';
|
|
import { utilities, Enums } from '@cornerstonejs/tools';
|
|
import { cache } from '@cornerstonejs/core';
|
|
|
|
const LABELMAP = Enums.SegmentationRepresentations.Labelmap;
|
|
|
|
const commandsModule = ({ commandsManager, servicesManager }: withAppTypes) => {
|
|
const services = servicesManager.services;
|
|
const { displaySetService, viewportGridService, segmentationService } = services;
|
|
|
|
const actions = {
|
|
...importedActions,
|
|
getDynamic4DDisplaySet: () => {
|
|
const displaySets = displaySetService.getActiveDisplaySets();
|
|
|
|
const dynamic4DDisplaySet = displaySets.find(displaySet => {
|
|
const anInstance = displaySet.instances?.[0];
|
|
|
|
if (anInstance) {
|
|
return (
|
|
anInstance.FrameReferenceTime !== undefined ||
|
|
anInstance.NumberOfTimeSlices !== undefined ||
|
|
anInstance.TemporalPositionIdentifier !== undefined
|
|
);
|
|
}
|
|
|
|
return false;
|
|
});
|
|
|
|
return dynamic4DDisplaySet;
|
|
},
|
|
getComputedDisplaySets: () => {
|
|
const displaySetCache = displaySetService.getDisplaySetCache();
|
|
const cachedDisplaySets = [...displaySetCache.values()];
|
|
const computedDisplaySets = cachedDisplaySets.filter(displaySet => {
|
|
return displaySet.isDerived;
|
|
});
|
|
return computedDisplaySets;
|
|
},
|
|
exportTimeReportCSV: ({ segmentations, config, options, summaryStats }) => {
|
|
const dynamic4DDisplaySet = actions.getDynamic4DDisplaySet();
|
|
|
|
const volumeId = dynamic4DDisplaySet?.displaySetInstanceUID;
|
|
|
|
// cache._volumeCache is a map that has a key that includes the volumeId
|
|
// it is not exactly the volumeId, but it is the key that includes the volumeId
|
|
// so we can't do cache._volumeCache.get(volumeId) we should iterate
|
|
// over the keys and find the one that includes the volumeId
|
|
let volumeCacheKey: string | undefined;
|
|
|
|
for (const [key] of cache._volumeCache) {
|
|
if (key.includes(volumeId)) {
|
|
volumeCacheKey = key;
|
|
break;
|
|
}
|
|
}
|
|
|
|
let dynamicVolume;
|
|
if (volumeCacheKey) {
|
|
dynamicVolume = cache.getVolume(volumeCacheKey);
|
|
}
|
|
|
|
const instance = dynamic4DDisplaySet.instances[0];
|
|
|
|
const csv = [];
|
|
|
|
// CSV header information with placeholder empty values for the metadata lines
|
|
csv.push(`Patient ID,${instance.PatientID},`);
|
|
csv.push(`Study Date,${instance.StudyDate},`);
|
|
csv.push(`StudyInstanceUID,${instance.StudyInstanceUID},`);
|
|
csv.push(`StudyDescription,${instance.StudyDescription},`);
|
|
csv.push(`SeriesInstanceUID,${instance.SeriesInstanceUID},`);
|
|
|
|
// empty line
|
|
csv.push('');
|
|
csv.push('');
|
|
|
|
// Helper function to calculate standard deviation
|
|
function calculateStandardDeviation(data) {
|
|
const n = data.length;
|
|
const mean = data.reduce((acc, value) => acc + value, 0) / n;
|
|
const squaredDifferences = data.map(value => (value - mean) ** 2);
|
|
const variance = squaredDifferences.reduce((acc, value) => acc + value, 0) / n;
|
|
const stdDeviation = Math.sqrt(variance);
|
|
return stdDeviation;
|
|
}
|
|
// Iterate through each segmentation to get the timeData and ijkCoords
|
|
segmentations.forEach(segmentation => {
|
|
const volume = segmentationService.getLabelmapVolume(segmentation.segmentationId);
|
|
const [timeData, ijkCoords] = utilities.dynamicVolume.getDataInTime(dynamicVolume, {
|
|
maskVolumeId: volume.volumeId,
|
|
}) as number[][];
|
|
|
|
if (summaryStats) {
|
|
// Adding column headers for pixel identifier and segmentation label ids
|
|
let headers = 'Operation,Segmentation Label ID';
|
|
const maxLength = dynamicVolume.numTimePoints;
|
|
for (let t = 0; t < maxLength; t++) {
|
|
headers += `,Time Point ${t}`;
|
|
}
|
|
csv.push(headers);
|
|
// // perform summary statistics on the timeData including for each time point, mean, median, min, max, and standard deviation for
|
|
// // all the voxels in the ROI
|
|
const mean = [];
|
|
const min = [];
|
|
const minIJK = [];
|
|
const max = [];
|
|
const maxIJK = [];
|
|
const std = [];
|
|
|
|
const numVoxels = timeData.length;
|
|
// Helper function to calculate standard deviation
|
|
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++) {
|
|
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(sum / numVoxels);
|
|
min.push(minValue);
|
|
minIJK.push(ijkCoords[minIndex]);
|
|
max.push(maxValue);
|
|
maxIJK.push(ijkCoords[maxIndex]);
|
|
std.push(calculateStandardDeviation(voxelValues));
|
|
}
|
|
|
|
let row = `Mean,${segmentation.label}`;
|
|
// Generate separate rows for each statistic
|
|
for (let t = 0; t < maxLength; t++) {
|
|
row += `,${mean[t]}`;
|
|
}
|
|
|
|
csv.push(row);
|
|
|
|
row = `Standard Deviation,${segmentation.label}`;
|
|
for (let t = 0; t < maxLength; t++) {
|
|
row += `,${std[t]}`;
|
|
}
|
|
|
|
csv.push(row);
|
|
|
|
row = `Min,${segmentation.label}`;
|
|
for (let t = 0; t < maxLength; t++) {
|
|
row += `,${min[t]}`;
|
|
}
|
|
|
|
csv.push(row);
|
|
|
|
row = `Max,${segmentation.label}`;
|
|
for (let t = 0; t < maxLength; t++) {
|
|
row += `,${max[t]}`;
|
|
}
|
|
|
|
csv.push(row);
|
|
} else {
|
|
// Adding column headers for pixel identifier and segmentation label ids
|
|
let headers = 'Pixel Identifier (IJK),Segmentation Label ID';
|
|
const maxLength = dynamicVolume.numTimePoints;
|
|
for (let t = 0; t < maxLength; t++) {
|
|
headers += `,Time Point ${t}`;
|
|
}
|
|
csv.push(headers);
|
|
// Assuming timeData and ijkCoords are of the same length
|
|
for (let i = 0; i < timeData.length; i++) {
|
|
// Generate the pixel identifier
|
|
const pixelIdentifier = `${ijkCoords[i][0]}_${ijkCoords[i][1]}_${ijkCoords[i][2]}`;
|
|
|
|
// Start a new row for the current pixel
|
|
let row = `${pixelIdentifier},${segmentation.label}`;
|
|
|
|
// Add time data points for this pixel
|
|
for (let t = 0; t < timeData[i].length; t++) {
|
|
row += `,${timeData[i][t]}`;
|
|
}
|
|
|
|
// Append the row to the CSV array
|
|
csv.push(row);
|
|
}
|
|
}
|
|
});
|
|
|
|
// Convert to CSV string
|
|
const csvContent = csv.join('\n');
|
|
|
|
// Generate filename and trigger download
|
|
const filename = `${instance.PatientID}.csv`;
|
|
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
|
|
const link = document.createElement('a');
|
|
const url = URL.createObjectURL(blob);
|
|
link.setAttribute('href', url);
|
|
link.setAttribute('download', filename);
|
|
link.style.visibility = 'hidden';
|
|
document.body.appendChild(link);
|
|
link.click();
|
|
document.body.removeChild(link);
|
|
},
|
|
swapDynamicWithComputedDisplaySet: ({ displaySet }) => {
|
|
const computedDisplaySet = displaySet;
|
|
|
|
const displaySetCache = displaySetService.getDisplaySetCache();
|
|
const cachedDisplaySetKeys = [displaySetCache.keys()];
|
|
const { displaySetInstanceUID } = computedDisplaySet;
|
|
// Check to see if computed display set is already in cache
|
|
if (!cachedDisplaySetKeys.includes(displaySetInstanceUID)) {
|
|
displaySetCache.set(displaySetInstanceUID, computedDisplaySet);
|
|
}
|
|
|
|
// Get all viewports and their corresponding indices
|
|
const { viewports } = viewportGridService.getState();
|
|
|
|
// get the viewports in the grid
|
|
// iterate over them and find the ones that are showing a dynamic
|
|
// volume (displaySet), and replace that exact displaySet with the
|
|
// computed displaySet
|
|
|
|
const dynamic4DDisplaySet = actions.getDynamic4DDisplaySet();
|
|
|
|
const viewportsToUpdate = [];
|
|
|
|
for (const [key, value] of viewports) {
|
|
const viewport = value;
|
|
const viewportOptions = viewport.viewportOptions;
|
|
const { displaySetInstanceUIDs } = viewport;
|
|
const displaySetInstanceUIDIndex = displaySetInstanceUIDs.indexOf(
|
|
dynamic4DDisplaySet.displaySetInstanceUID
|
|
);
|
|
if (displaySetInstanceUIDIndex !== -1) {
|
|
const newViewport = {
|
|
viewportId: viewport.viewportId,
|
|
// merge the other displaySetInstanceUIDs with the new one
|
|
displaySetInstanceUIDs: [
|
|
...displaySetInstanceUIDs.slice(0, displaySetInstanceUIDIndex),
|
|
displaySetInstanceUID,
|
|
...displaySetInstanceUIDs.slice(displaySetInstanceUIDIndex + 1),
|
|
],
|
|
viewportOptions: {
|
|
initialImageOptions: viewportOptions.initialImageOptions,
|
|
viewportType: 'volume',
|
|
orientation: viewportOptions.orientation,
|
|
background: viewportOptions.background,
|
|
},
|
|
};
|
|
viewportsToUpdate.push(newViewport);
|
|
}
|
|
}
|
|
|
|
viewportGridService.setDisplaySetsForViewports(viewportsToUpdate);
|
|
},
|
|
swapComputedWithDynamicDisplaySet: () => {
|
|
// Todo: this assumes there is only one dynamic display set in the viewer
|
|
const dynamicDisplaySet = actions.getDynamic4DDisplaySet();
|
|
|
|
const displaySetCache = displaySetService.getDisplaySetCache();
|
|
const cachedDisplaySetKeys = [...displaySetCache.keys()]; // Fix: Spread to get the array
|
|
const { displaySetInstanceUID } = dynamicDisplaySet;
|
|
|
|
// Check to see if dynamic display set is already in cache
|
|
if (!cachedDisplaySetKeys.includes(displaySetInstanceUID)) {
|
|
displaySetCache.set(displaySetInstanceUID, dynamicDisplaySet);
|
|
}
|
|
|
|
// Get all viewports and their corresponding indices
|
|
const { viewports } = viewportGridService.getState();
|
|
|
|
// Get the computed 4D display set
|
|
const computed4DDisplaySet = actions.getComputedDisplaySets()[0];
|
|
|
|
const viewportsToUpdate = [];
|
|
|
|
for (const [key, value] of viewports) {
|
|
const viewport = value;
|
|
const viewportOptions = viewport.viewportOptions;
|
|
const { displaySetInstanceUIDs } = viewport;
|
|
const displaySetInstanceUIDIndex = displaySetInstanceUIDs.indexOf(
|
|
computed4DDisplaySet.displaySetInstanceUID
|
|
);
|
|
if (displaySetInstanceUIDIndex !== -1) {
|
|
const newViewport = {
|
|
viewportId: viewport.viewportId,
|
|
// merge the other displaySetInstanceUIDs with the new one
|
|
displaySetInstanceUIDs: [
|
|
...displaySetInstanceUIDs.slice(0, displaySetInstanceUIDIndex),
|
|
displaySetInstanceUID,
|
|
...displaySetInstanceUIDs.slice(displaySetInstanceUIDIndex + 1),
|
|
],
|
|
viewportOptions: {
|
|
initialImageOptions: viewportOptions.initialImageOptions,
|
|
viewportType: 'volume',
|
|
orientation: viewportOptions.orientation,
|
|
background: viewportOptions.background,
|
|
},
|
|
};
|
|
viewportsToUpdate.push(newViewport);
|
|
}
|
|
}
|
|
|
|
viewportGridService.setDisplaySetsForViewports(viewportsToUpdate);
|
|
},
|
|
createNewLabelMapForDynamicVolume: async ({ label }) => {
|
|
const { viewports, activeViewportId } = viewportGridService.getState();
|
|
|
|
// get the dynamic 4D display set
|
|
const dynamic4DDisplaySet = actions.getDynamic4DDisplaySet();
|
|
const dynamic4DDisplaySetInstanceUID = dynamic4DDisplaySet.displaySetInstanceUID;
|
|
|
|
// check if the dynamic 4D display set is in the display, if not we might have
|
|
// the computed volumes and we should choose them for the segmentation
|
|
// creation
|
|
|
|
let referenceDisplaySet;
|
|
|
|
const activeViewport = viewports.get(activeViewportId);
|
|
const activeDisplaySetInstanceUIDs = activeViewport.displaySetInstanceUIDs;
|
|
const dynamicIsInActiveViewport = activeDisplaySetInstanceUIDs.includes(
|
|
dynamic4DDisplaySetInstanceUID
|
|
);
|
|
|
|
if (dynamicIsInActiveViewport) {
|
|
referenceDisplaySet = dynamic4DDisplaySet;
|
|
}
|
|
|
|
if (!referenceDisplaySet) {
|
|
// try to see if there is any derived displaySet in the active viewport
|
|
// which is referencing the dynamic 4D display set
|
|
|
|
// Todo: this is wrong but I don't have time to fix it now
|
|
const cachedDisplaySets = displaySetService.getDisplaySetCache();
|
|
for (const [key, displaySet] of cachedDisplaySets) {
|
|
if (displaySet.referenceDisplaySetUID === dynamic4DDisplaySetInstanceUID) {
|
|
referenceDisplaySet = displaySet;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!referenceDisplaySet) {
|
|
throw new Error('No reference display set found based on the dynamic data');
|
|
}
|
|
|
|
const displaySet = displaySetService.getDisplaySetByUID(
|
|
referenceDisplaySet.displaySetInstanceUID
|
|
);
|
|
|
|
const segmentationId = await segmentationService.createLabelmapForDisplaySet(displaySet, {
|
|
label,
|
|
});
|
|
|
|
const firstViewport = viewports.values().next().value;
|
|
|
|
await segmentationService.addSegmentationRepresentation(firstViewport.viewportId, {
|
|
segmentationId,
|
|
});
|
|
|
|
return segmentationId;
|
|
},
|
|
};
|
|
|
|
const definitions = {
|
|
updateSegmentationsChartDisplaySet: {
|
|
commandFn: actions.updateSegmentationsChartDisplaySet,
|
|
storeContexts: [],
|
|
options: {},
|
|
},
|
|
exportTimeReportCSV: {
|
|
commandFn: actions.exportTimeReportCSV,
|
|
storeContexts: [],
|
|
options: {},
|
|
},
|
|
swapDynamicWithComputedDisplaySet: {
|
|
commandFn: actions.swapDynamicWithComputedDisplaySet,
|
|
storeContexts: [],
|
|
options: {},
|
|
},
|
|
createNewLabelMapForDynamicVolume: {
|
|
commandFn: actions.createNewLabelMapForDynamicVolume,
|
|
storeContexts: [],
|
|
options: {},
|
|
},
|
|
swapComputedWithDynamicDisplaySet: {
|
|
commandFn: actions.swapComputedWithDynamicDisplaySet,
|
|
storeContexts: [],
|
|
options: {},
|
|
},
|
|
};
|
|
|
|
return {
|
|
actions,
|
|
definitions,
|
|
defaultContext: 'DYNAMIC-VOLUME:CORNERSTONE',
|
|
};
|
|
};
|
|
|
|
export default commandsModule;
|