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:
Bill Wallace authored and GitHub committed 2025-11-21 13:07:26 -05:00
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);
}
+4 -4
View File
@@ -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;
+8 -8
View File
@@ -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",
+3 -5
View File
@@ -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;
};
/**
+1 -1
View File
@@ -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();
}
+1 -1
View File
@@ -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 -1
View File
@@ -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",
+3 -3
View File
@@ -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",
@@ -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,
+1 -1
View File
@@ -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 -1
View File
@@ -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();
}
+2 -2
View File
@@ -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);