feat: Add support for labelmap seg images in any supported tsuid (also for compressed bitmap)#5806
feat: Add support for labelmap seg images in any supported tsuid (also for compressed bitmap)#5806wayfarer3130 wants to merge 116 commits into
Conversation
✅ Deploy Preview for ohif-dev ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
Viewers
|
||||||||||||||||||||||||||||
| Project |
Viewers
|
| Branch Review |
feat/load-seg-images
|
| Run status |
|
| Run duration | 02m 24s |
| Commit |
|
| Committer | Bill Wallace |
| View all properties for this run ↗︎ | |
| Test results | |
|---|---|
|
|
0
|
|
|
0
|
|
|
0
|
|
|
0
|
|
|
37
|
| View all changes introduced in this branch ↗︎ | |
* fix(security): Use resolution for diff package to patch CVE-2026-24001. --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Joe Boccanfuso <joe.boccanfuso@radicalimaging.com>
Wrap DicomMicroscopyViewport with React.memo and custom areEqual to prevent unnecessary re-renders that trigger clearAnnotations(). --------- Co-authored-by: Joe Boccanfuso <joe.boccanfuso@radicalimaging.com>
* Update playwright screenshots and fix broken tests. --------- Co-authored-by: Joe Boccanfuso <joe.boccanfuso@radicalimaging.com>
removed dead code
* Update all black to background * remove --canvas token, keep intentional bg-black * Move bg-black from grid container to individual viewport panes --------- Co-authored-by: Alireza <ar.sedghi@gmail.com>
--------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Joe Boccanfuso <joe.boccanfuso@radicalimaging.com>
…6960. (#5824) * fix(security): Bump tar to 7.5.9 and lerna to 9.0.4 to fix CVE-2026-26960. Bump sharp to 0.34.5 to fix tar-fs vulnerabilities. * Update node version to 20.19.0 in circleci config. Needed for lerna and cypress tests. * Now installing bun for cypress tests in circleci config. * Use node version 20.19.0 in netlify config.
* Add copy to clipboard option for capture * PR comments * Added toast feedback and updated labels --------- Co-authored-by: Dan Rukas <dan.rukas@gmail.com>
…OHIF-2425 (#5729) - add segmentationExists check to getSopClassHandlerModule - now firing SEGMENTATION_REMOVED and SEGMENTATION_REPRESENTATION_REMOVED events - centralized segmentation removal in a listener - when a segmentation is deleted (completely), remove it from all viewports it overlays - when a segmentation is removed from a viewport, remove it as overlay from the viewport --------- Co-authored-by: Joe Boccanfuso <joe.boccanfuso@radicalimaging.com> Co-authored-by: Bill Wallace <wayfarer3130@gmail.com>
…a result of CVE-2026-26996. (#5830) fix(security): CVE-2026-27212 patched. Various dependency updates as a result of CVE-2026-26996. Ultimately CVE-2026-26996 was ignored because it is only exposed in itk-wasm via CLI and OHIF's other use of minimatch is limited to build/dev environments.
* fix: A couple of changes to enable cs3d integration build * Bun update * fix: Crosshairs tests due to order changes * Fix a race in DicomTagBrowser.spec.ts and update the comparison for the screenshot for seg hydration. * Fix sorting issues by using consistent sort * fix: Inconsistency in scoord loader. Will need an update to screenshot * Fix crosshairs stability issues and random order issues in Scoord * Update the comparison image * Update navigate image * fix: Axios issue * Update to current CS3D
|
@wayfarer3130 please add details for testing this. Thank you. |
|
@claude review |
There was a problem hiding this comment.
Code review is billed via overage credits. To resume reviews, an organization admin can raise the monthly limit at claude.ai/admin-settings/claude-code.
Once credits are available, reopen this pull request to trigger a review.
There was a problem hiding this comment.
Code review is billed via overage credits. To resume reviews, an organization admin can raise the monthly limit at claude.ai/admin-settings/claude-code.
Once credits are available, reopen this pull request to trigger a review.
| return new Promise(resolve => { | ||
| const { unsubscribe } = segmentationService.subscribe( | ||
| segmentationService.EVENTS.SEGMENTATION_LOADING_COMPLETE, | ||
| async (evt: { segDisplaySet?: OhifTypes.DisplaySet }) => { | ||
| if (evt.segDisplaySet?.displaySetInstanceUID !== segmentationId) { | ||
| return; | ||
| } | ||
|
|
||
| unsubscribe(); | ||
| await applyRepresentation(); | ||
| resolve(); | ||
| } | ||
| ); | ||
| }); |
There was a problem hiding this comment.
Promise never settles on SEG load failure
addOverlayRepresentationForDisplaySet returns a Promise that resolves only when SEGMENTATION_LOADING_COMPLETE fires. If _loadSegments throws (e.g., no imageId, adapter error, missing referenced display set), the promise in _load rejects but SEGMENTATION_LOADING_COMPLETE is never broadcast. The Promise returned here never resolves or rejects, so _addOverlayRepresentations — which awaits this — hangs indefinitely, stalling the entire overlay setup for the viewport. The subscriber also leaks because unsubscribe() is never called in the error path.
Prompt To Fix With AI
This is a comment left during a code review.
Path: extensions/cornerstone/src/services/ViewportService/CornerstoneViewportService.ts
Line: 1297-1310
Comment:
**Promise never settles on SEG load failure**
`addOverlayRepresentationForDisplaySet` returns a `Promise` that resolves only when `SEGMENTATION_LOADING_COMPLETE` fires. If `_loadSegments` throws (e.g., no imageId, adapter error, missing referenced display set), the promise in `_load` rejects but `SEGMENTATION_LOADING_COMPLETE` is never broadcast. The `Promise` returned here never resolves or rejects, so `_addOverlayRepresentations` — which `await`s this — hangs indefinitely, stalling the entire overlay setup for the viewport. The subscriber also leaks because `unsubscribe()` is never called in the error path.
How can I resolve this? If you propose a fix, please make it concise.
fedorov
left a comment
There was a problem hiding this comment.
AI-Assisted Code Review
How this review was generated: This review was produced using Claude Code (model:
claude-sonnet-4-6, Anthropic, May 2026). The review process had two passes:
- Diff review — the full PR diff (2,621 lines, 39 files) was analyzed for correctness, style, and known anti-patterns.
- Codebase analysis — the branch was checked out locally at
/Users/af61/github/Viewersand ~160 targeted file reads and greps were performed across the full monorepo to assess cross-cutting impact on unchanged code.All findings were verified against the actual source. Sections marked "Cleared" below were flagged in the diff pass but found safe after full-codebase inspection.
Overview
The PR replaces the ArrayBuffer-based SEG loading path with a per-frame imageId approach that delegates pixel data loading to the Cornerstone image loader. Key changes:
- New imageId-based API for
adaptersSEG.Cornerstone3D.Segmentation.createFromDICOMSegBuffer - Support for the labelmap SEG SOP class (
1.2.840.10008.5.1.4.1.1.66.7) in addition to bitmap - Configurable store encoding (labelmap vs bitmap, transfer syntax) via
customizationService - OHIF runtime properties (
imageId,wadoRoot,frameNumber, etc.) made non-enumerable to prevent dcmjs from writing them into stored DICOM - Centralized DICOM serialization in a new
dicomWriter.tsutility dcmjsbumped from 0.49.4 → 0.51.1 across the monorepo
The architectural direction is sound and several pre-existing bugs are fixed. However there is one critical sequencing issue that would likely prevent the primary feature from working.
Critical Issues
1. SEGMENTATION_LOADING_COMPLETE fires before segmentation is in state — null-guard silently drops overlay
This is the most significant issue, found via full codebase analysis. In SegmentationService.ts, SEGMENTATION_LOADING_COMPLETE is broadcast (line ~587) before addOrUpdateSegmentation is called (line ~611), which is what actually puts the segmentation into state.
The new subscriber in CornerstoneViewportService.ts fires applyRepresentation() immediately on the event. That calls addSegmentationRepresentation → getSegmentation(segmentationId). Because the segmentation isn't in state yet at that point, the null-guard introduced by this same PR at line 302 triggers — logs a warning and silently returns without adding the overlay representation. The SEG labelmap is parsed and loaded, but never applied to the viewport.
Fix: Move the SEGMENTATION_LOADING_COMPLETE broadcast to after addOrUpdateSegmentation completes, or restructure the subscriber to wait until the segmentation is in state before calling addSegmentationRepresentation.
2. Promise in addOverlayRepresentationForDisplaySet never resolves on SEG load failure
If _loadSegments throws (no imageId, adapter error, missing referenced display set), SEGMENTATION_LOADING_COMPLETE is never fired. The Promise returned by addOverlayRepresentationForDisplaySet never resolves or rejects, so _addOverlayRepresentations stalls indefinitely. In practice this blocks the VIEWPORT_DATA_CHANGED broadcast but not viewport rendering (since viewport.setStack completes before overlay setup is awaited). Still, any downstream update dependent on that broadcast is silently dropped with no way to recover short of a page reload. No error event subscription, timeout, or rejection path is present.
Issues Flagged in Diff Pass, Cleared by Codebase Analysis
frameIndex → frameNumber rename: Not a breaking change. MetadataProvider.getUIDsFromImageID always overrides the stored field with the URL-parsed frame value; the stored field name was always ignored.
baseImageURIForMetadata coverage: Correct and consistent. The new function strips both ?frame= and &frame=, fixing an inconsistency in the old code that only stripped &frame=. Keys remain consistent within a session.
getImageIdsForInstance behavior change: Correct fix. The old code always used &frame= even for URLs with no existing query params; appendFrameQueryToImageId correctly chooses ? vs &. No callers depended on the old behavior.
displaySets={[referencedDisplaySet, segDisplaySet]}: Correct fix. OHIFCornerstoneViewport uses only viewportData.data[0].imageIds for the stack; SEG is picked up by _processExtraDisplaySetsForViewport as an overlay. No double rendering.
Non-enumerable frameNumber: Safe. All access patterns use direct property access (destructuring, dot notation) — no code uses Object.keys, JSON.stringify, or spread on combined frame instances expecting frameNumber.
Medium Issues
3. tmtv RT export not updated
tmtv/src/utils/dicomRTAnnotationExport/RTStructureSet/dicomRTAnnotationExport.js still calls dcmjs.data.datasetToBlob directly without makeExistingPropertiesNonEnumerable. Any OHIF runtime properties that remain enumerable on RT export instances will be serialized as DICOM tags, potentially corrupting RT output. This is pre-existing, but the PR introduces the fix pattern everywhere else and misses this file.
4. Blob URL memory leak in registerNaturalizedDatasetForLocalWadouri
dicomImageLoader.wadouri.fileManager.add(blob) registers a blob URL that is never revoked. Each report store/download/clipboard operation accumulates a blob in memory for the session lifetime. No fileManager.remove() call exists anywhere in the codebase. For long sessions with many reports this could be meaningful.
5. Duplicate getDatasetTransferSyntaxUID function
Defined privately in both dicomWriter.ts and DicomWebDataSource/index.ts with slightly different fallback logic (the latter falls back to EXPLICIT_VR_LITTLE_ENDIAN; the former returns undefined). The DicomWebDataSource version should import from dicomWriter.ts to avoid future divergence.
6. Dual transferSyntaxUID / transferSyntaxUid keys in segmentationConfig.ts
options.transferSyntaxUID = transferSyntaxUID;
options.transferSyntaxUid = transferSyntaxUID; // camelCase inconsistencyThis is clearly a workaround for an API inconsistency in @cornerstonejs/adapters. Without a comment referencing the upstream issue or PR, future readers will remove one key as an obvious duplicate and silently break one code path.
7. LABELMAP_SEG_SOP_CLASS_UID defined twice
Defined at the top of getSopClassHandlerModule.ts and again in segmentationConfig.ts. The local definition in getSopClassHandlerModule.ts is dead code since getSegmentationParserType is imported from segmentationConfig.ts.
8. registerNaturalizedDatasetForLocalWadouri.js formatting
Every line in this new file is separated by an extra blank line — the entire file is double-spaced. Likely an editor/diff artifact. Makes the file hard to read and should be cleaned up before merge.
Minor / Nits
- Verbose
console.infoin production paths —_logSegImageIdsdumps full imageId arrays on every SEG load;registerNaturalizedDatasetForLocalWadourilogs blob byte size and all frame imageIds. Useful for debugging, noisy in production. - PR checklist not filled out — all checkboxes use
[]; tested OS/Node/Browser fields are blank. @cornerstonejs/adapters 4.22.8still depends on dcmjs 0.49.4 — perbun.lock, adapters may bundle their own copy alongside OHIF's 0.51.1. Worth verifying the adapters' behavior is compatible with the API surface used indicomWriter.ts.
What's Done Well
- Event listener cleanup —
try/finallyaroundSEGMENTATION_LOAD_PROGRESScorrectly fixes a pre-existing leak where N SEG loads accumulated N persistent listeners. - Non-enumerable runtime properties — clean, surgical fix preventing dcmjs from writing OHIF bookkeeping fields into stored DICOM output.
dicomWriter.tscentralization — single place for dcmjs serialization with consistent options and transfer syntax preservation; a clear improvement over scattered ad-hocdatasetToBlobcalls._getSegDataSourcefallback logic — correctly prefersdicomlocalfor local-scheme URLs, falls back to the active data source.appendFrameQueryToImageId— fixes a real bug where&frame=was always used regardless of whether the base URL already had query params.- Null-guards in
addOrUpdateSegmentationanddetermineViewportAndSegmentationType— reasonable defensive programming; the warn log in each case makes skips visible rather than truly silent. - SEG viewport stack fix — passing
[referencedDisplaySet, segDisplaySet]instead of just[segDisplaySet]is a meaningful and correct fix; the old code left the viewport with only derived labelmap imageIds as the primary stack, which are not displayable as grayscale images.
Summary
The critical issue is the event ordering in SegmentationService.ts: SEGMENTATION_LOADING_COMPLETE is broadcast before the segmentation is in state, so the new CornerstoneViewportService subscriber calls addSegmentationRepresentation too early, hits the null-guard introduced by this same PR, and silently exits — meaning the labelmap overlay is never applied. This would be caught only by end-to-end testing; the null-guard makes it invisible as a code-level error.
Context
This PR adds integration with the cs3d adapter changes that provide an image loader instead of providing a full binary segmentation object. That results in significantly faster loading and saving of segmentations.
cs3d_ref: fix/use-imageLoader-for-seg
Changes & Results
Testing
Created testdata for all 4 formats we support:
OHIF/viewer-testdata#11
Will upload this data shortly for testing.
Checklist
PR
semantic-release format and guidelines.
Code
etc.)
Public Documentation Updates
additions or removals.
Tested Environment
Greptile Summary
This PR integrates the
@cornerstonejs/adaptersimage-loader-based SEG API, replacing the previous ArrayBuffer fetch path with a per-frame imageId approach for faster loading and saving of segmentations. It also adds labelmap SEG SOP class support, configurable store encoding viacustomizationService, and makes OHIF runtime instance properties non-enumerable to prevent dcmjs from serialising them into DICOM output.getSopClassHandlerModule.ts):_loadSegmentsnow resolves per-frame imageIds through the active data source instead of fetching a raw buffer;createFromDICOMSegBufferreceives an imageId string as its second argument matching the updated adapters API.CornerstoneViewportService.ts):addOverlayRepresentationForDisplaySetnow waits forSEGMENTATION_LOADING_COMPLETEbefore applying the labelmap representation, but the waiting Promise has no rejection or timeout path — a SEG load error leaves it pending indefinitely and leaks the subscriber.combineFrameInstance.ts, both data sources,dicomWriter.ts):imageId,wadoRoot,frameNumber, and_parentInstanceare redefined as non-enumerable so they are invisible todcmjs.datasetToDict, preventing them from being written into stored DICOM files.Confidence Score: 3/5
The overlay representation setup for SEG viewports can hang indefinitely when a SEG fails to load, blocking the viewport's entire overlay pipeline with no way to recover short of a page reload.
The new async wait-for-SEG pattern in CornerstoneViewportService subscribes to SEGMENTATION_LOADING_COMPLETE but provides no rejection path and no timeout. Because _addOverlayRepresentations sequentially awaits each overlay's addOverlayFn, a single SEG load error stalls it forever — the subscriber also accumulates with each failed attempt. This affects any user who opens a study containing a SEG series that fails to load.
extensions/cornerstone/src/services/ViewportService/CornerstoneViewportService.ts — the addOverlayRepresentationForDisplaySet method needs a rejection path (or timeout) wired to a SEG load-failure event so the returned Promise always settles.
Important Files Changed
Comments Outside Diff (1)
extensions/cornerstone-dicom-seg/src/getSopClassHandlerModule.ts, line 216-221 (link)The
SEGMENTATION_LOAD_PROGRESSlistener is registered every time_loadSegmentsruns but never cleaned up. Loading N SEG display sets accumulates N listeners, so each subsequent progress tick fires N broadcasts ofSEGMENT_LOADING_COMPLETE, causing incorrect progress UI and a growing memory leak.Then add
eventTarget.removeEventListener(Enums.Events.SEGMENTATION_LOAD_PROGRESS, onProgress);in afinallyblock (or right after theawait).Prompt To Fix With AI
Prompt To Fix All With AI
Reviews (10): Last reviewed commit: "fix: Use newer load/seg data set." | Re-trigger Greptile