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
@@ -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;
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
Reference in new issue
Block a user