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:
parent
b996070583
commit
dbb8b1526e
@ -41,8 +41,8 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "7.29.7",
|
||||
"@cornerstonejs/adapters": "5.1.3",
|
||||
"@cornerstonejs/core": "5.1.3",
|
||||
"@cornerstonejs/adapters": "5.4.12",
|
||||
"@cornerstonejs/core": "5.4.12",
|
||||
"@kitware/vtk.js": "35.5.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@ -41,8 +41,8 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "7.29.7",
|
||||
"@cornerstonejs/adapters": "5.1.3",
|
||||
"@cornerstonejs/core": "5.1.3",
|
||||
"@cornerstonejs/adapters": "5.4.12",
|
||||
"@cornerstonejs/core": "5.4.12",
|
||||
"@kitware/vtk.js": "35.5.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@ -6,6 +6,7 @@ import { adaptersRT, adaptersSEG } from '@cornerstonejs/adapters';
|
||||
import { createReportDialogPrompt, useUIStateStore } from '@ohif/extension-default';
|
||||
|
||||
import PROMPT_RESPONSES from '../../default/src/utils/_shared/PROMPT_RESPONSES';
|
||||
import { getSegmentationSaveOptions } from './utils/segmentationConfig';
|
||||
|
||||
const getTargetViewport = ({ viewportId, viewportGridService }) => {
|
||||
const { viewports, activeViewportId } = viewportGridService.getState();
|
||||
@ -34,7 +35,7 @@ const commandsModule = ({
|
||||
extensionManager,
|
||||
commandsManager,
|
||||
}: Types.Extensions.ExtensionParams): Types.Extensions.CommandsModule => {
|
||||
const { segmentationService, displaySetService, viewportGridService } =
|
||||
const { segmentationService, displaySetService, viewportGridService, customizationService } =
|
||||
servicesManager.services as AppTypes.Services;
|
||||
|
||||
const actions = {
|
||||
@ -88,13 +89,37 @@ const commandsModule = ({
|
||||
* @returns Returns the generated segmentation data.
|
||||
*/
|
||||
generateSegmentation: ({ segmentationId, options = {} }) => {
|
||||
// `dataSource` (a data source name) is consumed here to resolve the store
|
||||
// overrides; it must not be forwarded to the adapter's generateSegmentation.
|
||||
const { dataSource: dataSourceName, ...generateOptions } = options;
|
||||
const segmentation = cornerstoneToolsSegmentation.state.getSegmentation(segmentationId);
|
||||
const predecessorImageId = options.predecessorImageId ?? segmentation.predecessorImageId;
|
||||
const predecessorImageId =
|
||||
generateOptions.predecessorImageId ?? segmentation.predecessorImageId;
|
||||
|
||||
// A data source may override the app-wide `segmentation.store.*` defaults
|
||||
// via `configuration.segmentation.store` (different back ends support
|
||||
// different SEG encodings). Use the named target data source when storing,
|
||||
// otherwise the active one (e.g. download).
|
||||
const dataSourceDefinition = dataSourceName
|
||||
? extensionManager.getDataSourceDefinition(dataSourceName)
|
||||
: extensionManager.getActiveDataSourceDefinition();
|
||||
const dataSourceStoreOverride = dataSourceDefinition?.configuration?.segmentation?.store;
|
||||
|
||||
const { imageIds } = segmentation.representationData.Labelmap;
|
||||
|
||||
const segImages = imageIds.map(imageId => cache.getImage(imageId));
|
||||
const referencedImages = segImages.map(image => cache.getImage(image.referencedImageId));
|
||||
const referencedImages = segImages.map((segImage, sliceIndex) => {
|
||||
const referencedImage = cache.getImage(segImage.referencedImageId);
|
||||
|
||||
if (!referencedImage) {
|
||||
throw new Error(
|
||||
`Referenced source image not in cache for segmentation slice ${sliceIndex} ` +
|
||||
`(referencedImageId: ${segImage.referencedImageId}). Ensure the referenced series is fully loaded before storing.`
|
||||
);
|
||||
}
|
||||
|
||||
return referencedImage;
|
||||
});
|
||||
|
||||
const labelmaps2D = [];
|
||||
|
||||
@ -173,7 +198,8 @@ const commandsModule = ({
|
||||
|
||||
const generatedSegmentation = generateSegmentation(referencedImages, labelmap3D, metaData, {
|
||||
predecessorImageId,
|
||||
...options,
|
||||
...getSegmentationSaveOptions(customizationService, dataSourceStoreOverride),
|
||||
...generateOptions,
|
||||
});
|
||||
|
||||
return generatedSegmentation;
|
||||
@ -257,6 +283,8 @@ const commandsModule = ({
|
||||
const args = {
|
||||
segmentationId,
|
||||
options: {
|
||||
// Resolve store overrides against the data source we are storing into.
|
||||
dataSource: dataSourceName,
|
||||
SeriesDescription: series ? undefined : reportName || label || 'Contour Series',
|
||||
SeriesNumber: series ? undefined : 1 + priorSeriesNumber,
|
||||
predecessorImageId: series,
|
||||
@ -289,6 +317,8 @@ const commandsModule = ({
|
||||
|
||||
generateContour: async args => {
|
||||
const { segmentationId, options } = args;
|
||||
// `dataSource` is only used by the SEG store path; keep it out of the RTSS options.
|
||||
const { dataSource: _dataSource, ...contourOptions } = options ?? {};
|
||||
const segmentations = segmentationService.getSegmentation(segmentationId);
|
||||
|
||||
// inject colors to the segmentIndex
|
||||
@ -301,10 +331,11 @@ const commandsModule = ({
|
||||
Number(segmentIndex)
|
||||
);
|
||||
});
|
||||
const predecessorImageId = options?.predecessorImageId ?? segmentations.predecessorImageId;
|
||||
const predecessorImageId =
|
||||
contourOptions.predecessorImageId ?? segmentations.predecessorImageId;
|
||||
const dataset = await generateRTSSFromRepresentation(segmentations, {
|
||||
predecessorImageId,
|
||||
...options,
|
||||
...contourOptions,
|
||||
});
|
||||
return { dataset };
|
||||
},
|
||||
|
||||
@ -0,0 +1,29 @@
|
||||
import {
|
||||
DEFAULT_SEG_STORE_MODE,
|
||||
DEFAULT_SEG_STORE_TRANSFER_SYNTAX_UID,
|
||||
type SegmentationMode,
|
||||
} from '../utils/segmentationConfig';
|
||||
|
||||
export type SegmentLabelCustomization = {
|
||||
enabledByDefault?: boolean;
|
||||
labelColor?: number[];
|
||||
hoverTimeout?: number;
|
||||
background?: string;
|
||||
};
|
||||
|
||||
export type SegmentationStoreCustomization = {
|
||||
defaultMode?: SegmentationMode;
|
||||
transferSyntaxUID?: string;
|
||||
};
|
||||
|
||||
/** Extension-registered defaults: Label Map SEG + RLE Lossless. */
|
||||
const segmentationCustomization = {
|
||||
'segmentation.store.defaultMode': DEFAULT_SEG_STORE_MODE,
|
||||
'segmentation.store.transferSyntaxUID': DEFAULT_SEG_STORE_TRANSFER_SYNTAX_UID,
|
||||
'segmentation.segmentLabel': {
|
||||
enabledByDefault: false,
|
||||
hoverTimeout: 1,
|
||||
} satisfies SegmentLabelCustomization,
|
||||
};
|
||||
|
||||
export default segmentationCustomization;
|
||||
@ -0,0 +1,10 @@
|
||||
import segmentationCustomization from './customizations/segmentationCustomization';
|
||||
|
||||
export default function getCustomizationModule() {
|
||||
return [
|
||||
{
|
||||
name: 'default',
|
||||
value: segmentationCustomization,
|
||||
},
|
||||
];
|
||||
}
|
||||
@ -1,16 +1,237 @@
|
||||
import { utils, Types as OhifTypes } from '@ohif/core';
|
||||
import { utils, Types as OhifTypes, DicomMetadataStore, classes, log } from '@ohif/core';
|
||||
import i18n from '@ohif/i18n';
|
||||
import { metaData, eventTarget } from '@cornerstonejs/core';
|
||||
import { metaData, eventTarget, utilities as csUtils } from '@cornerstonejs/core';
|
||||
import { CONSTANTS, segmentation as cstSegmentation } from '@cornerstonejs/tools';
|
||||
import { adaptersSEG, Enums } from '@cornerstonejs/adapters';
|
||||
|
||||
import { SOPClassHandlerId } from './id';
|
||||
import { dicomlabToRGB } from './utils/dicomlabToRGB';
|
||||
import { getSegmentationParserType } from './utils/segmentationConfig';
|
||||
import {
|
||||
getFrameIndexFromImageId,
|
||||
isLocalSchemeImageId,
|
||||
stripFrameFromImageId,
|
||||
} from './utils/segLocalImageIds';
|
||||
|
||||
const sopClassUids = ['1.2.840.10008.5.1.4.1.1.66.4', '1.2.840.10008.5.1.4.1.1.66.7'];
|
||||
const LABELMAP_SEG_SOP_CLASS_UID = '1.2.840.10008.5.1.4.1.1.66.7';
|
||||
|
||||
const loadPromises = {};
|
||||
|
||||
const SEG_LOAD_LOG_PREFIX = '[SEG load]';
|
||||
|
||||
// Max number of SEG frames fetched/decoded concurrently by the segmentation
|
||||
// loader. Hard-coded to 16 for now; intended to become configurable (and to
|
||||
// pair with the full-instance prefetch capability) in a follow-up.
|
||||
const SEG_FRAME_DECODE_CONCURRENCY = 16;
|
||||
|
||||
|
||||
function _normalizeImageId(imageId: string | string[] | undefined): string | undefined {
|
||||
if (imageId == null) {
|
||||
return undefined;
|
||||
}
|
||||
return Array.isArray(imageId) ? imageId[0] : imageId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Expands a WADO-RS frame imageId (…/frames/1) into one imageId per frame.
|
||||
* Multiframe SEG is stored as separate /frames/N resources on the server.
|
||||
*/
|
||||
function getFrameImageIds(segImageId: string, numberOfFrames: number): string[] {
|
||||
const frameMatch = segImageId.match(/(.*\/frames\/)(\d+)(.*)$/);
|
||||
if (!frameMatch || numberOfFrames <= 1) {
|
||||
return [segImageId];
|
||||
}
|
||||
|
||||
const prefix = frameMatch[1];
|
||||
const suffix = frameMatch[3] || '';
|
||||
const frameImageIds: string[] = [];
|
||||
|
||||
for (let frameNumber = 1; frameNumber <= numberOfFrames; frameNumber++) {
|
||||
frameImageIds.push(`${prefix}${frameNumber}${suffix}`);
|
||||
}
|
||||
|
||||
return frameImageIds;
|
||||
}
|
||||
|
||||
function _getSegNumberOfFrames(instance: Record<string, unknown>): number {
|
||||
const fromTag = Number(instance.NumberOfFrames);
|
||||
if (fromTag > 0) {
|
||||
return fromTag;
|
||||
}
|
||||
|
||||
const perFrame = instance.PerFrameFunctionalGroupsSequence;
|
||||
if (Array.isArray(perFrame) && perFrame.length > 0) {
|
||||
return perFrame.length;
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
function _ensureSegImageIdMetadataRegistered(
|
||||
imageId: string | undefined,
|
||||
instance: Record<string, unknown>
|
||||
): void {
|
||||
if (!imageId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const metadataProvider = classes.MetadataProvider;
|
||||
|
||||
const StudyInstanceUID = instance.StudyInstanceUID as string | undefined;
|
||||
const SeriesInstanceUID = instance.SeriesInstanceUID as string | undefined;
|
||||
const SOPInstanceUID = (instance.SOPInstanceUID || instance.SopInstanceUID) as
|
||||
| string
|
||||
| undefined;
|
||||
|
||||
if (!StudyInstanceUID || !SeriesInstanceUID || !SOPInstanceUID) {
|
||||
return;
|
||||
}
|
||||
|
||||
metadataProvider.addImageIdToUIDs(imageId, {
|
||||
StudyInstanceUID,
|
||||
SeriesInstanceUID,
|
||||
SOPInstanceUID,
|
||||
frameNumber: getFrameIndexFromImageId(imageId),
|
||||
});
|
||||
}
|
||||
|
||||
/** Ensures metadataProvider.get('instance', imageId) resolves for frame-qualified local SEG ids. */
|
||||
function _ensureSegInstanceMetadataAvailable(
|
||||
imageId: string | undefined,
|
||||
instance: Record<string, unknown>
|
||||
): void {
|
||||
if (!imageId) {
|
||||
return;
|
||||
}
|
||||
|
||||
_ensureSegImageIdMetadataRegistered(imageId, instance);
|
||||
|
||||
if (metaData.get('instance', imageId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const StudyInstanceUID = instance.StudyInstanceUID as string | undefined;
|
||||
const SeriesInstanceUID = instance.SeriesInstanceUID as string | undefined;
|
||||
const SOPInstanceUID = (instance.SOPInstanceUID || instance.SopInstanceUID) as
|
||||
| string
|
||||
| undefined;
|
||||
|
||||
const storedInstance =
|
||||
StudyInstanceUID && SeriesInstanceUID && SOPInstanceUID
|
||||
? DicomMetadataStore.getInstance(StudyInstanceUID, SeriesInstanceUID, SOPInstanceUID)
|
||||
: undefined;
|
||||
|
||||
classes.MetadataProvider.addCustomMetadata(
|
||||
imageId,
|
||||
'instance',
|
||||
storedInstance || instance
|
||||
);
|
||||
}
|
||||
|
||||
function _getSegDataSource(extensionManager, instance: Record<string, unknown>) {
|
||||
const StudyInstanceUID = instance.StudyInstanceUID as string | undefined;
|
||||
const SeriesInstanceUID = instance.SeriesInstanceUID as string | undefined;
|
||||
const SOPInstanceUID = (instance.SOPInstanceUID || instance.SopInstanceUID) as
|
||||
| string
|
||||
| undefined;
|
||||
|
||||
let localUrl: string | undefined;
|
||||
|
||||
if (StudyInstanceUID && SeriesInstanceUID && SOPInstanceUID) {
|
||||
const storedInstance = DicomMetadataStore.getInstance(
|
||||
StudyInstanceUID,
|
||||
SeriesInstanceUID,
|
||||
SOPInstanceUID
|
||||
);
|
||||
localUrl = storedInstance?.url as string | undefined;
|
||||
}
|
||||
|
||||
localUrl = localUrl || (instance.url as string | undefined);
|
||||
|
||||
if (localUrl && isLocalSchemeImageId(localUrl)) {
|
||||
const dicomLocal = extensionManager.getDataSources('dicomlocal');
|
||||
|
||||
if (dicomLocal?.[0]) {
|
||||
return dicomLocal[0];
|
||||
}
|
||||
}
|
||||
|
||||
return extensionManager.getActiveDataSource()[0];
|
||||
}
|
||||
|
||||
function _getSegImageIdFromInstance(
|
||||
instance: Record<string, unknown>,
|
||||
dataSource: { getImageIdsForInstance?: (args: { instance: unknown; frame?: number }) => unknown }
|
||||
): string | undefined {
|
||||
const numberOfFrames = _getSegNumberOfFrames(instance);
|
||||
const frame = numberOfFrames > 1 ? 1 : undefined;
|
||||
|
||||
return _normalizeImageId(
|
||||
dataSource.getImageIdsForInstance?.({ instance, frame }) as string | string[] | undefined
|
||||
);
|
||||
}
|
||||
|
||||
function _resolveFrameImageIds(
|
||||
segImageIdStr: string,
|
||||
instance: Record<string, unknown>,
|
||||
dataSource: { getImageIdsForInstance?: (args: { instance: unknown; frame?: number }) => unknown }
|
||||
): string[] {
|
||||
const numberOfFrames = _getSegNumberOfFrames(instance);
|
||||
const fromFrameUrl = getFrameImageIds(segImageIdStr, numberOfFrames);
|
||||
|
||||
if (fromFrameUrl.length > 1) {
|
||||
return fromFrameUrl;
|
||||
}
|
||||
|
||||
if (numberOfFrames <= 1) {
|
||||
return [segImageIdStr];
|
||||
}
|
||||
|
||||
const frameImageIds: string[] = [];
|
||||
|
||||
for (let frame = 1; frame <= numberOfFrames; frame++) {
|
||||
const frameImageId = _normalizeImageId(
|
||||
dataSource.getImageIdsForInstance?.({ instance, frame }) as string | string[] | undefined
|
||||
);
|
||||
|
||||
if (frameImageId) {
|
||||
frameImageIds.push(frameImageId);
|
||||
}
|
||||
}
|
||||
|
||||
return frameImageIds.length ? frameImageIds : [segImageIdStr];
|
||||
}
|
||||
|
||||
function _logSegImageIds({
|
||||
segDisplaySet,
|
||||
segImageIdStr,
|
||||
frameImageIds,
|
||||
referencedImageIds,
|
||||
}: {
|
||||
segDisplaySet: AppTypes.DisplaySet;
|
||||
segImageIdStr: string;
|
||||
frameImageIds: string[];
|
||||
referencedImageIds: string[];
|
||||
}) {
|
||||
const instance = segDisplaySet.instance as Record<string, unknown>;
|
||||
const numberOfFrames = Number(instance?.NumberOfFrames) || 1;
|
||||
|
||||
log.debug(SEG_LOAD_LOG_PREFIX, 'Loading SEG pixel data', {
|
||||
SOPInstanceUID: segDisplaySet.SOPInstanceUID,
|
||||
SeriesInstanceUID: segDisplaySet.SeriesInstanceUID,
|
||||
SOPClassUID: segDisplaySet.SOPClassUID,
|
||||
NumberOfFrames: numberOfFrames,
|
||||
segmentCount: Object.keys(segDisplaySet.segments || {}).length,
|
||||
referencedDisplaySetInstanceUID: segDisplaySet.referencedDisplaySetInstanceUID,
|
||||
referencedImageIdCount: referencedImageIds.length,
|
||||
referencedImageIds,
|
||||
segImageIdForMetadata: segImageIdStr,
|
||||
frameImageIds,
|
||||
loadSegFramesIndividually: frameImageIds.length > 1,
|
||||
});
|
||||
}
|
||||
|
||||
function _getDisplaySetsFromSeries(
|
||||
instances,
|
||||
servicesManager: AppTypes.ServicesManager,
|
||||
@ -171,6 +392,11 @@ function _load(
|
||||
});
|
||||
});
|
||||
|
||||
// Expose the in-flight load promise so observers (e.g. the viewport service
|
||||
// waiting to attach the representation) can react to a load failure without
|
||||
// re-invoking load().
|
||||
segDisplaySet.loadingPromise = loadPromises[SOPInstanceUID];
|
||||
|
||||
return loadPromises[SOPInstanceUID];
|
||||
}
|
||||
|
||||
@ -178,16 +404,18 @@ async function _loadSegments({
|
||||
extensionManager,
|
||||
servicesManager,
|
||||
segDisplaySet,
|
||||
headers,
|
||||
}: withAppTypes) {
|
||||
const utilityModule = extensionManager.getModuleEntry(
|
||||
'@ohif/extension-cornerstone.utilityModule.common'
|
||||
);
|
||||
}: withAppTypes<{ segDisplaySet: AppTypes.DisplaySet }>) {
|
||||
const { segmentationService, uiNotificationService, customizationService } =
|
||||
servicesManager.services;
|
||||
const instance = segDisplaySet.instance as Record<string, unknown>;
|
||||
const dataSource = _getSegDataSource(extensionManager, instance);
|
||||
const segImageIdStr = _getSegImageIdFromInstance(instance, dataSource);
|
||||
|
||||
const { segmentationService, uiNotificationService } = servicesManager.services;
|
||||
|
||||
const { dicomLoaderService } = utilityModule.exports;
|
||||
const arrayBuffer = await dicomLoaderService.findDicomDataPromise(segDisplaySet, null, headers);
|
||||
if (!segImageIdStr) {
|
||||
throw new Error(
|
||||
'Could not get imageId for SEG instance (no local wadouri url and getImageIdsForInstance returned nothing).'
|
||||
);
|
||||
}
|
||||
|
||||
const referencedDisplaySet = servicesManager.services.displaySetService.getDisplaySetByUID(
|
||||
segDisplaySet.referencedDisplaySetInstanceUID
|
||||
@ -197,31 +425,114 @@ async function _loadSegments({
|
||||
throw new Error('referencedDisplaySet is missing for SEG');
|
||||
}
|
||||
|
||||
// Prefer cached stack imageIds (multiframe SEG fix #4890), then data source expansion.
|
||||
let { imageIds } = referencedDisplaySet;
|
||||
|
||||
if (!imageIds) {
|
||||
// try images
|
||||
const { images } = referencedDisplaySet;
|
||||
imageIds = images.map(image => image.imageId);
|
||||
if (!imageIds?.length) {
|
||||
imageIds = dataSource.getImageIdsForDisplaySet?.(referencedDisplaySet);
|
||||
}
|
||||
|
||||
// Todo: what should be defaults here
|
||||
if (!imageIds?.length) {
|
||||
imageIds = (referencedDisplaySet as { images?: { imageId: string }[] }).images?.map(
|
||||
(img: { imageId: string }) => img.imageId
|
||||
);
|
||||
}
|
||||
|
||||
if (!imageIds?.length) {
|
||||
throw new Error('referencedDisplaySet has no imageIds');
|
||||
}
|
||||
|
||||
(segDisplaySet as AppTypes.DisplaySet & { referencedImageIds?: string[] }).referencedImageIds =
|
||||
imageIds;
|
||||
|
||||
if (!referencedDisplaySet.imageIds?.length) {
|
||||
referencedDisplaySet.imageIds = imageIds;
|
||||
}
|
||||
|
||||
const frameImageIds = _resolveFrameImageIds(
|
||||
segImageIdStr,
|
||||
segDisplaySet.instance as Record<string, unknown>,
|
||||
dataSource
|
||||
);
|
||||
|
||||
const segImageIdForMetadata = isLocalSchemeImageId(segImageIdStr)
|
||||
? stripFrameFromImageId(segImageIdStr)
|
||||
: segImageIdStr;
|
||||
|
||||
_logSegImageIds({
|
||||
segDisplaySet,
|
||||
segImageIdStr: segImageIdForMetadata,
|
||||
frameImageIds,
|
||||
referencedImageIds: imageIds,
|
||||
});
|
||||
|
||||
_ensureSegInstanceMetadataAvailable(segImageIdForMetadata, instance);
|
||||
frameImageIds.forEach(id => _ensureSegInstanceMetadataAvailable(id, instance));
|
||||
|
||||
const tolerance = 0.001;
|
||||
eventTarget.addEventListener(Enums.Events.SEGMENTATION_LOAD_PROGRESS, evt => {
|
||||
const onProgress = evt => {
|
||||
const { percentComplete } = evt.detail;
|
||||
segmentationService._broadcastEvent(segmentationService.EVENTS.SEGMENT_LOADING_COMPLETE, {
|
||||
percentComplete,
|
||||
});
|
||||
});
|
||||
};
|
||||
eventTarget.addEventListener(Enums.Events.SEGMENTATION_LOAD_PROGRESS, onProgress);
|
||||
|
||||
const results = await adaptersSEG.Cornerstone3D.Segmentation.createFromDICOMSegBuffer(
|
||||
imageIds,
|
||||
arrayBuffer,
|
||||
{ metadataProvider: metaData, tolerance }
|
||||
);
|
||||
// Fetch the whole SEG instance as a single Part 10 object and register its
|
||||
// per-frame compressed pixels into the Cornerstone3D frame registry, so the
|
||||
// per-frame loads below are served locally instead of one network request
|
||||
// per frame: SEG frames are so small and numerous that one bulk fetch beats
|
||||
// hundreds of tiny requests. Enabled by default; per-frame loading is the
|
||||
// exception (loadMultiframeAsPart10: false in the data source config, or the
|
||||
// cornerstone.segmentation.loadMultiframeAsPart10 customization). The
|
||||
// prefetch is awaited until it completes OR fails — deliberately no timeout:
|
||||
// a failed/unsupported instance fetch resolves quickly and falls back to
|
||||
// per-frame, while a slow large fetch is still the fastest way to all frames.
|
||||
const loadMultiframeAsPart10 =
|
||||
(dataSource?.getConfig?.()?.loadMultiframeAsPart10 as boolean | undefined) ??
|
||||
(customizationService?.getCustomization?.(
|
||||
'cornerstone.segmentation.loadMultiframeAsPart10'
|
||||
) as boolean | undefined) ??
|
||||
true;
|
||||
|
||||
let prefetch;
|
||||
if (loadMultiframeAsPart10) {
|
||||
prefetch = dataSource.retrieve?.prefetchInstanceFrames?.({
|
||||
instance,
|
||||
imageId: segImageIdForMetadata,
|
||||
});
|
||||
|
||||
if (prefetch?.done) {
|
||||
await prefetch.done;
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
);
|
||||
} finally {
|
||||
eventTarget.removeEventListener(Enums.Events.SEGMENTATION_LOAD_PROGRESS, onProgress);
|
||||
prefetch?.cancel?.();
|
||||
}
|
||||
|
||||
let usedRecommendedDisplayCIELabValue = true;
|
||||
results.segMetadata.data.forEach((data, i) => {
|
||||
const resultsTyped = results as {
|
||||
segMetadata: { data: { rgba?: number[]; RecommendedDisplayCIELabValue?: number[] }[] };
|
||||
};
|
||||
resultsTyped.segMetadata.data.forEach((data, i) => {
|
||||
if (i > 0) {
|
||||
data.rgba = data.RecommendedDisplayCIELabValue;
|
||||
|
||||
@ -246,6 +557,17 @@ async function _loadSegments({
|
||||
}
|
||||
|
||||
Object.assign(segDisplaySet, results);
|
||||
|
||||
const labelMapImageIds = (results as { labelMapImages?: { imageId: string }[][] })
|
||||
.labelMapImages?.flat()
|
||||
.map(image => image.imageId);
|
||||
|
||||
log.debug(SEG_LOAD_LOG_PREFIX, 'SEG parse complete', {
|
||||
SOPInstanceUID: segDisplaySet.SOPInstanceUID,
|
||||
labelMapImageCount: labelMapImageIds?.length ?? 0,
|
||||
labelMapImageIds,
|
||||
segmentIndices: Object.keys(segDisplaySet.segments || {}),
|
||||
});
|
||||
}
|
||||
|
||||
function _segmentationExists(segDisplaySet) {
|
||||
|
||||
@ -4,6 +4,7 @@ import React from 'react';
|
||||
import getSopClassHandlerModule from './getSopClassHandlerModule';
|
||||
import getHangingProtocolModule from './getHangingProtocolModule';
|
||||
import getCommandsModule from './commandsModule';
|
||||
import getCustomizationModule from './getCustomizationModule';
|
||||
import { getToolbarModule } from './getToolbarModule';
|
||||
|
||||
const Component = React.lazy(() => {
|
||||
@ -28,6 +29,7 @@ const extension = {
|
||||
*/
|
||||
id,
|
||||
getCommandsModule,
|
||||
getCustomizationModule,
|
||||
getToolbarModule,
|
||||
getViewportModule({ servicesManager, extensionManager, commandsManager }) {
|
||||
const ExtendedOHIFCornerstoneSEGViewport = props => {
|
||||
|
||||
@ -0,0 +1,32 @@
|
||||
export function isLocalSchemeImageId(imageId: string): boolean {
|
||||
return /^(wadouri:|dicomfile:|dicomweb:)/.test(imageId);
|
||||
}
|
||||
|
||||
export function stripFrameFromImageId(imageId: string): string {
|
||||
const queryIndex = imageId.indexOf('?');
|
||||
if (queryIndex === -1) {
|
||||
return imageId;
|
||||
}
|
||||
|
||||
const basePath = imageId.slice(0, queryIndex);
|
||||
const rebuiltQuery = imageId
|
||||
.slice(queryIndex + 1)
|
||||
.split('&')
|
||||
.filter(param => param.split('=')[0] !== 'frame')
|
||||
.join('&');
|
||||
|
||||
return rebuiltQuery ? `${basePath}?${rebuiltQuery}` : basePath;
|
||||
}
|
||||
|
||||
export function appendFrameToImageId(baseImageId: string, frame: number): string {
|
||||
const withoutFrame = stripFrameFromImageId(baseImageId);
|
||||
const separator = withoutFrame.includes('?') ? '&' : '?';
|
||||
|
||||
return `${withoutFrame}${separator}frame=${frame}`;
|
||||
}
|
||||
|
||||
export function getFrameIndexFromImageId(imageId: string): number {
|
||||
const frameMatch = imageId.match(/(?:&|\?)frame=(\d+)/);
|
||||
|
||||
return frameMatch ? Number(frameMatch[1]) : 1;
|
||||
}
|
||||
100
extensions/cornerstone-dicom-seg/src/utils/segmentationConfig.ts
Normal file
100
extensions/cornerstone-dicom-seg/src/utils/segmentationConfig.ts
Normal file
@ -0,0 +1,100 @@
|
||||
export const LABELMAP_SEG_SOP_CLASS_UID = '1.2.840.10008.5.1.4.1.1.66.7';
|
||||
export const BITMAP_SEG_SOP_CLASS_UID = '1.2.840.10008.5.1.4.1.1.66.4';
|
||||
/** RLE Lossless — OHIF default SEG store transfer syntax. */
|
||||
export const DEFAULT_SEG_STORE_TRANSFER_SYNTAX_UID = '1.2.840.10008.1.2.5';
|
||||
/** OHIF default SEG store mode (Label Map Segmentation SOP Class). */
|
||||
export const DEFAULT_SEG_STORE_MODE = 'labelmap' as const;
|
||||
|
||||
export type SegmentationMode = 'labelmap' | 'bitmap';
|
||||
|
||||
/**
|
||||
* Per-data-source overrides for the SEG store defaults. A data source may set
|
||||
* these under `configuration.segmentation.store` to override the values coming
|
||||
* from the `segmentation.store.*` customizations. Different back ends support
|
||||
* different SEG encodings, so the data source is allowed to win over the
|
||||
* app-wide customization default.
|
||||
*/
|
||||
export type SegmentationStoreOverride = {
|
||||
defaultMode?: SegmentationMode;
|
||||
transferSyntaxUID?: string;
|
||||
};
|
||||
|
||||
type SegmentationCustomizationReader = {
|
||||
getCustomization: (customizationId: string) => unknown;
|
||||
};
|
||||
|
||||
function getStoreDefaultMode(
|
||||
customizationService?: SegmentationCustomizationReader,
|
||||
override?: SegmentationStoreOverride
|
||||
): SegmentationMode {
|
||||
const mode =
|
||||
override?.defaultMode ??
|
||||
(customizationService?.getCustomization('segmentation.store.defaultMode') as
|
||||
| SegmentationMode
|
||||
| undefined) ??
|
||||
DEFAULT_SEG_STORE_MODE;
|
||||
|
||||
return mode === 'bitmap' ? 'bitmap' : 'labelmap';
|
||||
}
|
||||
|
||||
function getStoreTransferSyntaxUID(
|
||||
customizationService?: SegmentationCustomizationReader,
|
||||
override?: SegmentationStoreOverride
|
||||
): string {
|
||||
return (
|
||||
override?.transferSyntaxUID ??
|
||||
(customizationService?.getCustomization(
|
||||
'segmentation.store.transferSyntaxUID'
|
||||
) as string | undefined) ??
|
||||
DEFAULT_SEG_STORE_TRANSFER_SYNTAX_UID
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the parser type for loading a DICOM SEG instance.
|
||||
* Uses the instance SOP Class UID when it is a known SEG class; otherwise falls back to store defaultMode.
|
||||
*/
|
||||
export function getSegmentationParserType(
|
||||
sopClassUID: string | undefined,
|
||||
customizationService?: SegmentationCustomizationReader
|
||||
): SegmentationMode {
|
||||
if (sopClassUID === LABELMAP_SEG_SOP_CLASS_UID) {
|
||||
return 'labelmap';
|
||||
}
|
||||
|
||||
if (sopClassUID === BITMAP_SEG_SOP_CLASS_UID) {
|
||||
return 'bitmap';
|
||||
}
|
||||
|
||||
return getStoreDefaultMode(customizationService);
|
||||
}
|
||||
|
||||
/**
|
||||
* Options passed to @cornerstonejs/adapters generateSegmentation when exporting or storing SEG.
|
||||
*
|
||||
* Defaults to **Label Map + RLE Lossless**. Customizations (or per-data-source
|
||||
* `configuration.segmentation.store`) are only needed to opt into bitmap and/or
|
||||
* uncompressed Explicit VR Little Endian.
|
||||
*/
|
||||
export function getSegmentationSaveOptions(
|
||||
customizationService?: SegmentationCustomizationReader,
|
||||
override?: SegmentationStoreOverride
|
||||
): {
|
||||
sopClassUID: string;
|
||||
transferSyntaxUID: string;
|
||||
transferSyntaxUid: string;
|
||||
} {
|
||||
const defaultMode = getStoreDefaultMode(customizationService, override);
|
||||
const sopClassUID =
|
||||
defaultMode === 'bitmap' ? BITMAP_SEG_SOP_CLASS_UID : LABELMAP_SEG_SOP_CLASS_UID;
|
||||
const transferSyntaxUID = getStoreTransferSyntaxUID(
|
||||
customizationService,
|
||||
override
|
||||
);
|
||||
|
||||
return {
|
||||
sopClassUID,
|
||||
transferSyntaxUID,
|
||||
transferSyntaxUid: transferSyntaxUID,
|
||||
};
|
||||
}
|
||||
@ -95,10 +95,13 @@ function OHIFCornerstoneSEGViewport(props: withAppTypes) {
|
||||
};
|
||||
|
||||
const getCornerstoneViewport = useCallback(() => {
|
||||
// Stack uses the referenced series (data[0]); SEG is applied as an overlay (data[1]).
|
||||
// Passing only the SEG display set leaves the stack with derived labelmap imageIds,
|
||||
// which are not displayable without the underlying grayscale series.
|
||||
return (
|
||||
<OHIFCornerstoneViewport
|
||||
{...props}
|
||||
displaySets={[segDisplaySet]}
|
||||
displaySets={[referencedDisplaySet, segDisplaySet]}
|
||||
viewportOptions={{
|
||||
viewportType: viewportOptions.viewportType,
|
||||
toolGroupId: toolGroupId,
|
||||
@ -111,7 +114,14 @@ function OHIFCornerstoneSEGViewport(props: withAppTypes) {
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}, [viewportId, segDisplaySet, toolGroupId, props, viewportOptions]);
|
||||
}, [
|
||||
viewportId,
|
||||
segDisplaySet,
|
||||
referencedDisplaySet,
|
||||
toolGroupId,
|
||||
props,
|
||||
viewportOptions,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (segIsLoading) {
|
||||
|
||||
@ -32,7 +32,7 @@
|
||||
"@ohif/core": "workspace:*",
|
||||
"@ohif/extension-cornerstone": "workspace:*",
|
||||
"@ohif/ui": "workspace:*",
|
||||
"dcmjs": "0.49.4",
|
||||
"dcmjs": "0.52.0",
|
||||
"dicom-parser": "1.8.21",
|
||||
"hammerjs": "2.0.8",
|
||||
"prop-types": "15.8.1",
|
||||
@ -40,9 +40,9 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "7.29.7",
|
||||
"@cornerstonejs/adapters": "5.1.3",
|
||||
"@cornerstonejs/core": "5.1.3",
|
||||
"@cornerstonejs/tools": "5.1.3",
|
||||
"@cornerstonejs/adapters": "5.4.12",
|
||||
"@cornerstonejs/core": "5.4.12",
|
||||
"@cornerstonejs/tools": "5.4.12",
|
||||
"classnames": "2.5.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@ -37,7 +37,7 @@
|
||||
"@ohif/extension-default": "workspace:*",
|
||||
"@ohif/i18n": "workspace:*",
|
||||
"@ohif/ui": "workspace:*",
|
||||
"dcmjs": "0.49.4",
|
||||
"dcmjs": "0.52.0",
|
||||
"dicom-parser": "1.8.21",
|
||||
"hammerjs": "2.0.8",
|
||||
"prop-types": "15.8.1",
|
||||
@ -45,8 +45,8 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "7.29.7",
|
||||
"@cornerstonejs/core": "5.1.3",
|
||||
"@cornerstonejs/tools": "5.1.3",
|
||||
"@cornerstonejs/core": "5.4.12",
|
||||
"@cornerstonejs/tools": "5.4.12",
|
||||
"classnames": "2.5.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@ -38,11 +38,11 @@
|
||||
"@cornerstonejs/codec-libjpeg-turbo-8bit": "1.2.2",
|
||||
"@cornerstonejs/codec-openjpeg": "1.3.0",
|
||||
"@cornerstonejs/codec-openjph": "2.4.7",
|
||||
"@cornerstonejs/dicom-image-loader": "5.1.3",
|
||||
"@cornerstonejs/dicom-image-loader": "5.4.12",
|
||||
"@ohif/core": "workspace:*",
|
||||
"@ohif/extension-default": "workspace:*",
|
||||
"@ohif/ui": "workspace:*",
|
||||
"dcmjs": "0.49.4",
|
||||
"dcmjs": "0.52.0",
|
||||
"dicom-parser": "1.8.21",
|
||||
"hammerjs": "2.0.8",
|
||||
"prop-types": "15.8.1",
|
||||
@ -52,13 +52,13 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "7.29.7",
|
||||
"@cornerstonejs/adapters": "5.1.3",
|
||||
"@cornerstonejs/ai": "5.1.3",
|
||||
"@cornerstonejs/core": "5.1.3",
|
||||
"@cornerstonejs/labelmap-interpolation": "5.1.3",
|
||||
"@cornerstonejs/metadata": "5.1.3",
|
||||
"@cornerstonejs/polymorphic-segmentation": "5.1.3",
|
||||
"@cornerstonejs/tools": "5.1.3",
|
||||
"@cornerstonejs/adapters": "5.4.12",
|
||||
"@cornerstonejs/ai": "5.4.12",
|
||||
"@cornerstonejs/core": "5.4.12",
|
||||
"@cornerstonejs/labelmap-interpolation": "5.4.12",
|
||||
"@cornerstonejs/metadata": "5.4.12",
|
||||
"@cornerstonejs/polymorphic-segmentation": "5.4.12",
|
||||
"@cornerstonejs/tools": "5.4.12",
|
||||
"@icr/polyseg-wasm": "0.4.0",
|
||||
"@itk-wasm/morphological-contour-interpolation": "1.1.0",
|
||||
"@kitware/vtk.js": "35.5.3",
|
||||
|
||||
@ -350,7 +350,10 @@ function _getInstanceNumberFromVolume(
|
||||
const imageId = imageIds[imageIndex];
|
||||
|
||||
if (!imageId) {
|
||||
return {};
|
||||
// No image at this index (e.g. a single-image volume scrolled out of
|
||||
// range). Return undefined so the overlay falls back to the slice count
|
||||
// instead of rendering an empty object as "[object Object]".
|
||||
return;
|
||||
}
|
||||
|
||||
const { instanceNumber } = metaData.get('generalImageModule', imageId) || {};
|
||||
|
||||
@ -1982,8 +1982,15 @@ function commandsModule({
|
||||
* Use it before initializing the toolGroup with the tools.
|
||||
*/
|
||||
initializeSegmentLabelTool: ({ tools }) => {
|
||||
const appConfig = extensionManager.appConfig;
|
||||
const segmentLabelConfig = appConfig.segmentation?.segmentLabel;
|
||||
const { customizationService } = servicesManager.services;
|
||||
const segmentLabelConfig = customizationService.getCustomization(
|
||||
'segmentation.segmentLabel'
|
||||
) as {
|
||||
enabledByDefault?: boolean;
|
||||
labelColor?: number[];
|
||||
hoverTimeout?: number;
|
||||
background?: string;
|
||||
};
|
||||
|
||||
if (segmentLabelConfig?.enabledByDefault) {
|
||||
const activeTools = tools?.active ?? [];
|
||||
|
||||
@ -7,6 +7,7 @@ import {
|
||||
imageRetrievalPoolManager,
|
||||
} from '@cornerstonejs/core';
|
||||
import { Enums as cs3DToolsEnums } from '@cornerstonejs/tools';
|
||||
import { utilities as csMetadataUtilities } from '@cornerstonejs/metadata';
|
||||
import { Types } from '@ohif/core';
|
||||
import Enums from './enums';
|
||||
|
||||
@ -169,6 +170,11 @@ const cornerstoneExtension: Types.Extensions.Extension = {
|
||||
imageRetrievalPoolManager.clearRequestStack(type);
|
||||
});
|
||||
|
||||
// Release the typed metadata registry (NATURALIZED instances registered via
|
||||
// prefetchPart10Instance hold full compressed Part 10 buffers that live
|
||||
// outside the size-capped cornerstone image cache)
|
||||
csMetadataUtilities.clearCacheData();
|
||||
|
||||
cineService.setIsCineEnabled(false);
|
||||
|
||||
enabledElementReset();
|
||||
|
||||
@ -1799,6 +1799,10 @@ describe('SegmentationService', () => {
|
||||
const segmentationId = 'segmentationId';
|
||||
const segmentationData = {
|
||||
segmentationId,
|
||||
representation: {
|
||||
type: csToolsEnums.SegmentationRepresentations.Labelmap,
|
||||
data: {},
|
||||
},
|
||||
config: {
|
||||
label: 'Segmentation 1',
|
||||
},
|
||||
|
||||
@ -302,7 +302,15 @@ class SegmentationService extends PubSubService {
|
||||
}
|
||||
): Promise<void> {
|
||||
const segmentation = this.getSegmentation(segmentationId);
|
||||
if (segmentation && !segmentation.predecessorImageId && predecessorImageId) {
|
||||
|
||||
if (!segmentation) {
|
||||
console.warn(
|
||||
`addSegmentationRepresentation: segmentation "${segmentationId}" is not in state yet`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!segmentation.predecessorImageId && predecessorImageId) {
|
||||
segmentation.predecessorImageId = predecessorImageId;
|
||||
}
|
||||
const csViewport = this.getAndValidateViewport(viewportId);
|
||||
@ -488,10 +496,22 @@ class SegmentationService extends PubSubService {
|
||||
throw new Error('No instances were provided for the referenced display set of the SEG');
|
||||
}
|
||||
|
||||
const imageIds = images.map(image => image.imageId);
|
||||
// Use the same imageIds as SEG parse (_loadSegments stores these on segDisplaySet).
|
||||
const imageIds =
|
||||
segDisplaySet.referencedImageIds ||
|
||||
(referencedDisplaySet.imageIds as string[] | undefined) ||
|
||||
images.map(image => image.imageId);
|
||||
|
||||
if (!imageIds?.length) {
|
||||
throw new Error('referencedDisplaySet has no imageIds for SEG');
|
||||
}
|
||||
const derivedImages = labelMapImages?.flat();
|
||||
const derivedImageIds = derivedImages.map(image => image.imageId);
|
||||
|
||||
// Note: instance runtime props (frameNumber, imageId, url, ...) are
|
||||
// intentionally non-enumerable, so this spread deliberately does NOT copy
|
||||
// them — frameNumber must not be carried onto these derived image entries.
|
||||
// Read such props off the original instance, never off a copy.
|
||||
segDisplaySet.images = derivedImages.map(image => ({
|
||||
...image,
|
||||
...metaData.get('instance', image.referencedImageId),
|
||||
@ -572,11 +592,6 @@ class SegmentationService extends PubSubService {
|
||||
const colorLUTIndex = addColorLUT(colorLUT);
|
||||
this._segmentationIdToColorLUTIndexMap.set(segmentationId, colorLUTIndex);
|
||||
|
||||
this._broadcastEvent(EVENTS.SEGMENTATION_LOADING_COMPLETE, {
|
||||
segmentationId,
|
||||
segDisplaySet,
|
||||
});
|
||||
|
||||
const seg: cstTypes.SegmentationPublicInput = {
|
||||
segmentationId,
|
||||
representation: {
|
||||
@ -596,8 +611,18 @@ class SegmentationService extends PubSubService {
|
||||
|
||||
segDisplaySet.isLoaded = true;
|
||||
|
||||
// Add the segmentation to cornerstone state BEFORE broadcasting that loading is
|
||||
// complete. Subscribers (e.g. CornerstoneViewportService) react synchronously and
|
||||
// call addSegmentationRepresentation, which now early-returns when the segmentation
|
||||
// is not yet in cornerstone state. Broadcasting first would make that guard always
|
||||
// fire on initial load, silently preventing the representation from being attached.
|
||||
this.addOrUpdateSegmentation(seg);
|
||||
|
||||
this._broadcastEvent(EVENTS.SEGMENTATION_LOADING_COMPLETE, {
|
||||
segmentationId,
|
||||
segDisplaySet,
|
||||
});
|
||||
|
||||
return segmentationId;
|
||||
}
|
||||
|
||||
@ -772,9 +797,16 @@ class SegmentationService extends PubSubService {
|
||||
if (existingSegmentation) {
|
||||
// Update the existing segmentation
|
||||
this.updateSegmentationInSource(segmentationId, data as Partial<cstTypes.Segmentation>);
|
||||
} else {
|
||||
} else if (
|
||||
'representation' in data &&
|
||||
(data as cstTypes.SegmentationPublicInput).representation
|
||||
) {
|
||||
// Add a new segmentation
|
||||
this.addSegmentationToSource(data as cstTypes.SegmentationPublicInput);
|
||||
} else {
|
||||
console.warn(
|
||||
`addOrUpdateSegmentation: skipping add for ${segmentationId} — missing representation`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1596,7 +1628,13 @@ class SegmentationService extends PubSubService {
|
||||
|
||||
private determineViewportAndSegmentationType(csViewport, segmentation) {
|
||||
const isVolumeViewport = isVolumeViewportType(csViewport);
|
||||
const isVolumeSegmentation = 'volumeId' in segmentation.representationData[LABELMAP];
|
||||
const labelmapData = segmentation?.representationData?.[LABELMAP];
|
||||
|
||||
if (!labelmapData) {
|
||||
return { isVolumeViewport, isVolumeSegmentation: false };
|
||||
}
|
||||
|
||||
const isVolumeSegmentation = 'volumeId' in labelmapData;
|
||||
return { isVolumeViewport, isVolumeSegmentation };
|
||||
}
|
||||
|
||||
|
||||
@ -1294,24 +1294,122 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
|
||||
? csToolsEnums.SegmentationRepresentations.Labelmap
|
||||
: csToolsEnums.SegmentationRepresentations.Contour;
|
||||
|
||||
const { predecessorImageId } = displaySet;
|
||||
const segmentationRepresentationPromise = segmentationService.addSegmentationRepresentation(
|
||||
viewport.id,
|
||||
{
|
||||
segmentationId,
|
||||
predecessorImageId,
|
||||
type: representationType,
|
||||
config: {
|
||||
blendMode:
|
||||
viewport?.getBlendMode?.() === 1
|
||||
? BlendModes.LABELMAP_EDGE_PROJECTION_BLEND
|
||||
: undefined,
|
||||
},
|
||||
const applyRepresentation = () => {
|
||||
const { predecessorImageId } = displaySet;
|
||||
const segmentationRepresentationPromise =
|
||||
segmentationService.addSegmentationRepresentation(viewport.id, {
|
||||
segmentationId,
|
||||
predecessorImageId,
|
||||
type: representationType,
|
||||
config: {
|
||||
blendMode:
|
||||
viewport?.getBlendMode?.() === 1
|
||||
? BlendModes.LABELMAP_EDGE_PROJECTION_BLEND
|
||||
: undefined,
|
||||
},
|
||||
});
|
||||
this.storePresentation({ viewportId: viewport.id });
|
||||
return segmentationRepresentationPromise;
|
||||
};
|
||||
|
||||
// SEG overlay is registered during stack setup, but cornerstone segmentation state
|
||||
// is created in displaySet.load() (async). Wait until it exists before adding representation.
|
||||
if (displaySet.Modality === 'SEG') {
|
||||
if (segmentationService.getSegmentation(segmentationId)) {
|
||||
return applyRepresentation();
|
||||
}
|
||||
);
|
||||
// store the segmentation presentation id in the viewport info
|
||||
this.storePresentation({ viewportId: viewport.id });
|
||||
return segmentationRepresentationPromise;
|
||||
|
||||
// Bound the wait so a failed/aborted SEG load (where SEGMENTATION_LOADING_COMPLETE
|
||||
// never fires) cannot leave this promise — and the viewport setup awaiting it —
|
||||
// hanging indefinitely.
|
||||
const SEG_LOADING_TIMEOUT_MS = 120000;
|
||||
|
||||
return new Promise<void>(resolve => {
|
||||
let settled = false;
|
||||
let timeoutId;
|
||||
|
||||
// Final resolution — extra calls are harmless (resolve is a no-op after
|
||||
// the first), which lets the timeout stay armed while a representation
|
||||
// apply is in flight and still bound it.
|
||||
const finish = () => {
|
||||
clearTimeout(timeoutId);
|
||||
resolve();
|
||||
};
|
||||
|
||||
// Give up without applying (load failure / timeout before the event).
|
||||
const settleWithoutApply = () => {
|
||||
settled = true;
|
||||
unsubscribe();
|
||||
finish();
|
||||
};
|
||||
|
||||
const { unsubscribe } = segmentationService.subscribe(
|
||||
segmentationService.EVENTS.SEGMENTATION_LOADING_COMPLETE,
|
||||
async (evt: { segDisplaySet?: OhifTypes.DisplaySet }) => {
|
||||
if (settled || evt.segDisplaySet?.displaySetInstanceUID !== segmentationId) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Dedupe immediately, but resolve only after the representation is
|
||||
// applied: awaiters continue into rotation/flip/render and broadcast
|
||||
// VIEWPORT_DATA_CHANGED (hanging-protocol callbacks, toolbar, e2e
|
||||
// readiness), which must not observe a viewport whose overlay
|
||||
// doesn't exist yet. applyRepresentation can be genuinely async
|
||||
// (stack→volume viewport conversion). The timeout is deliberately
|
||||
// NOT cleared here — a hung apply stays bounded.
|
||||
settled = true;
|
||||
unsubscribe();
|
||||
|
||||
try {
|
||||
await applyRepresentation();
|
||||
} catch (error) {
|
||||
console.warn(
|
||||
`Failed to apply segmentation representation for "${segmentationId}":`,
|
||||
error
|
||||
);
|
||||
} finally {
|
||||
finish();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Stop waiting immediately if the SEG load itself fails — otherwise the
|
||||
// loading-complete event never fires and we would idle until the timeout.
|
||||
const loadingPromise = (displaySet as { loadingPromise?: Promise<unknown> })
|
||||
.loadingPromise;
|
||||
loadingPromise?.catch(error => {
|
||||
if (settled) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.warn(
|
||||
`Segmentation "${segmentationId}" failed to load; skipping representation setup.`,
|
||||
error
|
||||
);
|
||||
settleWithoutApply();
|
||||
});
|
||||
|
||||
timeoutId = setTimeout(() => {
|
||||
if (settled) {
|
||||
// The load completed but applyRepresentation is hung — resolve so
|
||||
// viewport setup is never blocked past the bound (best-effort
|
||||
// semantics; the apply may still land later).
|
||||
console.warn(
|
||||
`Timed out applying segmentation representation for "${segmentationId}"; resolving viewport readiness anyway.`
|
||||
);
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
console.warn(
|
||||
`Timed out waiting for segmentation "${segmentationId}" to load; skipping representation setup.`
|
||||
);
|
||||
settleWithoutApply();
|
||||
}, SEG_LOADING_TIMEOUT_MS);
|
||||
});
|
||||
}
|
||||
|
||||
return applyRepresentation();
|
||||
}
|
||||
|
||||
private async _addOverlayRepresentations(
|
||||
|
||||
@ -53,10 +53,14 @@ export function setupSegmentationDataModifiedHandler({
|
||||
});
|
||||
|
||||
if (!isUnsubscribed && updatedSegmentation) {
|
||||
segmentationService.addOrUpdateSegmentation({
|
||||
segmentationId,
|
||||
segments: updatedSegmentation.segments,
|
||||
});
|
||||
const existingSegmentation = segmentationService.getSegmentation(segmentationId);
|
||||
|
||||
if (existingSegmentation) {
|
||||
segmentationService.addOrUpdateSegmentation({
|
||||
segmentationId,
|
||||
segments: updatedSegmentation.segments,
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
1000
|
||||
|
||||
@ -31,7 +31,7 @@
|
||||
"peerDependencies": {
|
||||
"@ohif/core": "workspace:*",
|
||||
"@ohif/i18n": "workspace:*",
|
||||
"dcmjs": "0.49.4",
|
||||
"dcmjs": "0.52.0",
|
||||
"dicomweb-client": "0.10.4",
|
||||
"prop-types": "15.8.1",
|
||||
"react": "18.3.1",
|
||||
@ -48,6 +48,7 @@
|
||||
"react-color": "2.19.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"cross-env": "7.0.3",
|
||||
"webpack-merge": "5.10.0"
|
||||
},
|
||||
"keywords": [
|
||||
|
||||
@ -1,6 +1,11 @@
|
||||
import { DicomMetadataStore, IWebApiDataSource, utils } from '@ohif/core';
|
||||
import OHIF from '@ohif/core';
|
||||
import dcmjs from 'dcmjs';
|
||||
import {
|
||||
datasetToDicomBlob,
|
||||
makeExistingPropertiesNonEnumerable,
|
||||
setNonEnumerableInstanceProperty,
|
||||
} from '../utils/dicomWriter';
|
||||
import { appendFrameQueryToImageId } from '../utils/appendFrameQueryToImageId';
|
||||
|
||||
const metadataProvider = OHIF.classes.MetadataProvider;
|
||||
const { EVENTS } = DicomMetadataStore;
|
||||
@ -152,14 +157,15 @@ function createDicomLocalApi(dicomLocalConfig) {
|
||||
SOPInstanceUID,
|
||||
} = instance;
|
||||
|
||||
instance.imageId = imageId;
|
||||
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,
|
||||
frameIndex: isMultiframe ? index : 1,
|
||||
frameNumber: isMultiframe ? index + 1 : 1,
|
||||
});
|
||||
});
|
||||
|
||||
@ -174,7 +180,7 @@ function createDicomLocalApi(dicomLocalConfig) {
|
||||
},
|
||||
store: {
|
||||
dicom: naturalizedReport => {
|
||||
const reportBlob = dcmjs.data.datasetToBlob(naturalizedReport);
|
||||
const reportBlob = datasetToDicomBlob(naturalizedReport);
|
||||
|
||||
//Create a URL for the binary.
|
||||
var objectUrl = URL.createObjectURL(reportBlob);
|
||||
@ -211,9 +217,6 @@ function createDicomLocalApi(dicomLocalConfig) {
|
||||
getImageIdsForInstance({ instance, frame }) {
|
||||
// Important: Never use instance.imageId because it might be multiframe,
|
||||
// which would make it an invalid imageId.
|
||||
// if (instance.imageId) {
|
||||
// return instance.imageId;
|
||||
// }
|
||||
|
||||
const { StudyInstanceUID, SeriesInstanceUID } = instance;
|
||||
const SOPInstanceUID = instance.SOPInstanceUID || instance.SopInstanceUID;
|
||||
@ -223,13 +226,20 @@ function createDicomLocalApi(dicomLocalConfig) {
|
||||
SOPInstanceUID
|
||||
);
|
||||
|
||||
let imageId = storedInstance.url;
|
||||
const baseImageId = storedInstance?.url || instance.url;
|
||||
|
||||
if (frame !== undefined) {
|
||||
imageId += `&frame=${frame}`;
|
||||
if (!baseImageId) {
|
||||
return;
|
||||
}
|
||||
|
||||
return imageId;
|
||||
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');
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { Types, DicomMetadataStore, utils } from '@ohif/core';
|
||||
import dcmjs from 'dcmjs';
|
||||
import { datasetToDicomBlob, setNonEnumerableInstanceProperty } from './utils/dicomWriter';
|
||||
import { registerNaturalizedDatasetsForLocalWadouri } from './utils/registerNaturalizedDatasetForLocalWadouri';
|
||||
|
||||
const { downloadBlob } = utils;
|
||||
|
||||
@ -784,11 +785,12 @@ const commandsModule = ({
|
||||
if (dataSource === 'download') {
|
||||
return async dicom => {
|
||||
const instances = Array.isArray(dicom) ? dicom : [dicom];
|
||||
registerNaturalizedDatasetsForLocalWadouri(instances);
|
||||
DicomMetadataStore.addInstances(instances, true);
|
||||
if (instances.length !== 1) {
|
||||
throw new Error('Download only supports a single DICOM instance');
|
||||
}
|
||||
const reportBlob = dcmjs.data.datasetToBlob(instances[0]);
|
||||
const reportBlob = datasetToDicomBlob(instances[0]);
|
||||
downloadBlob(reportBlob, { filename: defaultFileName || 'dicom.dcm' });
|
||||
};
|
||||
}
|
||||
@ -796,14 +798,15 @@ const commandsModule = ({
|
||||
if (dataSource === 'copyToClipboard') {
|
||||
return async dicom => {
|
||||
const instances = Array.isArray(dicom) ? dicom : [dicom];
|
||||
registerNaturalizedDatasetsForLocalWadouri(instances);
|
||||
DicomMetadataStore.addInstances(instances, true);
|
||||
if (instances.length !== 1) {
|
||||
throw new Error('Copy to clipboard only supports a single DICOM instance');
|
||||
}
|
||||
const reportBlob = dcmjs.data.datasetToBlob(instances[0]);
|
||||
const reportBlob = datasetToDicomBlob(instances[0]);
|
||||
const type = defaultContentType || 'application/dicom';
|
||||
await navigator.clipboard.write([
|
||||
new ClipboardItem({ [type]: new Blob([reportBlob], { type }) }),
|
||||
new ClipboardItem({ [type]: reportBlob }),
|
||||
]);
|
||||
};
|
||||
}
|
||||
@ -817,12 +820,18 @@ const commandsModule = ({
|
||||
|
||||
return async (dicom, { dicomDict } = {}) => {
|
||||
const instances = Array.isArray(dicom) ? dicom : [dicom];
|
||||
const config = resolvedDataSource.getConfig?.();
|
||||
if (config?.wadoRoot) {
|
||||
instances.forEach(instance => {
|
||||
instance.wadoRoot = config.wadoRoot;
|
||||
});
|
||||
// Always keep an in-memory wadouri copy so DICOM can be read without re-fetching.
|
||||
registerNaturalizedDatasetsForLocalWadouri(instances);
|
||||
|
||||
if (dataSource !== 'dicomlocal') {
|
||||
const config = resolvedDataSource.getConfig?.();
|
||||
if (config?.wadoRoot) {
|
||||
instances.forEach(instance => {
|
||||
setNonEnumerableInstanceProperty(instance, 'wadoRoot', config.wadoRoot);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
DicomMetadataStore.addInstances(instances, true);
|
||||
for (const instance of instances) {
|
||||
await resolvedDataSource.store.dicom(instance, null, dicomDict);
|
||||
|
||||
@ -10,6 +10,18 @@ type DataSource = {
|
||||
placeHolder: string;
|
||||
};
|
||||
|
||||
/** Radix Select item value for "Create new series" (state remains null). */
|
||||
const NEW_SERIES_SELECT_VALUE = '__new_series_id__';
|
||||
|
||||
type SeriesOption = {
|
||||
optionKey: string;
|
||||
selectValue: string;
|
||||
value: string | null;
|
||||
seriesNumber: number;
|
||||
description: string | null;
|
||||
label: string;
|
||||
};
|
||||
|
||||
type ReportDialogProps = {
|
||||
dataSources: DataSource[];
|
||||
modality?: string;
|
||||
@ -46,20 +58,32 @@ function ReportDialog({
|
||||
const [selectedSeries, setSelectedSeries] = useState<string | null>(predecessorImageId || null);
|
||||
const [reportName, setReportName] = useState('');
|
||||
|
||||
const seriesOptions = useMemo(() => {
|
||||
const seriesOptions = useMemo((): SeriesOption[] => {
|
||||
const displaySetsMap = displaySetService.getDisplaySetCache();
|
||||
const displaySets = Array.from(displaySetsMap.values());
|
||||
const options = displaySets
|
||||
.filter(ds => ds.Modality === modality)
|
||||
.map(ds => ({
|
||||
value: ds.predecessorImageId || ds.SeriesInstanceUID,
|
||||
seriesNumber: isFinite(ds.SeriesNumber) ? ds.SeriesNumber : minSeriesNumber,
|
||||
description: ds.SeriesDescription,
|
||||
label: `${ds.SeriesDescription} ${ds.SeriesDate}/${ds.SeriesTime} ${ds.SeriesNumber}`,
|
||||
}));
|
||||
.map(ds => {
|
||||
const value = ds.predecessorImageId || ds.SeriesInstanceUID;
|
||||
const selectValue = value || ds.displaySetInstanceUID;
|
||||
return {
|
||||
optionKey: `series-${ds.displaySetInstanceUID}`,
|
||||
selectValue,
|
||||
value: value || null,
|
||||
seriesNumber: isFinite(ds.SeriesNumber) ? ds.SeriesNumber : minSeriesNumber,
|
||||
description: ds.SeriesDescription,
|
||||
label: `${ds.SeriesDescription} ${ds.SeriesDate}/${ds.SeriesTime} ${ds.SeriesNumber}`,
|
||||
};
|
||||
})
|
||||
.filter(
|
||||
option =>
|
||||
option.selectValue && option.selectValue !== NEW_SERIES_SELECT_VALUE
|
||||
);
|
||||
|
||||
return [
|
||||
{
|
||||
optionKey: NEW_SERIES_SELECT_VALUE,
|
||||
selectValue: NEW_SERIES_SELECT_VALUE,
|
||||
value: null,
|
||||
description: null,
|
||||
seriesNumber: minSeriesNumber,
|
||||
@ -67,7 +91,17 @@ function ReportDialog({
|
||||
},
|
||||
...options,
|
||||
];
|
||||
}, [displaySetService, modality]);
|
||||
}, [displaySetService, modality, minSeriesNumber]);
|
||||
|
||||
const handleSeriesChange = useCallback(
|
||||
(selectValue: string) => {
|
||||
const option = seriesOptions.find(o => o.selectValue === selectValue);
|
||||
setSelectedSeries(
|
||||
selectValue === NEW_SERIES_SELECT_VALUE ? null : (option?.value ?? selectValue)
|
||||
);
|
||||
},
|
||||
[seriesOptions]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const seriesOption = seriesOptions.find(s => s.value === selectedSeries);
|
||||
@ -115,6 +149,11 @@ function ReportDialog({
|
||||
|
||||
const showDataSourceSelect = dataSources?.length > 1;
|
||||
const showDownloadButton = enableDownload;
|
||||
const selectedSeriesSelectValue =
|
||||
selectedSeries == null
|
||||
? NEW_SERIES_SELECT_VALUE
|
||||
: (seriesOptions.find(o => o.value === selectedSeries)?.selectValue ??
|
||||
selectedSeries);
|
||||
|
||||
return (
|
||||
<div className="text-foreground flex min-w-[400px] max-w-md flex-col">
|
||||
@ -146,8 +185,8 @@ function ReportDialog({
|
||||
<div className={showDataSourceSelect ? 'mt-1 w-1/2' : 'mt-1 w-full'}>
|
||||
<div className="mb-1 pl-1 text-base">Series</div>
|
||||
<Select
|
||||
value={selectedSeries}
|
||||
onValueChange={setSelectedSeries}
|
||||
value={selectedSeriesSelectValue}
|
||||
onValueChange={handleSeriesChange}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select a series" />
|
||||
@ -155,8 +194,8 @@ function ReportDialog({
|
||||
<SelectContent>
|
||||
{seriesOptions.map(series => (
|
||||
<SelectItem
|
||||
key={series.value}
|
||||
value={series.value}
|
||||
key={series.optionKey}
|
||||
value={series.selectValue}
|
||||
>
|
||||
{series.label}
|
||||
</SelectItem>
|
||||
@ -172,8 +211,8 @@ function ReportDialog({
|
||||
<div className="w-1/3">
|
||||
<div className="mb-1 pl-1 text-base">Series</div>
|
||||
<Select
|
||||
value={selectedSeries}
|
||||
onValueChange={setSelectedSeries}
|
||||
value={selectedSeriesSelectValue}
|
||||
onValueChange={handleSeriesChange}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select a series" />
|
||||
@ -181,8 +220,8 @@ function ReportDialog({
|
||||
<SelectContent>
|
||||
{seriesOptions.map(series => (
|
||||
<SelectItem
|
||||
key={series.value}
|
||||
value={series.value}
|
||||
key={series.optionKey}
|
||||
value={series.selectValue}
|
||||
>
|
||||
{series.label}
|
||||
</SelectItem>
|
||||
|
||||
@ -27,6 +27,7 @@ import { useHangingProtocolStageIndexStore } from './stores/useHangingProtocolSt
|
||||
import { useToggleHangingProtocolStore } from './stores/useToggleHangingProtocolStore';
|
||||
import { useToggleOneUpViewportGridStore } from './stores/useToggleOneUpViewportGridStore';
|
||||
import { callInputDialogAutoComplete, callInputDialog } from './utils/callInputDialog';
|
||||
import { releaseLocalWadouriRegistrations } from './utils/registerNaturalizedDatasetForLocalWadouri';
|
||||
import colorPickerDialog from './utils/colorPickerDialog';
|
||||
|
||||
import promptSaveReport from './utils/promptSaveReport';
|
||||
@ -52,6 +53,9 @@ const defaultExtension: Types.Extensions.Extension = {
|
||||
useHangingProtocolStageIndexStore.getState().clearHangingProtocolStageIndexMap();
|
||||
useToggleHangingProtocolStore.getState().clearToggleHangingProtocol();
|
||||
useViewportsByPositionStore.getState().clearViewportsByPosition();
|
||||
// Free the Part 10 Blobs (and any parsed copies) retained for locally
|
||||
// generated instances (SEG etc.) registered with the wadouri loader.
|
||||
releaseLocalWadouriRegistrations();
|
||||
},
|
||||
getDataSourcesModule,
|
||||
getViewportModule,
|
||||
|
||||
@ -0,0 +1,9 @@
|
||||
/**
|
||||
* Appends ?frame= or &frame= to a local/wadouri imageId.
|
||||
*/
|
||||
export function appendFrameQueryToImageId(baseImageId, frame) {
|
||||
const withoutFrame = baseImageId.split('&frame=')[0].split('?frame=')[0];
|
||||
const separator = withoutFrame.includes('?') ? '&' : '?';
|
||||
|
||||
return `${withoutFrame}${separator}frame=${frame}`;
|
||||
}
|
||||
228
extensions/default/src/utils/dicomWriter.test.ts
Normal file
228
extensions/default/src/utils/dicomWriter.test.ts
Normal file
@ -0,0 +1,228 @@
|
||||
import { TextEncoder, TextDecoder } from 'util';
|
||||
import dcmjs from 'dcmjs';
|
||||
import {
|
||||
DICOM_WRITE_OPTIONS,
|
||||
datasetToDicomPart10Buffer,
|
||||
writeDicomDictToPart10Buffer,
|
||||
} from './dicomWriter';
|
||||
|
||||
// jsdom does not expose TextEncoder/TextDecoder, which dcmjs's buffer streams need.
|
||||
if (typeof (global as { TextEncoder?: unknown }).TextEncoder === 'undefined') {
|
||||
(global as { TextEncoder?: unknown }).TextEncoder = TextEncoder;
|
||||
}
|
||||
if (typeof (global as { TextDecoder?: unknown }).TextDecoder === 'undefined') {
|
||||
(global as { TextDecoder?: unknown }).TextDecoder = TextDecoder;
|
||||
}
|
||||
|
||||
const RLE_LOSSLESS = '1.2.840.10008.1.2.5';
|
||||
const EXPLICIT_VR_LITTLE_ENDIAN = '1.2.840.10008.1.2.1';
|
||||
const IMPLICIT_VR_LITTLE_ENDIAN = '1.2.840.10008.1.2';
|
||||
const PIXEL_DATA_TAG = '7FE00010';
|
||||
|
||||
// Use frames larger than dcmjs's 20KB fragment size so that, if a frame were
|
||||
// ever split, we'd see more than one fragment per frame.
|
||||
const FRAME_BYTES = 30000;
|
||||
|
||||
function makeDicomDict(transferSyntaxUID, vr, pixelDataValue) {
|
||||
const dict = new dcmjs.data.DicomDict({
|
||||
'00020010': { vr: 'UI', Value: [transferSyntaxUID] },
|
||||
'00020002': { vr: 'UI', Value: ['1.2.840.10008.5.1.4.1.1.66.4'] },
|
||||
'00020003': { vr: 'UI', Value: ['1.2.3.4.5'] },
|
||||
});
|
||||
dict.upsertTag(PIXEL_DATA_TAG, vr, pixelDataValue);
|
||||
return dict;
|
||||
}
|
||||
|
||||
/**
|
||||
* Locates the PixelData element in a Part-10 buffer and, when it is encapsulated,
|
||||
* walks the encapsulated items (Basic Offset Table + fragments).
|
||||
*/
|
||||
function inspectPixelData(arrayBuffer) {
|
||||
const dv = new DataView(arrayBuffer);
|
||||
|
||||
// Skip the 128-byte preamble + "DICM", then read the file-meta group length
|
||||
// from (0002,0000) UL to find where the dataset begins.
|
||||
const metaGroupLength = dv.getUint32(140, true);
|
||||
let offset = 144 + metaGroupLength;
|
||||
|
||||
// PixelData (7FE0,0010), explicit VR (OB/OW) — long form: VR(2) + reserved(2) + length(4).
|
||||
const group = dv.getUint16(offset, true);
|
||||
const element = dv.getUint16(offset + 2, true);
|
||||
if (group !== 0x7fe0 || element !== 0x0010) {
|
||||
throw new Error(
|
||||
`Expected PixelData at offset ${offset}, found ${group.toString(16)},${element.toString(16)}`
|
||||
);
|
||||
}
|
||||
|
||||
const length = dv.getUint32(offset + 8, true);
|
||||
if (length !== 0xffffffff) {
|
||||
return { encapsulated: false, length };
|
||||
}
|
||||
|
||||
// Encapsulated: first item is the Basic Offset Table, then one item per fragment,
|
||||
// terminated by the Sequence Delimitation Item (FFFE,E0DD).
|
||||
let p = offset + 12;
|
||||
const itemSizes: number[] = [];
|
||||
|
||||
while (p + 8 <= dv.byteLength) {
|
||||
const itemGroup = dv.getUint16(p, true);
|
||||
const itemElement = dv.getUint16(p + 2, true);
|
||||
const itemLength = dv.getUint32(p + 4, true);
|
||||
p += 8;
|
||||
|
||||
if (itemGroup === 0xfffe && itemElement === 0xe0dd) {
|
||||
break; // sequence delimiter
|
||||
}
|
||||
if (itemGroup !== 0xfffe || itemElement !== 0xe000) {
|
||||
throw new Error(`Unexpected item tag at ${p - 8}`);
|
||||
}
|
||||
|
||||
itemSizes.push(itemLength);
|
||||
p += itemLength;
|
||||
}
|
||||
|
||||
// itemSizes[0] is the Basic Offset Table; the rest are frame fragments.
|
||||
return {
|
||||
encapsulated: true,
|
||||
totalItems: itemSizes.length,
|
||||
basicOffsetTableSize: itemSizes[0],
|
||||
fragmentSizes: itemSizes.slice(1),
|
||||
};
|
||||
}
|
||||
|
||||
describe('dicomWriter DICOM_WRITE_OPTIONS fragmentation', () => {
|
||||
it('writes exactly one fragment per frame (plus the Basic Offset Table) for compressed pixel data', () => {
|
||||
const frames = [new Uint8Array(FRAME_BYTES).buffer, new Uint8Array(FRAME_BYTES).buffer];
|
||||
const dict = makeDicomDict(RLE_LOSSLESS, 'OB', frames);
|
||||
|
||||
const buffer = writeDicomDictToPart10Buffer(dict);
|
||||
const result = inspectPixelData(buffer);
|
||||
|
||||
expect(result.encapsulated).toBe(true);
|
||||
// 1 Basic Offset Table item + 1 fragment per frame.
|
||||
expect(result.totalItems).toBe(frames.length + 1);
|
||||
expect(result.fragmentSizes).toHaveLength(frames.length);
|
||||
// Each frame stayed in a single fragment despite exceeding the 20KB fragment size.
|
||||
result.fragmentSizes.forEach(size => expect(size).toBe(FRAME_BYTES));
|
||||
});
|
||||
|
||||
it('does not fragment uncompressed pixel data', () => {
|
||||
const pixelData = new Uint16Array(FRAME_BYTES).buffer;
|
||||
const dict = makeDicomDict(EXPLICIT_VR_LITTLE_ENDIAN, 'OW', [pixelData]);
|
||||
|
||||
const buffer = writeDicomDictToPart10Buffer(dict);
|
||||
const result = inspectPixelData(buffer);
|
||||
|
||||
expect(result.encapsulated).toBe(false);
|
||||
expect(result.length).toBe(pixelData.byteLength);
|
||||
});
|
||||
|
||||
it('confirms fragmentMultiframe:false is what prevents a single large frame from splitting', () => {
|
||||
// Control: with fragmentMultiframe enabled, a >20KB frame splits into multiple
|
||||
// fragments — proving DICOM_WRITE_OPTIONS.fragmentMultiframe:false is meaningful.
|
||||
const frames = [new Uint8Array(FRAME_BYTES).buffer];
|
||||
const dict = makeDicomDict(RLE_LOSSLESS, 'OB', frames);
|
||||
|
||||
const buffer = dict.write({ ...DICOM_WRITE_OPTIONS, fragmentMultiframe: true });
|
||||
const result = inspectPixelData(buffer);
|
||||
|
||||
expect(result.encapsulated).toBe(true);
|
||||
expect(result.fragmentSizes.length).toBeGreaterThan(1);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Byte-level walk of the file meta group. Returns the meta element tags in file
|
||||
* order and asserts that FileMetaInformationGroupLength spans them exactly.
|
||||
*/
|
||||
function readFileMetaTags(arrayBuffer: ArrayBuffer) {
|
||||
const dv = new DataView(arrayBuffer);
|
||||
|
||||
// Preamble(128) + 'DICM'(4), then (0002,0000) UL <groupLength> must come first.
|
||||
expect(dv.getUint16(132, true)).toBe(0x0002);
|
||||
expect(dv.getUint16(134, true)).toBe(0x0000);
|
||||
const groupLength = dv.getUint32(140, true);
|
||||
const end = 144 + groupLength;
|
||||
|
||||
const tags: string[] = [];
|
||||
let p = 144;
|
||||
while (p < end) {
|
||||
const group = dv.getUint16(p, true);
|
||||
const element = dv.getUint16(p + 2, true);
|
||||
const vr = String.fromCharCode(dv.getUint8(p + 4), dv.getUint8(p + 5));
|
||||
|
||||
// File meta is always Explicit VR Little Endian.
|
||||
let length: number;
|
||||
if (['OB', 'OW', 'OF', 'SQ', 'UT', 'UN'].includes(vr)) {
|
||||
length = dv.getUint32(p + 8, true);
|
||||
p += 12 + length;
|
||||
} else {
|
||||
length = dv.getUint16(p + 6, true);
|
||||
p += 8 + length;
|
||||
}
|
||||
|
||||
tags.push(
|
||||
`${group.toString(16).padStart(4, '0')}${element.toString(16).padStart(4, '0')}`
|
||||
);
|
||||
}
|
||||
|
||||
// The declared group length covers the meta elements exactly — a garbage
|
||||
// element counted into it (or omitted from it) would break this.
|
||||
expect(p).toBe(end);
|
||||
|
||||
return tags;
|
||||
}
|
||||
|
||||
function makeNaturalizedDataset(meta) {
|
||||
return {
|
||||
_meta: meta,
|
||||
SOPClassUID: '1.2.840.10008.5.1.4.1.1.88.33',
|
||||
SOPInstanceUID: '1.2.3.4.5',
|
||||
StudyInstanceUID: '1.2.3.4',
|
||||
SeriesInstanceUID: '1.2.3.4.1',
|
||||
Modality: 'SR',
|
||||
PatientID: 'TEST-PATIENT',
|
||||
};
|
||||
}
|
||||
|
||||
describe('datasetToDicomPart10Buffer file meta', () => {
|
||||
it('writes only group-0002 elements (no garbage (0000,0000) tag) into the file meta', () => {
|
||||
const dataset = makeNaturalizedDataset({
|
||||
TransferSyntaxUID: { vr: 'UI', Value: [EXPLICIT_VR_LITTLE_ENDIAN] },
|
||||
});
|
||||
|
||||
const buffer = datasetToDicomPart10Buffer(dataset);
|
||||
|
||||
const tags = readFileMetaTags(buffer);
|
||||
expect(tags.length).toBeGreaterThan(0);
|
||||
tags.forEach(tag => expect(tag.startsWith('0002')).toBe(true));
|
||||
// Elements must appear in ascending tag order within group 2.
|
||||
expect(tags).toEqual([...tags].sort());
|
||||
|
||||
const reread = dcmjs.data.DicomMessage.readFile(buffer);
|
||||
const metaKeys = Object.keys(reread.meta);
|
||||
expect(metaKeys).not.toContain('00000000');
|
||||
metaKeys.forEach(key => expect(key.startsWith('0002')).toBe(true));
|
||||
expect(reread.meta['00020010'].Value).toEqual([EXPLICIT_VR_LITTLE_ENDIAN]);
|
||||
});
|
||||
|
||||
it('honors a plain-string _meta.TransferSyntaxUID that dcmjs alone would default away', () => {
|
||||
// dcmjs datasetToDict only reads the object form (_meta.TransferSyntaxUID.Value[0])
|
||||
// and silently falls back to Explicit VR LE for a plain string — the hex-key
|
||||
// assignment in applyTransferSyntaxToFileMeta is what preserves it.
|
||||
const dataset = makeNaturalizedDataset({
|
||||
TransferSyntaxUID: IMPLICIT_VR_LITTLE_ENDIAN,
|
||||
});
|
||||
|
||||
const buffer = datasetToDicomPart10Buffer(dataset);
|
||||
|
||||
const tags = readFileMetaTags(buffer);
|
||||
tags.forEach(tag => expect(tag.startsWith('0002')).toBe(true));
|
||||
|
||||
const reread = dcmjs.data.DicomMessage.readFile(buffer);
|
||||
expect(Object.keys(reread.meta)).not.toContain('00000000');
|
||||
expect(reread.meta['00020010'].Value).toEqual([IMPLICIT_VR_LITTLE_ENDIAN]);
|
||||
// The dataset body survives a round trip under the preserved syntax.
|
||||
expect(reread.dict['00080018'].Value).toEqual(['1.2.3.4.5']);
|
||||
});
|
||||
});
|
||||
110
extensions/default/src/utils/dicomWriter.ts
Normal file
110
extensions/default/src/utils/dicomWriter.ts
Normal file
@ -0,0 +1,110 @@
|
||||
import dcmjs from 'dcmjs';
|
||||
|
||||
export const EXPLICIT_VR_LITTLE_ENDIAN = '1.2.840.10008.1.2.1';
|
||||
|
||||
export const DICOM_WRITE_OPTIONS = {
|
||||
allowInvalidVRLength: false,
|
||||
// `fragmentMultiframe` only governs whether a SINGLE frame is split across
|
||||
// multiple fragments (dcmjs splits frames larger than its 20KB fragment size).
|
||||
// It does NOT merge frames: in an encapsulated (compressed) transfer syntax
|
||||
// every frame is always written as its own fragment, preceded by the Basic
|
||||
// Offset Table; in an uncompressed syntax pixel data is never fragmented at
|
||||
// all. Keeping this `false` therefore yields exactly one fragment per frame
|
||||
// for compressed SEG — the conformant layout — without splitting large frames.
|
||||
fragmentMultiframe: false,
|
||||
};
|
||||
|
||||
/** OHIF runtime fields — not DICOM tags; must not be enumerable for dcmjs datasetToDict. */
|
||||
export const RUNTIME_INSTANCE_PROPERTY_KEYS = [
|
||||
'url',
|
||||
'wadorsuri',
|
||||
'wadouri',
|
||||
'wadoRoot',
|
||||
'wadoUri',
|
||||
'wadoUriRoot',
|
||||
'imageRendering',
|
||||
'imageId',
|
||||
'_parentInstance',
|
||||
'frameNumber',
|
||||
] as const;
|
||||
|
||||
/**
|
||||
* Attaches OHIF runtime data on an instance without enumerable keys (safe for dcmjs datasetToDict).
|
||||
*/
|
||||
export function setNonEnumerableInstanceProperty(
|
||||
instance: Record<string, unknown>,
|
||||
key: string,
|
||||
value: unknown
|
||||
) {
|
||||
Object.defineProperty(instance, key, {
|
||||
value,
|
||||
enumerable: false,
|
||||
writable: true,
|
||||
configurable: true,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-defines any existing enumerable runtime properties as non-enumerable (keeps values).
|
||||
*/
|
||||
export function makeExistingPropertiesNonEnumerable(instance: Record<string, unknown>) {
|
||||
for (const key of RUNTIME_INSTANCE_PROPERTY_KEYS) {
|
||||
if (!Object.prototype.hasOwnProperty.call(instance, key)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const descriptor = Object.getOwnPropertyDescriptor(instance, key);
|
||||
|
||||
if (!descriptor || descriptor.enumerable === false) {
|
||||
continue;
|
||||
}
|
||||
|
||||
setNonEnumerableInstanceProperty(instance, key, descriptor.value);
|
||||
}
|
||||
}
|
||||
|
||||
export function getDatasetTransferSyntaxUID(dataset) {
|
||||
const fromMeta = dataset?._meta?.TransferSyntaxUID;
|
||||
|
||||
if (typeof fromMeta === 'string') {
|
||||
return fromMeta;
|
||||
}
|
||||
|
||||
if (Array.isArray(fromMeta?.Value)) {
|
||||
return fromMeta.Value[0];
|
||||
}
|
||||
|
||||
if (typeof dataset?.TransferSyntaxUID === 'string') {
|
||||
return dataset.TransferSyntaxUID;
|
||||
}
|
||||
|
||||
return EXPLICIT_VR_LITTLE_ENDIAN;
|
||||
}
|
||||
|
||||
function applyTransferSyntaxToFileMeta(dicomDict, transferSyntaxUID) {
|
||||
if (!transferSyntaxUID || !dicomDict?.meta) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Hex key only: DicomMessage.write treats every meta key as a tag string, so a
|
||||
// naturalized key like 'TransferSyntaxUID' would be parsed via parseInt(..., 16)
|
||||
// into tag (0000,0000) and written as a garbage element in group 2.
|
||||
dicomDict.meta['00020010'] = { vr: 'UI', Value: [transferSyntaxUID] };
|
||||
}
|
||||
|
||||
export function datasetToDicomPart10Buffer(dataset) {
|
||||
makeExistingPropertiesNonEnumerable(dataset);
|
||||
const transferSyntaxUID = getDatasetTransferSyntaxUID(dataset);
|
||||
const dicomDict = dcmjs.data.datasetToDict(dataset);
|
||||
applyTransferSyntaxToFileMeta(dicomDict, transferSyntaxUID);
|
||||
return dicomDict.write(DICOM_WRITE_OPTIONS);
|
||||
}
|
||||
|
||||
export function datasetToDicomBlob(dataset) {
|
||||
const part10Buffer = datasetToDicomPart10Buffer(dataset);
|
||||
return new Blob([part10Buffer], { type: 'application/dicom' });
|
||||
}
|
||||
|
||||
export function writeDicomDictToPart10Buffer(dicomDict) {
|
||||
return dicomDict.write(DICOM_WRITE_OPTIONS);
|
||||
}
|
||||
@ -0,0 +1,146 @@
|
||||
import dicomImageLoader from '@cornerstonejs/dicom-image-loader';
|
||||
import OHIF from '@ohif/core';
|
||||
|
||||
import {
|
||||
datasetToDicomBlob,
|
||||
makeExistingPropertiesNonEnumerable,
|
||||
setNonEnumerableInstanceProperty,
|
||||
} from './dicomWriter';
|
||||
import { appendFrameQueryToImageId } from './appendFrameQueryToImageId';
|
||||
|
||||
const metadataProvider = OHIF.classes.MetadataProvider;
|
||||
|
||||
const SEG_REGISTER_LOG_PREFIX = '[SEG register]';
|
||||
|
||||
// Every registration pins a full Part 10 Blob in the fileManager (and, once
|
||||
// loaded, a parsed copy in the dataSetCacheManager) with no other owner, so
|
||||
// track the imageIds and release them on mode exit.
|
||||
const registeredImageIds = new Set();
|
||||
|
||||
/**
|
||||
* Releases everything previously registered via
|
||||
* {@link registerNaturalizedDatasetForLocalWadouri}: the Part 10 Blob held by the
|
||||
* wadouri fileManager and any parsed dataset the dataSetCacheManager retained.
|
||||
* Call on mode exit.
|
||||
*/
|
||||
export function releaseLocalWadouriRegistrations() {
|
||||
const { fileManager, dataSetCacheManager } = dicomImageLoader.wadouri;
|
||||
|
||||
for (const imageId of registeredImageIds) {
|
||||
// 'dicomfile:<index>' — the index doubles as the fileManager slot and the
|
||||
// dataSetCacheManager uri (the scheme is stripped for cache keys).
|
||||
const uri = imageId.substring(imageId.indexOf(':') + 1);
|
||||
const fileIndex = Number(uri);
|
||||
|
||||
if (Number.isInteger(fileIndex) && fileIndex >= 0) {
|
||||
fileManager.remove(fileIndex);
|
||||
}
|
||||
|
||||
// unload() decrements a refcount shared with cornerstone's image decache;
|
||||
// nothing may outlive mode exit, so drive it to zero.
|
||||
for (let guard = 0; guard < 100000 && dataSetCacheManager.isLoaded(uri); guard++) {
|
||||
dataSetCacheManager.unload(uri);
|
||||
}
|
||||
}
|
||||
|
||||
registeredImageIds.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a naturalized DICOM dataset with the wadouri file manager so it can be
|
||||
* loaded like other locally stored instances (blob URL), instead of remote wadors URLs.
|
||||
*
|
||||
* Datasets without pixel data (SR, RTSTRUCT, ...) are skipped: they have no frames
|
||||
* to serve through the image loader, so registering them would only pin their
|
||||
* Part 10 Blob in memory for the rest of the session.
|
||||
*
|
||||
* @param {object} dataset - Naturalized DICOM instance (e.g. generated SEG).
|
||||
* @param {object} [options]
|
||||
* @param {string[]} [options.referencedImageIds] - Used for logging / frame count hints.
|
||||
* @returns {string|undefined} wadouri imageId assigned to dataset.url, or undefined when skipped
|
||||
*/
|
||||
export function registerNaturalizedDatasetForLocalWadouri(dataset, options = {}) {
|
||||
const { referencedImageIds = [] } = options;
|
||||
|
||||
if (
|
||||
dataset.PixelData === undefined &&
|
||||
!Array.isArray(dataset.PerFrameFunctionalGroupsSequence)
|
||||
) {
|
||||
OHIF.log.debug(
|
||||
SEG_REGISTER_LOG_PREFIX,
|
||||
'Skipping local wadouri registration (no pixel data)',
|
||||
{ SOPClassUID: dataset.SOPClassUID, SOPInstanceUID: dataset.SOPInstanceUID }
|
||||
);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const blob = datasetToDicomBlob(dataset);
|
||||
const imageId = dicomImageLoader.wadouri.fileManager.add(blob);
|
||||
registeredImageIds.add(imageId);
|
||||
|
||||
setNonEnumerableInstanceProperty(dataset, 'url', imageId);
|
||||
|
||||
const { StudyInstanceUID, SeriesInstanceUID } = dataset;
|
||||
const SOPInstanceUID = dataset.SOPInstanceUID || dataset.SopInstanceUID;
|
||||
|
||||
const perFrameGroups = dataset.PerFrameFunctionalGroupsSequence;
|
||||
if (Array.isArray(perFrameGroups) && !dataset.NumberOfFrames) {
|
||||
dataset.NumberOfFrames = perFrameGroups.length;
|
||||
}
|
||||
|
||||
// Local only — never written back to the dataset: single-frame IODs (SR,
|
||||
// RTSTRUCT) must not gain a NumberOfFrames element in their serialized form.
|
||||
const numberOfFrames = Math.max(
|
||||
Number(dataset.NumberOfFrames) || 0,
|
||||
Array.isArray(perFrameGroups) ? perFrameGroups.length : 0,
|
||||
1
|
||||
);
|
||||
|
||||
const frameImageIds = [];
|
||||
if (StudyInstanceUID && SeriesInstanceUID && SOPInstanceUID) {
|
||||
const registerMapping = (id, frameNumber = 1) => {
|
||||
metadataProvider.addImageIdToUIDs(id, {
|
||||
StudyInstanceUID,
|
||||
SeriesInstanceUID,
|
||||
SOPInstanceUID,
|
||||
frameNumber,
|
||||
});
|
||||
};
|
||||
|
||||
for (let frame = 1; frame <= numberOfFrames; frame++) {
|
||||
const frameImageId =
|
||||
numberOfFrames > 1 ? appendFrameQueryToImageId(imageId, frame) : imageId;
|
||||
|
||||
frameImageIds.push(frameImageId);
|
||||
registerMapping(frameImageId, frame);
|
||||
}
|
||||
}
|
||||
|
||||
makeExistingPropertiesNonEnumerable(dataset);
|
||||
|
||||
OHIF.log.debug(SEG_REGISTER_LOG_PREFIX, 'Registered local wadouri instance', {
|
||||
StudyInstanceUID,
|
||||
SeriesInstanceUID,
|
||||
SOPInstanceUID,
|
||||
numberOfFrames,
|
||||
baseImageId: imageId,
|
||||
frameImageIds,
|
||||
blobSizeBytes: blob.size,
|
||||
referencedImageIdCount: referencedImageIds.length,
|
||||
SegmentationType: dataset.SegmentationType,
|
||||
SOPClassUID: dataset.SOPClassUID,
|
||||
});
|
||||
|
||||
return imageId;
|
||||
}
|
||||
|
||||
export function registerNaturalizedDatasetsForLocalWadouri(instances, options = {}) {
|
||||
const list = Array.isArray(instances) ? instances : [instances];
|
||||
const { referencedImageIds = [] } = options;
|
||||
|
||||
list.forEach(instance =>
|
||||
registerNaturalizedDatasetForLocalWadouri(instance, { referencedImageIds })
|
||||
);
|
||||
|
||||
return list;
|
||||
}
|
||||
@ -0,0 +1,177 @@
|
||||
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();
|
||||
});
|
||||
});
|
||||
@ -30,7 +30,7 @@
|
||||
"peerDependencies": {
|
||||
"@ohif/core": "workspace:*",
|
||||
"@ohif/ui": "workspace:*",
|
||||
"dcmjs": "0.49.4",
|
||||
"dcmjs": "0.52.0",
|
||||
"dicom-parser": "1.8.21",
|
||||
"hammerjs": "2.0.8",
|
||||
"prop-types": "15.8.1",
|
||||
|
||||
@ -30,7 +30,7 @@
|
||||
"peerDependencies": {
|
||||
"@ohif/core": "workspace:*",
|
||||
"@ohif/ui": "workspace:*",
|
||||
"dcmjs": "0.49.4",
|
||||
"dcmjs": "0.52.0",
|
||||
"dicom-parser": "1.8.21",
|
||||
"hammerjs": "2.0.8",
|
||||
"prop-types": "15.8.1",
|
||||
|
||||
@ -27,14 +27,14 @@
|
||||
"start": "pnpm run dev"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@cornerstonejs/core": "5.1.3",
|
||||
"@cornerstonejs/tools": "5.1.3",
|
||||
"@cornerstonejs/core": "5.4.12",
|
||||
"@cornerstonejs/tools": "5.4.12",
|
||||
"@ohif/core": "workspace:*",
|
||||
"@ohif/extension-cornerstone-dicom-sr": "workspace:*",
|
||||
"@ohif/extension-default": "workspace:*",
|
||||
"@ohif/ui": "workspace:*",
|
||||
"classnames": "2.5.1",
|
||||
"dcmjs": "0.49.4",
|
||||
"dcmjs": "0.52.0",
|
||||
"lodash.debounce": "4.0.8",
|
||||
"prop-types": "15.8.1",
|
||||
"react": "18.3.1",
|
||||
@ -48,6 +48,7 @@
|
||||
"xstate": "4.38.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"cross-env": "7.0.3",
|
||||
"webpack-merge": "5.10.0"
|
||||
},
|
||||
"keywords": [
|
||||
|
||||
@ -30,7 +30,7 @@
|
||||
"peerDependencies": {
|
||||
"@ohif/core": "workspace:*",
|
||||
"@ohif/ui": "workspace:*",
|
||||
"dcmjs": "0.49.4",
|
||||
"dcmjs": "0.52.0",
|
||||
"dicom-parser": "1.8.21",
|
||||
"hammerjs": "2.0.8",
|
||||
"prop-types": "15.8.1",
|
||||
|
||||
@ -30,7 +30,7 @@
|
||||
"peerDependencies": {
|
||||
"@ohif/core": "workspace:*",
|
||||
"@ohif/ui": "workspace:*",
|
||||
"dcmjs": "0.49.4",
|
||||
"dcmjs": "0.52.0",
|
||||
"dicom-parser": "1.8.21",
|
||||
"hammerjs": "2.0.8",
|
||||
"prop-types": "15.8.1",
|
||||
|
||||
@ -36,8 +36,8 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "7.29.7",
|
||||
"@cornerstonejs/core": "5.1.3",
|
||||
"@cornerstonejs/tools": "5.1.3",
|
||||
"@cornerstonejs/core": "5.4.12",
|
||||
"@cornerstonejs/tools": "5.4.12",
|
||||
"@ohif/core": "workspace:*",
|
||||
"@ohif/extension-cornerstone": "workspace:*",
|
||||
"@ohif/extension-default": "workspace:*",
|
||||
|
||||
@ -41,6 +41,7 @@
|
||||
"i18next": "17.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"cross-env": "7.0.3",
|
||||
"webpack-merge": "5.10.0"
|
||||
}
|
||||
}
|
||||
|
||||
@ -43,6 +43,7 @@
|
||||
"i18next": "17.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"cross-env": "7.0.3",
|
||||
"webpack-merge": "5.10.0"
|
||||
},
|
||||
"keywords": [
|
||||
|
||||
@ -38,6 +38,7 @@
|
||||
"@babel/runtime": "7.29.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"cross-env": "7.0.3",
|
||||
"webpack-merge": "5.10.0"
|
||||
},
|
||||
"keywords": [
|
||||
|
||||
@ -45,6 +45,7 @@
|
||||
"i18next": "17.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"cross-env": "7.0.3",
|
||||
"webpack-merge": "5.10.0"
|
||||
},
|
||||
"keywords": [
|
||||
|
||||
@ -42,6 +42,7 @@
|
||||
"i18next": "17.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"cross-env": "7.0.3",
|
||||
"webpack-merge": "5.10.0"
|
||||
},
|
||||
"keywords": [
|
||||
|
||||
@ -29,8 +29,8 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "7.29.7",
|
||||
"@cornerstonejs/core": "5.1.3",
|
||||
"@cornerstonejs/tools": "5.1.3",
|
||||
"@cornerstonejs/core": "5.4.12",
|
||||
"@cornerstonejs/tools": "5.4.12",
|
||||
"@ohif/core": "workspace:*",
|
||||
"@ohif/extension-cornerstone-dicom-sr": "workspace:*",
|
||||
"@ohif/extension-ultrasound-pleura-bline": "workspace:*",
|
||||
|
||||
@ -7,5 +7,5 @@
|
||||
#
|
||||
|
||||
PUBLIC_URL=/
|
||||
APP_CONFIG=config/default.js
|
||||
# Can choose a default app config, but this over-rides dev scripts: APP_CONFIG=config/default.js
|
||||
USE_HASH_ROUTER=false
|
||||
|
||||
@ -51,7 +51,7 @@
|
||||
"@cornerstonejs/codec-libjpeg-turbo-8bit": "1.2.2",
|
||||
"@cornerstonejs/codec-openjpeg": "1.3.0",
|
||||
"@cornerstonejs/codec-openjph": "2.4.7",
|
||||
"@cornerstonejs/dicom-image-loader": "5.1.3",
|
||||
"@cornerstonejs/dicom-image-loader": "5.4.12",
|
||||
"@emotion/serialize": "1.3.3",
|
||||
"@ohif/core": "workspace:*",
|
||||
"@ohif/i18n": "workspace:*",
|
||||
@ -62,7 +62,7 @@
|
||||
"classnames": "2.5.1",
|
||||
"core-js": "3.45.1",
|
||||
"cornerstone-math": "0.1.10",
|
||||
"dcmjs": "0.49.4",
|
||||
"dcmjs": "0.52.0",
|
||||
"detect-gpu": "4.0.50",
|
||||
"dicom-parser": "1.8.21",
|
||||
"file-loader": "6.2.0",
|
||||
|
||||
@ -37,7 +37,6 @@ window.config = {
|
||||
// '/remote/': 'https://cdn.example.com/ohif-custom/', // ?customization=/remote/siteA
|
||||
// },
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
showStudyList: true,
|
||||
// some windows systems have issues with more than 3 web workers
|
||||
maxNumberOfWebWorkers: 3,
|
||||
@ -80,6 +79,11 @@ window.config = {
|
||||
supportsFuzzyMatching: true,
|
||||
supportsWildcard: true,
|
||||
staticWado: true,
|
||||
// Multiframe SEG loads fetch the whole instance as a single Part 10
|
||||
// object by default and wait for it: the per-frame endpoint is
|
||||
// efficient, but SEG frames are so small and numerous that one bulk
|
||||
// fetch beats hundreds of tiny requests. Per-frame loading is the
|
||||
// exception — set loadMultiframeAsPart10: false here to force it.
|
||||
singlepart: 'bulkdata,video',
|
||||
bulkDataURI: {
|
||||
enabled: true,
|
||||
|
||||
19
platform/app/public/customizations/segmentation/binary.jsonc
Normal file
19
platform/app/public/customizations/segmentation/binary.jsonc
Normal file
@ -0,0 +1,19 @@
|
||||
// URL-loaded customization: segmentation/binary
|
||||
//
|
||||
// Stores exported/saved DICOM SEG objects as a BINARY Segmentation — SOP Class
|
||||
// 1.2.840.10008.5.1.4.1.1.66.4 (one frame per segment) — instead of the OHIF
|
||||
// default Label Map Segmentation 1.2.840.10008.5.1.4.1.1.66.7. Binary SEG is
|
||||
// broadly compatible with existing PACS/viewers; Label Map was only added to
|
||||
// DICOM in 2024 and many back ends reject it.
|
||||
//
|
||||
// Applied in the `global` phase so it overrides the default registered by the
|
||||
// cornerstone-dicom-seg extension. A data source may still override this per
|
||||
// back end via `configuration.segmentation.store.defaultMode`.
|
||||
//
|
||||
// Load it with `?customization=segmentation/binary` (combine with
|
||||
// `&customization=segmentation/uncompressed` to also drop RLE compression).
|
||||
{
|
||||
"global": {
|
||||
"segmentation.store.defaultMode": { "$set": "bitmap" }
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,19 @@
|
||||
// URL-loaded customization: segmentation/uncompressed
|
||||
//
|
||||
// Stores exported/saved DICOM SEG objects UNCOMPRESSED — Explicit VR Little
|
||||
// Endian (1.2.840.10008.1.2.1) — instead of the OHIF default of RLE Lossless
|
||||
// (1.2.840.10008.1.2.5). Use this for back ends that reject compressed SEG
|
||||
// PixelData.
|
||||
//
|
||||
// Applied in the `global` phase. OHIF already defaults to RLE Lossless; this
|
||||
// customization opts into uncompressed Explicit VR Little Endian instead. A data
|
||||
// source may still override per back end via
|
||||
// `configuration.segmentation.store.transferSyntaxUID`.
|
||||
//
|
||||
// Load it with `?customization=segmentation/uncompressed` (combine with
|
||||
// `&customization=segmentation/binary` to also switch the SOP class).
|
||||
{
|
||||
"global": {
|
||||
"segmentation.store.transferSyntaxUID": { "$set": "1.2.840.10008.1.2.1" }
|
||||
}
|
||||
}
|
||||
@ -35,16 +35,16 @@
|
||||
"@cornerstonejs/codec-libjpeg-turbo-8bit": "1.2.2",
|
||||
"@cornerstonejs/codec-openjpeg": "1.3.0",
|
||||
"@cornerstonejs/codec-openjph": "2.4.7",
|
||||
"@cornerstonejs/core": "5.1.3",
|
||||
"@cornerstonejs/dicom-image-loader": "5.1.3",
|
||||
"@cornerstonejs/metadata": "5.1.3",
|
||||
"@cornerstonejs/core": "5.4.12",
|
||||
"@cornerstonejs/dicom-image-loader": "5.4.12",
|
||||
"@cornerstonejs/metadata": "5.4.12",
|
||||
"@ohif/ui": "workspace:*",
|
||||
"cornerstone-math": "0.1.10",
|
||||
"dicom-parser": "1.8.21"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "7.29.7",
|
||||
"dcmjs": "0.49.4",
|
||||
"dcmjs": "0.52.0",
|
||||
"dicomweb-client": "0.10.4",
|
||||
"gl-matrix": "3.4.3",
|
||||
"immutability-helper": "3.1.1",
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import queryString from 'query-string';
|
||||
import dicomParser from 'dicom-parser';
|
||||
import { utilities } from '@cornerstonejs/core';
|
||||
import { imageIdToURI } from '../utils';
|
||||
import { baseImageURIForMetadata } from '../utils/imageIdToURI';
|
||||
import DicomMetadataStore from '../services/DicomMetadataStore';
|
||||
import fetchPaletteColorLookupTableData from '../utils/metadataProvider/fetchPaletteColorLookupTableData';
|
||||
import toNumber from '../utils/toNumber';
|
||||
@ -24,12 +24,12 @@ class MetadataProvider {
|
||||
// This method is a fallback for when you don't have WADO-URI or WADO-RS.
|
||||
// You can add instances fetched by any method by calling addInstance, and hook an imageId to point at it here.
|
||||
// An example would be dicom hosted at some random site.
|
||||
const imageURI = imageIdToURI(imageId);
|
||||
const imageURI = baseImageURIForMetadata(imageId);
|
||||
this.imageURIToUIDs.set(imageURI, uids);
|
||||
}
|
||||
|
||||
addCustomMetadata(imageId, type, metadata) {
|
||||
const imageURI = imageIdToURI(imageId);
|
||||
const imageURI = baseImageURIForMetadata(imageId);
|
||||
if (!this.customMetadata.has(type)) {
|
||||
this.customMetadata.set(type, {});
|
||||
}
|
||||
@ -76,7 +76,7 @@ class MetadataProvider {
|
||||
// check inside custom metadata
|
||||
if (this.customMetadata.has(query)) {
|
||||
const customMetadata = this.customMetadata.get(query);
|
||||
const imageURI = imageIdToURI(imageId);
|
||||
const imageURI = baseImageURIForMetadata(imageId);
|
||||
if (customMetadata[imageURI]) {
|
||||
return customMetadata[imageURI];
|
||||
}
|
||||
@ -445,6 +445,9 @@ class MetadataProvider {
|
||||
if (imageId.includes('/frames')) {
|
||||
return getInformationFromURL('/frames', '/');
|
||||
}
|
||||
if (imageId.includes('?frame=')) {
|
||||
return getInformationFromURL('?frame=', '&');
|
||||
}
|
||||
if (imageId.includes('&frame=')) {
|
||||
return getInformationFromURL('&frame=', '&');
|
||||
}
|
||||
@ -473,20 +476,7 @@ class MetadataProvider {
|
||||
};
|
||||
}
|
||||
|
||||
// Maybe its a non-standard imageId
|
||||
// check if the imageId starts with http:// or https:// using regex
|
||||
// Todo: handle non http imageIds
|
||||
let imageURI;
|
||||
const urlRegex = /^(http|https|dicomfile):\/\//;
|
||||
if (urlRegex.test(imageId)) {
|
||||
imageURI = imageId;
|
||||
} else {
|
||||
imageURI = imageIdToURI(imageId);
|
||||
}
|
||||
|
||||
// remove &frame=number from imageId
|
||||
imageURI = imageURI.split('&frame=')[0];
|
||||
|
||||
const imageURI = baseImageURIForMetadata(imageId);
|
||||
const uids = this.imageURIToUIDs.get(imageURI);
|
||||
const frameNumber = this.getFrameInformationFromURL(imageId) || '1';
|
||||
|
||||
|
||||
@ -10,6 +10,12 @@ import { dicomSplit } from './dicomSplit';
|
||||
* will be ignored.
|
||||
* This can be safely called with an undefined frame in order to handle
|
||||
* single frame data. (eg frame is undefined is the same as frame===1).
|
||||
*
|
||||
* Note: instances carry non-enumerable runtime props (frameNumber, imageId,
|
||||
* url, wadoRoot, ...). This is intentional: dcmjs serialization skips them and
|
||||
* spreads/copies (including of anything from `metaData.get('instance', ...)`)
|
||||
* deliberately do not carry them — frameNumber in particular must not be
|
||||
* copied onto other objects. Read runtime props off the original instance.
|
||||
*/
|
||||
const combineFrameInstance = (frame, instance) => {
|
||||
const {
|
||||
@ -83,6 +89,9 @@ const combineFrameInstance = (frame, instance) => {
|
||||
if (!instance._parentInstance) {
|
||||
Object.defineProperty(instance, '_parentInstance', {
|
||||
value: { ...instance },
|
||||
enumerable: false,
|
||||
writable: false,
|
||||
configurable: false,
|
||||
});
|
||||
}
|
||||
const sharedInstance = createCombinedValue(
|
||||
@ -102,7 +111,7 @@ const combineFrameInstance = (frame, instance) => {
|
||||
Object.defineProperty(newInstance, 'frameNumber', {
|
||||
value: frameNumber,
|
||||
writable: true,
|
||||
enumerable: true,
|
||||
enumerable: false,
|
||||
configurable: true,
|
||||
});
|
||||
return newInstance;
|
||||
@ -113,6 +122,9 @@ const combineFrameInstance = (frame, instance) => {
|
||||
if (!instance._parentInstance) {
|
||||
Object.defineProperty(instance, '_parentInstance', {
|
||||
value: { ...instance },
|
||||
enumerable: false,
|
||||
writable: false,
|
||||
configurable: false,
|
||||
});
|
||||
}
|
||||
|
||||
@ -144,7 +156,7 @@ const combineFrameInstance = (frame, instance) => {
|
||||
Object.defineProperty(newInstance, 'frameNumber', {
|
||||
value: frameNumber,
|
||||
writable: true,
|
||||
enumerable: true,
|
||||
enumerable: false,
|
||||
configurable: true,
|
||||
});
|
||||
|
||||
|
||||
@ -10,3 +10,20 @@ export default function imageIdToURI(imageId) {
|
||||
|
||||
return imageId.substring(colonIndex + 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes an imageId to the metadata lookup key (scheme stripped, frame query removed).
|
||||
* Must match MetadataProvider.getUIDsFromImageID lookup behavior.
|
||||
*/
|
||||
export function baseImageURIForMetadata(imageId) {
|
||||
const urlRegex = /^(http|https|dicomfile):\/\//;
|
||||
let imageURI;
|
||||
|
||||
if (urlRegex.test(imageId)) {
|
||||
imageURI = imageId;
|
||||
} else {
|
||||
imageURI = imageIdToURI(imageId);
|
||||
}
|
||||
|
||||
return imageURI.split('&frame=')[0].split('?frame=')[0];
|
||||
}
|
||||
|
||||
44
platform/docs/docs/behaviours/README.md
Normal file
44
platform/docs/docs/behaviours/README.md
Normal file
@ -0,0 +1,44 @@
|
||||
---
|
||||
id: README
|
||||
title: Behaviours
|
||||
sidebar_position: 0
|
||||
---
|
||||
|
||||
# Behaviours
|
||||
|
||||
This section documents **how the system and UI actually behave** — the
|
||||
end-to-end behaviours that emerge from the interaction of services, extensions,
|
||||
the data source, and Cornerstone3D, rather than the API of any single module.
|
||||
|
||||
It is the place for:
|
||||
|
||||
- **Observed behaviours** — how a feature works today across the stack (e.g. how
|
||||
a DICOM SEG is fetched, decoded, and rendered; what the viewport does on study
|
||||
change; how measurements round-trip to SR).
|
||||
- **Design proposals** — proposed or in-progress changes to a behaviour, captured
|
||||
before/while they are implemented so the intent and trade-offs are recorded.
|
||||
These are clearly marked as proposals until they land.
|
||||
- **Edge cases and failure modes** — what happens when something is missing,
|
||||
slow, or malformed, and how the system is expected to degrade.
|
||||
|
||||
The goal is a durable, discoverable record of *behaviour* — the kind of
|
||||
cross-cutting knowledge that is otherwise only in people's heads or scattered
|
||||
across code comments. Prefer linking to the relevant source files (with line
|
||||
references) so each behaviour doc stays anchored to the code it describes.
|
||||
|
||||
## Index
|
||||
|
||||
- [Segmentation: loading a multiframe SEG as a single Part 10 instance](./segmentation-multiframe-part10-prefetch.md)
|
||||
— _implemented, enabled by default_. Prefetch the whole instance in one request
|
||||
and register it into the Cornerstone3D NATURALIZED frame registry so the
|
||||
per-frame load path (WADO-RS and WADO-URI) is served locally, while keeping the
|
||||
standard decode path unchanged. Per-frame loading is the exception — disable
|
||||
via `loadMultiframeAsPart10: false` (data source config or the
|
||||
`cornerstone.segmentation.loadMultiframeAsPart10` customization).
|
||||
|
||||
## Writing a new behaviour doc
|
||||
|
||||
1. Add a `kebab-case.md` file in this directory.
|
||||
2. State whether it documents **current behaviour** or is a **proposal**.
|
||||
3. Link to the code (`path:line`) that implements or will implement it.
|
||||
4. Add it to the **Index** above.
|
||||
8
platform/docs/docs/behaviours/_category_.json
Normal file
8
platform/docs/docs/behaviours/_category_.json
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"label": "Behaviours",
|
||||
"position": 14,
|
||||
"link": {
|
||||
"type": "doc",
|
||||
"id": "behaviours/README"
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,368 @@
|
||||
# Full-instance prefetch for segmentation (and multiframe) loading
|
||||
|
||||
Status: **Implemented — enabled and awaited to completion by default;
|
||||
per-frame loading is the explicit opt-out**
|
||||
|
||||
The boolean `loadMultiframeAsPart10` resolves, in order: the data source
|
||||
`configuration`, the global customization
|
||||
`cornerstone.segmentation.loadMultiframeAsPart10`, then the built-in default of
|
||||
`true` — i.e. by default the SEG load **waits for the whole-instance
|
||||
fetch+parse to complete or fail** (deliberately no timeout) and serves every
|
||||
frame from the registry. Set `loadMultiframeAsPart10: false` explicitly to
|
||||
force per-frame loading — the exception, for back ends that need to fetch the
|
||||
individual images instead (e.g. servers that cannot serve a whole-instance
|
||||
retrieve, or deployments where holding the full Part 10 object in memory is
|
||||
undesirable). A failed or unsupported instance fetch resolves quickly and
|
||||
falls back to per-frame regardless of the setting, so it never wedges the load.
|
||||
|
||||
Note the per-frame endpoint itself is not inefficient — each frame request is
|
||||
cheap — but SEG frames are so small and numerous that one bulk Part 10 fetch
|
||||
beats hundreds of tiny requests (see Problem below). This holds even for very
|
||||
large SEG objects (hundreds of MB): any finite race cap would simply expire on
|
||||
those and storm per-frame anyway while abandoning the bulk fetch's benefit,
|
||||
which is why there is no timeout.
|
||||
|
||||
Implemented across:
|
||||
|
||||
- `@cornerstonejs/dicom-image-loader`
|
||||
- `imageLoader/prefetchPart10Instance.ts` — registers a Part 10 instance into
|
||||
the frame registry (thin wrapper over `addDicomPart10Instance`).
|
||||
- `imageLoader/wadors/loadImageFromRegistry.ts` +
|
||||
`wadors/loadImage.ts` — WADO-RS loads now consult the registry first.
|
||||
- `@ohif/extension-default` `DicomWebDataSource` — `retrieve.prefetchInstanceFrames`.
|
||||
- `@ohif/extension-cornerstone-dicom-seg` `getSopClassHandlerModule.ts` — call
|
||||
site (resolves the config and awaits the prefetch).
|
||||
|
||||
Related: `@cornerstonejs/adapters` `labelmapImagesFromBuffer.ts`
|
||||
(`decodeSegPixelDataFromFrameIds`, bounded by `concurrency`, default 16).
|
||||
|
||||
## Problem
|
||||
|
||||
The SEG load path now fetches/decodes frames through the cornerstone image
|
||||
loader, one loadable `imageId` per frame, parallelized up to `N=16`
|
||||
(`concurrency`). For a SEG (or any multiframe instance) with **800+ small
|
||||
frames**, this means 800+ independent HTTP requests, each with its own
|
||||
request/response overhead. Even at 16-wide concurrency the *per-request* latency
|
||||
floor dominates: each frame is < 1 KB of payload but pays a full round trip.
|
||||
|
||||
Streaming **one large object** (the entire Part 10 / multiframe instance) is far
|
||||
cheaper than streaming hundreds of tiny ones — a single connection, a single set
|
||||
of headers, no per-frame TTFB. The whole instance for an 800-frame binary SEG is
|
||||
typically a few hundred KB to a few MB.
|
||||
|
||||
## Goal
|
||||
|
||||
Add a **generic data-source capability** that, _just before_ the segmentation
|
||||
loader runs, optionally fetches the **entire original instance** in one request
|
||||
and **registers it into the Cornerstone3D frame registry** (the
|
||||
`@cornerstonejs/metadata` NATURALIZED + `COMPRESSED_FRAME_DATA` framework, parsed
|
||||
by the dcmjs async reader) — so the existing per-frame `imageId` fetch path
|
||||
transparently hits local data instead of the network, while the **cornerstone
|
||||
decode path stays byte-for-byte identical** (same decompressor, same workers).
|
||||
|
||||
Crucially this is **best-effort**:
|
||||
|
||||
- By default (`loadMultiframeAsPart10` unset → `true`) the load **waits for the
|
||||
full-instance fetch+parse to complete or fail** — no timeout — then every
|
||||
frame is served from the registry.
|
||||
- If `loadMultiframeAsPart10` resolves to `false` (explicitly configured), the
|
||||
capability is **disabled** — no full-instance fetch is attempted. Per-frame
|
||||
loading is the exception, opted into per deployment.
|
||||
- If the full-instance fetch or parse **fails for any reason**, it must **never**
|
||||
fail the segmentation decode. We log and fall back to per-frame fetches.
|
||||
|
||||
## Why this is safe / transparent
|
||||
|
||||
There is **one uniform frame registry: the Cornerstone3D `@cornerstonejs/metadata`
|
||||
NATURALIZED framework**, populated by `addDicomPart10Instance` (which parses the
|
||||
Part 10 with the dcmjs `AsyncDicomReader`) and read per-frame via the
|
||||
`COMPRESSED_FRAME_DATA` typed provider. Frame imageIds are normalized to the base
|
||||
instance by `baseImageIdQueryFilter` (strips `/frames/N`, `?frame=N`, `&frame=N`),
|
||||
so one registration under the instance serves every frame.
|
||||
|
||||
This is the **same registry the WADO-URI / `dicomweb` loader already uses**:
|
||||
`wadouri/loadImage.ts`'s `loadImageFromNaturalizedMetadata` resolves each frame
|
||||
from `COMPRESSED_FRAME_DATA` and decodes it with `createImage`. The gap was that
|
||||
**WADO-RS** (`wadors/loadImage.ts`) always issued a per-frame `/frames/N` request
|
||||
and never consulted the registry. The adapter closes that gap:
|
||||
|
||||
- `wadors/loadImageFromRegistry.ts` — `loadImageFromCompressedFrameRegistry(imageId)`
|
||||
looks up `COMPRESSED_FRAME_DATA` for the frame; if present it decodes via the
|
||||
**same `createImage`** path and returns the image; if absent it returns
|
||||
`undefined`.
|
||||
- `wadors/loadImage.ts` calls it first and short-circuits when the registry has
|
||||
the frame, otherwise falls through to the existing network path.
|
||||
|
||||
So once `prefetchPart10Instance` has registered the instance, **both WADO-URI and
|
||||
WADO-RS** serve every frame from the single registry, with the **same decode
|
||||
pipeline** (`createImage` → worker decoder). From cornerstone's perspective
|
||||
nothing changed except the compressed bytes came from the registry instead of the
|
||||
wire.
|
||||
|
||||
> Rejected alternatives: (1) a bespoke `Map<imageId, frame>` registry plus
|
||||
> `getPixelData` shims — duplicates a registry that already exists; (2) seeding
|
||||
> the core image cache (`cache.putImageLoadObject`) per frame — works, but the
|
||||
> per-frame compressed data already lives in the NATURALIZED registry, so basing
|
||||
> the design on that registry (and teaching WADO-RS to read it) keeps a single
|
||||
> source of truth instead of copying frames into a second cache.
|
||||
|
||||
## API (as implemented)
|
||||
|
||||
A generic capability on the data source's `retrieve` namespace (so it works for
|
||||
any multiframe instance, not just SEG, and alternate data sources can override
|
||||
it), plus the SEG handler call site that awaits it.
|
||||
|
||||
`IWebApiDataSource.create` passes `retrieve` through verbatim, so the capability
|
||||
is added there (no `@ohif/core` change):
|
||||
|
||||
```ts
|
||||
// On the data source (DicomWebDataSource):
|
||||
dataSource.retrieve.prefetchInstanceFrames({
|
||||
instance, // study/series/sop UIDs for retrieveInstance
|
||||
imageId, // SEG instance imageId (frame qualifiers normalized away)
|
||||
}): {
|
||||
done: Promise<boolean>; // resolves true if fetched+registered, false if skipped/failed
|
||||
cancel: () => void;
|
||||
};
|
||||
```
|
||||
|
||||
The implementation fetches the instance with `wadoDicomWebClient.retrieveInstance`
|
||||
(which returns the Part 10 as an `ArrayBuffer`, unwrapping `multipart/related`
|
||||
transparently) and passes a **lazy resolver** to
|
||||
`dicomImageLoader.prefetchPart10Instance(imageId, resolver)`. All work is wrapped
|
||||
so any failure resolves `done` to `false` — it never throws into the loader.
|
||||
|
||||
### Call site (just before the loader)
|
||||
|
||||
In `getSopClassHandlerModule.ts`, immediately before
|
||||
`createFromDicomSegImageId(...)` (abridged from the actual code):
|
||||
|
||||
```ts
|
||||
const loadMultiframeAsPart10 =
|
||||
(dataSource?.getConfig?.()?.loadMultiframeAsPart10 as boolean | undefined) ??
|
||||
(customizationService?.getCustomization?.(
|
||||
'cornerstone.segmentation.loadMultiframeAsPart10'
|
||||
) as boolean | undefined) ??
|
||||
true;
|
||||
|
||||
let prefetch;
|
||||
if (loadMultiframeAsPart10) {
|
||||
prefetch = dataSource.retrieve?.prefetchInstanceFrames?.({
|
||||
instance,
|
||||
imageId: segImageIdForMetadata,
|
||||
});
|
||||
if (prefetch?.done) {
|
||||
// Wait for the bulk fetch to complete or fail — no timeout.
|
||||
await prefetch.done;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
results = await adaptersSEG.Cornerstone3D.Segmentation.createFromDicomSegImageId(
|
||||
imageIds,
|
||||
segImageIdForMetadata,
|
||||
{ metadataProvider: metaData, tolerance, parserType, frameImageIds,
|
||||
concurrency: SEG_FRAME_DECODE_CONCURRENCY }
|
||||
);
|
||||
} finally {
|
||||
eventTarget.removeEventListener(Enums.Events.SEGMENTATION_LOAD_PROGRESS, onProgress);
|
||||
prefetch?.cancel?.();
|
||||
}
|
||||
```
|
||||
|
||||
The SEG loader and adapter are **unchanged** — the loader still asks the image
|
||||
loader for each frame. Registered frames resolve from the registry; the rest
|
||||
fetch normally.
|
||||
|
||||
## Mechanism (the 4 requested points)
|
||||
|
||||
### 1. Async DICOM reader (dcmjs) to parse the instance
|
||||
|
||||
Parsing is done by `@cornerstonejs/metadata`'s `addDicomPart10Instance`, which
|
||||
uses the dcmjs **`AsyncDicomReader`** (`naturalizePart10Buffer` in
|
||||
`naturalizedHandlers.ts`) and understands encapsulated PixelData fragmentation,
|
||||
so the NATURALIZED instance exposes pixel data as an array of per-frame
|
||||
ArrayBuffer fragments. `COMPRESSED_FRAME_DATA` then slices out the requested
|
||||
frame. We did not re-implement any parsing — the prefetch just feeds the fetched
|
||||
Part 10 buffer to this existing reader.
|
||||
|
||||
**Implemented as full-buffer v1**: the whole instance is fetched, then parsed.
|
||||
`addDicomPart10Instance` accepts a lazy resolver, so the fetch is what the race
|
||||
waits on. Progressive/streaming registration (registering frames as bytes arrive)
|
||||
is a future optimization — see rollout step 4.
|
||||
|
||||
#### Response envelope: `multipart/related` vs raw DICOM
|
||||
|
||||
The full-instance fetch can come back in **two shapes**, and the parser must
|
||||
handle both before any DICOM parsing happens:
|
||||
|
||||
- **`multipart/related`** — a WADO-RS instance retrieve
|
||||
(`GET …/instances/{sop}` with `Accept: multipart/related; type="application/dicom"`)
|
||||
wraps the Part 10 object(s) in a MIME multipart envelope: leading part headers,
|
||||
a `--boundary` marker, the DICOM bytes, then a terminal `--boundary--`. The
|
||||
inner DICOM byte offsets are **shifted** by the MIME header length, so dcmjs
|
||||
must be fed the *unwrapped* inner part, never the raw response.
|
||||
- **raw DICOM** — WADO-URI, a direct file/object URL, or a `bulkDataURI` that
|
||||
returns a single `application/dicom` body: the response **is** the Part 10
|
||||
bytes, no envelope.
|
||||
|
||||
**As implemented**, the full-buffer path delegates this to **`dicomweb-client`'s
|
||||
`retrieveInstance`**, which performs the WADO-RS instance retrieve and returns the
|
||||
Part 10 as an `ArrayBuffer` with the `multipart/related` envelope already
|
||||
unwrapped (and returns the single-part body as-is). `prefetchInstanceFrames`
|
||||
additionally tolerates an `[ArrayBuffer]` or `ArrayBufferView` shape defensively.
|
||||
So the implemented path is:
|
||||
|
||||
```
|
||||
retrieveInstance() → ArrayBuffer (multipart unwrapped by dicomweb-client)
|
||||
→ prefetchPart10Instance → addDicomPart10Instance → dcmjs AsyncDicomReader
|
||||
```
|
||||
|
||||
Detection by `Content-Type` and manual unwrapping (the approach `wadors/extractMultipart.ts`
|
||||
uses: `contentType.indexOf('multipart') === -1` → bytes as-is; else strip the
|
||||
boundary/part headers) becomes relevant only for the **future streaming path**
|
||||
(rollout step 4), where these wrinkles must be handled directly:
|
||||
|
||||
- The multipart preamble/boundary stripped at the **start** of the stream and the
|
||||
terminal boundary detected at the **end**; a boundary token can **straddle chunk
|
||||
boundaries**, so the unwrapper must buffer across chunks (the existing
|
||||
`findIndexOfString` + `tokenIndex` carry-over in `extractMultipart` is built for
|
||||
incremental calls).
|
||||
- `multipart/related` may contain **multiple parts**; a single-instance retrieve
|
||||
yields one — guard for >1.
|
||||
- A truncated/partial body (no terminal boundary yet) must not be mis-parsed.
|
||||
|
||||
### 2. Register the compressed data into the frame registry
|
||||
|
||||
The fetched Part 10 buffer is registered **once per instance** via the
|
||||
`prefetchPart10Instance` adapter (a thin wrapper over `addDicomPart10Instance`):
|
||||
|
||||
```ts
|
||||
// @cornerstonejs/dicom-image-loader: imageLoader/prefetchPart10Instance.ts
|
||||
import { utilities } from '@cornerstonejs/metadata';
|
||||
const { addDicomPart10Instance } = utilities;
|
||||
|
||||
export function prefetchPart10Instance(baseImageId, part10 /* buffer | resolver */) {
|
||||
return addDicomPart10Instance(baseImageId, part10);
|
||||
}
|
||||
```
|
||||
|
||||
That populates NATURALIZED for the instance; `COMPRESSED_FRAME_DATA` then yields
|
||||
each frame's compressed bytes + transfer syntax on demand. Frame imageIds
|
||||
normalize to the instance key automatically (`baseImageIdQueryFilter`), so there
|
||||
is no per-frame registration loop and no second cache to keep coherent — the
|
||||
registry **is** the existing NATURALIZED framework.
|
||||
|
||||
The "few new adapters" the task calls for:
|
||||
|
||||
1. `prefetchPart10Instance(baseImageId, part10)` — entry point to register a
|
||||
fetched instance into the registry (exported from
|
||||
`@cornerstonejs/dicom-image-loader`).
|
||||
2. `loadImageFromCompressedFrameRegistry(imageId, options)` in
|
||||
`wadors/loadImageFromRegistry.ts` — teaches the **WADO-RS** loader to read the
|
||||
registry. It looks up `COMPRESSED_FRAME_DATA` for the frame and, if present,
|
||||
decodes via the same `createImage` the network path uses; `wadors/loadImage.ts`
|
||||
calls it first and short-circuits on a hit. (WADO-URI already reads the
|
||||
registry via `loadImageFromNaturalizedMetadata`.)
|
||||
|
||||
Metadata the decoder needs (`imagePixelModule`, transfer syntax) comes from the
|
||||
same NATURALIZED instance, plus the SEG handler's existing
|
||||
`_ensureSegInstanceMetadataAvailable` per frame imageId.
|
||||
|
||||
### 3. Cornerstone uses its existing decompressor — identical downstream
|
||||
|
||||
Both the WADO-URI path and the new WADO-RS registry path decode via
|
||||
`createImage` (→ the same web-worker decoder used for every network frame), so
|
||||
encapsulated transfer syntaxes are decompressed exactly as if fetched per-frame
|
||||
and native/uncompressed frames pass through unchanged. No new decode path, no
|
||||
codec changes, no divergence in pixel output — the only difference is that the
|
||||
compressed bytes came from the registry instead of the wire.
|
||||
|
||||
### 4. Failure is non-fatal — fall back to per-frame fetches
|
||||
|
||||
`prefetchInstanceFrames` wraps the fetch + parse so that **any** failure (network
|
||||
error, cancel, parse error, unexpected `retrieveInstance` shape) resolves
|
||||
`done` to `false` and logs a warning — it never throws into the SEG load. When
|
||||
the registry has no data for a frame, `loadImageFromCompressedFrameRegistry`
|
||||
returns `undefined` and the WADO-RS loader falls through to its normal
|
||||
`/frames/N` request. So if the prefetch is slow, disabled, or fails, loading is
|
||||
exactly today's per-frame behaviour.
|
||||
|
||||
Edge cases that degrade gracefully:
|
||||
|
||||
- `cancel()` (viewport closed / segmentation removed mid-load) → the resolver
|
||||
throws on its cancelled flag; `done` resolves `false`; any already-registered
|
||||
data stays usable.
|
||||
- A misbehaving metadata provider → the registry lookup is wrapped in try/catch
|
||||
and returns `undefined`, so per-frame loading is never broken.
|
||||
|
||||
## Semantics (the `loadMultiframeAsPart10` knob)
|
||||
|
||||
- Unset (default) or `true` → start the full-instance fetch+parse and **await
|
||||
it to completion or failure** (`await prefetch.done`) — deliberately no
|
||||
timeout; every frame is then served from the registry. Fetch failure resolves
|
||||
`done` quickly, so unsupported servers fall straight through to per-frame.
|
||||
- `false` (explicitly configured) → **disabled**; per-frame loading is the
|
||||
exception, opted into per deployment.
|
||||
|
||||
> `addDicomPart10Instance` registers the instance atomically (one parse), so
|
||||
> registration is all-or-nothing rather than progressive. Progressive
|
||||
> registration (rollout step 4) would let frames be served as they arrive.
|
||||
|
||||
There is no timeout because a bounded wait loses on exactly the objects where
|
||||
the prefetch matters most: a large SEG (hundreds of MB) cannot finish inside
|
||||
any small cap, so a capped race expires and storms per-frame anyway while
|
||||
abandoning the bulk fetch's benefit. The only failure mode a timeout would
|
||||
bound — a fetch that never settles — is already covered by the browser's
|
||||
network-level failure surfacing through `done`.
|
||||
|
||||
## Where the generic capability lives
|
||||
|
||||
- The **registry is the existing `@cornerstonejs/metadata` NATURALIZED framework**
|
||||
— no new cache. New code in `@cornerstonejs/dicom-image-loader` is the thin
|
||||
adapter `prefetchPart10Instance()` plus the WADO-RS read path
|
||||
(`wadors/loadImageFromRegistry.ts`, wired into `wadors/loadImage.ts`).
|
||||
- The **fetch + register orchestration** lives in the **data source**
|
||||
(`extensions/default/.../DicomWebDataSource` → `retrieve.prefetchInstanceFrames`),
|
||||
because it knows how to retrieve a full instance (auth headers, `wadoRoot`,
|
||||
WADO-RS instance retrieve via `dicomweb-client`, CORS), and so alternate data
|
||||
sources can implement/override it.
|
||||
- The SEG handler only **reads the configuration and awaits the prefetch**.
|
||||
|
||||
This keeps the layering clean: data source = "how to get bytes", image loader =
|
||||
"how to register/decode bytes", SEG handler = "when to ask".
|
||||
|
||||
## Open questions / risks
|
||||
|
||||
1. **NATURALIZED registry key alignment.** Both registration
|
||||
(`addDicomPart10Instance`) and the WADO-RS read normalize frame imageIds to the
|
||||
base instance via `baseImageIdQueryFilter`, so they agree. Worth a sanity check
|
||||
per data source that the SEG `imageId` passed to `prefetchInstanceFrames` and
|
||||
the frame imageIds the SEG loader requests share the same instance base.
|
||||
2. **Memory.** Holding the full instance plus the still-compressed frames in the
|
||||
registry raises memory for that instance until it is evicted. Compressed frames
|
||||
are small and decode is on-demand. Registry/cache eviction lifecycle for
|
||||
prefetched instances should be verified (especially repeated SEG loads).
|
||||
3. **Streaming availability.** v1 uses a single full-buffer `retrieveInstance`. A
|
||||
future streaming path (step 4) would register frames as bytes arrive.
|
||||
4. **Auth / headers.** Handled — the resolver sets
|
||||
`wadoDicomWebClient.headers = getAuthorizationHeader()` before retrieve, reusing
|
||||
the data source's existing decoration.
|
||||
5. **Per-frame overhead.** Every WADO-RS load now does one cheap
|
||||
`COMPRESSED_FRAME_DATA` lookup (a Map get after base normalization) before
|
||||
falling through. Negligible, and only short-circuits when an instance was
|
||||
actually prefetched.
|
||||
|
||||
## Incremental rollout
|
||||
|
||||
1. ✅ `prefetchPart10Instance` adapter + WADO-RS registry read
|
||||
(`wadors/loadImageFromRegistry.ts`) in dicom-image-loader. No behaviour change
|
||||
until something registers an instance.
|
||||
2. ✅ `retrieve.prefetchInstanceFrames` in DicomWebDataSource (full-buffer v1 via
|
||||
`retrieveInstance`); whether to call it is the caller's policy.
|
||||
3. ✅ SEG handler call site. Now **enabled and awaited to completion by
|
||||
default** (no timeout); per-frame loading requires an explicit
|
||||
`loadMultiframeAsPart10: false`.
|
||||
4. ⏳ Progressive (streaming) parse to register frames as they arrive (handle the
|
||||
`multipart/related` unwrap + boundary-straddle directly).
|
||||
5. ⏳ Extend to other multiframe loaders (the registry path is generic).
|
||||
@ -232,3 +232,122 @@ integration must trigger loading explicitly (for example by calling
|
||||
`customizationService.applyCustomizationUrlSearchParams` or `customizationService.requires` with
|
||||
the new list). New module keys not seen before can still be loaded that way; unloading or
|
||||
replacing an already-loaded pack is not supported out of the box.
|
||||
|
||||
## `segmentation.store.*` (DICOM SEG export encoding)
|
||||
|
||||
These customizations control how the `@ohif/extension-cornerstone-dicom-seg` extension
|
||||
encodes a DICOM SEG when it is **stored or downloaded**, and are read when a SEG is
|
||||
generated.
|
||||
|
||||
| Key | Values | Default | Controls |
|
||||
| --- | --- | --- | --- |
|
||||
| `segmentation.store.defaultMode` | `'labelmap'` \| `'bitmap'` | `'labelmap'` | SEG SOP Class |
|
||||
| `segmentation.store.transferSyntaxUID` | a transfer-syntax UID string | RLE Lossless (`1.2.840.10008.1.2.5`) | PixelData encoding |
|
||||
|
||||
- **`segmentation.store.defaultMode`**
|
||||
- `'labelmap'` → Label Map Segmentation Storage (`1.2.840.10008.5.1.4.1.1.66.7`). One
|
||||
multi-valued frame per slice. Added to DICOM in 2024, so many PACS/viewers do not
|
||||
accept it yet. **This is the OHIF default.**
|
||||
- `'bitmap'` → (binary) Segmentation Storage (`1.2.840.10008.5.1.4.1.1.66.4`). One frame
|
||||
per segment; broadly compatible with existing back ends and viewers. Opt in via
|
||||
customization when needed.
|
||||
- **`segmentation.store.transferSyntaxUID`**
|
||||
- Default → RLE Lossless (`1.2.840.10008.1.2.5`, compressed). Set by
|
||||
`getSegmentationSaveOptions` in `@ohif/extension-cornerstone-dicom-seg` and registered
|
||||
as an extension customization — no app config required.
|
||||
- `1.2.840.10008.1.2.1` → Explicit VR Little Endian (**uncompressed**). Opt in via
|
||||
customization for back ends that reject compressed SEG PixelData.
|
||||
|
||||
> OHIF's effective default is **Label Map + RLE Lossless (compressed)**. Customizations
|
||||
> (or per-data-source `configuration.segmentation.store`) are only needed to switch to
|
||||
> bitmap and/or uncompressed.
|
||||
|
||||
### URL-loaded files (recommended)
|
||||
|
||||
Two ready-made public customization files ship under
|
||||
`platform/app/public/customizations/segmentation/`, so a user can flip either default
|
||||
straight from the URL (requires `customizationUrlPrefixes.default` to be configured — it
|
||||
is in the `dev` / `netlify` configs):
|
||||
|
||||
| File | `?customization=` value | Effect |
|
||||
| --- | --- | --- |
|
||||
| `segmentation/uncompressed.jsonc` | `segmentation/uncompressed` | Explicit VR Little Endian instead of RLE |
|
||||
| `segmentation/binary.jsonc` | `segmentation/binary` | Binary SEG (66.4) instead of Label Map (66.7) |
|
||||
|
||||
They are independent and combine, so you can apply both at once:
|
||||
|
||||
```
|
||||
http://host/viewer?customization=segmentation/uncompressed&customization=segmentation/binary
|
||||
```
|
||||
|
||||
Each file just `$set`s one key in the `global` phase, e.g. `segmentation/binary.jsonc`:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"global": {
|
||||
"segmentation.store.defaultMode": { "$set": "bitmap" }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The remaining forms below set the same keys directly in `window.config` instead of via the
|
||||
URL.
|
||||
|
||||
### Store an uncompressed SEG instead of the compressed default
|
||||
|
||||
```js
|
||||
window.config = {
|
||||
customizationService: [
|
||||
{
|
||||
'segmentation.store.transferSyntaxUID': {
|
||||
$set: '1.2.840.10008.1.2.1', // Explicit VR Little Endian (uncompressed)
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
```
|
||||
|
||||
### Store a binary SEG (66.4) instead of the default Label Map (66.7)
|
||||
|
||||
```js
|
||||
window.config = {
|
||||
customizationService: [
|
||||
{
|
||||
'segmentation.store.defaultMode': { $set: 'bitmap' },
|
||||
},
|
||||
],
|
||||
};
|
||||
```
|
||||
|
||||
### Per data source override
|
||||
|
||||
A data source may override the app-wide default for one back end only, under
|
||||
`configuration.segmentation.store`. **The data source value wins over the customization
|
||||
default**, so different back ends can pick the SEG encoding they support:
|
||||
|
||||
```js
|
||||
window.config = {
|
||||
// App-wide default (optional): Label Map + RLE unless set here.
|
||||
customizationService: [
|
||||
{ 'segmentation.store.defaultMode': { $set: 'labelmap' } },
|
||||
],
|
||||
dataSources: [
|
||||
{
|
||||
namespace: '@ohif/extension-default.dataSourcesModule.dicomweb',
|
||||
sourceName: 'myPacs',
|
||||
configuration: {
|
||||
// ...wado/qido/stow roots...
|
||||
segmentation: {
|
||||
store: {
|
||||
defaultMode: 'bitmap', // this PACS only accepts binary SEG
|
||||
transferSyntaxUID: '1.2.840.10008.1.2.1', // and only uncompressed
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
```
|
||||
|
||||
When storing, the override is resolved against the data source being written to; for
|
||||
download (no target data source) the active data source's override applies.
|
||||
|
||||
336
pnpm-lock.yaml
generated
336
pnpm-lock.yaml
generated
@ -8,7 +8,7 @@ overrides:
|
||||
commander: 8.3.0
|
||||
cross-env: 7.0.3
|
||||
cross-spawn: 7.0.6
|
||||
dcmjs: 0.49.4
|
||||
dcmjs: 0.52.0
|
||||
path-to-regexp: 0.1.13
|
||||
nth-check: 2.1.1
|
||||
trim-newlines: 5.0.0
|
||||
@ -190,11 +190,11 @@ importers:
|
||||
specifier: 7.29.7
|
||||
version: 7.29.7
|
||||
'@cornerstonejs/adapters':
|
||||
specifier: 5.1.3
|
||||
version: 5.1.3(@cornerstonejs/core@5.1.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.1.3)(@cornerstonejs/utils@5.1.3)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@cornerstonejs/tools@5.1.3(@cornerstonejs/core@5.1.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.1.3)(@cornerstonejs/utils@5.1.3)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@kitware/vtk.js@35.5.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@types/d3-array@3.2.1)(@types/d3-interpolate@3.0.4)(d3-array@3.2.4)(d3-interpolate@3.0.1)(gl-matrix@3.4.3))
|
||||
specifier: 5.4.12
|
||||
version: 5.4.12(@cornerstonejs/core@5.4.12(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.4.12)(@cornerstonejs/utils@5.4.12)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@cornerstonejs/tools@5.4.12(@cornerstonejs/core@5.4.12(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.4.12)(@cornerstonejs/utils@5.4.12)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@kitware/vtk.js@35.5.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@types/d3-array@3.2.1)(@types/d3-interpolate@3.0.4)(d3-array@3.2.4)(d3-interpolate@3.0.1)(gl-matrix@3.4.3))
|
||||
'@cornerstonejs/ai':
|
||||
specifier: 5.1.3
|
||||
version: 5.1.3(@cornerstonejs/core@5.1.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.1.3)(@cornerstonejs/utils@5.1.3)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@cornerstonejs/tools@5.1.3(@cornerstonejs/core@5.1.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.1.3)(@cornerstonejs/utils@5.1.3)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@kitware/vtk.js@35.5.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@types/d3-array@3.2.1)(@types/d3-interpolate@3.0.4)(d3-array@3.2.4)(d3-interpolate@3.0.1)(gl-matrix@3.4.3))
|
||||
specifier: 5.4.12
|
||||
version: 5.4.12(@cornerstonejs/core@5.4.12(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.4.12)(@cornerstonejs/utils@5.4.12)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@cornerstonejs/tools@5.4.12(@cornerstonejs/core@5.4.12(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.4.12)(@cornerstonejs/utils@5.4.12)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@kitware/vtk.js@35.5.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@types/d3-array@3.2.1)(@types/d3-interpolate@3.0.4)(d3-array@3.2.4)(d3-interpolate@3.0.1)(gl-matrix@3.4.3))
|
||||
'@cornerstonejs/codec-charls':
|
||||
specifier: 1.2.3
|
||||
version: 1.2.3
|
||||
@ -208,23 +208,23 @@ importers:
|
||||
specifier: 2.4.7
|
||||
version: 2.4.7
|
||||
'@cornerstonejs/core':
|
||||
specifier: 5.1.3
|
||||
version: 5.1.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.1.3)(@cornerstonejs/utils@5.1.3)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0)
|
||||
specifier: 5.4.12
|
||||
version: 5.4.12(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.4.12)(@cornerstonejs/utils@5.4.12)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0)
|
||||
'@cornerstonejs/dicom-image-loader':
|
||||
specifier: 5.1.3
|
||||
version: 5.1.3(@cornerstonejs/core@5.1.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.1.3)(@cornerstonejs/utils@5.1.3)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@cornerstonejs/metadata@5.1.3)(dicom-parser@1.8.21)
|
||||
specifier: 5.4.12
|
||||
version: 5.4.12(@cornerstonejs/core@5.4.12(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.4.12)(@cornerstonejs/utils@5.4.12)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@cornerstonejs/metadata@5.4.12)(dicom-parser@1.8.21)
|
||||
'@cornerstonejs/labelmap-interpolation':
|
||||
specifier: 5.1.3
|
||||
version: 5.1.3(@cornerstonejs/core@5.1.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.1.3)(@cornerstonejs/utils@5.1.3)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@cornerstonejs/tools@5.1.3(@cornerstonejs/core@5.1.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.1.3)(@cornerstonejs/utils@5.1.3)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@kitware/vtk.js@35.5.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@types/d3-array@3.2.1)(@types/d3-interpolate@3.0.4)(d3-array@3.2.4)(d3-interpolate@3.0.1)(gl-matrix@3.4.3))(@kitware/vtk.js@35.5.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))
|
||||
specifier: 5.4.12
|
||||
version: 5.4.12(@cornerstonejs/core@5.4.12(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.4.12)(@cornerstonejs/utils@5.4.12)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@cornerstonejs/tools@5.4.12(@cornerstonejs/core@5.4.12(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.4.12)(@cornerstonejs/utils@5.4.12)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@kitware/vtk.js@35.5.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@types/d3-array@3.2.1)(@types/d3-interpolate@3.0.4)(d3-array@3.2.4)(d3-interpolate@3.0.1)(gl-matrix@3.4.3))(@kitware/vtk.js@35.5.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))
|
||||
'@cornerstonejs/metadata':
|
||||
specifier: 5.1.3
|
||||
version: 5.1.3
|
||||
specifier: 5.4.12
|
||||
version: 5.4.12
|
||||
'@cornerstonejs/polymorphic-segmentation':
|
||||
specifier: 5.1.3
|
||||
version: 5.1.3(@cornerstonejs/core@5.1.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.1.3)(@cornerstonejs/utils@5.1.3)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@cornerstonejs/tools@5.1.3(@cornerstonejs/core@5.1.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.1.3)(@cornerstonejs/utils@5.1.3)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@kitware/vtk.js@35.5.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@types/d3-array@3.2.1)(@types/d3-interpolate@3.0.4)(d3-array@3.2.4)(d3-interpolate@3.0.1)(gl-matrix@3.4.3))(@kitware/vtk.js@35.5.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))
|
||||
specifier: 5.4.12
|
||||
version: 5.4.12(@cornerstonejs/core@5.4.12(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.4.12)(@cornerstonejs/utils@5.4.12)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@cornerstonejs/tools@5.4.12(@cornerstonejs/core@5.4.12(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.4.12)(@cornerstonejs/utils@5.4.12)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@kitware/vtk.js@35.5.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@types/d3-array@3.2.1)(@types/d3-interpolate@3.0.4)(d3-array@3.2.4)(d3-interpolate@3.0.1)(gl-matrix@3.4.3))(@kitware/vtk.js@35.5.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))
|
||||
'@cornerstonejs/tools':
|
||||
specifier: 5.1.3
|
||||
version: 5.1.3(@cornerstonejs/core@5.1.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.1.3)(@cornerstonejs/utils@5.1.3)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@kitware/vtk.js@35.5.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@types/d3-array@3.2.1)(@types/d3-interpolate@3.0.4)(d3-array@3.2.4)(d3-interpolate@3.0.1)(gl-matrix@3.4.3)
|
||||
specifier: 5.4.12
|
||||
version: 5.4.12(@cornerstonejs/core@5.4.12(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.4.12)(@cornerstonejs/utils@5.4.12)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@kitware/vtk.js@35.5.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@types/d3-array@3.2.1)(@types/d3-interpolate@3.0.4)(d3-array@3.2.4)(d3-interpolate@3.0.1)(gl-matrix@3.4.3)
|
||||
'@icr/polyseg-wasm':
|
||||
specifier: 0.4.0
|
||||
version: 0.4.0
|
||||
@ -244,8 +244,8 @@ importers:
|
||||
specifier: workspace:*
|
||||
version: link:../../platform/ui
|
||||
dcmjs:
|
||||
specifier: 0.49.4
|
||||
version: 0.49.4
|
||||
specifier: 0.52.0
|
||||
version: 0.52.0
|
||||
dicom-parser:
|
||||
specifier: 1.8.21
|
||||
version: 1.8.21
|
||||
@ -299,11 +299,11 @@ importers:
|
||||
specifier: 7.29.7
|
||||
version: 7.29.7
|
||||
'@cornerstonejs/adapters':
|
||||
specifier: 5.1.3
|
||||
version: 5.1.3(@cornerstonejs/core@5.1.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.1.3)(@cornerstonejs/utils@5.1.3)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@cornerstonejs/tools@5.1.3(@cornerstonejs/core@5.1.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.1.3)(@cornerstonejs/utils@5.1.3)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@kitware/vtk.js@35.5.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@types/d3-array@3.2.1)(@types/d3-interpolate@3.0.4)(d3-array@3.2.4)(d3-interpolate@3.0.1)(gl-matrix@3.4.3))
|
||||
specifier: 5.4.12
|
||||
version: 5.4.12(@cornerstonejs/core@5.4.12(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.4.12)(@cornerstonejs/utils@5.4.12)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@cornerstonejs/tools@5.4.12(@cornerstonejs/core@5.4.12(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.4.12)(@cornerstonejs/utils@5.4.12)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@kitware/vtk.js@35.5.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@types/d3-array@3.2.1)(@types/d3-interpolate@3.0.4)(d3-array@3.2.4)(d3-interpolate@3.0.1)(gl-matrix@3.4.3))
|
||||
'@cornerstonejs/core':
|
||||
specifier: 5.1.3
|
||||
version: 5.1.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.1.3)(@cornerstonejs/utils@5.1.3)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0)
|
||||
specifier: 5.4.12
|
||||
version: 5.4.12(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.4.12)(@cornerstonejs/utils@5.4.12)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0)
|
||||
'@kitware/vtk.js':
|
||||
specifier: 35.5.3
|
||||
version: 35.5.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0)
|
||||
@ -388,11 +388,11 @@ importers:
|
||||
specifier: 7.29.7
|
||||
version: 7.29.7
|
||||
'@cornerstonejs/adapters':
|
||||
specifier: 5.1.3
|
||||
version: 5.1.3(@cornerstonejs/core@5.1.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.1.3)(@cornerstonejs/utils@5.1.3)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@cornerstonejs/tools@5.1.3(@cornerstonejs/core@5.1.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.1.3)(@cornerstonejs/utils@5.1.3)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@kitware/vtk.js@35.5.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@types/d3-array@3.2.1)(@types/d3-interpolate@3.0.4)(d3-array@3.2.4)(d3-interpolate@3.0.1)(gl-matrix@3.4.3))
|
||||
specifier: 5.4.12
|
||||
version: 5.4.12(@cornerstonejs/core@5.4.12(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.4.12)(@cornerstonejs/utils@5.4.12)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@cornerstonejs/tools@5.4.12(@cornerstonejs/core@5.4.12(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.4.12)(@cornerstonejs/utils@5.4.12)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@kitware/vtk.js@35.5.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@types/d3-array@3.2.1)(@types/d3-interpolate@3.0.4)(d3-array@3.2.4)(d3-interpolate@3.0.1)(gl-matrix@3.4.3))
|
||||
'@cornerstonejs/core':
|
||||
specifier: 5.1.3
|
||||
version: 5.1.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.1.3)(@cornerstonejs/utils@5.1.3)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0)
|
||||
specifier: 5.4.12
|
||||
version: 5.4.12(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.4.12)(@cornerstonejs/utils@5.4.12)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0)
|
||||
'@kitware/vtk.js':
|
||||
specifier: 35.5.3
|
||||
version: 35.5.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0)
|
||||
@ -437,14 +437,14 @@ importers:
|
||||
specifier: 7.29.7
|
||||
version: 7.29.7
|
||||
'@cornerstonejs/adapters':
|
||||
specifier: 5.1.3
|
||||
version: 5.1.3(@cornerstonejs/core@5.1.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.1.3)(@cornerstonejs/utils@5.1.3)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@cornerstonejs/tools@5.1.3(@cornerstonejs/core@5.1.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.1.3)(@cornerstonejs/utils@5.1.3)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@kitware/vtk.js@35.5.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@types/d3-array@3.2.1)(@types/d3-interpolate@3.0.4)(d3-array@3.2.4)(d3-interpolate@3.0.1)(gl-matrix@3.4.3))
|
||||
specifier: 5.4.12
|
||||
version: 5.4.12(@cornerstonejs/core@5.4.12(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.4.12)(@cornerstonejs/utils@5.4.12)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@cornerstonejs/tools@5.4.12(@cornerstonejs/core@5.4.12(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.4.12)(@cornerstonejs/utils@5.4.12)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@kitware/vtk.js@35.5.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@types/d3-array@3.2.1)(@types/d3-interpolate@3.0.4)(d3-array@3.2.4)(d3-interpolate@3.0.1)(gl-matrix@3.4.3))
|
||||
'@cornerstonejs/core':
|
||||
specifier: 5.1.3
|
||||
version: 5.1.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.1.3)(@cornerstonejs/utils@5.1.3)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0)
|
||||
specifier: 5.4.12
|
||||
version: 5.4.12(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.4.12)(@cornerstonejs/utils@5.4.12)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0)
|
||||
'@cornerstonejs/tools':
|
||||
specifier: 5.1.3
|
||||
version: 5.1.3(@cornerstonejs/core@5.1.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.1.3)(@cornerstonejs/utils@5.1.3)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@kitware/vtk.js@35.5.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@types/d3-array@3.2.1)(@types/d3-interpolate@3.0.4)(d3-array@3.2.4)(d3-interpolate@3.0.1)(gl-matrix@3.4.3)
|
||||
specifier: 5.4.12
|
||||
version: 5.4.12(@cornerstonejs/core@5.4.12(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.4.12)(@cornerstonejs/utils@5.4.12)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@kitware/vtk.js@35.5.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@types/d3-array@3.2.1)(@types/d3-interpolate@3.0.4)(d3-array@3.2.4)(d3-interpolate@3.0.1)(gl-matrix@3.4.3)
|
||||
'@ohif/core':
|
||||
specifier: workspace:*
|
||||
version: link:../../platform/core
|
||||
@ -458,8 +458,8 @@ importers:
|
||||
specifier: 2.5.1
|
||||
version: 2.5.1
|
||||
dcmjs:
|
||||
specifier: 0.49.4
|
||||
version: 0.49.4
|
||||
specifier: 0.52.0
|
||||
version: 0.52.0
|
||||
dicom-parser:
|
||||
specifier: 1.8.21
|
||||
version: 1.8.21
|
||||
@ -483,11 +483,11 @@ importers:
|
||||
specifier: 7.29.7
|
||||
version: 7.29.7
|
||||
'@cornerstonejs/core':
|
||||
specifier: 5.1.3
|
||||
version: 5.1.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.1.3)(@cornerstonejs/utils@5.1.3)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0)
|
||||
specifier: 5.4.12
|
||||
version: 5.4.12(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.4.12)(@cornerstonejs/utils@5.4.12)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0)
|
||||
'@cornerstonejs/tools':
|
||||
specifier: 5.1.3
|
||||
version: 5.1.3(@cornerstonejs/core@5.1.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.1.3)(@cornerstonejs/utils@5.1.3)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@kitware/vtk.js@35.5.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@types/d3-array@3.2.1)(@types/d3-interpolate@3.0.4)(d3-array@3.2.4)(d3-interpolate@3.0.1)(gl-matrix@3.4.3)
|
||||
specifier: 5.4.12
|
||||
version: 5.4.12(@cornerstonejs/core@5.4.12(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.4.12)(@cornerstonejs/utils@5.4.12)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@kitware/vtk.js@35.5.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@types/d3-array@3.2.1)(@types/d3-interpolate@3.0.4)(d3-array@3.2.4)(d3-interpolate@3.0.1)(gl-matrix@3.4.3)
|
||||
'@ohif/core':
|
||||
specifier: workspace:*
|
||||
version: link:../../platform/core
|
||||
@ -507,8 +507,8 @@ importers:
|
||||
specifier: 2.5.1
|
||||
version: 2.5.1
|
||||
dcmjs:
|
||||
specifier: 0.49.4
|
||||
version: 0.49.4
|
||||
specifier: 0.52.0
|
||||
version: 0.52.0
|
||||
dicom-parser:
|
||||
specifier: 1.8.21
|
||||
version: 1.8.21
|
||||
@ -541,8 +541,8 @@ importers:
|
||||
specifier: workspace:*
|
||||
version: link:../../platform/i18n
|
||||
dcmjs:
|
||||
specifier: 0.49.4
|
||||
version: 0.49.4
|
||||
specifier: 0.52.0
|
||||
version: 0.52.0
|
||||
dicomweb-client:
|
||||
specifier: 0.10.4
|
||||
version: 0.10.4
|
||||
@ -571,6 +571,9 @@ importers:
|
||||
specifier: 1.8.11
|
||||
version: 1.8.11(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
devDependencies:
|
||||
cross-env:
|
||||
specifier: 7.0.3
|
||||
version: 7.0.3
|
||||
webpack-merge:
|
||||
specifier: 5.10.0
|
||||
version: 5.10.0
|
||||
@ -651,8 +654,8 @@ importers:
|
||||
specifier: 2.5.1
|
||||
version: 2.5.1
|
||||
dcmjs:
|
||||
specifier: 0.49.4
|
||||
version: 0.49.4
|
||||
specifier: 0.52.0
|
||||
version: 0.52.0
|
||||
dicom-parser:
|
||||
specifier: 1.8.21
|
||||
version: 1.8.21
|
||||
@ -685,8 +688,8 @@ importers:
|
||||
specifier: 2.5.1
|
||||
version: 2.5.1
|
||||
dcmjs:
|
||||
specifier: 0.49.4
|
||||
version: 0.49.4
|
||||
specifier: 0.52.0
|
||||
version: 0.52.0
|
||||
dicom-parser:
|
||||
specifier: 1.8.21
|
||||
version: 1.8.21
|
||||
@ -710,11 +713,11 @@ importers:
|
||||
specifier: 7.29.7
|
||||
version: 7.29.7
|
||||
'@cornerstonejs/core':
|
||||
specifier: 5.1.3
|
||||
version: 5.1.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.1.3)(@cornerstonejs/utils@5.1.3)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0)
|
||||
specifier: 5.4.12
|
||||
version: 5.4.12(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.4.12)(@cornerstonejs/utils@5.4.12)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0)
|
||||
'@cornerstonejs/tools':
|
||||
specifier: 5.1.3
|
||||
version: 5.1.3(@cornerstonejs/core@5.1.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.1.3)(@cornerstonejs/utils@5.1.3)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@kitware/vtk.js@35.5.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@types/d3-array@3.2.1)(@types/d3-interpolate@3.0.4)(d3-array@3.2.4)(d3-interpolate@3.0.1)(gl-matrix@3.4.3)
|
||||
specifier: 5.4.12
|
||||
version: 5.4.12(@cornerstonejs/core@5.4.12(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.4.12)(@cornerstonejs/utils@5.4.12)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@kitware/vtk.js@35.5.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@types/d3-array@3.2.1)(@types/d3-interpolate@3.0.4)(d3-array@3.2.4)(d3-interpolate@3.0.1)(gl-matrix@3.4.3)
|
||||
'@ohif/core':
|
||||
specifier: workspace:*
|
||||
version: link:../../platform/core
|
||||
@ -734,8 +737,8 @@ importers:
|
||||
specifier: 2.5.1
|
||||
version: 2.5.1
|
||||
dcmjs:
|
||||
specifier: 0.49.4
|
||||
version: 0.49.4
|
||||
specifier: 0.52.0
|
||||
version: 0.52.0
|
||||
lodash.debounce:
|
||||
specifier: 4.0.8
|
||||
version: 4.0.8
|
||||
@ -752,6 +755,9 @@ importers:
|
||||
specifier: 4.38.3
|
||||
version: 4.38.3
|
||||
devDependencies:
|
||||
cross-env:
|
||||
specifier: 7.0.3
|
||||
version: 7.0.3
|
||||
webpack-merge:
|
||||
specifier: 5.10.0
|
||||
version: 5.10.0
|
||||
@ -771,8 +777,8 @@ importers:
|
||||
specifier: 2.5.1
|
||||
version: 2.5.1
|
||||
dcmjs:
|
||||
specifier: 0.49.4
|
||||
version: 0.49.4
|
||||
specifier: 0.52.0
|
||||
version: 0.52.0
|
||||
dicom-parser:
|
||||
specifier: 1.8.21
|
||||
version: 1.8.21
|
||||
@ -805,8 +811,8 @@ importers:
|
||||
specifier: 2.5.1
|
||||
version: 2.5.1
|
||||
dcmjs:
|
||||
specifier: 0.49.4
|
||||
version: 0.49.4
|
||||
specifier: 0.52.0
|
||||
version: 0.52.0
|
||||
dicom-parser:
|
||||
specifier: 1.8.21
|
||||
version: 1.8.21
|
||||
@ -830,11 +836,11 @@ importers:
|
||||
specifier: 7.29.7
|
||||
version: 7.29.7
|
||||
'@cornerstonejs/core':
|
||||
specifier: 5.1.3
|
||||
version: 5.1.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.1.3)(@cornerstonejs/utils@5.1.3)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0)
|
||||
specifier: 5.4.12
|
||||
version: 5.4.12(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.4.12)(@cornerstonejs/utils@5.4.12)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0)
|
||||
'@cornerstonejs/tools':
|
||||
specifier: 5.1.3
|
||||
version: 5.1.3(@cornerstonejs/core@5.1.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.1.3)(@cornerstonejs/utils@5.1.3)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@kitware/vtk.js@35.5.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@types/d3-array@3.2.1)(@types/d3-interpolate@3.0.4)(d3-array@3.2.4)(d3-interpolate@3.0.1)(gl-matrix@3.4.3)
|
||||
specifier: 5.4.12
|
||||
version: 5.4.12(@cornerstonejs/core@5.4.12(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.4.12)(@cornerstonejs/utils@5.4.12)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@kitware/vtk.js@35.5.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@types/d3-array@3.2.1)(@types/d3-interpolate@3.0.4)(d3-array@3.2.4)(d3-interpolate@3.0.1)(gl-matrix@3.4.3)
|
||||
'@ohif/core':
|
||||
specifier: workspace:*
|
||||
version: link:../../platform/core
|
||||
@ -954,6 +960,9 @@ importers:
|
||||
specifier: workspace:*
|
||||
version: link:../../extensions/dicom-video
|
||||
devDependencies:
|
||||
cross-env:
|
||||
specifier: 7.0.3
|
||||
version: 7.0.3
|
||||
webpack-merge:
|
||||
specifier: 5.10.0
|
||||
version: 5.10.0
|
||||
@ -985,6 +994,9 @@ importers:
|
||||
specifier: 17.3.1
|
||||
version: 17.3.1
|
||||
devDependencies:
|
||||
cross-env:
|
||||
specifier: 7.0.3
|
||||
version: 7.0.3
|
||||
webpack-merge:
|
||||
specifier: 5.10.0
|
||||
version: 5.10.0
|
||||
@ -1022,6 +1034,9 @@ importers:
|
||||
specifier: 17.3.1
|
||||
version: 17.3.1
|
||||
devDependencies:
|
||||
cross-env:
|
||||
specifier: 7.0.3
|
||||
version: 7.0.3
|
||||
webpack-merge:
|
||||
specifier: 5.10.0
|
||||
version: 5.10.0
|
||||
@ -1065,6 +1080,9 @@ importers:
|
||||
specifier: 17.3.1
|
||||
version: 17.3.1
|
||||
devDependencies:
|
||||
cross-env:
|
||||
specifier: 7.0.3
|
||||
version: 7.0.3
|
||||
webpack-merge:
|
||||
specifier: 5.10.0
|
||||
version: 5.10.0
|
||||
@ -1240,6 +1258,9 @@ importers:
|
||||
specifier: 17.3.1
|
||||
version: 17.3.1
|
||||
devDependencies:
|
||||
cross-env:
|
||||
specifier: 7.0.3
|
||||
version: 7.0.3
|
||||
webpack-merge:
|
||||
specifier: 5.10.0
|
||||
version: 5.10.0
|
||||
@ -1250,11 +1271,11 @@ importers:
|
||||
specifier: 7.29.7
|
||||
version: 7.29.7
|
||||
'@cornerstonejs/core':
|
||||
specifier: 5.1.3
|
||||
version: 5.1.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.1.3)(@cornerstonejs/utils@5.1.3)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0)
|
||||
specifier: 5.4.12
|
||||
version: 5.4.12(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.4.12)(@cornerstonejs/utils@5.4.12)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0)
|
||||
'@cornerstonejs/tools':
|
||||
specifier: 5.1.3
|
||||
version: 5.1.3(@cornerstonejs/core@5.1.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.1.3)(@cornerstonejs/utils@5.1.3)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@kitware/vtk.js@35.5.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@types/d3-array@3.2.1)(@types/d3-interpolate@3.0.4)(d3-array@3.2.4)(d3-interpolate@3.0.1)(gl-matrix@3.4.3)
|
||||
specifier: 5.4.12
|
||||
version: 5.4.12(@cornerstonejs/core@5.4.12(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.4.12)(@cornerstonejs/utils@5.4.12)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@kitware/vtk.js@35.5.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@types/d3-array@3.2.1)(@types/d3-interpolate@3.0.4)(d3-array@3.2.4)(d3-interpolate@3.0.1)(gl-matrix@3.4.3)
|
||||
'@ohif/core':
|
||||
specifier: workspace:*
|
||||
version: link:../../platform/core
|
||||
@ -1338,8 +1359,8 @@ importers:
|
||||
specifier: 2.4.7
|
||||
version: 2.4.7
|
||||
'@cornerstonejs/dicom-image-loader':
|
||||
specifier: 5.1.3
|
||||
version: 5.1.3(@cornerstonejs/core@5.1.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.1.3)(@cornerstonejs/utils@5.1.3)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@cornerstonejs/metadata@5.1.3)(dicom-parser@1.8.21)
|
||||
specifier: 5.4.12
|
||||
version: 5.4.12(@cornerstonejs/core@5.4.12(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.4.12)(@cornerstonejs/utils@5.4.12)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@cornerstonejs/metadata@5.4.12)(dicom-parser@1.8.21)
|
||||
'@emotion/serialize':
|
||||
specifier: 1.3.3
|
||||
version: 1.3.3
|
||||
@ -1371,8 +1392,8 @@ importers:
|
||||
specifier: 0.1.10
|
||||
version: 0.1.10
|
||||
dcmjs:
|
||||
specifier: 0.49.4
|
||||
version: 0.49.4
|
||||
specifier: 0.52.0
|
||||
version: 0.52.0
|
||||
detect-gpu:
|
||||
specifier: 4.0.50
|
||||
version: 4.0.50
|
||||
@ -1549,14 +1570,14 @@ importers:
|
||||
specifier: 2.4.7
|
||||
version: 2.4.7
|
||||
'@cornerstonejs/core':
|
||||
specifier: 5.1.3
|
||||
version: 5.1.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.1.3)(@cornerstonejs/utils@5.1.3)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0)
|
||||
specifier: 5.4.12
|
||||
version: 5.4.12(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.4.12)(@cornerstonejs/utils@5.4.12)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0)
|
||||
'@cornerstonejs/dicom-image-loader':
|
||||
specifier: 5.1.3
|
||||
version: 5.1.3(@cornerstonejs/core@5.1.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.1.3)(@cornerstonejs/utils@5.1.3)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@cornerstonejs/metadata@5.1.3)(dicom-parser@1.8.21)
|
||||
specifier: 5.4.12
|
||||
version: 5.4.12(@cornerstonejs/core@5.4.12(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.4.12)(@cornerstonejs/utils@5.4.12)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@cornerstonejs/metadata@5.4.12)(dicom-parser@1.8.21)
|
||||
'@cornerstonejs/metadata':
|
||||
specifier: 5.1.3
|
||||
version: 5.1.3
|
||||
specifier: 5.4.12
|
||||
version: 5.4.12
|
||||
'@ohif/ui':
|
||||
specifier: workspace:*
|
||||
version: link:../ui
|
||||
@ -1564,8 +1585,8 @@ importers:
|
||||
specifier: 0.1.10
|
||||
version: 0.1.10
|
||||
dcmjs:
|
||||
specifier: 0.49.4
|
||||
version: 0.49.4
|
||||
specifier: 0.52.0
|
||||
version: 0.52.0
|
||||
dicom-parser:
|
||||
specifier: 1.8.21
|
||||
version: 1.8.21
|
||||
@ -3325,8 +3346,8 @@ packages:
|
||||
resolution: {integrity: sha512-JfoPiD7f/vvd/PaOfu5cr9CyzwDMPg4T0nX3MQr6IgTq49DhjvUcmjmjA7j6+xih1Evq+QKZnge1SoIlYozv/Q==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@babel/runtime-corejs3@7.29.0':
|
||||
resolution: {integrity: sha512-TgUkdp71C9pIbBcHudc+gXZnihEDOjUAmXO1VO4HHGES7QLZcShR0stfKIxLSNIYx2fqhmJChOjm/wkF8wv4gA==}
|
||||
'@babel/runtime-corejs3@7.29.2':
|
||||
resolution: {integrity: sha512-Lc94FOD5+0aXhdb0Tdg3RUtqT6yWbI/BbFWvlaSJ3gAb9Ks+99nHRDKADVqC37er4eCB0fHyWT+y+K3QOvJKbw==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@babel/runtime@7.29.2':
|
||||
@ -3368,17 +3389,17 @@ packages:
|
||||
resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==}
|
||||
engines: {node: '>=0.1.90'}
|
||||
|
||||
'@cornerstonejs/adapters@5.1.3':
|
||||
resolution: {integrity: sha512-cA0NSsvL3tJABTsxTBgpBk7e5PMHsDEMaR0wGJ48kJ/Q9s3B5JMlYoC8Gnw89KOpxxAkgLwtmzQ6Dr1FXPHtkQ==}
|
||||
'@cornerstonejs/adapters@5.4.12':
|
||||
resolution: {integrity: sha512-WjNtvkxFXSlwJw2GS2oPRvt+rfZ10wHiT0xy7YubqdIqKfuB3T1RB1yziYut9RkOXYyBj7mgsZLKfddyzvwWiA==}
|
||||
peerDependencies:
|
||||
'@cornerstonejs/core': 5.1.3
|
||||
'@cornerstonejs/tools': 5.1.3
|
||||
'@cornerstonejs/core': 5.4.12
|
||||
'@cornerstonejs/tools': 5.4.12
|
||||
|
||||
'@cornerstonejs/ai@5.1.3':
|
||||
resolution: {integrity: sha512-5gpgj7E6lbvoYfhEadKGZofYcihpNX2YbURM2asehQPn1VdLzewfcQsdUhdfXNwROuz6zyU795xs4ZcynvQkHQ==}
|
||||
'@cornerstonejs/ai@5.4.12':
|
||||
resolution: {integrity: sha512-b9HlH4rxL8G0V59h8WHMraq8dISZAf+/WJNEvz+jKJDSdsOzXoMNq+G5vrI84Y/SqsMmWP3Tca14pUi2otZDsw==}
|
||||
peerDependencies:
|
||||
'@cornerstonejs/core': 5.1.3
|
||||
'@cornerstonejs/tools': 5.1.3
|
||||
'@cornerstonejs/core': 5.4.12
|
||||
'@cornerstonejs/tools': 5.4.12
|
||||
|
||||
'@cornerstonejs/calculate-suv@1.0.3':
|
||||
resolution: {integrity: sha512-2SwVJKzC1DzyxdxJtCht9dhTND2GFjLwhhkDyyC7vJq5tIgbhxgPk1CSwovO1pxmoybAXzjOxnaubllxLgoT+w==}
|
||||
@ -3404,40 +3425,40 @@ packages:
|
||||
resolution: {integrity: sha512-qvP4q4JDib7mi9r7LqKOwqz7YZ8gjtDX4ZCezeYf8+eb7MBXCz5uXAMeVF3yz9Axw4XiIMdB/pqXkm8tqCl13w==}
|
||||
engines: {node: '>=0.14'}
|
||||
|
||||
'@cornerstonejs/core@5.1.3':
|
||||
resolution: {integrity: sha512-Ly9nvO4s5ZuIta/Ozf6u8/aoAkTCX0p1LI0vkxCdyPL/5Tr4g8UuWhftDTxZKoGs99LsNLTaFz/yjpsPs09gYg==}
|
||||
'@cornerstonejs/core@5.4.12':
|
||||
resolution: {integrity: sha512-ieMKKmA+GsvLMePObU6C2+VyMAoMyfKqlwQiDcaneg3hF5VtMIXYY84pDMvxC45+MtatMqhlC08ubSy+eMZokA==}
|
||||
peerDependencies:
|
||||
'@cornerstonejs/metadata': 5.1.3
|
||||
'@cornerstonejs/utils': 5.1.3
|
||||
'@cornerstonejs/metadata': 5.4.12
|
||||
'@cornerstonejs/utils': 5.4.12
|
||||
|
||||
'@cornerstonejs/dicom-image-loader@5.1.3':
|
||||
resolution: {integrity: sha512-aaJq8kQs6BiloCanGlrZT/5YoHKflDZ1fbz8SRV12mi4yQNf8XWhKglSm9qk2do+tIY45VwbsBFAlHU2+CMSnA==}
|
||||
'@cornerstonejs/dicom-image-loader@5.4.12':
|
||||
resolution: {integrity: sha512-TwJuK2YcRwqmXXNMAVTs5qF7JnL6q5WsWU2UqCbfBpXAprqXkcuWYVe9Xp7DS8kZxuQ0X+bcLdHdtiekfMEAqg==}
|
||||
peerDependencies:
|
||||
'@cornerstonejs/core': 5.1.3
|
||||
'@cornerstonejs/metadata': 5.1.3
|
||||
'@cornerstonejs/core': 5.4.12
|
||||
'@cornerstonejs/metadata': 5.4.12
|
||||
dicom-parser: 1.8.21
|
||||
|
||||
'@cornerstonejs/labelmap-interpolation@5.1.3':
|
||||
resolution: {integrity: sha512-xrrNW/4wG/wIoivSJeyN0y1l50PXTEfhl/OMo98h+KA09k8R19Dj4bpyqle+tcXr7MId10gPvLNP88qMkJPa7Q==}
|
||||
'@cornerstonejs/labelmap-interpolation@5.4.12':
|
||||
resolution: {integrity: sha512-yTjg7jp16T9+mxVvDHrwePEzA1+fvn/rQmCB/x9Ry+6J81epQEgykUsNDOP1O8r3800fDqyy0Zl/akrcF+ZYJg==}
|
||||
peerDependencies:
|
||||
'@cornerstonejs/core': 5.1.3
|
||||
'@cornerstonejs/tools': 5.1.3
|
||||
'@cornerstonejs/core': 5.4.12
|
||||
'@cornerstonejs/tools': 5.4.12
|
||||
'@kitware/vtk.js': 35.5.3
|
||||
|
||||
'@cornerstonejs/metadata@5.1.3':
|
||||
resolution: {integrity: sha512-U/bQEj6+JYjXjKfDTWK+ThTCeAmy+o7+0Kw+vCuKNLpM9GMhq5l7uyihWoOT8Rk4ICfJ44BAZoD+27Fo5varIg==}
|
||||
'@cornerstonejs/metadata@5.4.12':
|
||||
resolution: {integrity: sha512-CkTvEbdGsyMHLmrVW9c0HyYe93Vh35Ok4mhtlv7SSlfXuSjIWedVQoNpQHhjFHQmj3aHzrvibWvuUL0VI+zC4w==}
|
||||
|
||||
'@cornerstonejs/polymorphic-segmentation@5.1.3':
|
||||
resolution: {integrity: sha512-9y7K2XRR20axzQRHT//Jkq7zN66gpn+fBvlf8nFFNLKBN5jBuUktjWbr9RAPxkD69OzmrrLbA4KoYLj5Cjlf6g==}
|
||||
'@cornerstonejs/polymorphic-segmentation@5.4.12':
|
||||
resolution: {integrity: sha512-mLklbspaCVwtfxus0Ou+63rKzlGWlHHTaqU1xORl4V9HyQ95RCXUde8S+86PpMoJAQ3Mq4B1+oCLgAOuCId1Cg==}
|
||||
peerDependencies:
|
||||
'@cornerstonejs/core': 5.1.3
|
||||
'@cornerstonejs/tools': 5.1.3
|
||||
'@cornerstonejs/core': 5.4.12
|
||||
'@cornerstonejs/tools': 5.4.12
|
||||
'@kitware/vtk.js': 35.5.3
|
||||
|
||||
'@cornerstonejs/tools@5.1.3':
|
||||
resolution: {integrity: sha512-lRR8CgzGaLurYsrJc1/oHuZUT15Iye1JV4UOSh/rl982B9yF6TSzy50TxNXDe0ckJj0nMGHGqvK4EyqoG8VUMQ==}
|
||||
'@cornerstonejs/tools@5.4.12':
|
||||
resolution: {integrity: sha512-ldmzgCtaVRsKjhDEMj8NIfczIAdR7KTYjH1FGPknpMU1HstZ6Eg+4AyDEz0vctyO4gW/tLXCME5ct1GhD6GiFA==}
|
||||
peerDependencies:
|
||||
'@cornerstonejs/core': 5.1.3
|
||||
'@cornerstonejs/core': 5.4.12
|
||||
'@kitware/vtk.js': 35.5.3
|
||||
'@types/d3-array': 3.2.1
|
||||
'@types/d3-interpolate': 3.0.4
|
||||
@ -3445,8 +3466,8 @@ packages:
|
||||
d3-interpolate: 3.0.1
|
||||
gl-matrix: 3.4.3
|
||||
|
||||
'@cornerstonejs/utils@5.1.3':
|
||||
resolution: {integrity: sha512-tqIjh7Q8jJ8U7BFbIH7LcipvACgGeExo6pbOPXytdAVSPcrXQ5i5BOJsgZ/yzW5KANX2lzpx57Hx3/L6HUS4Ag==}
|
||||
'@cornerstonejs/utils@5.4.12':
|
||||
resolution: {integrity: sha512-ycG7nL+oaRDgp2P18d7pXFqpaNQM7Eck5THnjIQng3CfUHyg6VwOgA3zQnAiK1mlyphvIunH8/+oEq5sx2WDwQ==}
|
||||
|
||||
'@cspotcode/source-map-support@0.8.1':
|
||||
resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==}
|
||||
@ -6336,8 +6357,8 @@ packages:
|
||||
resolution: {integrity: sha512-4B/qKCfeE/ODUaAUpSwfzazo5x29WD4r3vXiWsB7I2mSDAihwEqKO+g8GELZUQSSAo5e1XTYh3ZVfLyxBc12nA==}
|
||||
engines: {node: '>= 10.0.0'}
|
||||
|
||||
adm-zip@0.5.16:
|
||||
resolution: {integrity: sha512-TGw5yVi4saajsSEgz25grObGHEUaDrniwvA2qwSC060KfqGPdglhvPMA2lPIoxs3PQIItj2iag35fONcQqgUaQ==}
|
||||
adm-zip@0.5.17:
|
||||
resolution: {integrity: sha512-+Ut8d9LLqwEvHHJl1+PIHqoyDxFgVN847JTVM3Izi3xHDWPE4UtzzXysMZQs64DMcrJfBeS/uoEP4AD3HQHnQQ==}
|
||||
engines: {node: '>=12.0'}
|
||||
|
||||
agent-base@6.0.2:
|
||||
@ -7045,6 +7066,10 @@ packages:
|
||||
resolution: {integrity: sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==}
|
||||
engines: {node: '>= 10'}
|
||||
|
||||
clipper2-ts@2.0.1:
|
||||
resolution: {integrity: sha512-raIgNMpYN/PFYk/T9iRzaG99rf5nlXBWL1FyAdR3wjEYSzJy+pPfolLH5bCdRqCFQqn/eYUsF1jb3cQEMxmWGw==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
|
||||
cliui@6.0.0:
|
||||
resolution: {integrity: sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==}
|
||||
|
||||
@ -7590,8 +7615,9 @@ packages:
|
||||
dayjs@1.11.20:
|
||||
resolution: {integrity: sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==}
|
||||
|
||||
dcmjs@0.49.4:
|
||||
resolution: {integrity: sha512-w77Gde5JvLjg37FyGIAsTB+oWZIqkO/5NvBeheEVKy6k9XjBgeGsoAbVAByjuGAFLpGqRbf9iWI0edBq87yu/g==}
|
||||
dcmjs@0.52.0:
|
||||
resolution: {integrity: sha512-0/mT9NACKnwnOnkUx7JAqmWxRcUr7HiCoptw1KhN2StNYE0Hy6+tmpI7CU+lJuBc4rFpcMCalvI8QO6JkJ+7Fg==}
|
||||
engines: {node: '>=22.13'}
|
||||
|
||||
debounce@1.2.1:
|
||||
resolution: {integrity: sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==}
|
||||
@ -8508,6 +8534,9 @@ packages:
|
||||
gl-matrix@3.4.3:
|
||||
resolution: {integrity: sha512-wcCp8vu8FT22BnvKVPjXa/ICBWRq/zjFfdofZy1WSpQZpphblv12/bOQLBC1rMM7SGOFS9ltVmKOHil5+Ml7gA==}
|
||||
|
||||
gl-matrix@3.4.4:
|
||||
resolution: {integrity: sha512-latSnyDNt/8zYUB6VIJ6PCh2jBjJX6gnDsoCZ7LyW7GkqrD51EWwa9qCoGixj8YqBtETQK/xY7OmpTF8xz1DdQ==}
|
||||
|
||||
glob-parent@6.0.2:
|
||||
resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==}
|
||||
engines: {node: '>=10.13.0'}
|
||||
@ -16201,7 +16230,7 @@ snapshots:
|
||||
core-js: 3.45.1
|
||||
regenerator-runtime: 0.14.1
|
||||
|
||||
'@babel/runtime-corejs3@7.29.0':
|
||||
'@babel/runtime-corejs3@7.29.2':
|
||||
dependencies:
|
||||
core-js-pure: 3.49.0
|
||||
|
||||
@ -16260,23 +16289,23 @@ snapshots:
|
||||
'@colors/colors@1.5.0':
|
||||
optional: true
|
||||
|
||||
'@cornerstonejs/adapters@5.1.3(@cornerstonejs/core@5.1.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.1.3)(@cornerstonejs/utils@5.1.3)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@cornerstonejs/tools@5.1.3(@cornerstonejs/core@5.1.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.1.3)(@cornerstonejs/utils@5.1.3)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@kitware/vtk.js@35.5.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@types/d3-array@3.2.1)(@types/d3-interpolate@3.0.4)(d3-array@3.2.4)(d3-interpolate@3.0.1)(gl-matrix@3.4.3))':
|
||||
'@cornerstonejs/adapters@5.4.12(@cornerstonejs/core@5.4.12(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.4.12)(@cornerstonejs/utils@5.4.12)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@cornerstonejs/tools@5.4.12(@cornerstonejs/core@5.4.12(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.4.12)(@cornerstonejs/utils@5.4.12)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@kitware/vtk.js@35.5.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@types/d3-array@3.2.1)(@types/d3-interpolate@3.0.4)(d3-array@3.2.4)(d3-interpolate@3.0.1)(gl-matrix@3.4.3))':
|
||||
dependencies:
|
||||
'@babel/runtime-corejs2': 7.26.10
|
||||
'@cornerstonejs/core': 5.1.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.1.3)(@cornerstonejs/utils@5.1.3)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0)
|
||||
'@cornerstonejs/tools': 5.1.3(@cornerstonejs/core@5.1.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.1.3)(@cornerstonejs/utils@5.1.3)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@kitware/vtk.js@35.5.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@types/d3-array@3.2.1)(@types/d3-interpolate@3.0.4)(d3-array@3.2.4)(d3-interpolate@3.0.1)(gl-matrix@3.4.3)
|
||||
'@cornerstonejs/core': 5.4.12(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.4.12)(@cornerstonejs/utils@5.4.12)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0)
|
||||
'@cornerstonejs/tools': 5.4.12(@cornerstonejs/core@5.4.12(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.4.12)(@cornerstonejs/utils@5.4.12)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@kitware/vtk.js@35.5.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@types/d3-array@3.2.1)(@types/d3-interpolate@3.0.4)(d3-array@3.2.4)(d3-interpolate@3.0.1)(gl-matrix@3.4.3)
|
||||
buffer: 6.0.3
|
||||
dcmjs: 0.49.4
|
||||
dcmjs: 0.52.0
|
||||
gl-matrix: 3.4.3
|
||||
ndarray: 1.0.19
|
||||
|
||||
'@cornerstonejs/ai@5.1.3(@cornerstonejs/core@5.1.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.1.3)(@cornerstonejs/utils@5.1.3)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@cornerstonejs/tools@5.1.3(@cornerstonejs/core@5.1.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.1.3)(@cornerstonejs/utils@5.1.3)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@kitware/vtk.js@35.5.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@types/d3-array@3.2.1)(@types/d3-interpolate@3.0.4)(d3-array@3.2.4)(d3-interpolate@3.0.1)(gl-matrix@3.4.3))':
|
||||
'@cornerstonejs/ai@5.4.12(@cornerstonejs/core@5.4.12(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.4.12)(@cornerstonejs/utils@5.4.12)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@cornerstonejs/tools@5.4.12(@cornerstonejs/core@5.4.12(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.4.12)(@cornerstonejs/utils@5.4.12)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@kitware/vtk.js@35.5.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@types/d3-array@3.2.1)(@types/d3-interpolate@3.0.4)(d3-array@3.2.4)(d3-interpolate@3.0.1)(gl-matrix@3.4.3))':
|
||||
dependencies:
|
||||
'@babel/runtime-corejs2': 7.26.10
|
||||
'@cornerstonejs/core': 5.1.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.1.3)(@cornerstonejs/utils@5.1.3)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0)
|
||||
'@cornerstonejs/tools': 5.1.3(@cornerstonejs/core@5.1.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.1.3)(@cornerstonejs/utils@5.1.3)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@kitware/vtk.js@35.5.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@types/d3-array@3.2.1)(@types/d3-interpolate@3.0.4)(d3-array@3.2.4)(d3-interpolate@3.0.1)(gl-matrix@3.4.3)
|
||||
'@cornerstonejs/core': 5.4.12(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.4.12)(@cornerstonejs/utils@5.4.12)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0)
|
||||
'@cornerstonejs/tools': 5.4.12(@cornerstonejs/core@5.4.12(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.4.12)(@cornerstonejs/utils@5.4.12)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@kitware/vtk.js@35.5.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@types/d3-array@3.2.1)(@types/d3-interpolate@3.0.4)(d3-array@3.2.4)(d3-interpolate@3.0.1)(gl-matrix@3.4.3)
|
||||
buffer: 6.0.3
|
||||
dcmjs: 0.49.4
|
||||
dcmjs: 0.52.0
|
||||
gl-matrix: 3.4.3
|
||||
lodash.clonedeep: 4.5.0
|
||||
ndarray: 1.0.19
|
||||
@ -16295,10 +16324,10 @@ snapshots:
|
||||
|
||||
'@cornerstonejs/codec-openjph@2.4.7': {}
|
||||
|
||||
'@cornerstonejs/core@5.1.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.1.3)(@cornerstonejs/utils@5.1.3)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0)':
|
||||
'@cornerstonejs/core@5.4.12(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.4.12)(@cornerstonejs/utils@5.4.12)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0)':
|
||||
dependencies:
|
||||
'@cornerstonejs/metadata': 5.1.3
|
||||
'@cornerstonejs/utils': 5.1.3
|
||||
'@cornerstonejs/metadata': 5.4.12
|
||||
'@cornerstonejs/utils': 5.4.12
|
||||
'@kitware/vtk.js': 35.5.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0)
|
||||
comlink: 4.4.2
|
||||
gl-matrix: 3.4.3
|
||||
@ -16308,24 +16337,24 @@ snapshots:
|
||||
- autoprefixer
|
||||
- wslink
|
||||
|
||||
'@cornerstonejs/dicom-image-loader@5.1.3(@cornerstonejs/core@5.1.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.1.3)(@cornerstonejs/utils@5.1.3)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@cornerstonejs/metadata@5.1.3)(dicom-parser@1.8.21)':
|
||||
'@cornerstonejs/dicom-image-loader@5.4.12(@cornerstonejs/core@5.4.12(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.4.12)(@cornerstonejs/utils@5.4.12)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@cornerstonejs/metadata@5.4.12)(dicom-parser@1.8.21)':
|
||||
dependencies:
|
||||
'@cornerstonejs/codec-charls': 1.2.3
|
||||
'@cornerstonejs/codec-libjpeg-turbo-8bit': 1.2.2
|
||||
'@cornerstonejs/codec-openjpeg': 1.3.0
|
||||
'@cornerstonejs/codec-openjph': 2.4.7
|
||||
'@cornerstonejs/core': 5.1.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.1.3)(@cornerstonejs/utils@5.1.3)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0)
|
||||
'@cornerstonejs/metadata': 5.1.3
|
||||
'@cornerstonejs/core': 5.4.12(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.4.12)(@cornerstonejs/utils@5.4.12)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0)
|
||||
'@cornerstonejs/metadata': 5.4.12
|
||||
comlink: 4.4.2
|
||||
dicom-parser: 1.8.21
|
||||
jpeg-lossless-decoder-js: 2.1.2
|
||||
pako: 2.1.0
|
||||
uuid: 9.0.1
|
||||
|
||||
'@cornerstonejs/labelmap-interpolation@5.1.3(@cornerstonejs/core@5.1.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.1.3)(@cornerstonejs/utils@5.1.3)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@cornerstonejs/tools@5.1.3(@cornerstonejs/core@5.1.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.1.3)(@cornerstonejs/utils@5.1.3)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@kitware/vtk.js@35.5.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@types/d3-array@3.2.1)(@types/d3-interpolate@3.0.4)(d3-array@3.2.4)(d3-interpolate@3.0.1)(gl-matrix@3.4.3))(@kitware/vtk.js@35.5.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))':
|
||||
'@cornerstonejs/labelmap-interpolation@5.4.12(@cornerstonejs/core@5.4.12(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.4.12)(@cornerstonejs/utils@5.4.12)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@cornerstonejs/tools@5.4.12(@cornerstonejs/core@5.4.12(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.4.12)(@cornerstonejs/utils@5.4.12)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@kitware/vtk.js@35.5.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@types/d3-array@3.2.1)(@types/d3-interpolate@3.0.4)(d3-array@3.2.4)(d3-interpolate@3.0.1)(gl-matrix@3.4.3))(@kitware/vtk.js@35.5.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))':
|
||||
dependencies:
|
||||
'@cornerstonejs/core': 5.1.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.1.3)(@cornerstonejs/utils@5.1.3)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0)
|
||||
'@cornerstonejs/tools': 5.1.3(@cornerstonejs/core@5.1.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.1.3)(@cornerstonejs/utils@5.1.3)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@kitware/vtk.js@35.5.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@types/d3-array@3.2.1)(@types/d3-interpolate@3.0.4)(d3-array@3.2.4)(d3-interpolate@3.0.1)(gl-matrix@3.4.3)
|
||||
'@cornerstonejs/core': 5.4.12(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.4.12)(@cornerstonejs/utils@5.4.12)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0)
|
||||
'@cornerstonejs/tools': 5.4.12(@cornerstonejs/core@5.4.12(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.4.12)(@cornerstonejs/utils@5.4.12)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@kitware/vtk.js@35.5.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@types/d3-array@3.2.1)(@types/d3-interpolate@3.0.4)(d3-array@3.2.4)(d3-interpolate@3.0.1)(gl-matrix@3.4.3)
|
||||
'@itk-wasm/morphological-contour-interpolation': 1.1.0
|
||||
'@kitware/vtk.js': 35.5.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0)
|
||||
itk-wasm: 1.0.0-b.165
|
||||
@ -16333,36 +16362,37 @@ snapshots:
|
||||
- debug
|
||||
- supports-color
|
||||
|
||||
'@cornerstonejs/metadata@5.1.3':
|
||||
'@cornerstonejs/metadata@5.4.12':
|
||||
dependencies:
|
||||
'@cornerstonejs/calculate-suv': 1.0.3
|
||||
'@cornerstonejs/utils': 5.1.3
|
||||
dcmjs: 0.49.4
|
||||
'@cornerstonejs/utils': 5.4.12
|
||||
dcmjs: 0.52.0
|
||||
gl-matrix: 3.4.3
|
||||
|
||||
'@cornerstonejs/polymorphic-segmentation@5.1.3(@cornerstonejs/core@5.1.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.1.3)(@cornerstonejs/utils@5.1.3)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@cornerstonejs/tools@5.1.3(@cornerstonejs/core@5.1.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.1.3)(@cornerstonejs/utils@5.1.3)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@kitware/vtk.js@35.5.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@types/d3-array@3.2.1)(@types/d3-interpolate@3.0.4)(d3-array@3.2.4)(d3-interpolate@3.0.1)(gl-matrix@3.4.3))(@kitware/vtk.js@35.5.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))':
|
||||
'@cornerstonejs/polymorphic-segmentation@5.4.12(@cornerstonejs/core@5.4.12(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.4.12)(@cornerstonejs/utils@5.4.12)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@cornerstonejs/tools@5.4.12(@cornerstonejs/core@5.4.12(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.4.12)(@cornerstonejs/utils@5.4.12)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@kitware/vtk.js@35.5.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@types/d3-array@3.2.1)(@types/d3-interpolate@3.0.4)(d3-array@3.2.4)(d3-interpolate@3.0.1)(gl-matrix@3.4.3))(@kitware/vtk.js@35.5.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))':
|
||||
dependencies:
|
||||
'@cornerstonejs/core': 5.1.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.1.3)(@cornerstonejs/utils@5.1.3)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0)
|
||||
'@cornerstonejs/tools': 5.1.3(@cornerstonejs/core@5.1.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.1.3)(@cornerstonejs/utils@5.1.3)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@kitware/vtk.js@35.5.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@types/d3-array@3.2.1)(@types/d3-interpolate@3.0.4)(d3-array@3.2.4)(d3-interpolate@3.0.1)(gl-matrix@3.4.3)
|
||||
'@cornerstonejs/core': 5.4.12(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.4.12)(@cornerstonejs/utils@5.4.12)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0)
|
||||
'@cornerstonejs/tools': 5.4.12(@cornerstonejs/core@5.4.12(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.4.12)(@cornerstonejs/utils@5.4.12)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@kitware/vtk.js@35.5.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@types/d3-array@3.2.1)(@types/d3-interpolate@3.0.4)(d3-array@3.2.4)(d3-interpolate@3.0.1)(gl-matrix@3.4.3)
|
||||
'@icr/polyseg-wasm': 0.4.0
|
||||
'@kitware/vtk.js': 35.5.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0)
|
||||
|
||||
'@cornerstonejs/tools@5.1.3(@cornerstonejs/core@5.1.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.1.3)(@cornerstonejs/utils@5.1.3)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@kitware/vtk.js@35.5.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@types/d3-array@3.2.1)(@types/d3-interpolate@3.0.4)(d3-array@3.2.4)(d3-interpolate@3.0.1)(gl-matrix@3.4.3)':
|
||||
'@cornerstonejs/tools@5.4.12(@cornerstonejs/core@5.4.12(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.4.12)(@cornerstonejs/utils@5.4.12)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@kitware/vtk.js@35.5.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0))(@types/d3-array@3.2.1)(@types/d3-interpolate@3.0.4)(d3-array@3.2.4)(d3-interpolate@3.0.1)(gl-matrix@3.4.3)':
|
||||
dependencies:
|
||||
'@cornerstonejs/core': 5.1.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.1.3)(@cornerstonejs/utils@5.1.3)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0)
|
||||
'@cornerstonejs/core': 5.4.12(@babel/preset-env@7.29.7(@babel/core@7.29.7))(@cornerstonejs/metadata@5.4.12)(@cornerstonejs/utils@5.4.12)(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0)
|
||||
'@kitware/vtk.js': 35.5.3(@babel/preset-env@7.29.7(@babel/core@7.29.7))(autoprefixer@10.4.27(postcss@8.5.14))(wslink@2.5.0)
|
||||
'@types/d3-array': 3.2.1
|
||||
'@types/d3-interpolate': 3.0.4
|
||||
'@types/offscreencanvas': 2019.7.3
|
||||
clipper2-ts: 2.0.1
|
||||
comlink: 4.4.2
|
||||
d3-array: 3.2.4
|
||||
d3-interpolate: 3.0.1
|
||||
gl-matrix: 3.4.3
|
||||
lodash.get: 4.4.2
|
||||
|
||||
'@cornerstonejs/utils@5.1.3':
|
||||
'@cornerstonejs/utils@5.4.12':
|
||||
dependencies:
|
||||
dcmjs: 0.49.4
|
||||
dcmjs: 0.52.0
|
||||
|
||||
'@cspotcode/source-map-support@0.8.1':
|
||||
dependencies:
|
||||
@ -20293,7 +20323,7 @@ snapshots:
|
||||
|
||||
address@1.2.2: {}
|
||||
|
||||
adm-zip@0.5.16: {}
|
||||
adm-zip@0.5.17: {}
|
||||
|
||||
agent-base@6.0.2:
|
||||
dependencies:
|
||||
@ -21180,6 +21210,8 @@ snapshots:
|
||||
|
||||
cli-width@3.0.0: {}
|
||||
|
||||
clipper2-ts@2.0.1: {}
|
||||
|
||||
cliui@6.0.0:
|
||||
dependencies:
|
||||
string-width: 4.2.3
|
||||
@ -21830,11 +21862,11 @@ snapshots:
|
||||
|
||||
dayjs@1.11.20: {}
|
||||
|
||||
dcmjs@0.49.4:
|
||||
dcmjs@0.52.0:
|
||||
dependencies:
|
||||
'@babel/runtime-corejs3': 7.29.0
|
||||
adm-zip: 0.5.16
|
||||
gl-matrix: 3.4.3
|
||||
'@babel/runtime-corejs3': 7.29.2
|
||||
adm-zip: 0.5.17
|
||||
gl-matrix: 3.4.4
|
||||
lodash.clonedeep: 4.5.0
|
||||
loglevel: 1.9.2
|
||||
ndarray: 1.0.19
|
||||
@ -22029,7 +22061,7 @@ snapshots:
|
||||
'@cornerstonejs/codec-openjpeg': 1.3.0
|
||||
'@cornerstonejs/codec-openjph': 2.4.7
|
||||
colormap: 2.3.2
|
||||
dcmjs: 0.49.4
|
||||
dcmjs: 0.52.0
|
||||
dicomicc: 0.1.0
|
||||
dicomweb-client: 0.10.4
|
||||
image-type: 4.1.0
|
||||
@ -22255,7 +22287,7 @@ snapshots:
|
||||
has-property-descriptors: 1.0.2
|
||||
has-proto: 1.2.0
|
||||
has-symbols: 1.1.0
|
||||
hasown: 2.0.3
|
||||
hasown: 2.0.4
|
||||
internal-slot: 1.1.0
|
||||
is-array-buffer: 3.0.5
|
||||
is-callable: 1.2.7
|
||||
@ -22319,7 +22351,7 @@ snapshots:
|
||||
|
||||
es-shim-unscopables@1.1.0:
|
||||
dependencies:
|
||||
hasown: 2.0.3
|
||||
hasown: 2.0.4
|
||||
|
||||
es-to-primitive@1.3.0:
|
||||
dependencies:
|
||||
@ -22882,6 +22914,8 @@ snapshots:
|
||||
|
||||
gl-matrix@3.4.3: {}
|
||||
|
||||
gl-matrix@3.4.4: {}
|
||||
|
||||
glob-parent@6.0.2:
|
||||
dependencies:
|
||||
is-glob: 4.0.3
|
||||
@ -23560,7 +23594,7 @@ snapshots:
|
||||
|
||||
is-core-module@2.16.2:
|
||||
dependencies:
|
||||
hasown: 2.0.3
|
||||
hasown: 2.0.4
|
||||
|
||||
is-data-view@1.0.2:
|
||||
dependencies:
|
||||
|
||||
@ -42,7 +42,7 @@ overrides:
|
||||
commander: 8.3.0
|
||||
cross-env: 7.0.3
|
||||
cross-spawn: 7.0.6
|
||||
dcmjs: 0.49.4
|
||||
dcmjs: 0.52.0
|
||||
path-to-regexp: 0.1.13
|
||||
nth-check: 2.1.1
|
||||
trim-newlines: 5.0.0
|
||||
|
||||
@ -36,6 +36,17 @@ const OHIF_OPEN = process.env.OHIF_OPEN !== 'false';
|
||||
const WATCH_IGNORED = /node_modules[\\/](?!@cornerstonejs(?:[\\/]|$))/;
|
||||
const WATCH_AGGREGATE_TIMEOUT = Number(process.env.WATCH_AGGREGATE_TIMEOUT || 1500);
|
||||
|
||||
// `source-map-loader` is not a project dependency — it only serves the local
|
||||
// cs3d-linking workflow (libs/@cornerstonejs, gitignored), so it is resolved
|
||||
// opportunistically and the rule is skipped on installs that lack it.
|
||||
const SOURCE_MAP_LOADER = (() => {
|
||||
try {
|
||||
return require.resolve('source-map-loader');
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
})();
|
||||
|
||||
export default defineConfig({
|
||||
dev: {
|
||||
lazyCompilation: false,
|
||||
@ -75,6 +86,20 @@ export default defineConfig({
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
// Consume the source maps emitted by the linked local Cornerstone
|
||||
// packages (libs/@cornerstonejs, via cs3d:link + cs3d:watch) so browser
|
||||
// stack traces and breakpoints resolve to the original .ts instead of
|
||||
// the bundled dist/esm .js. Scoped to the linked packages only.
|
||||
...(SOURCE_MAP_LOADER
|
||||
? [
|
||||
{
|
||||
test: /\.js$/,
|
||||
enforce: 'pre' as const,
|
||||
use: [SOURCE_MAP_LOADER],
|
||||
include: /libs[\\/]@cornerstonejs[\\/]packages[\\/][^\\/]+[\\/]dist[\\/]esm/,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
test: /\.css$/,
|
||||
use: [
|
||||
|
||||
Loading…
Reference in New Issue
Block a user