Geometry and metadata¶
MedVol's guarantees:
- Faithful round trips. X →
.mla→ X reproduces the array bit-identically and the header to the accuracy of the format. - Exact geometry. Every voxel keeps its world position across formats, within the format's precision, including oblique, mirrored and gantry-tilted (sheared) grids.
- Nothing implicit. No reorientation, resampling, rescaling, dtype change or metadata removal unless a policy asks for it. MedVol does not de-identify.
- Everything reported. See Reports.
These are tested with a conformance phantom: an oblique, mirrored volume in which every voxel encodes its own file index, written with the reference libraries and read and written through every reader × writer pair. World positions must agree to < 1e-4 mm.
Array order¶
The array keeps the stored order of the file, in C order: the format's fastest-varying axis is last. A
NIfTI, NRRD or MetaImage volume indexed [i, j, k] in the file is array[k, j, i], and DICOM slices are
array[slice, row, column]. Axis names (z, y, x, t, ...) are names only; they say nothing about
anatomy. source[].index_map records how array axes relate to the file's own index order.
Spaces¶
The geometry is the MLArray space patient (image.space()):
- its axes are the patient directions: x, y, z increase toward left, posterior, superior (LPS,
world_frame="dicom_patient", default) or right, anterior, superior (world_frame="ras"); space.from_array.matrixmaps array indices of the axes inspace.from_array.array_axesto millimetres, with index = voxel centre;- non-space axes (time, channel, vector component) never enter the matrix.
Readers build the matrix from the format's own fields (NIfTI qform/sform, NRRD space directions and
space origin, MetaImage TransformMatrix/Offset, MGH Mdc/c_ras, DICOM ImagePositionPatient and
ImageOrientationPatient). Values are never rounded. Going between LPS and RAS changes the signs of matrix rows;
it never flips the array.
space = image.space()
space.matrix() # (n+1) x (n+1), columns in space.from_array.array_axes order
space.letter_codes() # e.g. ('R', 'I', 'P'): the direction each mapped axis points to
medvol.reorient(image, "LPS" | "RAS") changes the array order instead, losslessly (Policies).
Values¶
The array holds the stored values. Scaling (NIfTI scl_slope/scl_inter, DICOM RescaleSlope/Intercept) goes
to values.scale and values.offset: physical value = stored × scale + offset. Label maps from DICOM SEG and
RTSTRUCT have values.kind == "label" and a label table with names, colours and codes.
Original headers¶
Every reader stores the complete original header, readable, in image.meta.source (one entry per input):
entry = image.meta.source[0]
entry.format, entry.files # "nifti", ["ct.nii.gz"]
entry.header["descrip"] # any header field; DICOM: entry.header["shared"] / ["per_file"]
entry.fingerprint # hashes of shape, dtype, geometry and data at read time
When an image is written to the same format, each header field is handled by its class:
| class | on write | examples |
|---|---|---|
| core-owned | regenerated from the MLArray core | NIfTI dim, pixdim, qform/sform, datatype, scl_*; NRRD sizes, space directions, kinds; DICOM Rows, PixelSpacing, ImagePositionPatient, RescaleSlope |
| data-dependent | kept while the fingerprint still matches the array and geometry; otherwise recomputed or dropped (reported) | NIfTI cal_min/cal_max; NRRD min/max; DICOM SmallestImagePixelValue, SliceLocation, SOPInstanceUID |
| verbatim | written back as stored | descriptions, vendor and acquisition fields, private tags, NRRD custom keys, NIfTI extensions |
When writing a different format, only the core is converted. Header fields of one format are never copied into another format's custom keys; the report says so.