Skip to content

[Bug] NIfTI volume loader computes wrong imagePositionPatient for non-axial orientations (coronal/sagittal) #2601

@SrZhang4160

Description

@SrZhang4160

Describe the Bug

When loading NIfTI volumes whose main slice plane is coronal or sagittal, the computed imagePositionPatient values are incorrect, resulting in wrong z-spacing from calculateSpacingBetweenImageIds and a distorted (squished/stretched) volume.
The bug is in

. Each component incorrectly uses a different spacing axis (spacing[0], spacing[1], spacing[2]).
const imagePositionPatient = [
parseFloat((origin[0] + imageIdIndex * direction[6] * spacing[0]).toFixed(precision)),
parseFloat((origin[1] + imageIdIndex * direction[7] * spacing[1]).toFixed(precision)),
parseFloat((origin[2] + imageIdIndex * direction[8] * spacing[2]).toFixed(precision)),
];
Since direction[6..8] is the scan-axis-normal (k-axis direction cosine), all three components should use spacing[2] (the slice/k-axis spacing). position_k = origin + k × spacing[2] × scanAxisNormal

Steps to Reproduce

  1. Load any NIfTI file acquired in coronal orientation (or with a non-identity affine that maps the k-axis to a non-Z direction in LPS)
  2. The volume will have in-plane pixel spacing ≠ slice spacing
  3. Use createNiftiImageIdsAndCacheMetadata → volumeLoader.createAndCacheVolume
  4. Observe that the volume appears squished/stretched along the slice direction

The current behavior

When loading a non-axial NIfTI volume (e.g., coronal with in-plane spacing 0.5mm and slice spacing 2.0mm):

  1. createNiftiImageIdsAndCacheMetadata computes imagePositionPatient per slice using the wrong spacing for each world-axis component. For a coronal volume (scanAxisNormal ≈ [0, 1, 0]), the y-component uses spacing[1] (0.5mm, the row pixel spacing) instead of spacing[2] (2.0mm, the slice spacing). The cached slice positions are therefore spaced 0.5mm apart instead of 2.0mm.
  2. When volumeLoader.createAndCacheVolume is called, the chain generateVolumePropsFromImageIds → sortImageIdsAndGetSpacing → calculateSpacingBetweenImageIds reads back these incorrect positions and computes zSpacing = 0.5mm by measuring the dot-product distance between them along the scan axis.
  3. The volume is constructed with spacing = [pixelSpacing[1], pixelSpacing[0], zSpacing] = [0.5, 0.5, 0.5] instead of the correct [0.5, 0.5, 2.0].
  4. Visual result: the rendered volume is squished by a factor of 4 along the slice stacking direction (anterior-posterior for coronal). Anatomy that should span ~180mm appears ~45mm deep. MPR cross-sections through the volume show severely distorted proportions — circles appear as ellipses, organs look flattened.
  5. For sagittal volumes the same class of error occurs on the x-component. For oblique orientations, multiple components are wrong simultaneously, causing both scaling distortion and shear.
  6. Axial volumes are unaffected because the scan axis normal is [0, 0, 1] — the zero components in x and y mask the wrong spacing values, and the z-component happens to use spacing[2] correctly by coincidence.

The expected behavior

The volume should render with correct proportions. The inter-slice spacing used by calculateSpacingBetweenImageIds should equal spacing[2] from the NIfTI affine, not spacing[0] or spacing[1].

System Information

System:
OS: macOS 15.7.3
CPU: (14) arm64 Apple M4 Pro
Memory: 694.00 MB / 48.00 GB
Shell: 5.9 - /bin/zsh
Binaries:
Node: 20.19.5 - /Users/zlin/.nvm/versions/node/v20.19.5/bin/node
npm: 10.9.2 - /Users/zlin/.nvm/versions/node/v20.19.5/bin/npm
Browsers:
Chrome: 144.0.7559.133
Firefox: 147.0.2
Safari: 18.6

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions