Skip to content

fix(pt): resolve Philips PET private SUV bulkdata before scaling#6096

Open
sedghi wants to merge 1 commit into
masterfrom
fix/pt-suv-philips-bulkdata
Open

fix(pt): resolve Philips PET private SUV bulkdata before scaling#6096
sedghi wants to merge 1 commit into
masterfrom
fix/pt-suv-philips-bulkdata

Conversation

@sedghi

@sedghi sedghi commented Jun 20, 2026

Copy link
Copy Markdown
Member

Problem

When a DICOMweb server delivers the Philips PET private tags SUVScaleFactor (7053,1000) and ActivityConcentrationScaleFactor (7053,1009) as bulkdata, dcmjs naturalization leaves them as { BulkDataURI: '...' } objects under their raw hex keys. OHIF's MetadataProvider returns them verbatim and nothing resolves them.

getPTImageIdInstanceMetadata then copied those objects straight into PhilipsPETPrivateGroup.SUVScaleFactor, and @cornerstonejs/calculate-suv has no bulkdata awareness — its validity guards (!== null/undefined/0) all pass for an object, so for Units === 'CNTS' it returns the { BulkDataURI } object verbatim as suvbw (or multiplies to NaN). No exception is thrown, so SUV scaling is silently corrupted.

Fix

  • Resolve the Philips scalar private tags during ingestion, before INSTANCES_ADDED fires, in both the lazy (_retrieveSeriesMetadataAsyncstoreInstances) and non-lazy (_retrieveSeriesMetadataSync) DICOMweb metadata paths. Resolution reuses the existing value.retrieveBulkData resolver and decodes the buffer VR-aware (printable ASCII → DS/IS text; otherwise little-endian FL/FD). New helper: resolvePETPrivateScalarBulkData.ts.
  • The bulkdata-attach helper (addRetrieveBulkData) is hoisted to the data-source scope so both metadata paths share it (previously only the lazy path attached retrieveBulkData).
  • Harden getPTImageIdInstanceMetadata: coerce values to finite numbers and reject objects (a { BulkDataURI } can never reach calculate-suv), fix the broken Philips-tag guard, read RadiopharmaceuticalInformationSequence robustly (array or object), and import types from the package root instead of @cornerstonejs/calculate-suv/src/types.

Notes

This addresses the metadata/SUV-factor side. Rendering prescaled SUV for PET stored with an identity modality LUT (Philips CNTS, rescaleSlope === 1) additionally requires cornerstonejs/cornerstone3D#2767.

Verification

Tested live against a Philips whole-body PET (Units: CNTS, 255 instances) served via DICOMweb: the private tags resolve to numbers (e.g. SUVScaleFactor 0.00038, decoded from the DS bulkdata "0.00038 "), scalingModule.suvbw is populated for all 255 instances, and SUV scaling renders correctly.

Summary by CodeRabbit

Release Notes

  • Bug Fixes
    • Improved PET imaging bulk data handling by resolving bulk data references into usable numeric values during series/instance retrieval.
    • Strengthened radiopharmaceutical metadata extraction, including more reliable sequence parsing and validation when required fields are missing.
    • Improved numeric coercion and Philips PET private scale factor handling to avoid dropping valid values and to prevent unresolved bulk data from being treated as metadata.
  • Tests
    • Added coverage for PET bulk data decoding/resolution and PT metadata extraction edge cases.

Greptile Summary

This PR fixes silent SUV corruption on Philips PET studies served by DICOMweb servers that deliver the private scalar tags (7053,1000) and (7053,1009) as bulkdata. The fix has two complementary layers: an upstream resolver (resolvePETPrivateScalarBulkData) that decodes the bulkdata into a plain number before INSTANCES_ADDED fires, and a downstream backstop (coerceNumber) inside getPTImageIdInstanceMetadata that prevents any unresolved { BulkDataURI } object from ever reaching calculate-suv.

  • A new resolvePETPrivateScalarBulkData helper auto-detects DS/IS text vs FL/FD binary encoding without relying on the VR field (which dcmjs drops for bulkdata values), mutates the instance in place, and is wired into both the lazy and non-lazy metadata retrieval paths.
  • getPTImageIdInstanceMetadata fixes a logic bug in the Philips-tag guard (the previous || chain was always true), adds firstSequenceItem to handle both array and flattened-object sequence shapes, and coerces all numeric fields through coerceNumber so downstream consumers always see a finite number or undefined.

Confidence Score: 5/5

Safe to merge; the worst-case failure mode (bulkdata fetch fails) leaves SUV factors absent rather than corrupted.

Both code paths (lazy and non-lazy) now resolve the Philips private tags before instances are registered. Errors are caught per-instance so a single failed fetch cannot block the entire load. The coerceNumber backstop in getPTImageIdInstanceMetadata ensures a BulkDataURI object can never silently corrupt calculate-suv output. Test coverage is thorough across DS text, FL/FD binary, error paths, and the non-PT guard.

No files require special attention; the performance note on resolvePETPrivateScalarBulkData is worth considering for very large series on HTTP/1.1 servers.

Important Files Changed

Filename Overview
extensions/default/src/DicomWebDataSource/utils/resolvePETPrivateScalarBulkData.ts New helper that resolves Philips PET private scalar bulkdata tags into plain numbers; auto-detects DS/IS text vs FL/FD binary encoding; errors are caught and logged without breaking the load path.
extensions/default/src/DicomWebDataSource/utils/resolvePETPrivateScalarBulkData.test.ts Comprehensive tests for decodeNumericBulkData and resolvePETPrivateScalarBulkData covering DS text, FL/FD binary, typed-array views, multi-value, error paths, and non-PT modality guard.
extensions/default/src/DicomWebDataSource/index.ts Hoists addRetrieveBulkData/addRetrieveBulkDataNaturalized to datasource scope so both the non-lazy and lazy metadata paths share it; storeInstances made async and awaits resolvePETPrivateScalarBulkData before INSTANCES_ADDED fires.
extensions/default/src/getPTImageIdInstanceMetadata.ts Fixes broken Philips-tag guard (was always-true OR chain), adds coerceNumber backstop so BulkDataURI objects never reach calculate-suv, adds firstSequenceItem for robust array/object sequence handling, and coerces all numeric fields.
extensions/default/src/getPTImageIdInstanceMetadata.test.ts New tests covering missing metadata errors, array and flattened-object sequence shapes, numeric private tags, unresolved BulkDataURI objects, and DS-string coercion.

Reviews (2): Last reviewed commit: "fix(pt): resolve Philips PET private SUV..." | Re-trigger Greptile

@netlify

netlify Bot commented Jun 20, 2026

Copy link
Copy Markdown

Deploy Preview for ohif-dev ready!

Name Link
🔨 Latest commit 249de93
🔍 Latest deploy log https://app.netlify.com/projects/ohif-dev/deploys/6a36f7de82145500085c0731
😎 Deploy Preview https://deploy-preview-6096--ohif-dev.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

@coderabbitai

coderabbitai Bot commented Jun 20, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds a Philips PET private scalar bulkdata resolution pipeline: DicomWebDataSource now attaches retrieveBulkData methods to DICOM instances via new helpers, then calls the new resolvePETPrivateScalarBulkData utility (which fetches and decodes DS/IS text or IEEE-754 binary payloads) before metadata is processed. getPTImageIdInstanceMetadata is hardened to coerce the resolved numeric values defensively.

Changes

PET Bulkdata Scalar Resolution Pipeline

Layer / File(s) Summary
PET private scalar bulkdata decoder
extensions/default/src/DicomWebDataSource/utils/resolvePETPrivateScalarBulkData.ts
New file defining Philips PET private tag constants (70531000, 70531009), byte-level helpers (toUint8, isPrintableNumeric, decodeText, decodeBinaryFloat), exported decodeNumericBulkData that selects text vs. binary decoding, and resolvePETPrivateScalarBulkData that concurrently mutates PT-modality instances in place.
Decoder test coverage
extensions/default/src/DicomWebDataSource/utils/resolvePETPrivateScalarBulkData.test.ts
Jest tests for decodeNumericBulkData across DS/IS text formats and binary float32/float64; tests for resolvePETPrivateScalarBulkData covering PT instance resolution, fetch-and-decode, cached Value reuse, graceful error handling, and no-op for non-PT instances.
BulkData URI attachment helpers
extensions/default/src/DicomWebDataSource/index.ts
Adds addRetrieveBulkDataNaturalized and addRetrieveBulkData to recursively traverse naturalized datasets, fix relative bulkdata URIs, and attach a retrieveBulkData method from qidoDicomWebClient when bulkDataURI is enabled.
Series retrieval wiring
extensions/default/src/DicomWebDataSource/index.ts
Imports resolvePETPrivateScalarBulkData; replaces naturalizeDataset with addRetrieveBulkData in both sync and async retrieval paths; awaits resolvePETPrivateScalarBulkData before metadata is built; fixes seriesDeliveredPromises to return the storeInstances result.
PET metadata defensive coercion
extensions/default/src/getPTImageIdInstanceMetadata.ts
Adds coerceNumber (rejects object-like bulkdata, accepts finite number/string) and firstSequenceItem (tolerates array or object sequence shapes) helpers; all numeric DICOM field assignments switched to coerceNumber; optional-field guards changed from truthiness to !== undefined; removes unused time-conversion helpers.
Metadata extraction test coverage
extensions/default/src/getPTImageIdInstanceMetadata.test.ts
Jest tests covering error cases (missing metadata, missing required fields), radionuclide value reading from array and flattened RadiopharmaceuticalInformationSequence, Philips private tag handling for numeric vs. unresolved bulkdata values, and numeric string coercion.

Sequence Diagram

sequenceDiagram
  participant DicomWebDataSource
  participant addRetrieveBulkData
  participant qidoDicomWebClient
  participant resolvePETPrivateScalarBulkData
  participant getPTImageIdInstanceMetadata

  DicomWebDataSource->>addRetrieveBulkData: data.map(addRetrieveBulkData)
  addRetrieveBulkData->>qidoDicomWebClient: bind retrieveBulkData to BulkDataURI entries
  addRetrieveBulkData-->>DicomWebDataSource: naturalized instances
  DicomWebDataSource->>resolvePETPrivateScalarBulkData: await resolvePETPrivateScalarBulkData(instances)
  resolvePETPrivateScalarBulkData->>qidoDicomWebClient: retrieveBulkData() for tags 70531000/70531009
  qidoDicomWebClient-->>resolvePETPrivateScalarBulkData: raw byte payload
  resolvePETPrivateScalarBulkData->>resolvePETPrivateScalarBulkData: decodeNumericBulkData (text or binary float)
  resolvePETPrivateScalarBulkData-->>DicomWebDataSource: instances mutated with numeric scalars
  DicomWebDataSource->>getPTImageIdInstanceMetadata: build instance metadata
  getPTImageIdInstanceMetadata->>getPTImageIdInstanceMetadata: coerceNumber on all numeric fields
  getPTImageIdInstanceMetadata-->>DicomWebDataSource: instanceMetadata with resolved PET values
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Poem

🐇 Hop, hop through the bytes so small,
Float32 or DS text — I read them all!
BulkData URIs? No fear, I fetch with glee,
Philips PET scalars resolved, clear as can be.
coerceNumber guards each field with care,
A well-fed rabbit with numbers to spare! 🥕

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 61.54% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ❓ Inconclusive The pull request description is comprehensive and well-structured, covering the problem, fix, verification, and technical details, but does not follow the required template structure with explicit Context, Changes & Results, Testing, and Checklist sections. Reorganize the description to follow the required template format with explicit sections for Context (issue link), Changes & Results, Testing instructions, and a completed Checklist with semantic-release title and environment details.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: resolving Philips PET private SUV bulkdata before scaling.
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.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/pt-suv-philips-bulkdata

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.

@cypress

cypress Bot commented Jun 20, 2026

Copy link
Copy Markdown

Viewers    Run #6409

Run Properties:  status check passed Passed #6409  •  git commit 249de933b9: fix(pt): resolve Philips PET private SUV bulkdata before scaling
Project Viewers
Branch Review fix/pt-suv-philips-bulkdata
Run status status check passed Passed #6409
Run duration 01m 48s
Commit git commit 249de933b9: fix(pt): resolve Philips PET private SUV bulkdata before scaling
Committer Alireza
View all properties for this run ↗︎

Test results
Tests that failed  Failures 0
Tests that were flaky  Flaky 0
Tests that did not run due to a developer annotating a test with .skip  Pending 0
Tests that did not run due to a failure in a mocha hook  Skipped 0
Tests that passed  Passing 28
View all changes introduced in this branch ↗︎

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

🤖 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/default/src/DicomWebDataSource/utils/resolvePETPrivateScalarBulkData.ts`:
- Around line 148-153: The resolvePETPrivateScalarBulkData function uses
unbounded parallelism with Promise.all(instances.map(resolveInstance)), which
can trigger excessive concurrent bulkdata requests and overwhelm the DICOMweb
endpoint. Implement a concurrency limit to control the fan-out by processing
instances in batches or using a concurrency control mechanism (such as a
queue-based approach or a concurrency limiter utility) that allows only a fixed
number of resolveInstance calls to execute simultaneously, ensuring the bulkdata
resolution respects reasonable throughput constraints.

In `@extensions/default/src/getPTImageIdInstanceMetadata.ts`:
- Around line 21-23: The numeric conversion in the string value handling is
converting empty or whitespace-only strings to zero through Number(value), which
silently treats missing DICOM fields as valid zeros. Before calling
Number(value), add a check to ensure the string is not empty or contains only
whitespace; if it is blank, return undefined directly instead of proceeding with
the Number conversion. Only call Number(value) and check Number.isFinite(n) if
the string has actual content.
🪄 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: 80a683b1-39d9-4fcd-a20c-60199ed5218e

📥 Commits

Reviewing files that changed from the base of the PR and between 1f6670c and ea7c131.

📒 Files selected for processing (3)
  • extensions/default/src/DicomWebDataSource/index.ts
  • extensions/default/src/DicomWebDataSource/utils/resolvePETPrivateScalarBulkData.ts
  • extensions/default/src/getPTImageIdInstanceMetadata.ts

Comment on lines +148 to +153
export async function resolvePETPrivateScalarBulkData(instances): Promise<void> {
if (!Array.isArray(instances) || !instances.length) {
return;
}
await Promise.all(instances.map(resolveInstance));
}

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

Limit bulkdata resolution fan-out to avoid request storms.

Promise.all(instances.map(resolveInstance)) triggers unbounded parallel bulkdata fetches on large PT series. This can overload DICOMweb endpoints and destabilize ingestion latency.

Suggested fix
 export async function resolvePETPrivateScalarBulkData(instances): Promise<void> {
   if (!Array.isArray(instances) || !instances.length) {
     return;
   }
-  await Promise.all(instances.map(resolveInstance));
+  for (const instance of instances) {
+    await resolveInstance(instance);
+  }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export async function resolvePETPrivateScalarBulkData(instances): Promise<void> {
if (!Array.isArray(instances) || !instances.length) {
return;
}
await Promise.all(instances.map(resolveInstance));
}
export async function resolvePETPrivateScalarBulkData(instances): Promise<void> {
if (!Array.isArray(instances) || !instances.length) {
return;
}
for (const instance of instances) {
await resolveInstance(instance);
}
}
🤖 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/default/src/DicomWebDataSource/utils/resolvePETPrivateScalarBulkData.ts`
around lines 148 - 153, The resolvePETPrivateScalarBulkData function uses
unbounded parallelism with Promise.all(instances.map(resolveInstance)), which
can trigger excessive concurrent bulkdata requests and overwhelm the DICOMweb
endpoint. Implement a concurrency limit to control the fan-out by processing
instances in batches or using a concurrency control mechanism (such as a
queue-based approach or a concurrency limiter utility) that allows only a fixed
number of resolveInstance calls to execute simultaneously, ensuring the bulkdata
resolution respects reasonable throughput constraints.

Comment thread extensions/default/src/getPTImageIdInstanceMetadata.ts Outdated
When a DICOMweb server delivers the Philips PET private tags SUVScaleFactor
(7053,1000) / ActivityConcentrationScaleFactor (7053,1009) as bulkdata, dcmjs
naturalization leaves them as { BulkDataURI } objects. These were fed verbatim
to calculate-suv, which treats the object as a valid value and silently
corrupts the SUV scaling factors.

Resolve these scalar private tags to numbers during ingestion - in both the
lazy (async) and non-lazy (sync) DICOMweb metadata paths, before INSTANCES_ADDED
fires - by decoding the bulkdata (VR-aware: DS/IS text or little-endian FL/FD).
Harden getPTImageIdInstanceMetadata to coerce values to finite numbers (reusing
@OHIF/core utils.toNumber) and reject unresolved bulkdata objects so they can
never reach calculate-suv. Share the bulkdata-attach helper between both
metadata paths. Adds unit tests for the bulkdata decoder/resolver and for
getPTImageIdInstanceMetadata.
@sedghi sedghi force-pushed the fix/pt-suv-philips-bulkdata branch from ea7c131 to 249de93 Compare June 20, 2026 20:28

// Resolve PET private scalar tags (e.g. the Philips SUV Scale Factor)
// delivered as bulkdata into plain numbers BEFORE INSTANCES_ADDED fires.
await resolvePETPrivateScalarBulkData(naturalizedInstancesMetadata);

@sedghi sedghi Jun 21, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I really hate this solution, open to what you think is best, the issue is dicomimageloader needs this tags for scaling at earliest time before decodeimageframe

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.

1 participant