Fix: cannot load segmentation after export in local mode#5999
Fix: cannot load segmentation after export in local mode#5999nithin-trenser wants to merge 4 commits into
Conversation
❌ Deploy Preview for ohif-dev failed. Why did it fail? →
|
| const combined = frameNumber && combineFrameInstance(frameNumber, instance); | ||
| if (combined) { | ||
| // Add imageId to multiframe result so it matches single-frame instance. | ||
| combined.imageId = imageId; | ||
| return combined; | ||
| } |
There was a problem hiding this 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.
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.There was a problem hiding this comment.
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.
|
@sedghi Could you please take a look at this PR and provide your feedback? |
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
OHIFCornerstoneSEGViewport._getReferencedDisplaySetMetadata.Cannot destructure property 'SpacingBetweenSlices' of 'a' as it is undefined.SharedFunctionalGroupsSequence.PixelMeasuresSequencecan be an empty array ([]). The current code assumes it has an item and does:PixelMeasures = PixelMeasuresSequence[0]-> undefined then destructures{ SpacingBetweenSlices, SliceThickness }from undefined -> throwsFix : Make destructuring resilient to missing/empty
PixelMeasuresSequenceby defaultingPixelMeasuresto {}.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
Checklist
PR
semantic-release format and guidelines.
Code
etc.)
Public Documentation Updates
additions or removals.
Tested Environment
Greptile Summary
This PR fixes two bugs: a crash when loading locally-exported DICOM SEG files (caused by an empty
PixelMeasuresSequencearray), and a mutation-of-shared-metadata bug introduced when addingimageIdto combined multiframe instances.|| {}toPixelMeasuresso an empty/missingPixelMeasuresSequenceno longer throws during destructuring; aconsole.warnis emitted for observability.combineFrameInstance: TheNumberOfFrames < 2early-return now wraps the result inObject.create(instance)instead of returning the raw store reference, andcreateCombinedValueis refactored to cache a template once and return a freshObject.createproxy per call — preventing caller mutations (e.g.,imageIdassignment) from polluting the shared DicomMetadataStore entry.MetadataProvider._getInstance: Explicitly assignsimageIdonto the fresh combined object for multiframe instances, matching the shape of single-frame instances.Confidence Score: 4/5
The SEG crash fix and the
createCombinedValuetemplate-plus-fresh-proxy refactor are both correct; however, the final barereturn instanceincombineFrameInstance(line 157, the RTDOSE/fallback path) still hands the raw DicomMetadataStore reference toMetadataProvider._getInstance, wherecombined.imageId = imageIdwould 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: whenNumberOfFramesisundefined/NaN, no functional group sequences exist, andGridFrameOffsetVectoris absent, the function falls through toreturn instance. TheMetadataProvidercaller then setsimageIddirectly on that shared object, which can corrupt metadata for other callers resolving the same SOP instance.platform/core/src/utils/combineFrameInstance.ts— the fallbackreturn instanceat the bottom of the function (line 157) still returns the shared store reference unchanged.Important Files Changed
_getInstanceto explicitly assignimageIdon the fresh combined object; relies oncombineFrameInstancereturning a non-shared proxy, which holds for the fixed paths but not for the still-barereturn instanceat the end ofcombineFrameInstance.NumberOfFrames < 2path to returnObject.create(instance)instead of the raw reference; refactorscreateCombinedValueto cache a template and return a fresh proxy per call, preventing mutation of cached objects. The final barereturn instanceat line 157 (RTDOSE/fallback path) still returns the shared reference.Comments Outside Diff (1)
platform/core/src/utils/combineFrameInstance.ts, line 157-161 (link)return instanceat the bottom ofcombineFrameInstancestill returns the shared store reference directly. This path is reachable whenNumberOfFramesisundefined/NaN(skips the< 2early return and the> 1condition), no per-frame/shared functional groups exist, andGridFrameOffsetVectoris absent. In that scenario thecombined.imageId = imageIdassignment inMetadataProvider._getInstancemutates the original object fromDicomMetadataStore. Wrapping this inObject.createis consistent with how theNumberOfFrames < 2path was already fixed.Prompt To Fix With AI
Reviews (3): Last reviewed commit: "Merge branch 'master' into fix-segmentat..." | Re-trigger Greptile