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:
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);
|
||||
|
||||
Reference in new issue
Block a user