Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 73 additions & 27 deletions extensions/cornerstone-dicom-seg/src/getSopClassHandlerModule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import i18n from '@ohif/i18n';
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 { SOPClassHandlerId } from './id';
import { dicomlabToRGB } from './utils/dicomlabToRGB';
Expand Down Expand Up @@ -404,7 +405,8 @@ async function _loadSegments({
extensionManager,
servicesManager,
segDisplaySet,
}: withAppTypes<{ segDisplaySet: AppTypes.DisplaySet }>) {
headers,
}: withAppTypes<{ segDisplaySet: AppTypes.DisplaySet; headers?: Record<string, string> }>) {
const { segmentationService, uiNotificationService, customizationService } =
servicesManager.services;
const instance = segDisplaySet.instance as Record<string, unknown>;
Expand Down Expand Up @@ -495,37 +497,81 @@ async function _loadSegments({
) as boolean | undefined) ??
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.
*/
const hasPerFrameFunctionalGroups =
Array.isArray(instance.PerFrameFunctionalGroupsSequence) &&
instance.PerFrameFunctionalGroupsSequence.length > 0;

let results;
let prefetch;
if (loadMultiframeAsPart10) {
prefetch = dataSource.retrieve?.prefetchInstanceFrames?.({
instance,
imageId: segImageIdForMetadata,
});

if (prefetch?.done) {
await prefetch.done;
if (!hasPerFrameFunctionalGroups) {
/**
* 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,
null,
headers
);

results = await adaptersSEG.Cornerstone3D.Segmentation.createFromDICOMSegBuffer(
imageIds,
arrayBuffer,
{ metadataProvider: metaData, tolerance }
);
} finally {
eventTarget.removeEventListener(Enums.Events.SEGMENTATION_LOAD_PROGRESS, onProgress);
}
}
} else {
/**
* Normal path: PerFrameFunctionalGroupsSequence is present in metadata.
* Use the imageId-based loader which is more efficient for large SEGs.
*/
if (loadMultiframeAsPart10) {
prefetch = dataSource.retrieve?.prefetchInstanceFrames?.({
instance,
imageId: segImageIdForMetadata,
});

let results;
try {
results = await adaptersSEG.Cornerstone3D.Segmentation.createFromDicomSegImageId(
imageIds,
segImageIdForMetadata,
{
metadataProvider: metaData,
tolerance,
parserType: getSegmentationParserType(
segDisplaySet.SOPClassUID,
customizationService
),
frameImageIds,
concurrency: SEG_FRAME_DECODE_CONCURRENCY,
if (prefetch?.done) {
await prefetch.done;
}
);
} finally {
eventTarget.removeEventListener(Enums.Events.SEGMENTATION_LOAD_PROGRESS, onProgress);
prefetch?.cancel?.();
}

try {
results = await adaptersSEG.Cornerstone3D.Segmentation.createFromDicomSegImageId(
imageIds,
segImageIdForMetadata,
{
metadataProvider: metaData,
tolerance,
parserType: getSegmentationParserType(
segDisplaySet.SOPClassUID,
customizationService
),
frameImageIds,
concurrency: SEG_FRAME_DECODE_CONCURRENCY,
}
);
} finally {
eventTarget.removeEventListener(Enums.Events.SEGMENTATION_LOAD_PROGRESS, onProgress);
prefetch?.cancel?.();
}
}

let usedRecommendedDisplayCIELabValue = true;
Expand Down
118 changes: 31 additions & 87 deletions patches/@cornerstonejs__adapters.patch
Original file line number Diff line number Diff line change
@@ -1,87 +1,31 @@
diff --git a/dist/esm/adapters/Cornerstone3D/Segmentation/labelmapImagesFromBuffer.js b/dist/esm/adapters/Cornerstone3D/Segmentation/labelmapImagesFromBuffer.js
index db4972221ea40cd1eaad450e9a6334c8eef4ef72..120316e2ac6929810b1da4b5cc4cfb083f0f8ce0 100644
--- a/dist/esm/adapters/Cornerstone3D/Segmentation/labelmapImagesFromBuffer.js
+++ b/dist/esm/adapters/Cornerstone3D/Segmentation/labelmapImagesFromBuffer.js
@@ -552,10 +552,11 @@ function insertPixelDataPlanar(_ref0) {
PerFrameFunctionalGroupsSequence = multiframe.PerFrameFunctionalGroupsSequence,
Rows = multiframe.Rows,
Columns = multiframe.Columns;
- const sharedImageOrientationPatient = SharedFunctionalGroupsSequence.PlaneOrientationSequence ? SharedFunctionalGroupsSequence.PlaneOrientationSequence.ImageOrientationPatient : undefined;
+ const sharedImageOrientationPatient = SharedFunctionalGroupsSequence?.PlaneOrientationSequence ? SharedFunctionalGroupsSequence.PlaneOrientationSequence.ImageOrientationPatient : undefined;
const sliceLength = Columns * Rows;
- const metadataFrameCount = Number(multiframe.NumberOfFrames) || PerFrameFunctionalGroupsSequence.length;
- const groupsLenFromMetadata = PerFrameFunctionalGroupsSequence.length || metadataFrameCount;
+ const perFrameLength = PerFrameFunctionalGroupsSequence?.length || 0;
+ const metadataFrameCount = Number(multiframe.NumberOfFrames) || perFrameLength;
+ const groupsLenFromMetadata = perFrameLength || metadataFrameCount;
const groupsLen = typeof decodedFrameCount === 'number' ? Math.min(groupsLenFromMetadata, decodedFrameCount) : groupsLenFromMetadata;
let overlapping = false;
return new Promise(resolve => {
@@ -564,7 +565,7 @@ function insertPixelDataPlanar(_ref0) {
const processChunk = firstIndex => {
for (let i = firstIndex; i < firstIndex + imagesPerChunk && i < groupsLen; i++) {
var _PerFrameFunctionalGr;
- const PerFrameFunctionalGroups = PerFrameFunctionalGroupsSequence[i];
+ const PerFrameFunctionalGroups = PerFrameFunctionalGroupsSequence?.[i];
const ImageOrientationPatientI = sharedImageOrientationPatient || (PerFrameFunctionalGroups === null || PerFrameFunctionalGroups === void 0 || (_PerFrameFunctionalGr = PerFrameFunctionalGroups.PlaneOrientationSequence) === null || _PerFrameFunctionalGr === void 0 ? void 0 : _PerFrameFunctionalGr.ImageOrientationPatient);
if (!ImageOrientationPatientI) {
throw new Error("SEG frame ".concat(i + 1, " is missing ImageOrientationPatient in per-frame and shared functional groups."));
@@ -817,7 +818,7 @@ function insertOverlappingPixelDataPlanar(_ref12) {
PerFrameFunctionalGroupsSequence = multiframe.PerFrameFunctionalGroupsSequence,
Rows = multiframe.Rows,
Columns = multiframe.Columns;
- const sharedImageOrientationPatient = SharedFunctionalGroupsSequence.PlaneOrientationSequence ? SharedFunctionalGroupsSequence.PlaneOrientationSequence.ImageOrientationPatient : undefined;
+ const sharedImageOrientationPatient = SharedFunctionalGroupsSequence?.PlaneOrientationSequence ? SharedFunctionalGroupsSequence.PlaneOrientationSequence.ImageOrientationPatient : undefined;
const sliceLength = Columns * Rows;
const arrayOfSegmentData = getArrayOfSegmentData({
sliceLength,
@@ -910,7 +911,7 @@ const getSegmentData = _ref14 => {
for (let currentLabelMapImageIndex = 0; currentLabelMapImageIndex < labelMapImages.length; currentLabelMapImageIndex++) {
const currentLabelMapImage = labelMapImages[currentLabelMapImageIndex];
const referencedImageId = currentLabelMapImage.referencedImageId;
- const PerFrameFunctionalGroupsIndex = PerFrameFunctionalGroupsSequence.findIndex((PerFrameFunctionalGroups, currentSequenceIndex) => {
+ const PerFrameFunctionalGroupsIndex = PerFrameFunctionalGroupsSequence?.findIndex((PerFrameFunctionalGroups, currentSequenceIndex) => {
const _extractInfoFromPerFr = extractInfoFromPerFrameFunctionalGroups({
PerFrameFunctionalGroups,
sequenceIndex: currentSequenceIndex,
@@ -922,10 +923,10 @@ const getSegmentData = _ref14 => {
const isCorrectPerFrameFunctionalGroup = groupsSegmentIndex === segmentIndex && groupsReferenceImageId === currentLabelMapImage.referencedImageId;
return isCorrectPerFrameFunctionalGroup;
});
- if (PerFrameFunctionalGroupsIndex === -1) {
+ if (PerFrameFunctionalGroupsIndex === -1 || PerFrameFunctionalGroupsIndex === undefined) {
continue;
}
- const PerFrameFunctionalGroups = PerFrameFunctionalGroupsSequence[PerFrameFunctionalGroupsIndex];
+ const PerFrameFunctionalGroups = PerFrameFunctionalGroupsSequence?.[PerFrameFunctionalGroupsIndex];
const alignedPixelDataI = getAlignedPixelData({
sharedImageOrientationPatient,
PerFrameFunctionalGroups,
diff --git a/dist/esm/adapters/helpers/checkOrientation.js b/dist/esm/adapters/helpers/checkOrientation.js
index f147ce19972dfe2f30ed820509943bb2ab2f34d0..47868b5ae063ee61e4061aa46ef5ccf49692fb34 100644
--- a/dist/esm/adapters/helpers/checkOrientation.js
+++ b/dist/esm/adapters/helpers/checkOrientation.js
@@ -4,9 +4,20 @@ import { utilities } from '@cornerstonejs/core';
function checkOrientation(multiframe, validOrientations, sourceDataDimensions, tolerance) {
const SharedFunctionalGroupsSequence = multiframe.SharedFunctionalGroupsSequence,
PerFrameFunctionalGroupsSequence = multiframe.PerFrameFunctionalGroupsSequence;
- const sharedImageOrientationPatient = SharedFunctionalGroupsSequence.PlaneOrientationSequence ? SharedFunctionalGroupsSequence.PlaneOrientationSequence.ImageOrientationPatient : undefined;
- const PerFrameFunctionalGroups = PerFrameFunctionalGroupsSequence[0];
- const iopRaw = sharedImageOrientationPatient || PerFrameFunctionalGroups.PlaneOrientationSequence.ImageOrientationPatient;
+ const sharedImageOrientationPatient = SharedFunctionalGroupsSequence?.PlaneOrientationSequence ? SharedFunctionalGroupsSequence.PlaneOrientationSequence.ImageOrientationPatient : undefined;
+
+ /** Get orientation from per-frame if shared doesn't have it and per-frame exists */
+ let iopRaw = sharedImageOrientationPatient;
+ if (!iopRaw && PerFrameFunctionalGroupsSequence && PerFrameFunctionalGroupsSequence.length > 0) {
+ const PerFrameFunctionalGroups = PerFrameFunctionalGroupsSequence[0];
+ iopRaw = PerFrameFunctionalGroups?.PlaneOrientationSequence?.ImageOrientationPatient;
+ }
+
+ if (!iopRaw) {
+ /** No orientation found, default to Planar to avoid throwing */
+ return 'Planar';
+ }
+
const iop = Array.isArray(iopRaw) ? iopRaw.map(Number) : iopRaw;
const inPlane = validOrientations.some(operation => utilities.isEqual(iop, operation, tolerance));
if (inPlane) {
diff --git a/dist/esm/adapters/Cornerstone/Segmentation_4X.js b/dist/esm/adapters/Cornerstone/Segmentation_4X.js
index f40a914216e4d403fb6f49a880a1329482b5219f..62bec9ef69359bab5f9bf8a18f9ff6803efb7553 100644
--- a/dist/esm/adapters/Cornerstone/Segmentation_4X.js
+++ b/dist/esm/adapters/Cornerstone/Segmentation_4X.js
@@ -1169,12 +1169,20 @@ function alignPixelDataWithSourceData(pixelData2D, iop, orientations, tolerance)
}
function getSegmentMetadata(multiframe, seriesInstanceUid) {
const segmentSequence = multiframe.SegmentSequence;
- let data = [];
- if (Array.isArray(segmentSequence)) {
- data = [undefined, ...segmentSequence];
- } else {
- // Only one segment, will be stored as an object.
- data = [undefined, segmentSequence];
+ const data = [];
+ /**
+ * Index segments by their SegmentNumber rather than array position.
+ * This is required for LABELMAP segmentations where SegmentNumber may
+ * start at 0 (for background) or have gaps. The DICOM standard only
+ * requires SegmentNumber >= 1 for BINARY/FRACTIONAL types, but LABELMAP
+ * allows more flexibility (see DICOM Part 3, Section C.8.20.2.4).
+ * Backported from: https://github.com/cornerstonejs/cornerstone3D/pull/2882
+ */
+ const segments = Array.isArray(segmentSequence) ? segmentSequence : [segmentSequence];
+ for (const segment of segments) {
+ if (segment?.SegmentNumber >= 0) {
+ data[segment.SegmentNumber] = segment;
+ }
}
return {
seriesInstanceUid,
Loading
Loading