brain_deer.warp.field

Dense displacement fields and operations on them.

A DeformationField is a regular grid of RAS-millimetre displacements plus the affine that places that grid in world space. It defines the point map phi(x) = x + sample(x). It carries no “forward”/”inverse” label – that is the caller’s bookkeeping about which file went where.

The big ANTs fields are hundreds of MB compressed and over a gigabyte in RAM, so compose_streaming() composes a small correction into a large on-disk field without ever holding the large field whole. (Measured: slab-by-slab access on a .nii.gz is O(n**2) because gzip is not seekable, so we decompress once to a temporary uncompressed file and memory-map that instead.)

Classes

DeformationField

A dense displacement field over a regular grid.

Functions

resample_field(→ DeformationField)

Resample field onto a new grid, preserving the displacement values.

compose(→ DeformationField)

Compose phi_outer o phi_inner on inner's grid.

invert(→ DeformationField)

Numeric fixed-point inverse of phi, on the same grid.

compose_streaming(→ None)

Compose the small outer field onto the large field at inner_path.

compose_streaming_outer(→ None)

Compose the large field at outer_path onto the small inner field.

Module Contents

class brain_deer.warp.field.DeformationField

A dense displacement field over a regular grid.

displacement
Type:

(I, J, K, 3) float32 array of RAS-mm displacements.

affine
Type:

(4, 4) float64, mapping voxel index -> RAS world mm.

property spacing: FloatArray

Per-axis voxel size in mm (column norms of the affine).

sample(points_world: FloatArray, order: int = 3) → FloatArray

Interpolate the displacement at (N,3) world points.

Points outside the grid get zero displacement.

transform_points(points_world: FloatArray, order: int = 3) → FloatArray

phi(x) = x + sample(x).

brain_deer.warp.field.resample_field(field: DeformationField, *, affine: FloatArray, shape: tuple[int, int, int], order: int = 3) → DeformationField

Resample field onto a new grid, preserving the displacement values.

brain_deer.warp.field.compose(outer: DeformationField, inner: DeformationField, order: int = 3) → DeformationField

Compose phi_outer o phi_inner on inner’s grid.

d(x) = d_inner(x) + outer.sample(x + d_inner(x)) – the displacement that first applies inner then outer.

brain_deer.warp.field.invert(field: DeformationField, *, max_iter: int = 20, tol_mm: float = 0.01, order: int = 3) → DeformationField

Numeric fixed-point inverse of phi, on the same grid.

Solves psi(y) = -field.sample(y + psi(y)) by fixed-point iteration. This converges only for near-identity fields (||J(d)|| < 1); it is intended for the small, smooth correction fields authored here, never for the large ANTs warps – those already ship as separate forward/inverse files.

brain_deer.warp.field.compose_streaming(outer: DeformationField, inner_path: str | pathlib.Path, out_path: str | pathlib.Path, *, slab: int = 16, order: int = 3) → None

Compose the small outer field onto the large field at inner_path.

Writes phi_outer o phi_inner (d(x) = d_inner(x) + outer.sample(x + d_inner(x))) to out_path on inner’s grid, holding only one K-slab of the large field in RAM at a time. This is the direction where the correction is applied last; Lead-DBS uses it for the inverse composite (with the inverted correction as outer). See compose_streaming_outer().

brain_deer.warp.field.compose_streaming_outer(outer_path: str | pathlib.Path, inner: DeformationField, out_path: str | pathlib.Path, *, slab: int = 16, order: int = 3, big_field_order: int = 1) → None

Compose the large field at outer_path onto the small inner field.

Writes phi_outer o phi_inner (d(x) = d_inner(x) + outer.sample(x + d_inner(x))) to out_path on the outer (large) field’s grid. This is the direction where the correction (inner) is applied first and the big warp wraps it – Lead-DBS’s forward composite (compose(outer=forwardWarp, inner=corrections)).

The large field must be sampled at x + d_inner(x), which is only a few mm from x since the correction is small; the memmapped grid gives that random access without holding the field in RAM.

big_field_order defaults to 1 (linear): the large ANTs field is smooth at its native resolution, so linear differs from cubic by ~0.005mm here, and it avoids map_coordinates’ cubic prefilter – which would otherwise re-filter the whole 500MB+ field once per slab per component and dominate the runtime. order still controls the (cheap) correction sampling.