* 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>
274 lines
8.7 KiB
JavaScript
274 lines
8.7 KiB
JavaScript
import { DicomMetadataStore, IWebApiDataSource, utils } from '@ohif/core';
|
|
import OHIF from '@ohif/core';
|
|
import {
|
|
datasetToDicomBlob,
|
|
makeExistingPropertiesNonEnumerable,
|
|
setNonEnumerableInstanceProperty,
|
|
} from '../utils/dicomWriter';
|
|
import { appendFrameQueryToImageId } from '../utils/appendFrameQueryToImageId';
|
|
|
|
const metadataProvider = OHIF.classes.MetadataProvider;
|
|
const { EVENTS } = DicomMetadataStore;
|
|
|
|
const END_MODALITIES = {
|
|
SR: true,
|
|
SEG: true,
|
|
DOC: true,
|
|
};
|
|
|
|
const compareValue = (v1, v2, def = 0) => {
|
|
if (v1 === v2) {
|
|
return def;
|
|
}
|
|
if (v1 < v2) {
|
|
return -1;
|
|
}
|
|
return 1;
|
|
};
|
|
|
|
// Sorting SR modalities to be at the end of series list
|
|
const customSort = (seriesA, seriesB) => {
|
|
const instanceA = seriesA.instances[0];
|
|
const instanceB = seriesB.instances[0];
|
|
const modalityA = instanceA.Modality;
|
|
const modalityB = instanceB.Modality;
|
|
|
|
const isEndA = END_MODALITIES[modalityA];
|
|
const isEndB = END_MODALITIES[modalityB];
|
|
|
|
if (isEndA && isEndB) {
|
|
// Compare by series date
|
|
return compareValue(instanceA.SeriesNumber, instanceB.SeriesNumber);
|
|
}
|
|
if (!isEndA && !isEndB) {
|
|
return compareValue(instanceB.SeriesNumber, instanceA.SeriesNumber);
|
|
}
|
|
return isEndA ? -1 : 1;
|
|
};
|
|
|
|
function createDicomLocalApi(dicomLocalConfig) {
|
|
const { name } = dicomLocalConfig;
|
|
|
|
const implementation = {
|
|
initialize: ({ params, query }) => {},
|
|
query: {
|
|
studies: {
|
|
mapParams: () => {},
|
|
search: params => {
|
|
const studyUIDs = DicomMetadataStore.getStudyInstanceUIDs();
|
|
|
|
return studyUIDs.map(StudyInstanceUID => {
|
|
let numInstances = 0;
|
|
const modalities = new Set();
|
|
|
|
// Calculating the number of instances in the study and modalities
|
|
// present in the study
|
|
const study = DicomMetadataStore.getStudy(StudyInstanceUID);
|
|
study.series.forEach(aSeries => {
|
|
numInstances += aSeries.instances.length;
|
|
modalities.add(aSeries.instances[0].Modality);
|
|
});
|
|
|
|
// first instance in the first series
|
|
const firstInstance = study?.series[0]?.instances[0];
|
|
|
|
if (firstInstance) {
|
|
return {
|
|
accession: firstInstance.AccessionNumber,
|
|
date: firstInstance.StudyDate,
|
|
description: firstInstance.StudyDescription,
|
|
mrn: firstInstance.PatientID,
|
|
patientName: utils.formatPN(firstInstance.PatientName),
|
|
studyInstanceUid: firstInstance.StudyInstanceUID,
|
|
time: firstInstance.StudyTime,
|
|
//
|
|
instances: numInstances,
|
|
modalities: Array.from(modalities).join('/'),
|
|
NumInstances: numInstances,
|
|
};
|
|
}
|
|
});
|
|
},
|
|
processResults: () => {
|
|
console.warn(' DICOMLocal QUERY processResults not implemented');
|
|
},
|
|
},
|
|
series: {
|
|
search: studyInstanceUID => {
|
|
const study = DicomMetadataStore.getStudy(studyInstanceUID);
|
|
return study.series.map(aSeries => {
|
|
const firstInstance = aSeries?.instances[0];
|
|
return {
|
|
studyInstanceUid: studyInstanceUID,
|
|
seriesInstanceUid: firstInstance.SeriesInstanceUID,
|
|
modality: firstInstance.Modality,
|
|
seriesNumber: firstInstance.SeriesNumber,
|
|
seriesDate: firstInstance.SeriesDate,
|
|
numSeriesInstances: aSeries.instances.length,
|
|
description: firstInstance.SeriesDescription,
|
|
};
|
|
});
|
|
},
|
|
},
|
|
instances: {
|
|
search: () => {
|
|
console.warn(' DICOMLocal QUERY instances SEARCH not implemented');
|
|
},
|
|
},
|
|
},
|
|
retrieve: {
|
|
directURL: params => {
|
|
const { instance, tag, defaultType } = params;
|
|
|
|
const value = instance[tag];
|
|
if (value instanceof Array && value[0] instanceof ArrayBuffer) {
|
|
return URL.createObjectURL(
|
|
new Blob([value[0]], {
|
|
type: defaultType,
|
|
})
|
|
);
|
|
}
|
|
},
|
|
series: {
|
|
metadata: async ({ StudyInstanceUID, madeInClient = false } = {}) => {
|
|
if (!StudyInstanceUID) {
|
|
throw new Error('Unable to query for SeriesMetadata without StudyInstanceUID');
|
|
}
|
|
|
|
// Instances metadata already added via local upload
|
|
const study = DicomMetadataStore.getStudy(StudyInstanceUID, madeInClient);
|
|
|
|
// Series metadata already added via local upload
|
|
DicomMetadataStore._broadcastEvent(EVENTS.SERIES_ADDED, {
|
|
StudyInstanceUID,
|
|
madeInClient,
|
|
});
|
|
|
|
study.series.forEach(aSeries => {
|
|
const { SeriesInstanceUID } = aSeries;
|
|
|
|
const isMultiframe = aSeries.instances[0].NumberOfFrames > 1;
|
|
|
|
aSeries.instances.forEach((instance, index) => {
|
|
const {
|
|
url: imageId,
|
|
StudyInstanceUID,
|
|
SeriesInstanceUID,
|
|
SOPInstanceUID,
|
|
} = instance;
|
|
|
|
setNonEnumerableInstanceProperty(instance, 'imageId', imageId);
|
|
makeExistingPropertiesNonEnumerable(instance);
|
|
|
|
// Add imageId specific mapping to this data as the URL isn't necessarily WADO-URI.
|
|
metadataProvider.addImageIdToUIDs(imageId, {
|
|
StudyInstanceUID,
|
|
SeriesInstanceUID,
|
|
SOPInstanceUID,
|
|
frameNumber: isMultiframe ? index + 1 : 1,
|
|
});
|
|
});
|
|
|
|
DicomMetadataStore._broadcastEvent(EVENTS.INSTANCES_ADDED, {
|
|
StudyInstanceUID,
|
|
SeriesInstanceUID,
|
|
madeInClient,
|
|
});
|
|
});
|
|
},
|
|
},
|
|
},
|
|
store: {
|
|
dicom: naturalizedReport => {
|
|
const reportBlob = datasetToDicomBlob(naturalizedReport);
|
|
|
|
//Create a URL for the binary.
|
|
var objectUrl = URL.createObjectURL(reportBlob);
|
|
window.location.assign(objectUrl);
|
|
},
|
|
},
|
|
getImageIdsForDisplaySet(displaySet) {
|
|
const images = displaySet.images;
|
|
const imageIds = [];
|
|
|
|
if (!images) {
|
|
return imageIds;
|
|
}
|
|
|
|
displaySet.images.forEach(instance => {
|
|
const NumberOfFrames = instance.NumberOfFrames;
|
|
if (NumberOfFrames > 1) {
|
|
// in multiframe we start at frame 1
|
|
for (let i = 1; i <= NumberOfFrames; i++) {
|
|
const imageId = this.getImageIdsForInstance({
|
|
instance,
|
|
frame: i,
|
|
});
|
|
imageIds.push(imageId);
|
|
}
|
|
} else {
|
|
const imageId = this.getImageIdsForInstance({ instance });
|
|
imageIds.push(imageId);
|
|
}
|
|
});
|
|
|
|
return imageIds;
|
|
},
|
|
getImageIdsForInstance({ instance, frame }) {
|
|
// Important: Never use instance.imageId because it might be multiframe,
|
|
// which would make it an invalid imageId.
|
|
|
|
const { StudyInstanceUID, SeriesInstanceUID } = instance;
|
|
const SOPInstanceUID = instance.SOPInstanceUID || instance.SopInstanceUID;
|
|
const storedInstance = DicomMetadataStore.getInstance(
|
|
StudyInstanceUID,
|
|
SeriesInstanceUID,
|
|
SOPInstanceUID
|
|
);
|
|
|
|
const baseImageId = storedInstance?.url || instance.url;
|
|
|
|
if (!baseImageId) {
|
|
return;
|
|
}
|
|
|
|
const numberOfFrames = Number(storedInstance?.NumberOfFrames || instance.NumberOfFrames) || 1;
|
|
|
|
if (numberOfFrames > 1) {
|
|
const frameNumber = frame !== undefined ? frame : 1;
|
|
return appendFrameQueryToImageId(baseImageId, frameNumber);
|
|
}
|
|
|
|
return baseImageId;
|
|
},
|
|
deleteStudyMetadataPromise() {
|
|
console.log('deleteStudyMetadataPromise not implemented');
|
|
},
|
|
getStudyInstanceUIDs: ({ params, query }) => {
|
|
const { StudyInstanceUIDs: paramsStudyInstanceUIDs } = params;
|
|
const queryStudyInstanceUIDs = query.getAll('StudyInstanceUIDs');
|
|
|
|
const StudyInstanceUIDs = queryStudyInstanceUIDs || paramsStudyInstanceUIDs;
|
|
const StudyInstanceUIDsAsArray =
|
|
StudyInstanceUIDs && Array.isArray(StudyInstanceUIDs)
|
|
? StudyInstanceUIDs
|
|
: [StudyInstanceUIDs];
|
|
|
|
// Put SRs at the end of series list to make sure images are loaded first
|
|
let isStudyInCache = false;
|
|
StudyInstanceUIDsAsArray.forEach(StudyInstanceUID => {
|
|
const study = DicomMetadataStore.getStudy(StudyInstanceUID);
|
|
if (study) {
|
|
study.series = study.series.sort(customSort);
|
|
isStudyInCache = true;
|
|
}
|
|
});
|
|
|
|
return isStudyInCache ? StudyInstanceUIDsAsArray : [];
|
|
},
|
|
};
|
|
return IWebApiDataSource.create(implementation);
|
|
}
|
|
|
|
export { createDicomLocalApi };
|