Skip to content

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

Open
nithin-trenser wants to merge 8 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 8 commits into
OHIF:masterfrom
nithin-trenser:fix-segmentation-load-error

Conversation

@nithin-trenser

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

Copy link
Copy Markdown
Contributor

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

Summary by CodeRabbit

  • Bug Fixes
    • Improved robustness when handling missing metadata sequences.
    • Enhanced multiframe medical image instance processing.
    • Refined object handling to prevent unintended data mutations.

Greptile Summary

This PR fixes a crash when loading an exported DICOM SEG in local mode, caused by PixelMeasuresSequence being an empty array, and addresses a imageId mutation issue on shared/cached metadata objects in combineFrameInstance.

  • SEG viewport crash fix: _getReferencedDisplaySetMetadata now defaults PixelMeasures to {} when PixelMeasuresSequence is missing or empty, preventing the destructuring crash on SpacingBetweenSlices/SliceThickness.
  • Mutation protection in combineFrameInstance: The NumberOfFrames < 2 early-return and createCombinedValue both now return Object.create(…) fresh proxies, so MetadataProvider._getInstance's combined.imageId = imageId assignment no longer stamps the shared DicomMetadataStore reference.
  • MetadataProvider._getInstance: Explicitly captures combined and stamps imageId only on the now-safe per-frame proxy.

Confidence Score: 4/5

Safe to merge for the crash fix; the combineFrameInstance refactor introduces a per-frame cache regression worth addressing before high-frame-count multiframe workflows regress in practice.

The SEG viewport crash fix and MetadataProvider imageId-stamping changes are straightforward and correct. The createCombinedValue refactor, however, stores the per-frame template on an ephemeral sharedProxy instead of the stable shared template, so the per-frame cache never hits after the first lookup. The bare return instance fall-through at the bottom of combineFrameInstance (reachable when NumberOfFrames is absent) also still returns the shared store object directly, which combined.imageId = imageId in MetadataProvider can mutate.

platform/core/src/utils/combineFrameInstance.ts — per-frame cache parent selection and the bare return instance fall-through.

Important Files Changed

Filename Overview
extensions/cornerstone-dicom-seg/src/viewports/OHIFCornerstoneSEGViewport.tsx Adds `
platform/core/src/classes/MetadataProvider.ts Extracts combined explicitly and stamps imageId onto it before returning; safe because createCombinedValue now returns a fresh proxy instead of the cached reference.
platform/core/src/utils/combineFrameInstance.ts NumberOfFrames < 2 path now returns Object.create(instance) to protect the store. createCombinedValue refactored to return fresh proxies — fixes mutation but breaks per-frame caching because the ephemeral sharedProxy is used as the cache parent for per-frame data.

Comments Outside Diff (2)

  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.
  2. platform/core/src/utils/combineFrameInstance.ts, line 157-161 (link)

    P1 The bare return instance returns the shared DicomMetadataStore reference directly. Since MetadataProvider._getInstance then does combined.imageId = imageId on the returned value, this permanently mutates the store object for any instance where NumberOfFrames is undefined (single-frame DICOM that omits the tag) and the imageId URL carries a truthy frame token. Returning Object.create(instance) here is consistent with the fix already applied to the NumberOfFrames < 2 path above.

    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:
    The bare `return instance` returns the shared `DicomMetadataStore` reference directly. Since `MetadataProvider._getInstance` then does `combined.imageId = imageId` on the returned value, this permanently mutates the store object for any instance where `NumberOfFrames` is `undefined` (single-frame DICOM that omits the tag) and the imageId URL carries a truthy frame token. Returning `Object.create(instance)` here is consistent with the fix already applied to the `NumberOfFrames < 2` path above.
    
    
    
    How can I resolve this? If you propose a fix, please make it concise.

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

@netlify

netlify Bot commented May 7, 2026

Copy link
Copy Markdown

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

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

Comment thread platform/core/src/classes/MetadataProvider.ts
@sen-trenser

Copy link
Copy Markdown

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

@wayfarer3130 wayfarer3130 left a comment

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.

There is a larger PR to fix a number of segmentation store issues, and that one reliably re-loads afterwards. I'd like to get that one merged, and then see if there is still anything required here to fix.

@coderabbitai

coderabbitai Bot commented Jun 12, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Three files receive defensive improvements to metadata handling: combineFrameInstance protects cached objects from caller mutations by returning isolated inheriting objects; MetadataProvider explicitly assigns imageId to combined multiframe instances; OHIFCornerstoneSEGViewport safely handles missing pixel measures metadata with fallback and warning.

Changes

Metadata Instance Mutation Protection and Defensive Fallbacks

Layer / File(s) Summary
Instance combination and mutation protection
platform/core/src/utils/combineFrameInstance.ts
combineFrameInstance returns fresh inheriting objects when NumberOfFrames < 2 and createCombinedValue caches a non-writable template while returning per-call inheriting objects, preventing caller mutations from affecting shared state. Formatting adjusted for readability.
MetadataProvider multiframe handling with imageId assignment
platform/core/src/classes/MetadataProvider.ts
MetadataProvider._getInstance explicitly computes combined multiframe instances, assigns imageId to the combined result, and falls back to the original instance when no combined result is produced.
Defensive pixel measures derivation in viewport
extensions/cornerstone-dicom-seg/src/viewports/OHIFCornerstoneSEGViewport.tsx
_getReferencedDisplaySetMetadata derives PixelMeasures with an empty-object fallback and emits a console warning when PixelMeasuresSequence is absent or empty.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 A rabbit hops through metadata fields,
Creating fresh copies, never shields—
No mutations lurk in shared old state,
Each caller gets their own, pristine and great!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The pull request title clearly and concisely describes the primary fix: addressing the segmentation load failure after export in local mode, which is directly supported by the changes.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description check ✅ Passed PR description is comprehensive and well-structured, including context (linked issues #5540 and cornerstone3D#2727), detailed changes with error explanation, testing steps, and completed checklist items.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@greptile-apps

greptile-apps Bot commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

Want your agent to iterate on Greptile's feedback? Try greploops.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@extensions/cornerstone-dicom-seg/src/viewports/OHIFCornerstoneSEGViewport.tsx`:
- Around line 293-300: Guard against a missing SharedFunctionalGroup before
destructuring: in OHIFCornerstoneSEGViewport, check that SharedFunctionalGroup
exists/truthy (and that SharedFunctionalGroupsSequence is present) before doing
"const { PixelMeasuresSequence } = SharedFunctionalGroup"; if it is missing,
fall back to extracting PixelMeasuresSequence from per-frame data (e.g.,
PerFrameFunctionalGroupsSequence or the first frame's FunctionalGroups) and then
compute PixelMeasures (PixelMeasuresSequence -> PixelMeasures) as before; update
the console warning to reflect which source was used (shared vs per-frame) and
ensure PixelMeasures is initialized to {} when neither source provides values to
avoid the crash.

In `@platform/core/src/classes/MetadataProvider.ts`:
- Around line 63-69: The code assigns imageId to combined which can sometimes be
the original shared instance returned by combineFrameInstance; avoid mutating
shared store objects by checking if combined === instance and, if so, create a
shallow copy (or an isolated clone) before assigning imageId; update the logic
around combined/instance in MetadataProvider.ts (referencing
combineFrameInstance, combined, instance, imageId) so you only mutate a newly
created object, then return that copy, otherwise return the original instance
unchanged.

In `@platform/core/src/utils/combineFrameInstance.ts`:
- Around line 166-197: The per-frame template cache is being bypassed because
callers pass a fresh parent object each lookup, so parent[key] never stabilizes;
fix by ensuring the cached-template parent is stable across frame calls—either
make the caller reuse the same sharedInstance object for all frames (so
parent[key] can be reused), or implement a persistent cache keyed by frame
identifier (e.g., a module-level Map) and attach the template to that stable
parent instead of ephemeral objects; adjust code that constructs the
parent/sharedInstance so templateInstance (parent[key]) is created once and
reused.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 01117ce1-38d1-46c4-aa5a-6b29e68fe87b

📥 Commits

Reviewing files that changed from the base of the PR and between 571c2b4 and e32cb82.

📒 Files selected for processing (3)
  • extensions/cornerstone-dicom-seg/src/viewports/OHIFCornerstoneSEGViewport.tsx
  • platform/core/src/classes/MetadataProvider.ts
  • platform/core/src/utils/combineFrameInstance.ts

Comment on lines 293 to +300
const { PixelMeasuresSequence } = SharedFunctionalGroup;

const PixelMeasures = Array.isArray(PixelMeasuresSequence)
? PixelMeasuresSequence[0]
: PixelMeasuresSequence;
const PixelMeasures =
(Array.isArray(PixelMeasuresSequence) ? PixelMeasuresSequence[0] : PixelMeasuresSequence) || {};

if (!PixelMeasuresSequence || Object.keys(PixelMeasures).length === 0) {
console.warn('PixelMeasuresSequence missing from SEG instance metadata.');
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Guard SharedFunctionalGroup before destructuring to avoid a remaining crash path.

On Line 293, const { PixelMeasuresSequence } = SharedFunctionalGroup; can still throw when SharedFunctionalGroupsSequence is missing/empty, so the new fallback on Lines 295–300 is never reached in that case. Please guard SharedFunctionalGroup first (and ideally fall back to per-frame pixel measures).

Proposed fix
 function _getReferencedDisplaySetMetadata(referencedDisplaySet, segDisplaySet) {
-  const { SharedFunctionalGroupsSequence } = segDisplaySet.instance;
+  const { SharedFunctionalGroupsSequence, PerFrameFunctionalGroupsSequence } = segDisplaySet.instance;

   const SharedFunctionalGroup = Array.isArray(SharedFunctionalGroupsSequence)
     ? SharedFunctionalGroupsSequence[0]
     : SharedFunctionalGroupsSequence;

-  const { PixelMeasuresSequence } = SharedFunctionalGroup;
+  const PerFrameFunctionalGroup = Array.isArray(PerFrameFunctionalGroupsSequence)
+    ? PerFrameFunctionalGroupsSequence[0]
+    : PerFrameFunctionalGroupsSequence;
+
+  const PixelMeasuresSequence =
+    SharedFunctionalGroup?.PixelMeasuresSequence ?? PerFrameFunctionalGroup?.PixelMeasuresSequence;

   const PixelMeasures =
     (Array.isArray(PixelMeasuresSequence) ? PixelMeasuresSequence[0] : PixelMeasuresSequence) || {};

   if (!PixelMeasuresSequence || Object.keys(PixelMeasures).length === 0) {
     console.warn('PixelMeasuresSequence missing from SEG instance metadata.');
   }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@extensions/cornerstone-dicom-seg/src/viewports/OHIFCornerstoneSEGViewport.tsx`
around lines 293 - 300, Guard against a missing SharedFunctionalGroup before
destructuring: in OHIFCornerstoneSEGViewport, check that SharedFunctionalGroup
exists/truthy (and that SharedFunctionalGroupsSequence is present) before doing
"const { PixelMeasuresSequence } = SharedFunctionalGroup"; if it is missing,
fall back to extracting PixelMeasuresSequence from per-frame data (e.g.,
PerFrameFunctionalGroupsSequence or the first frame's FunctionalGroups) and then
compute PixelMeasures (PixelMeasuresSequence -> PixelMeasures) as before; update
the console warning to reflect which source was used (shared vs per-frame) and
ensure PixelMeasures is initialized to {} when neither source provides values to
avoid the crash.

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Avoid mutating shared store instances when combineFrameInstance returns the original object.

This block assumes combined is isolated, but some paths can return the original instance; in those cases, assigning imageId here reintroduces shared metadata mutation.

💡 Suggested fix (isolate before assignment when needed)
-    const combined = frameNumber && combineFrameInstance(frameNumber, instance);
-    if (combined) {
-      // Add imageId to multiframe result so it matches single-frame instance.
-      combined.imageId = imageId;
-      return combined;
-    }
-    return instance;
+    const combined = frameNumber && combineFrameInstance(frameNumber, instance);
+    if (!combined) {
+      return instance;
+    }
+
+    // Some paths can return the original cached instance.
+    const result = combined === instance ? Object.create(instance) : combined;
+    result.imageId = imageId;
+    return result;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@platform/core/src/classes/MetadataProvider.ts` around lines 63 - 69, The code
assigns imageId to combined which can sometimes be the original shared instance
returned by combineFrameInstance; avoid mutating shared store objects by
checking if combined === instance and, if so, create a shallow copy (or an
isolated clone) before assigning imageId; update the logic around
combined/instance in MetadataProvider.ts (referencing combineFrameInstance,
combined, instance, imageId) so you only mutate a newly created object, then
return that copy, otherwise return the original instance unchanged.

Comment on lines +166 to 197
// Ensure there's a cached template on the parent (a non-writable, non-enumerable object)
if (!parent[key]) {
const templateInstance = Object.create(parent);
Object.defineProperty(parent, key, {
value: templateInstance,
writable: false,
enumerable: false,
});

if (functionalGroups) {
const shared = Object.values(functionalGroups)
.filter(Boolean)
.map(it => it[0])
.filter(it => typeof it === 'object')
: [];

// merge the shared first then the per frame to override
[...shared].forEach(item => {
if (item.SOPInstanceUID) {
// This sub-item is a previous value information item, so don't merge it
return;
.filter(it => typeof it === 'object');

// merge the shared/per-frame attributes onto the template
[...shared].forEach(item => {
if (item.SOPInstanceUID) {
// This sub-item is a previous value information item, so don't merge it
return;
}
Object.entries(item).forEach(([k, value]) => {
templateInstance[k] = value;
});
});
}
Object.entries(item).forEach(([key, value]) => {
newInstance[key] = value;
});
});
return newInstance;
}

// Return a fresh object that inherits from the cached template so
// mutations by the caller do not affect the cached template.
return Object.create(parent[key]);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Per-frame template caching is effectively bypassed.

createCombinedValue caches by parent[key], but frame-level calls currently pass a fresh sharedInstance parent each lookup, so frame templates are rebuilt repeatedly instead of reused.

💡 Suggested fix (preserve stable frame-cache parent)
@@
     const sharedInstance = createCombinedValue(
       instance._parentInstance,
       SharedFunctionalGroupsSequence?.[0],
       '_shared'
     );
+    const sharedTemplate = Object.getPrototypeOf(sharedInstance);
     const newInstance = createCombinedValue(
-      sharedInstance,
+      sharedTemplate,
       PerFrameFunctionalGroupsSequence?.[frameNumber - 1],
       frameNumber
     );
@@
     const sharedInstance = createCombinedValue(
       instance._parentInstance,
       SharedFunctionalGroupsSequence?.[0],
       '_shared'
     );
+    const sharedTemplate = Object.getPrototypeOf(sharedInstance);

     const newInstance = createCombinedValue(
-      sharedInstance,
+      sharedTemplate,
       PerFrameFunctionalGroupsSequence?.[frameNumber - 1],
       frameNumber
     );
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@platform/core/src/utils/combineFrameInstance.ts` around lines 166 - 197, The
per-frame template cache is being bypassed because callers pass a fresh parent
object each lookup, so parent[key] never stabilizes; fix by ensuring the
cached-template parent is stable across frame calls—either make the caller reuse
the same sharedInstance object for all frames (so parent[key] can be reused), or
implement a persistent cache keyed by frame identifier (e.g., a module-level
Map) and attach the template to that stable parent instead of ephemeral objects;
adjust code that constructs the parent/sharedInstance so templateInstance
(parent[key]) is created once and reused.

@karthik-trenser

Copy link
Copy Markdown

There is a larger PR to fix a number of segmentation store issues, and that one reliably re-loads afterwards. I'd like to get that one merged, and then see if there is still anything required here to fix.

@wayfarer3130 Could you please share the PR link that addresses the segmentation store issues? We'd like to test it and see if it resolves the current problem.

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.

7 participants