* 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>
178 lines
5.8 KiB
JavaScript
178 lines
5.8 KiB
JavaScript
import { TextEncoder, TextDecoder } from 'util';
|
|
import dicomImageLoader from '@cornerstonejs/dicom-image-loader';
|
|
import OHIF from '@ohif/core';
|
|
import {
|
|
registerNaturalizedDatasetForLocalWadouri,
|
|
releaseLocalWadouriRegistrations,
|
|
} from './registerNaturalizedDatasetForLocalWadouri';
|
|
|
|
// jsdom does not expose TextEncoder/TextDecoder, which dcmjs's buffer streams need.
|
|
if (typeof global.TextEncoder === 'undefined') {
|
|
global.TextEncoder = TextEncoder;
|
|
}
|
|
if (typeof global.TextDecoder === 'undefined') {
|
|
global.TextDecoder = TextDecoder;
|
|
}
|
|
|
|
jest.mock('@cornerstonejs/dicom-image-loader', () => ({
|
|
__esModule: true,
|
|
default: {
|
|
wadouri: {
|
|
fileManager: {
|
|
add: jest.fn(() => 'dicomfile:0'),
|
|
remove: jest.fn(),
|
|
},
|
|
dataSetCacheManager: {
|
|
isLoaded: jest.fn(() => false),
|
|
unload: jest.fn(),
|
|
},
|
|
},
|
|
},
|
|
}));
|
|
|
|
jest.mock('@ohif/core', () => ({
|
|
__esModule: true,
|
|
default: {
|
|
classes: {
|
|
MetadataProvider: {
|
|
addImageIdToUIDs: jest.fn(),
|
|
},
|
|
},
|
|
log: {
|
|
debug: jest.fn(),
|
|
},
|
|
},
|
|
}));
|
|
|
|
const EXPLICIT_VR_LITTLE_ENDIAN = '1.2.840.10008.1.2.1';
|
|
|
|
const addBlob = dicomImageLoader.wadouri.fileManager.add;
|
|
const addImageIdToUIDs = OHIF.classes.MetadataProvider.addImageIdToUIDs;
|
|
|
|
function makeDataset(extra = {}) {
|
|
return {
|
|
_meta: { TransferSyntaxUID: { vr: 'UI', Value: [EXPLICIT_VR_LITTLE_ENDIAN] } },
|
|
SOPInstanceUID: '1.2.3.4.5',
|
|
StudyInstanceUID: '1.2.3.4',
|
|
SeriesInstanceUID: '1.2.3.4.1',
|
|
PatientID: 'TEST-PATIENT',
|
|
...extra,
|
|
};
|
|
}
|
|
|
|
describe('registerNaturalizedDatasetForLocalWadouri', () => {
|
|
beforeEach(() => {
|
|
jest.clearAllMocks();
|
|
});
|
|
|
|
afterEach(() => {
|
|
// Drain the module-level registration tracking between tests.
|
|
releaseLocalWadouriRegistrations();
|
|
});
|
|
|
|
it('skips datasets without pixel data (SR/RTSTRUCT) and does not mutate them', () => {
|
|
const dataset = makeDataset({
|
|
SOPClassUID: '1.2.840.10008.5.1.4.1.1.88.33',
|
|
Modality: 'SR',
|
|
});
|
|
|
|
const imageId = registerNaturalizedDatasetForLocalWadouri(dataset);
|
|
|
|
// No frames to serve through the image loader: nothing registered, no Blob pinned.
|
|
expect(imageId).toBeUndefined();
|
|
expect(addBlob).not.toHaveBeenCalled();
|
|
expect(addImageIdToUIDs).not.toHaveBeenCalled();
|
|
|
|
// The serialized form of an SR must not gain a (0028,0008) element or a url.
|
|
expect(Object.prototype.hasOwnProperty.call(dataset, 'NumberOfFrames')).toBe(false);
|
|
expect(Object.prototype.hasOwnProperty.call(dataset, 'url')).toBe(false);
|
|
});
|
|
|
|
it('registers a single-frame pixel-bearing dataset without stamping NumberOfFrames', () => {
|
|
const dataset = makeDataset({
|
|
SOPClassUID: '1.2.840.10008.5.1.4.1.1.7',
|
|
Modality: 'OT',
|
|
PixelData: new Uint8Array(4).buffer,
|
|
});
|
|
|
|
const imageId = registerNaturalizedDatasetForLocalWadouri(dataset);
|
|
|
|
expect(Object.prototype.hasOwnProperty.call(dataset, 'NumberOfFrames')).toBe(false);
|
|
|
|
expect(imageId).toBe('dicomfile:0');
|
|
expect(addBlob).toHaveBeenCalledTimes(1);
|
|
// Single frame: the bare imageId is mapped, no ?frame= query.
|
|
expect(addImageIdToUIDs).toHaveBeenCalledTimes(1);
|
|
expect(addImageIdToUIDs).toHaveBeenCalledWith('dicomfile:0', {
|
|
StudyInstanceUID: '1.2.3.4',
|
|
SeriesInstanceUID: '1.2.3.4.1',
|
|
SOPInstanceUID: '1.2.3.4.5',
|
|
frameNumber: 1,
|
|
});
|
|
});
|
|
|
|
it('still registers per-frame imageIds (and NumberOfFrames) for a multiframe SEG', () => {
|
|
const dataset = makeDataset({
|
|
SOPClassUID: '1.2.840.10008.5.1.4.1.1.66.4',
|
|
Modality: 'SEG',
|
|
PerFrameFunctionalGroupsSequence: [{}, {}, {}],
|
|
});
|
|
|
|
registerNaturalizedDatasetForLocalWadouri(dataset);
|
|
|
|
// Legitimately multiframe: the count is derived from the per-frame groups
|
|
// and must be serialized (enumerable).
|
|
expect(dataset.NumberOfFrames).toBe(3);
|
|
expect(Object.getOwnPropertyDescriptor(dataset, 'NumberOfFrames').enumerable).toBe(true);
|
|
|
|
expect(addImageIdToUIDs).toHaveBeenCalledTimes(3);
|
|
for (let frame = 1; frame <= 3; frame++) {
|
|
expect(addImageIdToUIDs).toHaveBeenNthCalledWith(frame, `dicomfile:0?frame=${frame}`, {
|
|
StudyInstanceUID: '1.2.3.4',
|
|
SeriesInstanceUID: '1.2.3.4.1',
|
|
SOPInstanceUID: '1.2.3.4.5',
|
|
frameNumber: frame,
|
|
});
|
|
}
|
|
});
|
|
|
|
it('keeps the wadouri imageId on the dataset without making it serializable', () => {
|
|
const dataset = makeDataset({
|
|
SOPClassUID: '1.2.840.10008.5.1.4.1.1.66.4',
|
|
Modality: 'SEG',
|
|
PixelData: new Uint8Array(4).buffer,
|
|
});
|
|
|
|
registerNaturalizedDatasetForLocalWadouri(dataset);
|
|
|
|
expect(dataset.url).toBe('dicomfile:0');
|
|
expect(Object.getOwnPropertyDescriptor(dataset, 'url').enumerable).toBe(false);
|
|
});
|
|
|
|
it('releases the retained Blob and cached dataset on releaseLocalWadouriRegistrations', () => {
|
|
const dataset = makeDataset({
|
|
SOPClassUID: '1.2.840.10008.5.1.4.1.1.66.4',
|
|
Modality: 'SEG',
|
|
PixelData: new Uint8Array(4).buffer,
|
|
});
|
|
dicomImageLoader.wadouri.fileManager.add.mockReturnValueOnce('dicomfile:7');
|
|
// Simulate a parsed dataset held with a refcount of 2 (e.g. two frame loads).
|
|
dicomImageLoader.wadouri.dataSetCacheManager.isLoaded
|
|
.mockReturnValueOnce(true)
|
|
.mockReturnValueOnce(true)
|
|
.mockReturnValue(false);
|
|
|
|
registerNaturalizedDatasetForLocalWadouri(dataset);
|
|
releaseLocalWadouriRegistrations();
|
|
|
|
expect(dicomImageLoader.wadouri.fileManager.remove).toHaveBeenCalledWith(7);
|
|
expect(dicomImageLoader.wadouri.dataSetCacheManager.unload).toHaveBeenCalledTimes(2);
|
|
expect(dicomImageLoader.wadouri.dataSetCacheManager.unload).toHaveBeenCalledWith('7');
|
|
|
|
// Idempotent: the tracking set was drained.
|
|
dicomImageLoader.wadouri.fileManager.remove.mockClear();
|
|
releaseLocalWadouriRegistrations();
|
|
expect(dicomImageLoader.wadouri.fileManager.remove).not.toHaveBeenCalled();
|
|
});
|
|
});
|