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

+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;
};
/**