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

@@ -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;