Fix contour annotations RTSS saving (#5545)
* [WIP] Fix contour annotations RTSS saving * fix: Export image sop image reference provider * Fixes for download with filename * Updates to fix metadata issues with CS3D * Updates to save menu * Fixing some additional re-save metadata * Add support for saving with predecessor image sequence * PR comment fixes * fix: Load RTSS after save * fix segmentation save * fix load of mixed seg/rt studies * Use frame module to get information on the frame shown * Fix study browser to use instances.length when numImageFrames not present * fix: Load of display set split from multiple non-FOR referenes * Fix RT and SEG applying to wrong series because of using referenced series sequence without checkout sop instances * Fix save seg * Update to released CS3D versions * test: Add revokeObjectURL to test download blob * Add hard dependency on mode-basic to fix build order issues * fix: build * Try to fix build segmentation * PR comments * PR comment update
This commit is contained in:
1 parent
9c5043ca2c
commit
57205703bb
68 files changed
+962
-648
No files matched your search
@@ -46,8 +46,8 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "7.28.2",
|
||||
"@cornerstonejs/adapters": "4.11.2",
|
||||
"@cornerstonejs/core": "4.11.2",
|
||||
"@cornerstonejs/adapters": "4.11.3",
|
||||
"@cornerstonejs/core": "4.11.3",
|
||||
"@kitware/vtk.js": "32.12.0",
|
||||
"react-color": "2.19.3"
|
||||
}
|
||||
|
||||
@@ -17,19 +17,25 @@ function _getDisplaySetsFromSeries(
|
||||
servicesManager: AppTypes.ServicesManager,
|
||||
extensionManager
|
||||
) {
|
||||
const instance = instances[0];
|
||||
utils.sortStudyInstances(instances);
|
||||
// Choose the LAST instance in the list as the most recently created one.
|
||||
const instance = instances[instances.length - 1];
|
||||
|
||||
const {
|
||||
StudyInstanceUID,
|
||||
SeriesInstanceUID,
|
||||
SOPInstanceUID,
|
||||
SeriesDescription,
|
||||
SeriesDescription = '',
|
||||
SeriesNumber,
|
||||
SeriesDate,
|
||||
SeriesTime,
|
||||
StructureSetDate,
|
||||
StructureSetTime,
|
||||
SOPClassUID,
|
||||
wadoRoot,
|
||||
wadoUri,
|
||||
wadoUriRoot,
|
||||
imageId: predecessorImageId,
|
||||
} = instance;
|
||||
|
||||
const displaySet = {
|
||||
@@ -39,7 +45,13 @@ function _getDisplaySetsFromSeries(
|
||||
displaySetInstanceUID: utils.guid(),
|
||||
SeriesDescription,
|
||||
SeriesNumber,
|
||||
SeriesDate,
|
||||
/**
|
||||
* The "SeriesDate" for a display set is really the display set date, which
|
||||
* should be the date of the instance being used, which will be the structure
|
||||
* set date in this case.
|
||||
*/
|
||||
SeriesDate: StructureSetDate || SeriesDate,
|
||||
SeriesTime: StructureSetTime || SeriesTime,
|
||||
SOPInstanceUID,
|
||||
SeriesInstanceUID,
|
||||
StudyInstanceUID,
|
||||
@@ -54,6 +66,8 @@ function _getDisplaySetsFromSeries(
|
||||
structureSet: null,
|
||||
sopClassUids,
|
||||
instance,
|
||||
instances,
|
||||
predecessorImageId,
|
||||
wadoRoot,
|
||||
wadoUriRoot,
|
||||
wadoUri,
|
||||
@@ -62,7 +76,10 @@ function _getDisplaySetsFromSeries(
|
||||
};
|
||||
|
||||
let referencedSeriesSequence = instance.ReferencedSeriesSequence;
|
||||
if (instance.ReferencedFrameOfReferenceSequence && !instance.ReferencedSeriesSequence) {
|
||||
if (
|
||||
instance.ReferencedFrameOfReferenceSequence?.RTReferencedStudySequence &&
|
||||
!instance.ReferencedSeriesSequence
|
||||
) {
|
||||
instance.ReferencedSeriesSequence = _deriveReferencedSeriesSequenceFromFrameOfReferenceSequence(
|
||||
instance.ReferencedFrameOfReferenceSequence
|
||||
);
|
||||
@@ -70,7 +87,8 @@ function _getDisplaySetsFromSeries(
|
||||
}
|
||||
|
||||
if (!referencedSeriesSequence) {
|
||||
throw new Error('ReferencedSeriesSequence is missing for the RTSTRUCT');
|
||||
console.error('ReferencedSeriesSequence is missing for the RTSTRUCT');
|
||||
return;
|
||||
}
|
||||
|
||||
const referencedSeries = referencedSeriesSequence[0];
|
||||
@@ -79,9 +97,13 @@ function _getDisplaySetsFromSeries(
|
||||
displaySet.referencedSeriesInstanceUID = referencedSeries.SeriesInstanceUID;
|
||||
|
||||
const { displaySetService } = servicesManager.services;
|
||||
const referencedDisplaySets = displaySetService.getDisplaySetsForSeries(
|
||||
displaySet.referencedSeriesInstanceUID
|
||||
);
|
||||
const referencedDisplaySets =
|
||||
displaySetService.getDisplaySetsForReferences(referencedSeriesSequence);
|
||||
if (referencedDisplaySets?.length > 1) {
|
||||
console.warn(
|
||||
'Reference applies to more than 1 display set for Contours, applying only to first display set'
|
||||
);
|
||||
}
|
||||
|
||||
if (!referencedDisplaySets || referencedDisplaySets.length === 0) {
|
||||
// Instead of throwing error, subscribe to display sets added
|
||||
@@ -97,7 +119,7 @@ function _getDisplaySetsFromSeries(
|
||||
}
|
||||
);
|
||||
} else {
|
||||
const referencedDisplaySet = referencedDisplaySets[0];
|
||||
const [referencedDisplaySet] = referencedDisplaySets;
|
||||
displaySet.referencedDisplaySetInstanceUID = referencedDisplaySet.displaySetInstanceUID;
|
||||
displaySet.isReconstructable = referencedDisplaySet.isReconstructable;
|
||||
}
|
||||
|
||||
@@ -89,9 +89,12 @@ async function checkAndLoadContourData({
|
||||
ROIContour.ContourSequence.forEach((Contour, index) => {
|
||||
const promise = resolvedPromises[index];
|
||||
if (promise.status === 'fulfilled') {
|
||||
if (Array.isArray(promise.value) && promise.value.every(Number.isFinite)) {
|
||||
if (
|
||||
Array.isArray(promise.value) &&
|
||||
promise.value.every(it => Number.isFinite(Number(it)))
|
||||
) {
|
||||
// If promise.value is already an array of numbers, use it directly
|
||||
Contour.ContourData = promise.value;
|
||||
Contour.ContourData = promise.value.map(Number);
|
||||
} else {
|
||||
// If the resolved promise value is a byte array (Blob), it needs to be decoded
|
||||
const uint8Array = new Uint8Array(promise.value);
|
||||
|
||||
@@ -103,7 +103,7 @@ function OHIFCornerstoneRTViewport(props: withAppTypes) {
|
||||
const { unsubscribe } = segmentationService.subscribe(
|
||||
segmentationService.EVENTS.SEGMENTATION_LOADING_COMPLETE,
|
||||
evt => {
|
||||
if (evt.rtDisplaySet.displaySetInstanceUID === rtDisplaySet.displaySetInstanceUID) {
|
||||
if (evt.rtDisplaySet?.displaySetInstanceUID === rtDisplaySet.displaySetInstanceUID) {
|
||||
setRtIsLoading(false);
|
||||
}
|
||||
|
||||
|
||||
@@ -46,8 +46,8 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "7.28.2",
|
||||
"@cornerstonejs/adapters": "4.11.2",
|
||||
"@cornerstonejs/core": "4.11.2",
|
||||
"@cornerstonejs/adapters": "4.11.3",
|
||||
"@cornerstonejs/core": "4.11.3",
|
||||
"@kitware/vtk.js": "32.12.0",
|
||||
"react-color": "2.19.3"
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import dcmjs from 'dcmjs';
|
||||
import { classes, Types } from '@ohif/core';
|
||||
import { classes, Types, utils } from '@ohif/core';
|
||||
import { cache, metaData } from '@cornerstonejs/core';
|
||||
import { segmentation as cornerstoneToolsSegmentation } from '@cornerstonejs/tools';
|
||||
import { adaptersRT, helpers, adaptersSEG } from '@cornerstonejs/adapters';
|
||||
@@ -8,8 +8,6 @@ import { DicomMetadataStore } from '@ohif/core';
|
||||
|
||||
import PROMPT_RESPONSES from '../../default/src/utils/_shared/PROMPT_RESPONSES';
|
||||
|
||||
const { datasetToBlob } = dcmjs.data;
|
||||
|
||||
const getTargetViewport = ({ viewportId, viewportGridService }) => {
|
||||
const { viewports, activeViewportId } = viewportGridService.getState();
|
||||
const targetViewportId = viewportId || activeViewportId;
|
||||
@@ -27,7 +25,7 @@ const {
|
||||
|
||||
const {
|
||||
Cornerstone3D: {
|
||||
RTSS: { generateRTSSFromSegmentations },
|
||||
RTSS: { generateRTSSFromRepresentation },
|
||||
},
|
||||
} = adaptersRT;
|
||||
|
||||
@@ -92,6 +90,7 @@ const commandsModule = ({
|
||||
*/
|
||||
generateSegmentation: ({ segmentationId, options = {} }) => {
|
||||
const segmentation = cornerstoneToolsSegmentation.state.getSegmentation(segmentationId);
|
||||
const predecessorImageId = options.predecessorImageId ?? segmentation.predecessorImageId;
|
||||
|
||||
const { imageIds } = segmentation.representationData.Labelmap;
|
||||
|
||||
@@ -173,12 +172,10 @@ const commandsModule = ({
|
||||
labelmap3D.metadata[segmentIndex] = segmentMetadata;
|
||||
});
|
||||
|
||||
const generatedSegmentation = generateSegmentation(
|
||||
referencedImages,
|
||||
labelmap3D,
|
||||
metaData,
|
||||
options
|
||||
);
|
||||
const generatedSegmentation = generateSegmentation(referencedImages, labelmap3D, metaData, {
|
||||
predecessorImageId,
|
||||
...options,
|
||||
});
|
||||
|
||||
return generatedSegmentation;
|
||||
},
|
||||
@@ -212,24 +209,28 @@ const commandsModule = ({
|
||||
* @returns {Object|void} Returns the naturalized report if successfully stored,
|
||||
* otherwise throws an error.
|
||||
*/
|
||||
storeSegmentation: async ({ segmentationId, dataSource }) => {
|
||||
storeSegmentation: async ({ segmentationId, dataSource, modality = 'SEG' }) => {
|
||||
const segmentation = segmentationService.getSegmentation(segmentationId);
|
||||
|
||||
if (!segmentation) {
|
||||
throw new Error('No segmentation found');
|
||||
}
|
||||
|
||||
const { label } = segmentation;
|
||||
const { label, predecessorImageId } = segmentation;
|
||||
const defaultDataSource = dataSource ?? extensionManager.getActiveDataSource()[0];
|
||||
|
||||
const {
|
||||
value: reportName,
|
||||
dataSourceName: selectedDataSource,
|
||||
series,
|
||||
priorSeriesNumber,
|
||||
action,
|
||||
} = await createReportDialogPrompt({
|
||||
servicesManager,
|
||||
extensionManager,
|
||||
predecessorImageId,
|
||||
title: 'Store Segmentation',
|
||||
modality,
|
||||
});
|
||||
|
||||
if (action === PROMPT_RESPONSES.CREATE_REPORT) {
|
||||
@@ -238,12 +239,18 @@ const commandsModule = ({
|
||||
? extensionManager.getDataSources(selectedDataSource)[0]
|
||||
: defaultDataSource;
|
||||
|
||||
const generatedData = actions.generateSegmentation({
|
||||
const args = {
|
||||
segmentationId,
|
||||
options: {
|
||||
SeriesDescription: reportName || label || 'Research Derived Series',
|
||||
SeriesDescription: series ? undefined : reportName || label || 'Contour Series',
|
||||
SeriesNumber: series ? undefined : 1 + priorSeriesNumber,
|
||||
predecessorImageId: series,
|
||||
},
|
||||
});
|
||||
};
|
||||
const generatedDataAsync =
|
||||
(modality === 'SEG' && actions.generateSegmentation(args)) ||
|
||||
(modality === 'RTSTRUCT' && actions.generateContour(args));
|
||||
const generatedData = await generatedDataAsync;
|
||||
|
||||
if (!generatedData || !generatedData.dataset) {
|
||||
throw new Error('Error during segmentation generation');
|
||||
@@ -270,14 +277,9 @@ const commandsModule = ({
|
||||
}
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Converts segmentations into RTSS for download.
|
||||
* This sample function retrieves all segentations and passes to
|
||||
* cornerstone tool adapter to convert to DICOM RTSS format. It then
|
||||
* converts dataset to downloadable blob.
|
||||
*
|
||||
*/
|
||||
downloadRTSS: async ({ segmentationId }) => {
|
||||
|
||||
generateContour: async args => {
|
||||
const { segmentationId, options } = args;
|
||||
const segmentations = segmentationService.getSegmentation(segmentationId);
|
||||
|
||||
// inject colors to the segmentIndex
|
||||
@@ -287,26 +289,34 @@ const commandsModule = ({
|
||||
segment.color = segmentationService.getSegmentColor(
|
||||
firstRepresentation.viewportId,
|
||||
segmentationId,
|
||||
segmentIndex
|
||||
Number(segmentIndex)
|
||||
);
|
||||
});
|
||||
const predecessorImageId = options?.predecessorImageId ?? segmentations.predecessorImageId;
|
||||
const dataset = await generateRTSSFromRepresentation(segmentations, {
|
||||
predecessorImageId,
|
||||
...options,
|
||||
});
|
||||
return { dataset };
|
||||
},
|
||||
|
||||
const RTSS = await generateRTSSFromSegmentations(
|
||||
segmentations,
|
||||
classes.MetadataProvider,
|
||||
DicomMetadataStore
|
||||
);
|
||||
/**
|
||||
* Downloads an RTSS instance from a segmentation or contour
|
||||
* representation.
|
||||
*/
|
||||
downloadRTSS: async args => {
|
||||
const { dataset } = await actions.generateContour(args);
|
||||
const { InstanceNumber: instanceNumber = 1, SeriesInstanceUID: seriesUID } = dataset;
|
||||
|
||||
try {
|
||||
const reportBlob = datasetToBlob(RTSS);
|
||||
|
||||
//Create a URL for the binary.
|
||||
const objectUrl = URL.createObjectURL(reportBlob);
|
||||
window.location.assign(objectUrl);
|
||||
const filename = `rtss-${seriesUID}-${instanceNumber}.dcm`;
|
||||
downloadDICOMData(dataset, filename);
|
||||
} catch (e) {
|
||||
console.warn(e);
|
||||
}
|
||||
},
|
||||
|
||||
toggleActiveSegmentationUtility: ({ itemId: buttonId }) => {
|
||||
const { uiState, setUIState } = useUIStateStore.getState();
|
||||
const isButtonActive = uiState['activeSegmentationUtility'] === buttonId;
|
||||
@@ -321,25 +331,12 @@ const commandsModule = ({
|
||||
};
|
||||
|
||||
const definitions = {
|
||||
loadSegmentationsForViewport: {
|
||||
commandFn: actions.loadSegmentationsForViewport,
|
||||
},
|
||||
|
||||
generateSegmentation: {
|
||||
commandFn: actions.generateSegmentation,
|
||||
},
|
||||
downloadSegmentation: {
|
||||
commandFn: actions.downloadSegmentation,
|
||||
},
|
||||
storeSegmentation: {
|
||||
commandFn: actions.storeSegmentation,
|
||||
},
|
||||
downloadRTSS: {
|
||||
commandFn: actions.downloadRTSS,
|
||||
},
|
||||
toggleActiveSegmentationUtility: {
|
||||
commandFn: actions.toggleActiveSegmentationUtility,
|
||||
},
|
||||
loadSegmentationsForViewport: actions.loadSegmentationsForViewport,
|
||||
generateSegmentation: actions.generateSegmentation,
|
||||
downloadSegmentation: actions.downloadSegmentation,
|
||||
storeSegmentation: actions.storeSegmentation,
|
||||
downloadRTSS: actions.downloadRTSS,
|
||||
toggleActiveSegmentationUtility: actions.toggleActiveSegmentationUtility,
|
||||
};
|
||||
|
||||
return {
|
||||
|
||||
@@ -16,19 +16,24 @@ function _getDisplaySetsFromSeries(
|
||||
servicesManager: AppTypes.ServicesManager,
|
||||
extensionManager
|
||||
) {
|
||||
const instance = instances[0];
|
||||
utils.sortStudyInstances(instances);
|
||||
|
||||
// Choose the LAST instance in the list as the most recently created one.
|
||||
const instance = instances[instances.length - 1];
|
||||
|
||||
const {
|
||||
StudyInstanceUID,
|
||||
SeriesInstanceUID,
|
||||
SOPInstanceUID,
|
||||
SeriesDescription,
|
||||
SeriesDescription = '',
|
||||
SeriesNumber,
|
||||
SeriesDate,
|
||||
StructureSetDate,
|
||||
SOPClassUID,
|
||||
wadoRoot,
|
||||
wadoUri,
|
||||
wadoUriRoot,
|
||||
imageId: predecessorImageId,
|
||||
} = instance;
|
||||
|
||||
const displaySet = {
|
||||
@@ -38,7 +43,7 @@ function _getDisplaySetsFromSeries(
|
||||
displaySetInstanceUID: utils.guid(),
|
||||
SeriesDescription,
|
||||
SeriesNumber,
|
||||
SeriesDate,
|
||||
SeriesDate: SeriesDate || StructureSetDate || '',
|
||||
SOPInstanceUID,
|
||||
SeriesInstanceUID,
|
||||
StudyInstanceUID,
|
||||
@@ -53,6 +58,7 @@ function _getDisplaySetsFromSeries(
|
||||
segments: {},
|
||||
sopClassUids,
|
||||
instance,
|
||||
predecessorImageId,
|
||||
instances: [instance],
|
||||
wadoRoot,
|
||||
wadoUriRoot,
|
||||
@@ -73,10 +79,16 @@ function _getDisplaySetsFromSeries(
|
||||
displaySet.referencedImages = instance.ReferencedSeriesSequence.ReferencedInstanceSequence;
|
||||
displaySet.referencedSeriesInstanceUID = referencedSeries.SeriesInstanceUID;
|
||||
const { displaySetService } = servicesManager.services;
|
||||
const referencedDisplaySets = displaySetService.getDisplaySetsForSeries(
|
||||
displaySet.referencedSeriesInstanceUID
|
||||
const referencedDisplaySets = displaySetService.getDisplaySetsForReferences(
|
||||
instance.ReferencedSeriesSequence
|
||||
);
|
||||
|
||||
if (referencedDisplaySets?.length > 1) {
|
||||
console.warn(
|
||||
'Segmentation does not currently handle references to multiple series, defaulting to first series'
|
||||
);
|
||||
}
|
||||
|
||||
const referencedDisplaySet = referencedDisplaySets[0];
|
||||
|
||||
if (!referencedDisplaySet) {
|
||||
|
||||
@@ -134,7 +134,7 @@ function OHIFCornerstoneSEGViewport(props: withAppTypes) {
|
||||
const { unsubscribe } = segmentationService.subscribe(
|
||||
segmentationService.EVENTS.SEGMENTATION_LOADING_COMPLETE,
|
||||
evt => {
|
||||
if (evt.segDisplaySet.displaySetInstanceUID === segDisplaySet.displaySetInstanceUID) {
|
||||
if (evt.segDisplaySet?.displaySetInstanceUID === segDisplaySet?.displaySetInstanceUID) {
|
||||
setSegIsLoading(false);
|
||||
}
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
"@ohif/extension-cornerstone": "3.12.0-beta.90",
|
||||
"@ohif/extension-measurement-tracking": "3.12.0-beta.90",
|
||||
"@ohif/ui": "3.12.0-beta.90",
|
||||
"dcmjs": "0.43.1",
|
||||
"dcmjs": "0.45.0",
|
||||
"dicom-parser": "1.8.21",
|
||||
"hammerjs": "2.0.8",
|
||||
"prop-types": "15.8.1",
|
||||
@@ -46,9 +46,9 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "7.28.2",
|
||||
"@cornerstonejs/adapters": "4.11.2",
|
||||
"@cornerstonejs/core": "4.11.2",
|
||||
"@cornerstonejs/tools": "4.11.2",
|
||||
"@cornerstonejs/adapters": "4.11.3",
|
||||
"@cornerstonejs/core": "4.11.3",
|
||||
"@cornerstonejs/tools": "4.11.3",
|
||||
"classnames": "2.5.1"
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,14 @@
|
||||
import { metaData, utilities } from '@cornerstonejs/core';
|
||||
import { metaData } from '@cornerstonejs/core';
|
||||
|
||||
import OHIF, { DicomMetadataStore } from '@ohif/core';
|
||||
import OHIF, { DicomMetadataStore, utils } from '@ohif/core';
|
||||
import dcmjs from 'dcmjs';
|
||||
import { adaptersSR } from '@cornerstonejs/adapters';
|
||||
|
||||
import getFilteredCornerstoneToolState from './utils/getFilteredCornerstoneToolState';
|
||||
import hydrateStructuredReport from './utils/hydrateStructuredReport';
|
||||
|
||||
const { downloadBlob } = utils;
|
||||
|
||||
const { MeasurementReport } = adaptersSR.Cornerstone3D;
|
||||
const { log } = OHIF;
|
||||
|
||||
@@ -42,8 +44,6 @@ const _generateReport = (measurementData, additionalFindingTypes, options: Optio
|
||||
dataset.SpecificCharacterSet = 'ISO_IR 192';
|
||||
}
|
||||
|
||||
dataset.InstanceNumber = options.InstanceNumber ?? 1;
|
||||
|
||||
return dataset;
|
||||
};
|
||||
|
||||
@@ -85,8 +85,7 @@ const commandsModule = (props: withAppTypes) => {
|
||||
const reportBlob = dcmjs.data.datasetToBlob(srDataset);
|
||||
|
||||
//Create a URL for the binary.
|
||||
const objectUrl = URL.createObjectURL(reportBlob);
|
||||
window.location.assign(objectUrl);
|
||||
downloadBlob(reportBlob, { filename: 'dicom-sr.dcm' });
|
||||
},
|
||||
|
||||
/**
|
||||
|
||||
@@ -100,6 +100,7 @@ function _getDisplaySetsFromSeries(
|
||||
SeriesTime,
|
||||
ConceptNameCodeSequence,
|
||||
SOPClassUID,
|
||||
imageId: predecessorImageId,
|
||||
} = instance;
|
||||
validateSameStudyUID(instance.StudyInstanceUID, instances);
|
||||
|
||||
@@ -128,6 +129,7 @@ function _getDisplaySetsFromSeries(
|
||||
isImagingMeasurementReport,
|
||||
sopClassUids,
|
||||
instance,
|
||||
predecessorImageId,
|
||||
addInstances,
|
||||
label: SeriesDescription || `${i18n.t('Series')} ${SeriesNumber} - ${i18n.t('SR')}`,
|
||||
};
|
||||
@@ -184,6 +186,10 @@ async function _load(
|
||||
srDisplaySet.referencedImages = [];
|
||||
srDisplaySet.measurements = [];
|
||||
}
|
||||
const { predecessorImageId } = srDisplaySet;
|
||||
for (const measurement of srDisplaySet.measurements) {
|
||||
measurement.predecessorImageId = predecessorImageId;
|
||||
}
|
||||
|
||||
const mappings = measurementService.getSourceMappings(
|
||||
CORNERSTONE_3D_TOOLS_SOURCE_NAME,
|
||||
|
||||
@@ -54,6 +54,11 @@ export default function hydrateStructuredReport(
|
||||
const disableEditing = customizationService.getCustomization('panelMeasurement.disableEditing');
|
||||
|
||||
const displaySet = displaySetService.getDisplaySetByUID(displaySetInstanceUID);
|
||||
const {
|
||||
StudyInstanceUID: studyUID,
|
||||
SeriesInstanceUID: seriesUID,
|
||||
instance: { SOPInstanceUID: sopUID },
|
||||
} = displaySet;
|
||||
|
||||
// TODO -> We should define a strict versioning somewhere.
|
||||
const mappings = measurementService.getSourceMappings(
|
||||
@@ -67,16 +72,12 @@ export default function hydrateStructuredReport(
|
||||
);
|
||||
}
|
||||
|
||||
const instance = DicomMetadataStore.getInstance(
|
||||
displaySet.StudyInstanceUID,
|
||||
displaySet.SeriesInstanceUID,
|
||||
displaySet.SOPInstanceUID
|
||||
);
|
||||
const instance = DicomMetadataStore.getInstance(studyUID, seriesUID, sopUID);
|
||||
|
||||
const sopInstanceUIDToImageId = {};
|
||||
|
||||
displaySet.measurements.forEach(measurement => {
|
||||
const { ReferencedSOPInstanceUID, imageId, frameNumber=1 } = measurement;
|
||||
const { ReferencedSOPInstanceUID, imageId, frameNumber = 1 } = measurement;
|
||||
const key = `${ReferencedSOPInstanceUID}:${frameNumber}`;
|
||||
|
||||
if (!sopInstanceUIDToImageId[key]) {
|
||||
@@ -200,6 +201,7 @@ export default function hydrateStructuredReport(
|
||||
const annotation = {
|
||||
annotationUID: toolData.annotation.annotationUID,
|
||||
data: toolData.annotation.data,
|
||||
predecessorImageId: toolData.predecessorImageId,
|
||||
metadata: {
|
||||
...referenceData,
|
||||
toolName: annotationType,
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
"@ohif/extension-default": "3.12.0-beta.90",
|
||||
"@ohif/i18n": "3.12.0-beta.90",
|
||||
"@ohif/ui": "3.12.0-beta.90",
|
||||
"dcmjs": "0.43.1",
|
||||
"dcmjs": "0.45.0",
|
||||
"dicom-parser": "1.8.21",
|
||||
"hammerjs": "2.0.8",
|
||||
"prop-types": "15.8.1",
|
||||
@@ -42,8 +42,8 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "7.28.2",
|
||||
"@cornerstonejs/core": "4.11.2",
|
||||
"@cornerstonejs/tools": "4.11.2",
|
||||
"@cornerstonejs/core": "4.11.3",
|
||||
"@cornerstonejs/tools": "4.11.3",
|
||||
"classnames": "2.5.1"
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,10 @@
|
||||
import * as importedActions from './actions';
|
||||
import { utilities, Enums } from '@cornerstonejs/tools';
|
||||
import { cache } from '@cornerstonejs/core';
|
||||
import { utils } from '@ohif/core';
|
||||
|
||||
const LABELMAP = Enums.SegmentationRepresentations.Labelmap;
|
||||
import * as importedActions from './actions';
|
||||
|
||||
const { downloadCsv } = utils;
|
||||
|
||||
const commandsModule = ({ commandsManager, servicesManager }: withAppTypes) => {
|
||||
const services = servicesManager.services;
|
||||
@@ -37,7 +39,7 @@ const commandsModule = ({ commandsManager, servicesManager }: withAppTypes) => {
|
||||
});
|
||||
return computedDisplaySets;
|
||||
},
|
||||
exportTimeReportCSV: ({ segmentations, config, options, summaryStats }) => {
|
||||
exportTimeReportCSV: ({ segmentations, summaryStats }) => {
|
||||
const dynamic4DDisplaySet = actions.getDynamic4DDisplaySet();
|
||||
|
||||
const volumeId = dynamic4DDisplaySet?.displaySetInstanceUID;
|
||||
@@ -203,15 +205,7 @@ const commandsModule = ({ commandsManager, servicesManager }: withAppTypes) => {
|
||||
|
||||
// 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);
|
||||
downloadCsv(csvContent, { filename });
|
||||
},
|
||||
swapDynamicWithComputedDisplaySet: ({ displaySet }) => {
|
||||
const computedDisplaySet = displaySet;
|
||||
|
||||
@@ -40,10 +40,10 @@
|
||||
"@cornerstonejs/codec-libjpeg-turbo-8bit": "1.2.2",
|
||||
"@cornerstonejs/codec-openjpeg": "1.3.0",
|
||||
"@cornerstonejs/codec-openjph": "2.4.7",
|
||||
"@cornerstonejs/dicom-image-loader": "4.11.2",
|
||||
"@cornerstonejs/dicom-image-loader": "4.11.3",
|
||||
"@ohif/core": "3.12.0-beta.90",
|
||||
"@ohif/ui": "3.12.0-beta.90",
|
||||
"dcmjs": "0.43.1",
|
||||
"dcmjs": "0.45.0",
|
||||
"dicom-parser": "1.8.21",
|
||||
"hammerjs": "2.0.8",
|
||||
"prop-types": "15.8.1",
|
||||
@@ -53,12 +53,12 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "7.28.2",
|
||||
"@cornerstonejs/adapters": "4.11.2",
|
||||
"@cornerstonejs/ai": "4.11.2",
|
||||
"@cornerstonejs/core": "4.11.2",
|
||||
"@cornerstonejs/labelmap-interpolation": "4.11.2",
|
||||
"@cornerstonejs/polymorphic-segmentation": "4.11.2",
|
||||
"@cornerstonejs/tools": "4.11.2",
|
||||
"@cornerstonejs/adapters": "4.11.3",
|
||||
"@cornerstonejs/ai": "4.11.3",
|
||||
"@cornerstonejs/core": "4.11.3",
|
||||
"@cornerstonejs/labelmap-interpolation": "4.11.3",
|
||||
"@cornerstonejs/polymorphic-segmentation": "4.11.3",
|
||||
"@cornerstonejs/tools": "4.11.3",
|
||||
"@itk-wasm/morphological-contour-interpolation": "1.1.0",
|
||||
"@kitware/vtk.js": "32.12.0",
|
||||
"html2canvas": "1.4.1",
|
||||
|
||||
@@ -1647,15 +1647,13 @@ function commandsModule({
|
||||
* Stores a segmentation and shows it in the viewport
|
||||
* @param props.segmentationId - The ID of the segmentation to store
|
||||
*/
|
||||
storeSegmentationCommand: async ({ segmentationId }) => {
|
||||
storeSegmentationCommand: async args => {
|
||||
const { segmentationId } = args;
|
||||
const { segmentationService, viewportGridService } = servicesManager.services;
|
||||
|
||||
const displaySetInstanceUIDs = await createReportAsync({
|
||||
servicesManager,
|
||||
getReport: () =>
|
||||
commandsManager.runCommand('storeSegmentation', {
|
||||
segmentationId,
|
||||
}),
|
||||
getReport: () => commandsManager.runCommand('storeSegmentation', args),
|
||||
reportType: 'Segmentation',
|
||||
});
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ interface ExportSegmentationSubMenuItemProps {
|
||||
segmentationRepresentationType: string;
|
||||
allowExport: boolean;
|
||||
actions: {
|
||||
storeSegmentation: (segmentationId: string) => Promise<void>;
|
||||
storeSegmentation: (segmentationId: string, modality?: string) => Promise<unknown>;
|
||||
onSegmentationDownloadRTSS: (segmentationId: string) => void;
|
||||
onSegmentationDownload: (segmentationId: string) => void;
|
||||
downloadCSVSegmentationReport: (segmentationId: string) => void;
|
||||
@@ -34,66 +34,39 @@ export const ExportSegmentationSubMenuItem: React.FC<ExportSegmentationSubMenuIt
|
||||
const { t } = useTranslation('SegmentationTable');
|
||||
|
||||
return (
|
||||
<>
|
||||
{segmentationRepresentationType === SegmentationRepresentations.Labelmap && (
|
||||
<DropdownMenuSub>
|
||||
<DropdownMenuSubTrigger className="pl-1">
|
||||
<Icons.Export className="text-foreground" />
|
||||
<span className="pl-2">{t('Download & Export')}</span>
|
||||
</DropdownMenuSubTrigger>
|
||||
<DropdownMenuPortal>
|
||||
<DropdownMenuSubContent>
|
||||
<DropdownMenuLabel className="flex items-center pl-0">
|
||||
<Icons.Download className="h-5 w-5" />
|
||||
<span className="pl-1">{t('Download')}</span>
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuItem
|
||||
onClick={e => {
|
||||
e.preventDefault();
|
||||
actions.downloadCSVSegmentationReport(segmentationId);
|
||||
}}
|
||||
disabled={!allowExport}
|
||||
>
|
||||
{t('CSV Report')}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={e => {
|
||||
e.preventDefault();
|
||||
actions.onSegmentationDownload(segmentationId);
|
||||
}}
|
||||
disabled={!allowExport}
|
||||
>
|
||||
{t('DICOM SEG')}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={e => {
|
||||
e.preventDefault();
|
||||
actions.onSegmentationDownloadRTSS(segmentationId);
|
||||
}}
|
||||
disabled={!allowExport}
|
||||
>
|
||||
{t('DICOM RTSS')}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuLabel className="flex items-center pl-0">
|
||||
<Icons.Export className="h-5 w-5" />
|
||||
<span className="pl-1 pt-1">{t('Export')}</span>
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuItem
|
||||
onClick={e => {
|
||||
e.preventDefault();
|
||||
actions.storeSegmentation(segmentationId);
|
||||
}}
|
||||
disabled={!allowExport}
|
||||
>
|
||||
{t('DICOM SEG')}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuSubContent>
|
||||
</DropdownMenuPortal>
|
||||
</DropdownMenuSub>
|
||||
)}
|
||||
{segmentationRepresentationType === SegmentationRepresentations.Contour && (
|
||||
<>
|
||||
<DropdownMenuSub>
|
||||
<DropdownMenuSubTrigger className="pl-1">
|
||||
<Icons.Export className="text-foreground" />
|
||||
<span className="pl-2">{t('Download & Export')}</span>
|
||||
</DropdownMenuSubTrigger>
|
||||
<DropdownMenuPortal>
|
||||
<DropdownMenuSubContent>
|
||||
<DropdownMenuLabel className="flex items-center pl-0">
|
||||
<Icons.Download className="h-5 w-5" />
|
||||
<span className="pl-1">{t('Download')}</span>
|
||||
</DropdownMenuLabel>
|
||||
{segmentationRepresentationType === SegmentationRepresentations.Labelmap && (
|
||||
<DropdownMenuItem
|
||||
onClick={e => {
|
||||
e.preventDefault();
|
||||
actions.downloadCSVSegmentationReport(segmentationId);
|
||||
}}
|
||||
disabled={!allowExport}
|
||||
>
|
||||
{t('CSV Report')}
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{segmentationRepresentationType === SegmentationRepresentations.Labelmap && (
|
||||
<DropdownMenuItem
|
||||
onClick={e => {
|
||||
e.preventDefault();
|
||||
actions.onSegmentationDownload(segmentationId);
|
||||
}}
|
||||
disabled={!allowExport}
|
||||
>
|
||||
{t('DICOM SEG')}
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
<DropdownMenuItem
|
||||
onClick={e => {
|
||||
e.preventDefault();
|
||||
@@ -101,11 +74,35 @@ export const ExportSegmentationSubMenuItem: React.FC<ExportSegmentationSubMenuIt
|
||||
}}
|
||||
disabled={!allowExport}
|
||||
>
|
||||
<Icons.Export className="text-foreground" />
|
||||
<span className="pl-2">{t('Download DICOM RTSS')}</span>
|
||||
{t('DICOM RTSS')}
|
||||
</DropdownMenuItem>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuLabel className="flex items-center pl-0">
|
||||
<Icons.Export className="h-5 w-5" />
|
||||
<span className="pl-1 pt-1">{t('Export')}</span>
|
||||
</DropdownMenuLabel>
|
||||
{segmentationRepresentationType === SegmentationRepresentations.Labelmap && (
|
||||
<DropdownMenuItem
|
||||
onClick={e => {
|
||||
e.preventDefault();
|
||||
actions.storeSegmentation(segmentationId, 'SEG');
|
||||
}}
|
||||
disabled={!allowExport}
|
||||
>
|
||||
{t('DICOM SEG')}
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
<DropdownMenuItem
|
||||
onClick={e => {
|
||||
e.preventDefault();
|
||||
actions.storeSegmentation(segmentationId, 'RTSTRUCT');
|
||||
}}
|
||||
disabled={!allowExport}
|
||||
>
|
||||
{t('DICOM RTSS')}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuSubContent>
|
||||
</DropdownMenuPortal>
|
||||
</DropdownMenuSub>
|
||||
);
|
||||
};
|
||||
@@ -46,21 +46,21 @@ export const CustomDropdownMenuContent = () => {
|
||||
segmentationId = activeSegmentationId;
|
||||
}
|
||||
|
||||
// Determine if export is allowed for this segmentation
|
||||
if (exportOptions && segmentationId) {
|
||||
const exportOption = exportOptions.find(opt => opt.segmentationId === segmentationId);
|
||||
allowExport = exportOption?.isExportable || false;
|
||||
}
|
||||
|
||||
if (!segmentation || !segmentationId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Determine if export is allowed for this segmentation
|
||||
if (exportOptions) {
|
||||
const exportOption = exportOptions.find(opt => opt.segmentationId === segmentationId);
|
||||
allowExport = exportOption?.isExportable || false;
|
||||
}
|
||||
|
||||
const actions = {
|
||||
storeSegmentation: async segmentationId => {
|
||||
storeSegmentation: async (segmentationId, modality = 'SEG') => {
|
||||
commandsManager.run({
|
||||
commandName: 'storeSegmentation',
|
||||
commandOptions: { segmentationId },
|
||||
commandOptions: { segmentationId, modality },
|
||||
context: 'CORNERSTONE',
|
||||
});
|
||||
},
|
||||
|
||||
@@ -461,6 +461,9 @@ const connectMeasurementServiceToTools = ({
|
||||
const annotationManager = annotation.state.getAnnotationManager();
|
||||
const newAnnotation = {
|
||||
annotationUID: measurement.uid,
|
||||
// Not used in CS3D but stored in the CS3D state so that saving
|
||||
// can apply the predecessor consistently.
|
||||
predecessorImageId: measurement?.predecessorImageId,
|
||||
highlighted: false,
|
||||
isLocked: false,
|
||||
// This is used to force a re-render of the annotation to
|
||||
|
||||
@@ -278,11 +278,13 @@ class SegmentationService extends PubSubService {
|
||||
viewportId: string,
|
||||
{
|
||||
segmentationId,
|
||||
predecessorImageId,
|
||||
type,
|
||||
config,
|
||||
suppressEvents = false,
|
||||
}: {
|
||||
segmentationId: string;
|
||||
predecessorImageId?: string;
|
||||
type?: csToolsEnums.SegmentationRepresentations;
|
||||
config?: {
|
||||
blendMode?: csEnums.BlendModes;
|
||||
@@ -291,6 +293,9 @@ class SegmentationService extends PubSubService {
|
||||
}
|
||||
): Promise<void> {
|
||||
const segmentation = this.getSegmentation(segmentationId);
|
||||
if (segmentation && !segmentation.predecessorImageId && predecessorImageId) {
|
||||
segmentation.predecessorImageId = predecessorImageId;
|
||||
}
|
||||
const csViewport = this.getAndValidateViewport(viewportId);
|
||||
|
||||
if (!csViewport) {
|
||||
|
||||
@@ -1090,8 +1090,10 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
|
||||
? csToolsEnums.SegmentationRepresentations.Labelmap
|
||||
: csToolsEnums.SegmentationRepresentations.Contour;
|
||||
|
||||
const { predecessorImageId } = displaySet;
|
||||
segmentationService.addSegmentationRepresentation(viewport.id, {
|
||||
segmentationId,
|
||||
predecessorImageId,
|
||||
type: representationType,
|
||||
});
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { utils } from '@ohif/core';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import html2canvas from 'html2canvas';
|
||||
import { getEnabledElement, StackViewport, BaseVolumeViewport } from '@cornerstonejs/core';
|
||||
@@ -5,6 +6,8 @@ import { ToolGroupManager, segmentation, Enums } from '@cornerstonejs/tools';
|
||||
import { getEnabledElement as OHIFgetEnabledElement } from '../state';
|
||||
import { useSystem } from '@ohif/core/src';
|
||||
|
||||
const { downloadUrl } = utils;
|
||||
|
||||
const DEFAULT_SIZE = 512;
|
||||
const MAX_TEXTURE_SIZE = 10000;
|
||||
const VIEWPORT_ID = 'cornerstone-viewport-download-form';
|
||||
@@ -218,10 +221,7 @@ const CornerstoneViewportDownloadForm = ({
|
||||
}
|
||||
|
||||
const canvas = await html2canvas(divForDownloadViewport as HTMLElement);
|
||||
const link = document.createElement('a');
|
||||
link.download = `${filename}.${fileType}`;
|
||||
link.href = canvas.toDataURL(`image/${fileType}`, 1.0);
|
||||
link.click();
|
||||
downloadUrl(canvas.toDataURL(`image/${fileType}`, 1.0));
|
||||
};
|
||||
|
||||
const ViewportDownloadFormNew = customizationService.getCustomization(
|
||||
|
||||
@@ -13,6 +13,7 @@ Object.defineProperty(global, 'URL', {
|
||||
writable: true,
|
||||
value: {
|
||||
createObjectURL: jest.fn().mockReturnValue('mock-url'),
|
||||
revokeObjectURL: () => undefined,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
import { utils } from '@ohif/core';
|
||||
|
||||
const { downloadCsv } = utils;
|
||||
|
||||
export function generateSegmentationCSVReport(
|
||||
segmentationData,
|
||||
info: {
|
||||
@@ -121,19 +125,7 @@ export function generateSegmentationCSVReport(
|
||||
csvString += formattedRow.join(',') + '\n';
|
||||
}
|
||||
|
||||
// Create a download link and trigger the download
|
||||
const blob = new Blob([csvString], { type: 'text/csv;charset=utf-8;' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
|
||||
link.setAttribute('href', url);
|
||||
link.setAttribute(
|
||||
'download',
|
||||
`${segmentationData.label || 'Segmentation'}_Report_${new Date().toISOString().split('T')[0]}.csv`
|
||||
);
|
||||
link.style.visibility = 'hidden';
|
||||
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
downloadCsv(csvString, {
|
||||
filename: `${segmentationData.label || 'Segmentation'}_Report_${new Date().toISOString().split('T')[0]}.csv`,
|
||||
});
|
||||
}
|
||||
@@ -59,7 +59,7 @@ export const hasExportableLabelMapData = (
|
||||
SeriesInstanceUID
|
||||
);
|
||||
|
||||
return displaySet?.isReconstructable;
|
||||
return !!displaySet;
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
"peerDependencies": {
|
||||
"@ohif/core": "3.12.0-beta.90",
|
||||
"@ohif/i18n": "3.12.0-beta.90",
|
||||
"dcmjs": "0.43.1",
|
||||
"dcmjs": "0.45.0",
|
||||
"dicomweb-client": "0.10.4",
|
||||
"prop-types": "15.8.1",
|
||||
"react": "18.3.1",
|
||||
|
||||
@@ -28,7 +28,8 @@ function PanelStudyBrowser({
|
||||
const { servicesManager, commandsManager, extensionManager } = useSystem();
|
||||
const { displaySetService, customizationService } = servicesManager.services;
|
||||
const navigate = useNavigate();
|
||||
const studyMode = (customizationService.getCustomization('studyBrowser.studyMode') as string) || 'all';
|
||||
const studyMode =
|
||||
(customizationService.getCustomization('studyBrowser.studyMode') as string) || 'all';
|
||||
|
||||
const internalImageViewer = useImageViewer();
|
||||
const StudyInstanceUIDs = internalImageViewer.StudyInstanceUIDs;
|
||||
@@ -384,7 +385,11 @@ function PanelStudyBrowser({
|
||||
|
||||
const displaySetInstanceUID = jumpToDisplaySet;
|
||||
// It is possible to navigate to a study not currently in view
|
||||
const thumbnailLocation = _findTabAndStudyOfDisplaySet(displaySetInstanceUID, tabs, activeTabName);
|
||||
const thumbnailLocation = _findTabAndStudyOfDisplaySet(
|
||||
displaySetInstanceUID,
|
||||
tabs,
|
||||
activeTabName
|
||||
);
|
||||
if (!thumbnailLocation) {
|
||||
return;
|
||||
}
|
||||
@@ -489,7 +494,7 @@ function _mapDisplaySets(displaySets, displaySetLoadingState, thumbnailImageSrcM
|
||||
seriesNumber: ds.SeriesNumber,
|
||||
modality: ds.Modality,
|
||||
seriesDate: formatDate(ds.SeriesDate),
|
||||
numInstances: ds.numImageFrames,
|
||||
numInstances: ds.numImageFrames ?? ds.instances?.length,
|
||||
loadingProgress,
|
||||
countIcon: ds.countIcon,
|
||||
messages: ds.messages,
|
||||
@@ -538,11 +543,13 @@ function _findTabAndStudyOfDisplaySet(
|
||||
tabs: TabsProps,
|
||||
currentTabName: string
|
||||
) {
|
||||
const current = tabs.find(tab => tab.name===currentTabName) || tabs[0];
|
||||
const current = tabs.find(tab => tab.name === currentTabName) || tabs[0];
|
||||
const biasedTabs = [current, ...tabs];
|
||||
|
||||
for (let t = 0; t < biasedTabs.length; t++) {
|
||||
const study = biasedTabs[t].studies.find(study => study.displaySets.find(ds => ds.displaySetInstanceUID ===displaySetInstanceUID));
|
||||
const study = biasedTabs[t].studies.find(study =>
|
||||
study.displaySets.find(ds => ds.displaySetInstanceUID === displaySetInstanceUID)
|
||||
);
|
||||
if (study) {
|
||||
return {
|
||||
tabName: biasedTabs[t].name,
|
||||
|
||||
@@ -1,12 +1,39 @@
|
||||
import PROMPT_RESPONSES from '../utils/_shared/PROMPT_RESPONSES';
|
||||
|
||||
/**
|
||||
* Creates and shows a report dialog prompt.
|
||||
* The input for this is:
|
||||
* - `title` shown in the dialog
|
||||
* - `modality` being stored, used to query existing series
|
||||
* - `minSeriesNumber` is the start of new series of this modality type.
|
||||
* Will get set to 4000 if not determined by the modality
|
||||
* - predecessorImageId is the image id that this series was currently loaded
|
||||
* from. That allows defaulting the dialog to show the specified series instead
|
||||
* of always creating a new series.
|
||||
*
|
||||
* The response is:
|
||||
* - `value`, the default name of the object/series being created
|
||||
* - `dataSourceName`, where to store the object to
|
||||
* - `series`, is the series to store do, as referenced by a predecessorImageId value.
|
||||
* - `priorSeriesNumber` is the previously lowest series number at least minSeriesNumber
|
||||
* of all the seris of the given modality type.
|
||||
*
|
||||
* This should be provided to the DICOM encoder, which will get the predecessor
|
||||
* sequence from the metaData provider so that the saved series will replace
|
||||
* the existing instance in the same series.
|
||||
* This will be falsy for a new series.
|
||||
*/
|
||||
export default function CreateReportDialogPrompt({
|
||||
title = 'Create Report',
|
||||
modality = 'SR',
|
||||
minSeriesNumber = 0,
|
||||
predecessorImageId,
|
||||
extensionManager,
|
||||
servicesManager,
|
||||
}): Promise<{
|
||||
value: string;
|
||||
dataSourceName: string;
|
||||
priorSeriesNumber?: number;
|
||||
series: string;
|
||||
action: (typeof PROMPT_RESPONSES)[keyof typeof PROMPT_RESPONSES];
|
||||
}> {
|
||||
@@ -16,18 +43,33 @@ export default function CreateReportDialogPrompt({
|
||||
|
||||
const allowMultipleDataSources = window.config.allowMultiSelectExport;
|
||||
|
||||
return new Promise(function (resolve, reject) {
|
||||
minSeriesNumber ||=
|
||||
(modality === 'SR' && 3000) ||
|
||||
(modality === 'SEG' && 3100) ||
|
||||
(modality === 'RTSTRUCT' && 3200) ||
|
||||
4000;
|
||||
|
||||
return new Promise(function (resolve) {
|
||||
uiDialogService.show({
|
||||
id: 'report-dialog',
|
||||
title,
|
||||
content: ReportDialog,
|
||||
contentProps: {
|
||||
dataSources: allowMultipleDataSources ? dataSources : undefined,
|
||||
onSave: async ({ reportName, dataSource: selectedDataSource, series }) => {
|
||||
predecessorImageId,
|
||||
minSeriesNumber,
|
||||
modality,
|
||||
onSave: async ({
|
||||
reportName,
|
||||
dataSource: selectedDataSource,
|
||||
series,
|
||||
priorSeriesNumber,
|
||||
}) => {
|
||||
resolve({
|
||||
value: reportName,
|
||||
dataSourceName: selectedDataSource,
|
||||
series,
|
||||
priorSeriesNumber,
|
||||
action: PROMPT_RESPONSES.CREATE_REPORT,
|
||||
});
|
||||
},
|
||||
|
||||
@@ -11,28 +11,44 @@ type DataSource = {
|
||||
|
||||
type ReportDialogProps = {
|
||||
dataSources: DataSource[];
|
||||
modality?: string;
|
||||
predecessorImageId?: string;
|
||||
hide: () => void;
|
||||
onSave: (data: { reportName: string; dataSource: string | null; series: string | null }) => void;
|
||||
onSave: (data: {
|
||||
reportName: string;
|
||||
dataSource: string | null;
|
||||
series: string | null;
|
||||
priorSeriesNumber: number;
|
||||
}) => void;
|
||||
onCancel: () => void;
|
||||
};
|
||||
|
||||
function ReportDialog({ dataSources, hide, onSave, onCancel }: ReportDialogProps) {
|
||||
function ReportDialog({
|
||||
dataSources,
|
||||
modality = 'SR',
|
||||
predecessorImageId,
|
||||
minSeriesNumber = 3000,
|
||||
hide,
|
||||
onSave,
|
||||
onCancel,
|
||||
}: ReportDialogProps) {
|
||||
const { servicesManager } = useSystem();
|
||||
const [selectedDataSource, setSelectedDataSource] = useState<string | null>(
|
||||
dataSources?.[0]?.value ?? null
|
||||
);
|
||||
const [selectedSeries, setSelectedSeries] = useState<string | null>(null);
|
||||
const [reportName, setReportName] = useState('');
|
||||
|
||||
const { displaySetService } = servicesManager.services;
|
||||
|
||||
const [selectedSeries, setSelectedSeries] = useState<string | null>(predecessorImageId || null);
|
||||
const [reportName, setReportName] = useState('');
|
||||
|
||||
const seriesOptions = useMemo(() => {
|
||||
const displaySetsMap = displaySetService.getDisplaySetCache();
|
||||
const displaySets = Array.from(displaySetsMap.values());
|
||||
const options = displaySets
|
||||
.filter(ds => ds.Modality === 'SR')
|
||||
.filter(ds => ds.Modality === modality)
|
||||
.map(ds => ({
|
||||
value: ds.SeriesInstanceUID,
|
||||
value: ds.predecessorImageId || ds.SeriesInstanceUID,
|
||||
seriesNumber: isFinite(ds.SeriesNumber) ? ds.SeriesNumber : minSeriesNumber,
|
||||
description: ds.SeriesDescription,
|
||||
label: `${ds.SeriesDescription} ${ds.SeriesDate}/${ds.SeriesTime} ${ds.SeriesNumber}`,
|
||||
}));
|
||||
@@ -41,11 +57,12 @@ function ReportDialog({ dataSources, hide, onSave, onCancel }: ReportDialogProps
|
||||
{
|
||||
value: null,
|
||||
description: null,
|
||||
seriesNumber: minSeriesNumber,
|
||||
label: 'Create new series',
|
||||
},
|
||||
...options,
|
||||
];
|
||||
}, [displaySetService]);
|
||||
}, [displaySetService, modality]);
|
||||
|
||||
useEffect(() => {
|
||||
const seriesOption = seriesOptions.find(s => s.value === selectedSeries);
|
||||
@@ -58,6 +75,7 @@ function ReportDialog({ dataSources, hide, onSave, onCancel }: ReportDialogProps
|
||||
onSave({
|
||||
reportName,
|
||||
dataSource: selectedDataSource,
|
||||
priorSeriesNumber: Math.max(...seriesOptions.map(it => it.seriesNumber)),
|
||||
series: selectedSeries,
|
||||
});
|
||||
hide();
|
||||
|
||||
@@ -32,7 +32,8 @@ export default function getDisplaySetsFromUnsupportedSeries(instances) {
|
||||
SOPClassUID: instance.SOPClassUID,
|
||||
SeriesDescription: instance.SeriesDescription || '',
|
||||
Modality: instance.Modality,
|
||||
numImageFrames: instances.length,
|
||||
instances,
|
||||
instance: instances[instance.length - 1],
|
||||
unsupported: true,
|
||||
SOPClassHandlerId: 'unsupported',
|
||||
isReconstructable: false,
|
||||
|
||||
@@ -3,8 +3,6 @@ import { utils } from '@ohif/core';
|
||||
import createReportAsync from '../Actions/createReportAsync';
|
||||
import { createReportDialogPrompt } from '../Panels';
|
||||
import PROMPT_RESPONSES from './_shared/PROMPT_RESPONSES';
|
||||
import { getSRSeriesAndInstanceNumber } from './getSRSeriesAndInstanceNumber';
|
||||
import { getSeriesDateTime } from './getCurrentDicomDateTime';
|
||||
|
||||
const { filterAnd, filterMeasurementsByStudyUID, filterMeasurementsBySeriesUID } =
|
||||
utils.MeasurementFilters;
|
||||
@@ -27,9 +25,14 @@ async function promptSaveReport({ servicesManager, commandsManager, extensionMan
|
||||
} = ctx;
|
||||
let displaySetInstanceUIDs;
|
||||
|
||||
const measurementData = measurementService.getMeasurements(measurementFilter);
|
||||
const predecessorImageId = findPredecessorImageId(measurementData);
|
||||
|
||||
try {
|
||||
const promptResult = await createReportDialogPrompt({
|
||||
title: defaultSaveTitle,
|
||||
predecessorImageId,
|
||||
minSeriesNumber: 3000,
|
||||
extensionManager,
|
||||
servicesManager,
|
||||
});
|
||||
@@ -37,16 +40,9 @@ async function promptSaveReport({ servicesManager, commandsManager, extensionMan
|
||||
if (promptResult.action === PROMPT_RESPONSES.CREATE_REPORT) {
|
||||
const dataSources = extensionManager.getDataSources(promptResult.dataSourceName);
|
||||
const dataSource = dataSources[0];
|
||||
const measurementData = measurementService.getMeasurements(measurementFilter);
|
||||
|
||||
const SeriesDescription = promptResult.value || defaultSaveTitle;
|
||||
|
||||
const { SeriesNumber, InstanceNumber, referenceDisplaySet } = getSRSeriesAndInstanceNumber({
|
||||
displaySetService,
|
||||
SeriesInstanceUid: promptResult.series,
|
||||
});
|
||||
|
||||
const { SeriesDate, SeriesTime } = referenceDisplaySet ?? getSeriesDateTime();
|
||||
const { series, priorSeriesNumber, value: reportName } = promptResult;
|
||||
const SeriesDescription = reportName || defaultSaveTitle;
|
||||
|
||||
const getReport = async () => {
|
||||
return commandsManager.runCommand(
|
||||
@@ -57,11 +53,8 @@ async function promptSaveReport({ servicesManager, commandsManager, extensionMan
|
||||
additionalFindingTypes: ['ArrowAnnotate'],
|
||||
options: {
|
||||
SeriesDescription,
|
||||
SeriesNumber,
|
||||
InstanceNumber,
|
||||
SeriesInstanceUID: promptResult.series,
|
||||
SeriesDate,
|
||||
SeriesTime,
|
||||
SeriesNumber: 1 + priorSeriesNumber,
|
||||
predecessorImageId: series,
|
||||
},
|
||||
},
|
||||
'CORNERSTONE_STRUCTURED_REPORT'
|
||||
@@ -90,4 +83,20 @@ async function promptSaveReport({ servicesManager, commandsManager, extensionMan
|
||||
}
|
||||
}
|
||||
|
||||
export function findPredecessorImageId(annotations) {
|
||||
let predecessorImageId;
|
||||
for (const annotation of annotations) {
|
||||
if (
|
||||
predecessorImageId &&
|
||||
annotation.predecessorImageId &&
|
||||
annotation.predecessorImageId !== predecessorImageId
|
||||
) {
|
||||
console.warn('Found multiple source predecessors, not defaulting to same series');
|
||||
return;
|
||||
}
|
||||
predecessorImageId ||= annotation.predecessorImageId;
|
||||
}
|
||||
return predecessorImageId;
|
||||
}
|
||||
|
||||
export default promptSaveReport;
|
||||
@@ -1,13 +1,14 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { callInputDialog } from '@ohif/extension-default';
|
||||
import { ExtensionManager, CommandsManager, DicomMetadataStore } from '@ohif/core';
|
||||
import { ExtensionManager, CommandsManager, DicomMetadataStore, utils } from '@ohif/core';
|
||||
import { DataRow } from '@ohif/ui-next';
|
||||
import { withTranslation, WithTranslation } from 'react-i18next';
|
||||
import { EVENTS as MicroscopyEvents } from '../../services/MicroscopyService';
|
||||
import dcmjs from 'dcmjs';
|
||||
import constructSR from '../../utils/constructSR';
|
||||
import { saveByteArray } from '../../utils/saveByteArray';
|
||||
|
||||
const { downloadDicom } = utils;
|
||||
|
||||
let saving = false;
|
||||
const { datasetToBuffer } = dcmjs.data;
|
||||
@@ -197,7 +198,7 @@ function MicroscopyPanel(props: IMicroscopyPanelProps) {
|
||||
if (dataSource.wadoRoot == 'saveDicom') {
|
||||
// download as DICOM file
|
||||
const part10Buffer = datasetToBuffer(dataset);
|
||||
saveByteArray(part10Buffer, `sr-microscopy.dcm`);
|
||||
downloadDicom(part10Buffer, { filename: `sr-microscopy.dcm` });
|
||||
} else {
|
||||
// Save into Web Data source
|
||||
const { StudyInstanceUID } = dataset;
|
||||
@@ -277,11 +278,11 @@ function MicroscopyPanel(props: IMicroscopyPanelProps) {
|
||||
* Handler for "Edit" action of an annotation item
|
||||
* @param param0
|
||||
*/
|
||||
const onMeasurementItemEditHandler = ({ uid, isActive }: { uid: string; isActive: boolean }) => {
|
||||
const onMeasurementItemEditHandler = ({ uid }: { uid: string; isActive: boolean }) => {
|
||||
props.commandsManager.runCommand('setLabel', { uid }, 'MICROSCOPY');
|
||||
};
|
||||
|
||||
const onMeasurementDeleteHandler = ({ uid, isActive }: { uid: string; isActive: boolean }) => {
|
||||
const onMeasurementDeleteHandler = ({ uid }: { uid: string; isActive: boolean }) => {
|
||||
const roiAnnotation = microscopyService.getAnnotation(uid);
|
||||
microscopyService.removeAnnotation(roiAnnotation);
|
||||
};
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
/**
|
||||
* Trigger file download from an array buffer
|
||||
* @param buffer
|
||||
* @param filename
|
||||
*/
|
||||
export function saveByteArray(buffer: ArrayBuffer, filename: string) {
|
||||
const blob = new Blob([buffer], { type: 'application/dicom' });
|
||||
const link = document.createElement('a');
|
||||
link.href = window.URL.createObjectURL(blob);
|
||||
link.download = filename;
|
||||
link.click();
|
||||
}
|
||||
@@ -32,7 +32,7 @@
|
||||
"peerDependencies": {
|
||||
"@ohif/core": "3.12.0-beta.90",
|
||||
"@ohif/ui": "3.12.0-beta.90",
|
||||
"dcmjs": "0.43.1",
|
||||
"dcmjs": "0.45.0",
|
||||
"dicom-parser": "1.8.21",
|
||||
"hammerjs": "2.0.8",
|
||||
"prop-types": "15.8.1",
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
"peerDependencies": {
|
||||
"@ohif/core": "3.12.0-beta.90",
|
||||
"@ohif/ui": "3.12.0-beta.90",
|
||||
"dcmjs": "0.43.1",
|
||||
"dcmjs": "0.45.0",
|
||||
"dicom-parser": "1.8.21",
|
||||
"hammerjs": "2.0.8",
|
||||
"prop-types": "15.8.1",
|
||||
|
||||
@@ -32,14 +32,14 @@
|
||||
"start": "yarn run dev"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@cornerstonejs/core": "4.11.2",
|
||||
"@cornerstonejs/tools": "4.11.2",
|
||||
"@cornerstonejs/core": "4.11.3",
|
||||
"@cornerstonejs/tools": "4.11.3",
|
||||
"@ohif/core": "3.12.0-beta.90",
|
||||
"@ohif/extension-cornerstone-dicom-sr": "3.12.0-beta.90",
|
||||
"@ohif/extension-default": "3.12.0-beta.90",
|
||||
"@ohif/ui": "3.12.0-beta.90",
|
||||
"classnames": "2.5.1",
|
||||
"dcmjs": "0.43.1",
|
||||
"dcmjs": "0.45.0",
|
||||
"lodash.debounce": "4.0.8",
|
||||
"prop-types": "15.8.1",
|
||||
"react": "18.3.1",
|
||||
|
||||
+1
-1
@@ -95,7 +95,7 @@ export default function PanelStudyBrowserTracking({
|
||||
seriesNumber: ds.SeriesNumber,
|
||||
modality: ds.Modality,
|
||||
seriesDate: ds.SeriesDate ? new Date(ds.SeriesDate).toLocaleDateString() : '',
|
||||
numInstances: ds.numImageFrames,
|
||||
numInstances: ds.numImageFrames ?? ds.instances?.length,
|
||||
loadingProgress,
|
||||
countIcon: ds.countIcon,
|
||||
messages: ds.messages,
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
"peerDependencies": {
|
||||
"@ohif/core": "3.12.0-beta.90",
|
||||
"@ohif/ui": "3.12.0-beta.90",
|
||||
"dcmjs": "0.43.1",
|
||||
"dcmjs": "0.45.0",
|
||||
"dicom-parser": "1.8.21",
|
||||
"hammerjs": "2.0.8",
|
||||
"prop-types": "15.8.1",
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
"peerDependencies": {
|
||||
"@ohif/core": "3.12.0-beta.90",
|
||||
"@ohif/ui": "3.12.0-beta.90",
|
||||
"dcmjs": "0.43.1",
|
||||
"dcmjs": "0.45.0",
|
||||
"dicom-parser": "1.8.21",
|
||||
"hammerjs": "2.0.8",
|
||||
"prop-types": "15.8.1",
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
import { utils } from '@ohif/core';
|
||||
|
||||
const { downloadCsv } = utils;
|
||||
|
||||
export default function createAndDownloadTMTVReport(segReport, additionalReportRows, options = {}) {
|
||||
const firstReport = segReport[Object.keys(segReport)[0]];
|
||||
const columns = Object.keys(firstReport);
|
||||
@@ -44,14 +48,7 @@ export default function createAndDownloadTMTVReport(segReport, additionalReportR
|
||||
csv.push(temp.join(','));
|
||||
});
|
||||
|
||||
const blob = new Blob([csv.join('\n')], {
|
||||
type: 'text/csv;charset=utf-8',
|
||||
downloadCsv(csv.join('\n'), {
|
||||
filename: options.filename ?? `${firstReport.PatientID}_tmtv.csv`,
|
||||
});
|
||||
|
||||
const url = URL.createObjectURL(blob);
|
||||
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = options.filename ?? `${firstReport.PatientID}_tmtv.csv`;
|
||||
a.click();
|
||||
}
|
||||
@@ -42,8 +42,8 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "7.28.2",
|
||||
"@cornerstonejs/core": "4.11.2",
|
||||
"@cornerstonejs/tools": "4.11.2",
|
||||
"@cornerstonejs/core": "4.11.3",
|
||||
"@cornerstonejs/tools": "4.11.3",
|
||||
"@ohif/core": "3.12.0-beta.90",
|
||||
"@ohif/extension-cornerstone": "3.12.0-beta.90",
|
||||
"@ohif/extension-default": "3.12.0-beta.90",
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { UltrasoundPleuraBLineTool, Enums as csToolsEnums } from '@cornerstonejs/tools';
|
||||
import { Types as OhifTypes } from '@ohif/core';
|
||||
import { Types as OhifTypes, utils } from '@ohif/core';
|
||||
import { eventTarget, triggerEvent, utilities } from '@cornerstonejs/core';
|
||||
import getInstanceByImageId from './getInstanceByImageId';
|
||||
import { setShowPercentage } from './PleuraBlinePercentage';
|
||||
|
||||
const { downloadBlob } = utils;
|
||||
|
||||
const { transformWorldToIndex } = utilities;
|
||||
|
||||
/**
|
||||
@@ -13,7 +15,6 @@ const { transformWorldToIndex } = utilities;
|
||||
*/
|
||||
function commandsModule({
|
||||
servicesManager,
|
||||
commandsManager,
|
||||
}: OhifTypes.Extensions.ExtensionParams): OhifTypes.Extensions.CommandsModule {
|
||||
const { viewportGridService, toolGroupService, cornerstoneViewportService } =
|
||||
servicesManager.services as AppTypes.Services;
|
||||
@@ -275,14 +276,9 @@ function commandsModule({
|
||||
|
||||
// Create a blob with the JSON data
|
||||
const blob = new Blob([jsonString], { type: 'application/json' });
|
||||
|
||||
// Create a URL for the blob
|
||||
const url = URL.createObjectURL(blob);
|
||||
|
||||
// Create an anchor element
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `ultrasound_annotations_${new Date().toISOString().slice(0, 10)}.json`;
|
||||
downloadBlob(blob, {
|
||||
filename: `ultrasound_annotations_${new Date().toISOString().slice(0, 10)}.json`,
|
||||
});
|
||||
|
||||
// Append to the document, click to download, and remove
|
||||
document.body.appendChild(a);
|
||||
|
||||
Reference in new issue
Block a user