Skip to content

Fix: cannot load segmentation after export in local mode#5999

Open
nithin-trenser wants to merge 4 commits into
OHIF:masterfrom
nithin-trenser:fix-segmentation-load-error
Open

Fix: cannot load segmentation after export in local mode#5999
nithin-trenser wants to merge 4 commits into
OHIF:masterfrom
nithin-trenser:fix-segmentation-load-error

Conversation

@nithin-trenser
Copy link
Copy Markdown
Contributor

@nithin-trenser nithin-trenser commented May 7, 2026

Context

Fix issue : #5540 and related fix in segmentation load for multiframe data : cornerstonejs/cornerstone3D#2727

Segmentation is not loading after export in /local mode.
Fixed jump to segment feature for multiframe data.

Changes & Results

  • Loading a DICOM SEG locally in the OSS viewer crashes in OHIFCornerstoneSEGViewport._getReferencedDisplaySetMetadata.
  • Cannot destructure property 'SpacingBetweenSlices' of 'a' as it is undefined.
  • For the SEG instance, SharedFunctionalGroupsSequence.PixelMeasuresSequence can be an empty array ([]). The current code assumes it has an item and does: PixelMeasures = PixelMeasuresSequence[0] -> undefined then destructures { SpacingBetweenSlices, SliceThickness } from undefined -> throws

Fix : Make destructuring resilient to missing/empty PixelMeasuresSequence by defaulting PixelMeasures to {}.

Added jump to segment feature for multiframe data -> Bug ticket in CS3D

Before fix

Before-fix-segmentation-load.mp4

After Fix

After-Fix-segmentation-load.mp4

Testing

  • Go to ohif website. Go to the /local mode.
  • Upload any DICOM data.
  • Add a segmentation to the image with the brush tool.
  • Click on EXPORT DICOM SEG.
  • Load the exported DICOM SEG and observe.

Checklist

PR

  • My Pull Request title is descriptive, accurate and follows the
    semantic-release format and guidelines.

Code

  • My code has been well-documented (function documentation, inline comments,
    etc.)

Public Documentation Updates

  • The documentation page has been updated as necessary for any public API
    additions or removals.

Tested Environment

  • OS: Ubuntu 24.04
  • Node version: 22
  • Browser:Chrome 147.0.7727.138

Greptile Summary

This PR fixes two bugs: a crash when loading locally-exported DICOM SEG files (caused by an empty PixelMeasuresSequence array), and a mutation-of-shared-metadata bug introduced when adding imageId to combined multiframe instances.

  • SEGViewport: Applies || {} to PixelMeasures so an empty/missing PixelMeasuresSequence no longer throws during destructuring; a console.warn is emitted for observability.
  • combineFrameInstance: The NumberOfFrames < 2 early-return now wraps the result in Object.create(instance) instead of returning the raw store reference, and createCombinedValue is refactored to cache a template once and return a fresh Object.create proxy per call — preventing caller mutations (e.g., imageId assignment) from polluting the shared DicomMetadataStore entry.
  • MetadataProvider._getInstance: Explicitly assigns imageId onto the fresh combined object for multiframe instances, matching the shape of single-frame instances.

Confidence Score: 4/5

The SEG crash fix and the createCombinedValue template-plus-fresh-proxy refactor are both correct; however, the final bare return instance in combineFrameInstance (line 157, the RTDOSE/fallback path) still hands the raw DicomMetadataStore reference to MetadataProvider._getInstance, where combined.imageId = imageId would mutate it.

The two primary goals of the PR are achieved cleanly. The remaining gap is the unguarded fallback return at the bottom of combineFrameInstance: when NumberOfFrames is undefined/NaN, no functional group sequences exist, and GridFrameOffsetVector is absent, the function falls through to return instance. The MetadataProvider caller then sets imageId directly on that shared object, which can corrupt metadata for other callers resolving the same SOP instance.

platform/core/src/utils/combineFrameInstance.ts — the fallback return instance at the bottom of the function (line 157) still returns the shared store reference unchanged.

Important Files Changed

Filename Overview
extensions/cornerstone-dicom-seg/src/viewports/OHIFCornerstoneSEGViewport.tsx Adds `
platform/core/src/classes/MetadataProvider.ts Expands _getInstance to explicitly assign imageId on the fresh combined object; relies on combineFrameInstance returning a non-shared proxy, which holds for the fixed paths but not for the still-bare return instance at the end of combineFrameInstance.
platform/core/src/utils/combineFrameInstance.ts Fixes NumberOfFrames < 2 path to return Object.create(instance) instead of the raw reference; refactors createCombinedValue to cache a template and return a fresh proxy per call, preventing mutation of cached objects. The final bare return instance at line 157 (RTDOSE/fallback path) still returns the shared reference.

Comments Outside Diff (1)

  1. platform/core/src/utils/combineFrameInstance.ts, line 157-161 (link)

    P1 Unprotected bare return instance at the bottom of combineFrameInstance still returns the shared store reference directly. This path is reachable when NumberOfFrames is undefined/NaN (skips the < 2 early return and the > 1 condition), no per-frame/shared functional groups exist, and GridFrameOffsetVector is absent. In that scenario the combined.imageId = imageId assignment in MetadataProvider._getInstance mutates the original object from DicomMetadataStore. Wrapping this in Object.create is consistent with how the NumberOfFrames < 2 path was already fixed.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: platform/core/src/utils/combineFrameInstance.ts
    Line: 157-161
    
    Comment:
    Unprotected bare `return instance` at the bottom of `combineFrameInstance` still returns the shared store reference directly. This path is reachable when `NumberOfFrames` is `undefined`/`NaN` (skips the `< 2` early return and the `> 1` condition), no per-frame/shared functional groups exist, and `GridFrameOffsetVector` is absent. In that scenario the `combined.imageId = imageId` assignment in `MetadataProvider._getInstance` mutates the original object from `DicomMetadataStore`. Wrapping this in `Object.create` is consistent with how the `NumberOfFrames < 2` path was already fixed.
    
    
    
    How can I resolve this? If you propose a fix, please make it concise.

Reviews (3): Last reviewed commit: "Merge branch 'master' into fix-segmentat..." | Re-trigger Greptile

@netlify
Copy link
Copy Markdown

netlify Bot commented May 7, 2026

Deploy Preview for ohif-dev failed. Why did it fail? →

Name Link
🔨 Latest commit 1756006
🔍 Latest deploy log https://app.netlify.com/projects/ohif-dev/deploys/6a143be8d46de600081d43d5

Comment on lines +63 to +68
const combined = frameNumber && combineFrameInstance(frameNumber, instance);
if (combined) {
// Add imageId to multiframe result so it matches single-frame instance.
combined.imageId = imageId;
return combined;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Mutation of a cached combined instance

createCombinedValue inside combineFrameInstance caches the per-frame object at instance._parentInstance[frameNumber] and returns the same reference on every subsequent call. Setting combined.imageId = imageId permanently stamps that imageId onto the cached object. If the same SOP-instance frame is ever resolved through two different imageIds (e.g., the same DICOM served from two WADO-RS endpoints), the second caller receives a combined instance whose imageId belongs to the first caller.

Additionally, when NumberOfFrames < 2 combineFrameInstance returns the original instance reference unchanged. If frameNumber is somehow truthy for such an instance, combined === instance, and combined.imageId = imageId mutates the shared object stored in DicomMetadataStore directly — not just its per-frame projection.

Prompt To Fix With AI
This is a comment left during a code review.
Path: platform/core/src/classes/MetadataProvider.ts
Line: 63-68

Comment:
**Mutation of a cached combined instance**

`createCombinedValue` inside `combineFrameInstance` caches the per-frame object at `instance._parentInstance[frameNumber]` and returns the same reference on every subsequent call. Setting `combined.imageId = imageId` permanently stamps that `imageId` onto the cached object. If the same SOP-instance frame is ever resolved through two different imageIds (e.g., the same DICOM served from two WADO-RS endpoints), the second caller receives a combined instance whose `imageId` belongs to the first caller.

Additionally, when `NumberOfFrames < 2` `combineFrameInstance` returns the original `instance` reference unchanged. If `frameNumber` is somehow truthy for such an instance, `combined === instance`, and `combined.imageId = imageId` mutates the shared object stored in `DicomMetadataStore` directly — not just its per-frame projection.

How can I resolve this? If you propose a fix, please make it concise.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed by avoiding mutation of cached/shared metadata objects in combineFrameInstance.

Previously, combined.imageId = imageId modified shared references returned from createCombinedValue, which could leak imageId values across callers. For single-frame instances, it could also mutate the original metadata object stored in DicomMetadataStore.

Updated the implementation to return a cloned object before assigning imageId.

@sen-trenser
Copy link
Copy Markdown

@sedghi Could you please take a look at this PR and provide your feedback?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants