Set the fov header field on the .mgz files we write - #873
Merged
Conversation
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.
Contributor
There was a problem hiding this comment.
🟢 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.
This was referenced Sep 5, 2026
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.
This was referenced Sep 5, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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:
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:
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.