Skip to content

Set the fov header field on the .mgz files we write - #873

Merged
m-reuter merged 1 commit into
Deep-MI:devfrom
m-reuter:fov-header
Sep 5, 2026
Merged

Set the fov header field on the .mgz files we write#873
m-reuter merged 1 commit into
Deep-MI:devfrom
m-reuter:fov-header

Conversation

@m-reuter

@m-reuter m-reuter commented Sep 5, 2026

Copy link
Copy Markdown
Member

Every  .mgz  FastSurfer writes from a NIfTI input carried  fov=0 , where the files FreeSurfer writes carry the real extent.  MGHHeader  defaults the field to 0, and a NIfTI header has no  fov  at all, so there was nothing to inherit when the header was converted.
conform  returns an image in the container the input arrived in, so the trigger is the input format, not the voxel size:

input target conform returns saved  .mgz  fov
MGH 1.0  MGHImage  256.0
MGH 0.8  MGHImage  256.0
NIfTI 1.0  Nifti1Image  0.0
NIfTI 0.8  Nifti1Image  0.0

On a released  cpu-v2.5.4  run, 11 of the 41  mri/*.mgz  of the 0.8mm subject have  fov=0 , against 0 of 39 in the v2.3.3 reference.

The value. FreeSurfer stores the largest of the three extents in an MGZ. Probed on FreeSurfer 7.4.1 with volumes whose extents all differ:

extents x/y/z  mri_info  fov largest first axis
100 / 50 / 180 180 yes no
30 / 30 / 30 30 yes yes
80 / 100 / 120 120 yes no
Note this is per container: the same geometry reports the largest extent as  .mgz  and the first-axis extent as  .nii.gz , so  mri_info  on a NIfTI is not the rule to copy for the files we write.

Where it is set. Two places, rather than at each of the eleven files:
 conform  sets it on the target header, immediately after the target zooms and shape are known. That is the root, since  MGHHeader.from_header(nifti_header)  is where the 0 entered, and it covers the  conform.py  CLI and every conformed output.

A new  as_mgh_image()  in  data_utils  derives it from the data being written, so it is also right when a header is inherited from a volume of a different shape. That case is real:  callosum.CC.upright.mgz  was carrying the parent volume's 256 for a 5 mm wide slab, which is wrong on the 1 mm path too, where nothing was zero.

All MGH write sites now route through one of the two. recon_surf/image_io.py applies the same rule inline, because nothing in recon_surf imports FastSurferCNN and this did not seem the place to start.  sample_parc.py  is untouched, since it builds an image to sample from and never writes it, and HypVINN and  apply_warp  write NIfTI, which has no such field.

Impact. No measurement changes, and no FastSurfer or FreeSurfer result changes: FreeSurfer recomputes fov on read and ignores what is stored, which the probes confirm (a file written with a deliberately wrong  fov=42.0  is still reported as 256). What this fixes is the metadata other tools read, including nibabel-based downstream code and our own header comparison in quicktest.

Header bytes do change in those eleven files per subject, so the quicktest reference will differ and should be regenerated after this is released.

Verification. Checked against FreeSurfer rather than against expectation: a slab written with a stale inherited fov of 256 now writes 80.0, and  mri_info  independently computes 80.000 for that same file. test/image/test_mgh_fov.py adds 8 tests covering conform from both containers at both resolutions, the NIfTI target that must gain no fov, the  save_image  round trip that reproduces the 0.8mm path, the stale-slab recompute, and one that separates the two candidate rules. 412 image tests and 77 shell tests pass, ruff clean.

MGHHeader defaults fov to 0 and a NIfTI header has none to inherit, so every .mgz written from a
NIfTI input carried fov=0, which is any resolution and most users, not just the 0.8mm case where we
first saw it. conform now sets it on the target header, and a new as_mgh_image derives it from the
data, which also drops a stale value when the header comes from a volume of another shape, as the
corpus callosum slab did. The value is the largest of the three extents, matching what mri_info
computes for an MGZ.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟢 Approval recommended

The implementation consistently derives MGZ field of view from output geometry and includes focused regression coverage.

Pull request overview

Sets accurate field-of-view metadata on generated MGZ images without changing image measurements.

Changes:

  • Adds centralized MGZ construction with computed fov.
  • Updates conforming and write paths to use the largest spatial extent.
  • Adds regression coverage for formats, resolutions, and stale headers.
File summaries
File Description
test/image/test_mgh_fov.py Tests MGZ field-of-view behavior.
recon_surf/image_io.py Sets fov during SimpleITK conversion.
FastSurferCNN/reduce_to_aseg.py Uses the centralized MGZ constructor.
FastSurferCNN/data_loader/data_utils.py Adds as_mgh_image and updates saving.
FastSurferCNN/data_loader/conform.py Sets fov on MGH target headers.
CorpusCallosum/utils/mapping_helpers.py Corrects metadata on mapped outputs.
CorpusCallosum/paint_cc_into_pred.py Corrects metadata on painted segmentations.
CorpusCallosum/fastsurfer_cc.py Corrects metadata on slab outputs.
CerebNet/datasets/wm_merge_clean.py Uses the centralized MGZ constructor.
Review details
  • Files reviewed: 9/9 changed files
  • Comments generated: 0
  • Review effort level: Balanced

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

@m-reuter
m-reuter merged commit 8bb20d5 into Deep-MI:dev Sep 5, 2026
5 checks passed
m-reuter added a commit to Deep-MI/neuroreg that referenced this pull request Sep 5, 2026
MGHHeader.from_header does not carry two fields over from a non-MGH source header (e.g. NIfTI): fov defaults to 0, and the data dtype defaults to float32 regardless of the array's actual (possibly MGH-supported) dtype -- e.g. a uint8 segmentation silently balloons to float32 on disk. Same root cause as Deep-MI/FastSurfer#873, on two fields.

Added a central as_mgh_image() helper that recomputes both fov (largest physical extent) and the data dtype from the actual array/affine rather than inheriting them, and routed all MGH-writing call sites (save_image, segmentation.py, robreg.py's outlier map, multireg's duplicate _save_image) through it instead of constructing MGHImage directly. Added regression tests covering both fields, a stale inherited header, and an anisotropic round-trip.
m-reuter added a commit to Deep-MI/neuroreg that referenced this pull request Sep 5, 2026
MGHHeader.from_header does not carry two fields over from a non-MGH source header (e.g. NIfTI): fov defaults to 0, and the data dtype defaults to float32 regardless of the array's actual (possibly MGH-supported) dtype -- e.g. a uint8 segmentation silently balloons to float32 on disk. Same root cause as Deep-MI/FastSurfer#873, on two fields.

Added a central as_mgh_image() helper that recomputes both fov (largest physical extent) and the data dtype from the actual array/affine rather than inheriting them, and routed all MGH-writing call sites (save_image, segmentation.py, robreg.py's outlier map, multireg's duplicate _save_image) through it instead of constructing MGHImage directly. Since save_image requires a recognized output extension, robreg's --outliers CLI flag now resolves an extensionless path to --mapmov's format, else NIfTI if an input is NIfTI, else MGZ, instead of erroring or silently defaulting to MGZ. Added regression tests covering both header fields, a stale inherited header, an anisotropic round-trip, and the --outliers format resolution.
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