Skip to content

fix(dicom): match the volume buffer to the decoded pixel values - #934

Merged
PaulHax merged 1 commit into
mainfrom
dicom-volume-buffer
Sep 4, 2026
Merged

fix(dicom): match the volume buffer to the decoded pixel values#934
PaulHax merged 1 commit into
mainfrom
dicom-volume-buffer

Conversation

@PaulHax

@PaulHax PaulHax commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator

Summary

VolView allocates one typed array for the whole DICOM volume. The old allocator chose that array from the first file's metadata, but ITK decodes pixels after applying each file's rescale. The declared storage type and the decoded values can differ.

That caused three failures:

  • Fractional rescale values were truncated when the volume used an integer array.
  • Large rescaled values could exceed vtk.js integer array types.
  • A later file could decode outside the range chosen from the first file and silently wrap or truncate when copied.

Loading flow

DICOM files
    |
    v
read pixel metadata from every file
    |
    v
choose one volume buffer type
    |
    v
decode each file with ITK
    |
    +-- decoded values fit ------> copy into its slice
    |
    +-- values do not fit ------> mark that chunk errored

Choosing the volume buffer

The allocator now uses the union of the ranges declared by every usable file.

  • Fractional slope or intercept uses Float64Array.
  • Integral values use the narrowest supported integer array.
  • Integral ranges outside the supported 32-bit arrays use Float64Array.
  • Files with missing or invalid range metadata are skipped during allocation and checked after decode.
  • Allocation fails only when no file provides usable metadata.

Checking decoded pixels

Before copying a decoded chunk, VolView checks the actual values returned by ITK.

  • Values outside the volume buffer range are rejected.
  • Fractional values are rejected when the volume buffer is an integer array.
  • A bad chunk fails on its own instead of corrupting another slice or the whole volume.

Tradeoffs

  • Mixed metadata can select a wider array than the decoded pixels ultimately need.
  • Fractional rescale uses 8 bytes per voxel in memory.
  • Grouping inconsistent files earlier would be clearer, but is outside this change.

Tests

Coverage includes fractional and negative rescale, mixed bit depths, ranges beyond 32 bits, missing metadata, decoded values that disagree with metadata, the public DSC slope case, and a browser test using generated DICOM files.

@netlify

netlify Bot commented Sep 2, 2026

Copy link
Copy Markdown

Deploy Preview for volview-dev ready!

Name Link
🔨 Latest commit 9dfd670
🔍 Latest deploy log https://app.netlify.com/projects/volview-dev/deploys/6a9b43c76bc6020008a0608c
😎 Deploy Preview https://deploy-preview-934--volview-dev.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

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

The volume buffer's element type was derived from the first instance's
stored range put through its rescale, floored and ceiled to integers. ITK
applies the rescale while decoding and picks the output type from it: an
integral slope and intercept keep an integer type, anything fractional
produces float64. A fractional rescale whose endpoints landed on integers
got an integer buffer, and every fraction was dropped on write. An integral
range past 32 bits threw at allocation. A later instance decoding outside
the first instance's type wrote through TypedArray.set with no error,
wrapping every value the type cannot hold.

Allocate for the union of every instance's declared range, choosing
Float64Array whenever the slope or intercept is fractional or the range is
not a safe 32-bit integer range, and the narrowest integer type otherwise.
Ordinary CT and MR allocation is unchanged. An instance whose tags give no
finite range is left out of the union and judged on its decoded values.

Check each chunk after it decodes and before it is written. Its actual
range must fit the buffer's element type, and float output bound for an
integer buffer must be whole numbers. Both rejections name the file, the
values it decoded to, and the buffer it has to fit. Only decoder output
that disagrees with the tags can fail these checks.

Reproduced with a public DSC perfusion series whose slope is
112067.85375182 over 16-bit unsigned storage.
@PaulHax
PaulHax force-pushed the dicom-volume-buffer branch from 12b54f7 to 9dfd670 Compare September 4, 2026 22:18
@PaulHax
PaulHax added this pull request to the merge queue Sep 4, 2026
Merged via the queue into main with commit 0dd3577 Sep 4, 2026
8 checks passed
@PaulHax
PaulHax deleted the dicom-volume-buffer branch September 4, 2026 22:44
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