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:
Bill Wallace 2026-07-09 20:54:23 -04:00 committed by GitHub
parent b996070583
commit dbb8b1526e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
59 changed files with 2501 additions and 322 deletions

View File

@ -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": {

View File

@ -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": {

View File

@ -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 };
},

View File

@ -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;

View File

@ -0,0 +1,10 @@
import segmentationCustomization from './customizations/segmentationCustomization';
export default function getCustomizationModule() {
return [
{
name: 'default',
value: segmentationCustomization,
},
];
}

View File

@ -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) {

View File

@ -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 => {

View File

@ -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;
}

View 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,
};
}

View File

@ -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) {

View File

@ -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": {

View File

@ -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": {

View File

@ -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",

View File

@ -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) || {};

View File

@ -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 ?? [];

View File

@ -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();

View File

@ -1799,6 +1799,10 @@ describe('SegmentationService', () => {
const segmentationId = 'segmentationId';
const segmentationData = {
segmentationId,
representation: {
type: csToolsEnums.SegmentationRepresentations.Labelmap,
data: {},
},
config: {
label: 'Segmentation 1',
},

View File

@ -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 };
}

View File

@ -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(

View File

@ -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

View File

@ -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": [

View File

@ -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');

View File

@ -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);

View File

@ -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);

View File

@ -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>

View File

@ -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,

View File

@ -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}`;
}

View 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']);
});
});

View 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);
}

View File

@ -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;
}

View File

@ -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();
});
});

View File

@ -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",

View File

@ -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",

View File

@ -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": [

View File

@ -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",

View File

@ -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",

View File

@ -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:*",

View File

@ -41,6 +41,7 @@
"i18next": "17.3.1"
},
"devDependencies": {
"cross-env": "7.0.3",
"webpack-merge": "5.10.0"
}
}

View File

@ -43,6 +43,7 @@
"i18next": "17.3.1"
},
"devDependencies": {
"cross-env": "7.0.3",
"webpack-merge": "5.10.0"
},
"keywords": [

View File

@ -38,6 +38,7 @@
"@babel/runtime": "7.29.7"
},
"devDependencies": {
"cross-env": "7.0.3",
"webpack-merge": "5.10.0"
},
"keywords": [

View File

@ -45,6 +45,7 @@
"i18next": "17.3.1"
},
"devDependencies": {
"cross-env": "7.0.3",
"webpack-merge": "5.10.0"
},
"keywords": [

View File

@ -42,6 +42,7 @@
"i18next": "17.3.1"
},
"devDependencies": {
"cross-env": "7.0.3",
"webpack-merge": "5.10.0"
},
"keywords": [

View File

@ -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:*",

View File

@ -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

View File

@ -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",

View File

@ -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,

View 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" }
}
}

View File

@ -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" }
}
}

View File

@ -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",

View File

@ -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';

View File

@ -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,
});

View File

@ -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];
}

View 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.

View File

@ -0,0 +1,8 @@
{
"label": "Behaviours",
"position": 14,
"link": {
"type": "doc",
"id": "behaviours/README"
}
}

View File

@ -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).

View File

@ -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
View File

@ -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:

View File

@ -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

View File

@ -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: [