Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Analyze 7.5 Format Boundary

Analyze 7.5 stores one logical dataset in two files with the same stem:

brain.hdr   348-byte binary header
brain.img   contiguous binary voxel payload

The original format description is the Analyze 7.5 File Format specification, especially the header organization and image-dimension definitions on pages 3–8. The header gives dimensions, scalar type, bit depth, spacing, and payload offset. The payload contains no delimiters: its expected length follows from the dimension product and bits per voxel.

RITK’s supported subset

RITK deliberately reads a narrow, unambiguous subset:

PropertyAccepted contract
Byte orderlittle-endian
Logical shapeone 3-D volume
Headerexactly 348 bytes
Scalar typesu8, i16, i32, f32, or f64
Returned voxelsf32
Payload offsetfinite, non-negative whole-byte offset
Payload lengthexactly offset plus declared voxel bytes

Big-endian files, four-dimensional series, complex values, RGB values, and negative per-image offsets are rejected with contextual errors. Rejecting an unsupported variant is preferable to decoding it with the wrong byte order or geometry.

The extensions are not sufficient format identification. A paired NIfTI-1 dataset uses ni1\0 at header bytes 344–347 and commonly includes the four-byte extension indicator after the shared 348-byte header. RITK reports that case as paired NIfTI instead of decoding NIfTI affine fields as Analyze history fields. Use the .nii single-file form with RITK’s NIfTI reader. The NIfTI-1 FAQ documents the shared pair layout and extension indicator; the SimpleITK I/O list documents NiftiImageIO as the standard handler for these extensions.

The public API is backend-bound but format behavior is shared:

use coeus_core::SequentialBackend;
use ritk_analyze::{read_analyze, write_analyze};

let backend = SequentialBackend;
let image = read_analyze("brain.hdr", &backend)?;
write_analyze("copy.hdr", &image, &backend)?;

Passing brain.img to read_analyze is equivalent; the reader derives both sibling paths from the stem.

Axis order and byte order

The file describes dimensions as [x, y, z]. X varies fastest in the payload:

file_index(x, y, z) = x + nx·y + nx·ny·z

RITK stores a three-dimensional image with shape [z, y, x] and the same X-fastest flat order:

ritk_index(z, y, x) = z·ny·nx + y·nx + x

The flat sequences are identical, so reading does not transpose or copy an intermediate volume. The header spacing does require a semantic reorder: file [sx, sy, sz] becomes RITK tensor-axis spacing [sz, sy, sx].

Scalar conversion and scaling

datatype selects the stored scalar and bitpix must match it. RITK checks that pair before calculating payload bytes. Each stored value is converted to f32; integer values outside binary32’s exact integer range and finite f64 values can round under that explicit output contract.

The historical funused1 field is used by several Analyze-derived writers as an intensity scale. RITK applies a finite nonzero scale after scalar decoding; zero means one. This convention is not uniform across every Analyze variant, so a foreign pipeline should verify representative values rather than infer calibration from the filename.

Spatial metadata limits

Analyze pixdim[1..3] carries voxel spacing. Non-finite values are rejected; legacy zero or negative spacing is normalized to unit spacing for compatibility.

That normalization deliberately differs from the NIfTI reader, which reports a non-positive pixdim as a scoped spatial loss. NIfTI has sform/qform alternatives and can still describe the volume when pixdim is unusable; Analyze has no other spatial field, so a loss would leave the volume with no geometry at all. Normalizing is the only outcome that returns an image, so the divergence is a property of the formats rather than an inconsistency between the readers.

The original header defines originator as ten history bytes, not a complete world-space transform. RITK’s writer uses the common five-i16 convention and stores rounded voxel coordinates. A physical origin therefore round-trips only to the nearest voxel. The format has no direction matrix, so the RITK reader returns identity direction and the writer rejects a non-identity direction before creating either file rather than writing a dataset whose geometry silently disagrees with the source. Analyze files cannot establish scanner-space orientation as precisely as NIfTI, DICOM, NRRD, or MGH.

The NIfTI-1 rationale explains why NIfTI retained the 348-byte pair layout while adding explicit coordinate-system semantics. Because both formats can use .hdr and .img, select the reader from an authoritative source; extensions alone do not prove which format is present.

Bounded decoding and writing

Before allocating output, the reader validates signed dimensions, checked voxel and byte products, datatype/bit-depth agreement, finite metadata, offset, and exact file length. It then fallibly reserves the final Vec<f32> and streams conversion through an 8 KiB fixed buffer. Peak decoder-owned storage is therefore the returned f32 volume plus constant scratch, not the complete encoded payload plus the returned volume.

The writer validates dimensions, value count, checked byte size, finite spacing representable in the header’s f32 fields, and origin voxel coordinates within the format’s i16 range before creating either file. Header spacing can round from RITK’s f64 metadata to f32. The writer streams little-endian voxel bytes through an 8 KiB buffer and publishes the header after the payload completes; it does not construct a second volume-sized byte vector.

Failure behavior

Reading returns no partial image when any contract fails. Errors identify:

  • invalid or unsupported dimensionality;
  • unsupported endianness or scalar type;
  • paired NIfTI presented through the ambiguous .hdr/.img extensions;
  • mismatched datatype and bitpix;
  • non-finite spacing, scaling, or offset;
  • offset and byte-count overflow;
  • truncated or trailing payload data;
  • allocation, seek, read, or image-construction failure.

Writing rejects zero or oversized dimensions, storage/shape disagreement, non-identity direction, and invalid spatial metadata before creating output. I/O failures remain visible with their source chain.

Checked conversion matrix

Each row maps one Analyze pair property to the observable RITK behavior, the representable-or-lossy verdict, and the value test that pins it. Tests are identified as path::test relative to repos/ritk; run them with cargo test -p ritk-analyze (and -p ritk-io for the dispatch row).

PropertyRITK behaviorTyped verdictValue test
Pair atomicity — preflightValidation runs before either file is created; a rejected write leaves neither .hdr nor .img.consistent pair, or neither destinationcrates/ritk-analyze/src/writer.rs::writer_rejects_invalid_input_before_creating_files, ...::writer_rejects_a_non_identity_direction_before_creating_files
Pair atomicity — commit orderThe .img payload is written and flushed first; the .hdr is published last and is the commit marker. A failure after the payload leaves an inert orphan .img, never a header without its payload.consistent pair, or orphan payload (recovery: re-run the write)crates/ritk-analyze/src/tests.rs::analyze_writer_publishes_the_header_only_after_the_payload
Overwrite behaviorA write to an existing stem replaces both files; the .img is truncated to the new volume rather than appended to.last write winscrates/ritk-analyze/src/tests.rs::analyze_writer_overwrites_an_existing_pair
Scalar types — readu8 (2), i16 (4), i32 (8), f32 (16), f64 (64) decode to f32; bitpix must equal the datatype width; other codes are rejected before allocation.narrowing to f32; u16/u32/i64/u64 unrepresentablecrates/ritk-analyze/src/tests.rs::analyze_reader_decodes_every_supported_scalar_with_scale, ...::analyze_reader_rejects_invalid_geometry_and_bit_depth_before_allocation
Scalar types — writeEmits DT_FLOAT (16) only.f32 → stored f32 is exactcrates/ritk-analyze/src/tests.rs::analyze_roundtrip_preserves_shape_spacing_origin_and_values
Intensity scalefunused1 (0 → 1) multiplies every decoded voxel; a non-finite value is rejected.linear calibration folded into samplescrates/ritk-analyze/src/tests.rs::analyze_reader_decodes_every_supported_scalar_with_scale, ...::analyze_reader_rejects_non_finite_metadata_and_invalid_offsets
Byte order — readLittle-endian only; a big-endian header is identified by name and rejected, not reported as corruption.big-endian unrepresentablecrates/ritk-analyze/src/tests.rs::analyze_reader_rejects_invalid_geometry_and_bit_depth_before_allocation (asserts "big-endian")
Byte order — writeEmits little-endian header fields and little-endian f32 payload; output is byte-stable for a fixed logical image.deterministiccrates/ritk-analyze/src/tests.rs::analyze_writer_output_is_byte_stable_for_native_image
SpacingFile pixdim[1..3] = [sx, sy, sz] is reversed to RITK tensor-axis [sz, sy, sx]; the writer reverses back.exact for f32-representable spacingcrates/ritk-analyze/src/tests.rs::analyze_writer_emits_pixdim_in_file_axis_order, ...::analyze_roundtrip_preserves_shape_spacing_origin_and_values
Spacing — legacy zero/negativeNon-finite spacing is rejected; pixdim ≤ 0 falls back to unit spacing. The fallback is load-bearing: without it a legacy file panics in Spacing::new.normalized to 1.0 mmcrates/ritk-analyze/src/tests.rs::analyze_reader_falls_back_to_unit_spacing_for_non_positive_pixdim
OriginReconstructed as originator[i] × spacing[i]; the writer rounds each world coordinate to the nearest i16 voxel index and rejects an out-of-range result.quantized to the nearest voxelcrates/ritk-analyze/src/tests.rs::analyze_roundtrip_preserves_shape_spacing_origin_and_values, crates/ritk-analyze/src/writer.rs::writer_rejects_invalid_input_before_creating_files
DirectionAnalyze has no direction field. The reader returns identity; the writer rejects a non-identity direction before touching the file system.non-identity unrepresentable (rejected, not dropped)crates/ritk-analyze/src/writer.rs::writer_rejects_a_non_identity_direction_before_creating_files
Payload length and offsetvox_offset must be a finite non-negative whole byte count; the .img length must equal vox_offset + voxel_count × width.exactcrates/ritk-analyze/src/tests.rs::analyze_reader_requires_exact_payload_and_honors_offset, ...::analyze_reader_rejects_non_finite_metadata_and_invalid_offsets
Format identificationA paired NIfTI-1 (ni1\0) header is reported as NIfTI rather than decoded as Analyze.routed to the NIfTI readercrates/ritk-analyze/src/tests.rs::analyze_reader_identifies_paired_nifti_header
Dispatch contractritk-io’s AnalyzeReader/AnalyzeWriter round-trip a [2, 3, 4] volume under SpatialFidelity::SpacingAndOrigin.shared contract paritycrates/ritk-io/src/format/tests_native_readers.rs::native_analyze_writer_reader_contract_round_trips

A round trip alone cannot prove the axis order, because a self-consistent transposition would pass it. analyze_writer_emits_pixdim_in_file_axis_order therefore reads the raw header bytes at offsets 80, 84, and 88 and pins pixdim[1..3] = [3.75, 2.5, 1.25] for core spacing [1.25, 2.5, 3.75].

Next

The Analyze round-trip example uses a shared display scale and a separate absolute-difference panel so visual similarity is not mistaken for proof of equality.