feat: Add support for labelmap seg images in any supported tsuid (also for compressed bitmap) (#5806)

* feat: Update to use pnpm (#6031) [simulated squash-merge]

* feat: load DICOM SEG images via imageLoader

* feat: load DICOM SEG images via imageLoader

* PR review fixes

* Test fragment of compressed multiframes

* fix: Removed dependencies incorrectly

* Update frame as a targetted change to avoid stripping remaining params

* lock

* Update test to agree with the missing representation branch

* test(contour): contour color change coverage (#6042)

* test(contour): contour interactions delete segment (#6069)

---------

Co-authored-by: Bill Wallace <wayfarer3130@gmail.com>

* chore(version): Update package versions to 3.13.0-beta.98 [skip ci]

* feat(testing): OHIF Test Agent Skills (#5993)

* chore(version): Update package versions to 3.13.0-beta.99 [skip ci]

* test(contour): Add the ContourSegmentToggleLock.spec.ts test file to test contour locking (#6072)

* chore(version): Update package versions to 3.13.0-beta.100 [skip ci]

* chore(testing): Flock lock for playwright tests; Pin node version for OHIF; add more workers (#6099)

* update Cypress apt deps for Ubuntu Noble (drop libgconf-2-4, libasound2→libasound2t64)

* chore(version): Update package versions to 3.13.0-beta.101 [skip ci]

* Debug fixes

* perf(seg): pass explicit frame decode concurrency (16) to SEG loader

Pass an explicit concurrency value (SEG_FRAME_DECODE_CONCURRENCY = 16) into
createFromDicomSegImageId rather than relying on the adapter default, so the
SEG frame fetch/decode parallelism is set at the call site and is ready to
become configurable.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(behaviours): add behaviours section + multiframe Part 10 prefetch proposal

Start a "Behaviours" docs section for documenting how the system and UI behave
end-to-end (observed behaviours, design proposals, and failure modes), with an
index README and a Docusaurus category.

First entry is the proposal for loading a multiframe SEG as a single Part 10
instance: prefetch the whole instance (gated by loadMultiframeAsPart10RaceTimeMs),
parse it with dcmjs (handling multipart/related vs raw DICOM), and register the
per-frame compressed pixels into the Cornerstone3D core image cache (the single
uniform frame registry) so the per-frame load path is served locally while the
standard decode path is unchanged. Best-effort: falls back to per-frame fetches
on any failure.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Pre-cache the image data using a full part10 file for performance

* Add customizations to specify type of segmentation save

* Add clearcache of the cacheData

* Bump @cornerstonejs/* pins 5.1.3 -> 5.4.10 to match libs/@cornerstonejs base

libs/@cornerstonejs (fix/use-imageLoader-for-seg) is based on the released
cs3d 5.4.10 (merge-base with origin/main is the 5.4.10 version bump), so pin
OHIF to that release. The local branch changes still reach the app via the
cs3d:link symlinks; these pins keep the lockfile/npm fallback aligned.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Fix seg OOM

* fix: file meta garbage element, bogus NumberOfFrames, unguarded source-map-loader

- dicomWriter: drop the naturalized meta.TransferSyntaxUID assignment that
  dcmjs wrote as a garbage (0000,0000) element into every saved file's meta
  group (download / clipboard / local wadouri blob / local store); keep the
  hex 00020010 assignment, which is required for string-form _meta fallbacks.
- registerNaturalizedDatasetForLocalWadouri: keep the computed frame count
  local so single-frame IODs (SR, RTSTRUCT) no longer gain a bogus
  NumberOfFrames element in their serialized form.
- rsbuild.config: resolve source-map-loader opportunistically (it is not a
  project dependency; the rule only serves the gitignored cs3d-link
  workflow) so fresh clones no longer crash at config load.
- Regression tests for both writer fixes (mutation-checked).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* PR review comment fixes

* Update versions

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: diattamo <mmddiatta@gmail.com>
Co-authored-by: ohif-bot <danny.ri.brown+ohif-bot@gmail.com>
Co-authored-by: Joe Boccanfuso <109477394+jbocce@users.noreply.github.com>
This commit is contained in:
authored and GitHub committed 2026-07-09 20:54:23 -04:00
1 parent b996070583
commit dbb8b1526e
59 files changed
+2501 -322

No files matched your search

@@ -12,11 +12,17 @@ import dcm4cheeReject from './dcm4cheeReject.js';
import getImageId from './utils/getImageId.js';
import dcmjs from 'dcmjs';
import dicomImageLoader from '@cornerstonejs/dicom-image-loader';
import { retrieveStudyMetadata, deleteStudyMetadataPromise } from './retrieveStudyMetadata.js';
import StaticWadoClient from './utils/StaticWadoClient';
import getDirectURL from '../utils/getDirectURL';
import { fixBulkDataURI } from './utils/fixBulkDataURI';
import { HeadersInterface } from '@ohif/core/src/types/RequestHeaders';
import {
getDatasetTransferSyntaxUID,
setNonEnumerableInstanceProperty,
writeDicomDictToPart10Buffer,
} from '../utils/dicomWriter';
import { getGetThumbnailSrc, ThumbnailContext } from './retrieveThumbnail';
import { getRenderedURL } from './retrieveRendered';
@@ -26,7 +32,6 @@ const { naturalizeDataset, denaturalizeDataset } = DicomMetaDictionary;
const ImplementationClassUID = '2.25.270695996825855179949881587723571202391.2.0.0';
const ImplementationVersionName = 'OHIF-3.11.0';
const EXPLICIT_VR_LITTLE_ENDIAN = '1.2.840.10008.1.2.1';
const metadataProvider = classes.MetadataProvider;
@@ -308,6 +313,86 @@ function createDicomWebApi(dicomWebConfig: DicomWebConfig, servicesManager) {
*/
getWadoDicomWebClient: () => wadoDicomWebClient,
/**
* Best-effort prefetch of a whole multiframe instance as a single Part 10
* object, registered into the Cornerstone3D NATURALIZED frame registry so
* subsequent per-frame image loads are served locally instead of issuing
* one network request per frame (see the "Behaviours" doc
* segmentation-multiframe-part10-prefetch).
*
* Whether to use it at all is the caller's policy (the SEG handler
* resolves the `loadMultiframeAsPart10` config/customization, defaulting
* it on — per-frame loading is the explicit opt-out there).
* Never throws into the caller — on any failure it resolves `done` to
* `false` and the normal per-frame load path is used.
*
* @returns `{ done: Promise<boolean>, cancel: () => void }`.
*/
prefetchInstanceFrames: ({ instance, imageId }) => {
const noop = { done: Promise.resolve(false), cancel: () => {} };
if (!instance || !imageId) {
return noop;
}
const StudyInstanceUID = instance.StudyInstanceUID;
const SeriesInstanceUID = instance.SeriesInstanceUID;
const SOPInstanceUID = instance.SOPInstanceUID || instance.SopInstanceUID;
if (!StudyInstanceUID || !SeriesInstanceUID || !SOPInstanceUID) {
return noop;
}
let cancelled = false;
// Lazy resolver: dicomweb-client.retrieveInstance returns the Part 10
// instance as an ArrayBuffer, unwrapping multipart/related transparently
// (and returning the raw object for single-part responses).
const resolvePart10 = async () => {
wadoDicomWebClient.headers = getAuthorizationHeader();
const result = await wadoDicomWebClient.retrieveInstance({
studyInstanceUID: StudyInstanceUID,
seriesInstanceUID: SeriesInstanceUID,
sopInstanceUID: SOPInstanceUID,
});
if (cancelled) {
throw new Error('prefetchInstanceFrames cancelled');
}
if (result instanceof ArrayBuffer) {
return result;
}
if (Array.isArray(result) && result[0] instanceof ArrayBuffer) {
return result[0];
}
if (result && (result as { buffer?: ArrayBuffer }).buffer instanceof ArrayBuffer) {
return (result as ArrayBufferView).buffer as ArrayBuffer;
}
throw new Error('Unexpected retrieveInstance result for instance prefetch');
};
const done = (async () => {
try {
await dicomImageLoader.prefetchPart10Instance(imageId, resolvePart10);
return !cancelled;
} catch (error) {
console.warn(
'[prefetchInstanceFrames] full-instance prefetch failed; falling back to per-frame loads',
error
);
return false;
}
})();
return {
done,
cancel: () => {
cancelled = true;
},
};
},
bulkDataURI: async ({ StudyInstanceUID, BulkDataURI }) => {
qidoDicomWebClient.headers = getAuthorizationHeader();
const options = {
@@ -372,7 +457,7 @@ function createDicomWebApi(dicomWebConfig: DicomWebConfig, servicesManager) {
FileMetaInformationVersion: dataset._meta?.FileMetaInformationVersion?.Value,
MediaStorageSOPClassUID: dataset.SOPClassUID,
MediaStorageSOPInstanceUID: dataset.SOPInstanceUID,
TransferSyntaxUID: EXPLICIT_VR_LITTLE_ENDIAN,
TransferSyntaxUID: getDatasetTransferSyntaxUID(dataset),
ImplementationClassUID,
ImplementationVersionName,
};
@@ -384,7 +469,7 @@ function createDicomWebApi(dicomWebConfig: DicomWebConfig, servicesManager) {
effectiveDicomDict = defaultDicomDict;
}
const part10Buffer = effectiveDicomDict.write();
const part10Buffer = writeDicomDictToPart10Buffer(effectiveDicomDict);
const options = {
datasets: [part10Buffer],
@@ -445,9 +530,9 @@ function createDicomWebApi(dicomWebConfig: DicomWebConfig, servicesManager) {
instance,
});
instance.imageId = imageId;
instance.wadoRoot = dicomWebConfig.wadoRoot;
instance.wadoUri = dicomWebConfig.wadoUri;
setNonEnumerableInstanceProperty(instance, 'imageId', imageId);
setNonEnumerableInstanceProperty(instance, 'wadoRoot', dicomWebConfig.wadoRoot);
setNonEnumerableInstanceProperty(instance, 'wadoUri', dicomWebConfig.wadoUri);
metadataProvider.addImageIdToUIDs(imageId, {
StudyInstanceUID,
@@ -544,8 +629,8 @@ function createDicomWebApi(dicomWebConfig: DicomWebConfig, servicesManager) {
// Adding instanceMetadata to OHIF MetadataProvider
naturalizedInstances.forEach(instance => {
instance.wadoRoot = dicomWebConfig.wadoRoot;
instance.wadoUri = dicomWebConfig.wadoUri;
setNonEnumerableInstanceProperty(instance, 'wadoRoot', dicomWebConfig.wadoRoot);
setNonEnumerableInstanceProperty(instance, 'wadoUri', dicomWebConfig.wadoUri);
const { StudyInstanceUID, SeriesInstanceUID, SOPInstanceUID } = instance;
const numberOfFrames = instance.NumberOfFrames || 1;
@@ -571,7 +656,7 @@ function createDicomWebApi(dicomWebConfig: DicomWebConfig, servicesManager) {
const imageId = implementation.getImageIdsForInstance({
instance,
});
instance.imageId = imageId;
setNonEnumerableInstanceProperty(instance, 'imageId', imageId);
});
DicomMetadataStore.addInstances(naturalizedInstances, madeInClient);