From d8cd034ef8873b8bc792846fc41f220d0267fcad Mon Sep 17 00:00:00 2001 From: Igor Octaviano Date: Tue, 6 Oct 2026 11:29:08 -0300 Subject: [PATCH] fix(seg): try bulk data fetch for PerFrameFunctionalGroupsSequence before full Part 10 fallback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Before falling back to fetching the full DICOM Part 10 file when PerFrameFunctionalGroupsSequence is not inline, first check if the server provides it via BulkDataURI. If available, fetch the bulk data and parse it as JSON, then use the metadata-based loader. Loading strategy (in order of preference): 1. Inline PerFrameFunctionalGroupsSequence (array) → metadata-based loader 2. BulkDataURI for PerFrameFunctionalGroupsSequence → fetch bulk data, then metadata-based loader 3. No bulk data or fetch fails → buffer-based loader (full Part 10 file) --- .../src/getSopClassHandlerModule.ts | 75 ++++++++++++++++--- 1 file changed, 64 insertions(+), 11 deletions(-) diff --git a/extensions/cornerstone-dicom-seg/src/getSopClassHandlerModule.ts b/extensions/cornerstone-dicom-seg/src/getSopClassHandlerModule.ts index f117b816769..6ad8f330ecc 100644 --- a/extensions/cornerstone-dicom-seg/src/getSopClassHandlerModule.ts +++ b/extensions/cornerstone-dicom-seg/src/getSopClassHandlerModule.ts @@ -4,8 +4,12 @@ import { metaData, eventTarget, utilities as csUtils } from '@cornerstonejs/core import { CONSTANTS, segmentation as cstSegmentation } from '@cornerstonejs/tools'; import { adaptersSEG, Enums } from '@cornerstonejs/adapters'; import { dicomLoaderService } from '@ohif/extension-cornerstone'; +import dcmjs from 'dcmjs'; import { SOPClassHandlerId } from './id'; + +const { DicomMetaDictionary } = dcmjs.data; +const { naturalizeDataset } = DicomMetaDictionary; import { dicomlabToRGB } from './utils/dicomlabToRGB'; import { getSegmentationParserType } from './utils/segmentationConfig'; import { @@ -460,15 +464,70 @@ async function _loadSegments({ true; /** - * Check if the metadata has PerFrameFunctionalGroupsSequence. - * Some DICOMweb servers (like IDC's static WADO) omit this sequence from JSON metadata - * for large SEGs to save space. When missing, we need to fetch the full DICOM file - * and use the buffer-based loader which parses the complete metadata from the file. + * Some DICOMweb servers (e.g. IDC's static WADO) omit large sequences like + * PerFrameFunctionalGroupsSequence from the JSON metadata to save bandwidth. + * The metadata-based loader (createFromDicomSegImageId) requires this sequence + * to map frames to segments. + * + * Loading strategy (in order of preference): + * 1. If PerFrameFunctionalGroupsSequence is inline (array) → use metadata-based loader + * 2. If PerFrameFunctionalGroupsSequence has BulkDataURI → fetch bulk data, then use metadata-based loader + * 3. If bulk data fetch fails or no BulkDataURI → fall back to buffer-based loader (full Part 10 file) */ - const hasPerFrameFunctionalGroups = + let hasPerFrameFunctionalGroups = Array.isArray(instance.PerFrameFunctionalGroupsSequence) && instance.PerFrameFunctionalGroupsSequence.length > 0; + /** + * Check if PerFrameFunctionalGroupsSequence is available via bulk data. + * Some servers return it as { BulkDataURI: '...' } instead of inline array. + */ + const perFrameValue = instance.PerFrameFunctionalGroupsSequence as + | unknown[] + | { BulkDataURI?: string; retrieveBulkData?: () => Promise } + | undefined; + + if ( + !hasPerFrameFunctionalGroups && + perFrameValue && + typeof perFrameValue === 'object' && + !Array.isArray(perFrameValue) && + (perFrameValue.BulkDataURI || typeof perFrameValue.retrieveBulkData === 'function') + ) { + try { + let buffer: ArrayBuffer | undefined; + + if (typeof perFrameValue.retrieveBulkData === 'function') { + buffer = await perFrameValue.retrieveBulkData(); + } else if (perFrameValue.BulkDataURI && dataSource.retrieve?.bulkDataURI) { + buffer = await dataSource.retrieve.bulkDataURI({ + StudyInstanceUID: instance.StudyInstanceUID, + BulkDataURI: perFrameValue.BulkDataURI, + }); + } + + if (buffer && buffer.byteLength > 0) { + /** + * Parse the bulk data as JSON. The server returns the sequence as a JSON array + * following the DICOMweb JSON model (denaturalized form). + */ + const jsonText = new TextDecoder().decode(buffer); + const denaturalizedSequence = JSON.parse(jsonText); + + if (Array.isArray(denaturalizedSequence) && denaturalizedSequence.length > 0) { + /** Naturalize each item in the sequence to match OHIF's internal format */ + const naturalizedSequence = denaturalizedSequence.map(item => naturalizeDataset(item)); + + /** Update the instance metadata with the fetched sequence */ + instance.PerFrameFunctionalGroupsSequence = naturalizedSequence; + hasPerFrameFunctionalGroups = true; + } + } + } catch { + /** Bulk data fetch failed; fall back to buffer-based loader */ + } + } + let results; let prefetch; @@ -477,13 +536,7 @@ async function _loadSegments({ * Fallback: PerFrameFunctionalGroupsSequence is missing from metadata. * Fetch the full DICOM file and use createFromDICOMSegBuffer which parses * the complete metadata directly from the DICOM binary. - * This was the loading method used in OHIF 3.12 and earlier. */ - log.info( - SEG_LOAD_LOG_PREFIX, - 'PerFrameFunctionalGroupsSequence missing from metadata, using buffer-based loader' - ); - try { const arrayBuffer = await dicomLoaderService.findDicomDataPromise( segDisplaySet,