From d88a341884f7361865bc10159c1a82633667ab5b Mon Sep 17 00:00:00 2001 From: Igor Octaviano Date: Thu, 25 Sep 2025 15:07:35 -0300 Subject: [PATCH] fix: Improve rt handling for bulkdata (#5415) --- .../cornerstone-dicom-rt/src/loadRTStruct.js | 140 +++++++++++++++--- 1 file changed, 120 insertions(+), 20 deletions(-) diff --git a/extensions/cornerstone-dicom-rt/src/loadRTStruct.js b/extensions/cornerstone-dicom-rt/src/loadRTStruct.js index 72fcdb56b..1642c2865 100644 --- a/extensions/cornerstone-dicom-rt/src/loadRTStruct.js +++ b/extensions/cornerstone-dicom-rt/src/loadRTStruct.js @@ -2,9 +2,34 @@ import dcmjs from 'dcmjs'; const { DicomMessage, DicomMetaDictionary } = dcmjs.data; const dicomlab2RGB = dcmjs.data.Colors.dicomlab2RGB; -async function checkAndLoadContourData(instance, datasource) { +/** + * Checks and loads contour data for RT Structure Set, handling both inline and bulk data URIs. + * Processes ROIContourSequence to extract contour data and resolve any bulk data references. + * + * @async + * @function checkAndLoadContourData + * @param {Object} params - Parameters object + * @param {Object} params.instance - Initial RT Structure instance + * @param {Object} params.dataSource - Data source for retrieving bulk data + * @param {Object} params.extensionManager - OHIF extension manager + * @param {Object} params.rtStructDisplaySet - RT Structure display set + * @param {Object} params.headers - HTTP headers for requests + * @returns {Promise} Promise that resolves to the processed RT Structure instance with loaded contour data + * @throws {Promise} Rejects with error message if instance is invalid or data retrieval fails + */ +async function checkAndLoadContourData({ + instance: initialInstance, + dataSource, + extensionManager, + rtStructDisplaySet, + headers, +}) { + let instance = initialInstance; if (!instance || !instance.ROIContourSequence) { - return Promise.reject('Invalid instance object or ROIContourSequence'); + instance = await getRTStructInstance({ extensionManager, rtStructDisplaySet, headers }); + if (!instance || !instance.ROIContourSequence) { + return Promise.reject('Invalid instance object or ROIContourSequence'); + } } const promisesMap = new Map(); @@ -30,11 +55,11 @@ async function checkAndLoadContourData(instance, datasource) { } else if (contourData && contourData.BulkDataURI) { const bulkDataURI = contourData.BulkDataURI; - if (!datasource || !datasource.retrieve || !datasource.retrieve.bulkDataURI) { - return Promise.reject('Invalid datasource object or retrieve function'); + if (!dataSource || !dataSource.retrieve || !dataSource.retrieve.bulkDataURI) { + return Promise.reject('Invalid dataSource object or retrieve function'); } - const bulkDataPromise = datasource.retrieve.bulkDataURI({ + const bulkDataPromise = dataSource.retrieve.bulkDataURI({ BulkDataURI: bulkDataURI, StudyInstanceUID: instance.StudyInstanceUID, SeriesInstanceUID: instance.SeriesInstanceUID, @@ -87,35 +112,74 @@ async function checkAndLoadContourData(instance, datasource) { console.error(error); } }); + + return instance; } -export default async function loadRTStruct(extensionManager, rtStructDisplaySet, headers) { +/** + * Retrieves and parses RT Structure Set instance from DICOM data. + * Uses the cornerstone utility module to load DICOM data and converts it to a naturalized dataset. + * + * @async + * @function getRTStructInstance + * @param {Object} params - Parameters object + * @param {Object} params.extensionManager - OHIF extension manager + * @param {Object} params.rtStructDisplaySet - RT Structure display set + * @param {Object} params.headers - HTTP headers for requests + * @returns {Promise} Promise that resolves to the parsed RT Structure dataset + */ +const getRTStructInstance = async ({ extensionManager, rtStructDisplaySet, headers }) => { const utilityModule = extensionManager.getModuleEntry( '@ohif/extension-cornerstone.utilityModule.common' ); + const { dicomLoaderService } = utilityModule.exports; + const segArrayBuffer = await dicomLoaderService.findDicomDataPromise( + rtStructDisplaySet, + null, + headers + ); + const dicomData = DicomMessage.readFile(segArrayBuffer); + const rtStructDataset = DicomMetaDictionary.naturalizeDataset(dicomData.dict); + rtStructDataset._meta = DicomMetaDictionary.namifyDataset(dicomData.meta); + return rtStructDataset; +}; + +/** + * Main function to load and process RT Structure Set data. + * Creates a structure set object with ROI contours, metadata, and visualization properties. + * Handles both bulk data URI and inline contour data scenarios. + * + * @async + * @function loadRTStruct + * @param {Object} extensionManager - OHIF extension manager + * @param {Object} rtStructDisplaySet - RT Structure display set to process + * @param {Object} headers - HTTP headers for data requests + * @returns {Promise} Promise that resolves to a structure set object containing: + * - StructureSetLabel: Label of the structure set + * - SeriesInstanceUID: Series instance UID + * - ROIContours: Array of ROI contour data with points and metadata + * - visible: Visibility state + * - ReferencedSOPInstanceUIDsSet: Set of referenced SOP instance UIDs + */ +export default async function loadRTStruct(extensionManager, rtStructDisplaySet, headers) { const dataSource = extensionManager.getActiveDataSource()[0]; const { bulkDataURI } = dataSource.getConfig?.() || {}; - const { dicomLoaderService } = utilityModule.exports; - // Set here is loading is asynchronous. // If this function throws its set back to false. rtStructDisplaySet.isLoaded = true; let instance = rtStructDisplaySet.instance; if (!bulkDataURI || !bulkDataURI.enabled) { - const segArrayBuffer = await dicomLoaderService.findDicomDataPromise( - rtStructDisplaySet, - null, - headers - ); - - const dicomData = DicomMessage.readFile(segArrayBuffer); - const rtStructDataset = DicomMetaDictionary.naturalizeDataset(dicomData.dict); - rtStructDataset._meta = DicomMetaDictionary.namifyDataset(dicomData.meta); - instance = rtStructDataset; + instance = await getRTStructInstance({ extensionManager, rtStructDisplaySet, headers }); } else { - await checkAndLoadContourData(instance, dataSource); + instance = await checkAndLoadContourData({ + instance, + dataSource, + extensionManager, + rtStructDisplaySet, + headers, + }); } const { StructureSetROISequence, ROIContourSequence, RTROIObservationsSequence } = instance; @@ -132,7 +196,6 @@ export default async function loadRTStruct(extensionManager, rtStructDisplaySet, for (let i = 0; i < ROIContourSequence.length; i++) { const ROIContour = ROIContourSequence[i]; const { ContourSequence } = ROIContour; - if (!ContourSequence) { continue; } @@ -185,6 +248,18 @@ export default async function loadRTStruct(extensionManager, rtStructDisplaySet, return structureSet; } +/** + * Sets metadata for ROI contour data and adds it to the structure set. + * Extracts ROI information from StructureSetROISequence and RTROIObservationsSequence, + * then creates a complete ROI contour data object with visualization properties. + * + * @function _setROIContourMetadata + * @param {Object} structureSet - The structure set object to add ROI contour to + * @param {Array} StructureSetROISequence - Array of structure set ROI definitions + * @param {Array} RTROIObservationsSequence - Array of RT ROI observations + * @param {Object} ROIContour - ROI contour object containing contour data + * @param {Array} contourPoints - Array of processed contour points + */ function _setROIContourMetadata( structureSet, StructureSetROISequence, @@ -220,6 +295,14 @@ function _setROIContourMetadata( structureSet.ROIContours.push(ROIContourData); } +/** + * Sets the display color for ROI contour data. + * Uses ROIDisplayColor if available, otherwise converts RecommendedDisplayCIELabValue to RGB. + * + * @function _setROIContourDataColor + * @param {Object} ROIContour - ROI contour object containing color information + * @param {Object} ROIContourData - ROI contour data object to set color on + */ function _setROIContourDataColor(ROIContour, ROIContourData) { let { ROIDisplayColor, RecommendedDisplayCIELabValue } = ROIContour; @@ -233,6 +316,15 @@ function _setROIContourDataColor(ROIContour, ROIContourData) { } } +/** + * Sets RT ROI observations metadata for ROI contour data. + * Finds matching RTROIObservations by ROINumber and adds observation details to contour data. + * + * @function _setROIContourRTROIObservations + * @param {Object} ROIContourData - ROI contour data object to add observations to + * @param {Array} RTROIObservationsSequence - Array of RT ROI observations + * @param {number} ROINumber - ROI number to match observations + */ function _setROIContourRTROIObservations(ROIContourData, RTROIObservationsSequence, ROINumber) { const RTROIObservations = RTROIObservationsSequence.find( RTROIObservations => RTROIObservations.ReferencedROINumber === ROINumber @@ -252,6 +344,14 @@ function _setROIContourRTROIObservations(ROIContourData, RTROIObservationsSequen } } +/** + * Converts a single object or array to an array. + * Utility function to ensure consistent array handling for DICOM sequences. + * + * @function _toArray + * @param {*} objOrArray - Object or array to convert + * @returns {Array} Array containing the input (if already array) or wrapped in array + */ function _toArray(objOrArray) { return Array.isArray(objOrArray) ? objOrArray : [objOrArray]; }