Skip to content

Harmonize qform/sform of inputs to avoid mislocated segmentation (#44) - #45

Closed
CedricConday wants to merge 1 commit into
CompImg:mainfrom
CedricConday:fix/harmonize-qform-sform-44
Closed

Harmonize qform/sform of inputs to avoid mislocated segmentation (#44)#45
CedricConday wants to merge 1 commit into
CompImg:mainfrom
CedricConday:fix/harmonize-qform-sform-44

Conversation

@CedricConday

Copy link
Copy Markdown
Contributor

Problem (#44)

A NIfTI stores two affines. The pipeline reads images with nibabel's img.affine, which prefers the sform when sform_code > 0, but greedy (registration) reads the qform. When an earlier co-registration (e.g. SPM) updates only the sform, the two disagree, so the warped/annotated mask is mislocated in FLAIR space. The reporter confirmed that setting qform = sform resolves it.

Quick demonstration of the divergence:

import numpy as np, nibabel as nib
img = nib.Nifti1Image(np.zeros((4, 4, 4), np.float32), np.eye(4))
img.set_qform(np.eye(4), code=1)
sf = np.eye(4); sf[:3, 3] = [10, -5, 3]; img.set_sform(sf, code=2)
img.affine[:3, 3]       # [10, -5, 3]  (nibabel / this pipeline: sform)
img.get_qform()[:3, 3]  # [0,  0,  0]  (greedy: qform)

Fix

Add harmonize_affines() (in utils.py) and call it once on the --t1/--flair inputs, right after the work dir is created, before any tool reads them. It sets both the qform and sform to nibabel's resolved affine, so greedy and nibabel agree. It is a no-op when the two forms already match, so existing inputs are unaffected.

Tests

Added tests/test_utils.py: one test reproducing the SPM-style mismatch (asserts qform == sform == resolved affine after harmonization), one asserting it is a no-op (data + affine unchanged) when the forms already agree. Both pass.

Note: I validated the affine harmonization in isolation; I do not have a greedy/GPU setup to run the full pipeline end-to-end, so a sanity check on a real mismatched scan would be worth confirming. The fix matches the reporter's verified qform = sform workaround.


Disclosure: authored with AI assistance (Claude Code); reviewed by me.

…pImg#44)

nibabel (used throughout the pipeline) reads img.affine, which prefers the
sform when sform_code > 0, while greedy reads the qform. When a prior
co-registration (e.g. SPM) updates only the sform, the two disagree and the
segmentation is mislocated in FLAIR space.

Harmonize both forms to the resolved affine once, on the inputs, before any
tool reads them. No-op when the forms already agree. Adds a unit test.

Fixes CompImg#44
@jqmcginnis

Copy link
Copy Markdown
Collaborator

Hey @CedricConday, thank you very much for the PR and contribution. We will have a look asap and integrate with the next version bump!

CedricConday added a commit to CedricConday/nifti-qc that referenced this pull request Jul 6, 2026
A dependency-light QC gate for NIfTI images. The headline check catches the
silent qform/sform disagreement that mislocates images in world space (the
same class of bug fixed in CompImg/LST-AI#45): tools reading the qform and
tools reading the sform place the image differently, with no error raised.

Also checks affine validity, voxel anisotropy, direction-cosine shear,
non-finite/empty data, and cross-image grid/world-space alignment (T1+FLAIR).
CLI exits non-zero on error-severity findings for use as a CI/preprocessing gate.

20 oracle-verified tests: each check has a synthetic NIfTI built to trigger it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011LLkhd8Cv4YfiCNB2SptYi
@CedricConday
CedricConday force-pushed the fix/harmonize-qform-sform-44 branch from 6b4a00c to 152b2ff Compare August 18, 2026 10:19
jqmcginnis added a commit to jqmcginnis/LST-AI that referenced this pull request Aug 20, 2026
Port of CompImg#45 (fixes CompImg#44) to the PyTorch branch.

A NIfTI stores two affines; nibabel prefers the sform while the v1.x greedy
binary read the qform, so inputs whose sform was updated by a prior
co-registration (e.g. SPM) produced mislocated segmentations. The fix writes
work-dir copies of --t1/--flair with qform and sform both set to nibabel's
resolved affine before anything reads them; originals are untouched and
consistent inputs pass through unchanged.

Beyond the original PR, --existing_seg inputs are harmonized the same way:
a mask read in FLAIR space has the same two-affine ambiguity.

Controlled end-to-end check on this branch (subject with FLAIR qform shifted
10 mm against a correct sform): the v2 stack does NOT reproduce the v1
mislocation - picsl-greedy 1.4.0 resolves the sform like nibabel, and the
un-fixed pipeline already matched the clean-input reference (Dice 0.969).
The harmonization therefore lands as defense in depth: a no-op for
consistent inputs that removes the ambiguity for every downstream reader.

Co-authored-by: Julian McGinnis <julian.mcginnis@tum.de>
@jqmcginnis

Copy link
Copy Markdown
Collaborator

Adressed in #48, closing this.

@jqmcginnis jqmcginnis closed this Aug 20, 2026
@jqmcginnis

Copy link
Copy Markdown
Collaborator

Closing as superseded — but merged in substance, with your authorship: the v2.0.0 line (PR #48) includes your fix ported to the PyTorch codebase, committed with you as the git author (2ecc545), extended to --existing_seg inputs, and with your tests included. It ships in the just-published v2.0.0rc1 (pip install --pre lst-ai).

One finding from validating it end to end that you'll appreciate, given your PR noted you couldn't run the full pipeline: on the v2 stack the original mislocation no longer reproduces even on a deliberately corrupted input (FLAIR qform shifted 10 mm against a correct sform) — picsl-greedy 1.4.0.1 resolves the sform the same way nibabel does, unlike the v1.x compiled greedy. So on v2 your harmonization lands as defense in depth; for the v1.x line, where #44 was reported, it remains the actual fix. Thank you for the contribution and the careful writeup!

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.

2 participants