Skip to content

Image Registration Module

Affine registration and image warping

Small multiview-stitcher adapters for MERFISH registration.

Functions:

Name Description
register_pair_to_fixed

Register a moving image to a fixed image with staged GPU phase correlation.

warp_array_to_reference_gpu

Warp an image into a reference ZYX grid using CuPy affine interpolation.

warp_array_to_reference_with_affine_and_sofima_flow_gpu

Warp an image with a stored affine transform and SOFIMA flow field.

_clear_cupy_memory(cp)

Release cached CuPy allocations and FFT plans after a registration stage.

Parameters:

Name Type Description Default
cp Any

Imported cupy module.

required

Returns:

Type Description
None

CuPy memory pools and FFT plan cache are cleared in-place.

Source code in src/merfish3danalysis/utils/multiview_registration.py
def _clear_cupy_memory(cp: Any) -> None:
    """
    Release cached CuPy allocations and FFT plans after a registration stage.

    Parameters
    ----------
    cp : Any
        Imported ``cupy`` module.

    Returns
    -------
    None
        CuPy memory pools and FFT plan cache are cleared in-place.
    """
    cp.cuda.Stream.null.synchronize()
    try:
        cp.fft.config.get_plan_cache().clear()
    except Exception:
        pass
    cp.get_default_memory_pool().free_all_blocks()
    cp.get_default_pinned_memory_pool().free_all_blocks()
    gc.collect()

_diag(message, *, enabled)

Print one multiview registration diagnostic message when enabled.

Parameters:

Name Type Description Default
message str

Diagnostic message body.

required
enabled bool

If True, print the diagnostic message.

required

Returns:

Type Description
None

The message is printed only when diagnostics are enabled.

Source code in src/merfish3danalysis/utils/multiview_registration.py
def _diag(message: str, *, enabled: bool) -> None:
    """
    Print one multiview registration diagnostic message when enabled.

    Parameters
    ----------
    message : str
        Diagnostic message body.
    enabled : bool
        If True, print the diagnostic message.

    Returns
    -------
    None
        The message is printed only when diagnostics are enabled.
    """
    if enabled:
        print(f"[multiview-registration] {message}", flush=True)

_max_z_projection_gpu(image, cp)

Compute a maximum Z projection without retaining the full GPU volume.

Parameters:

Name Type Description Default
image ndarray

Input image in Z, Y, X order.

required
cp Any

Imported cupy module.

required

Returns:

Type Description
ndarray

Maximum projection over Z as a GPU array.

Source code in src/merfish3danalysis/utils/multiview_registration.py
def _max_z_projection_gpu(image: np.ndarray, cp: Any) -> Any:
    """
    Compute a maximum Z projection without retaining the full GPU volume.

    Parameters
    ----------
    image : numpy.ndarray
        Input image in Z, Y, X order.
    cp : Any
        Imported ``cupy`` module.

    Returns
    -------
    cupy.ndarray
        Maximum projection over Z as a GPU array.
    """
    image_gpu = cp.asarray(image, dtype=cp.float32)
    projection = cp.max(image_gpu, axis=0)
    del image_gpu
    _clear_cupy_memory(cp)
    return projection

_maximum_overlap_phase_shift_px(shift_px, shape)

Select periodic phase-shift aliases with maximum image overlap.

FFT phase correlation identifies shifts modulo each axis length. Real-space disambiguation can therefore report a shift close to +/-axis_size that overlaps only a small part of the images even when the equivalent near-zero shift is physically supported. For equal-shaped local- registration images, the representative in [-axis_size / 2, axis_size / 2) retains at least half of every field of view.

Parameters:

Name Type Description Default
shift_px Sequence[float]

Phase-correlation shift in pixels for each axis.

required
shape Sequence[int]

Phase-correlation image shape in the same axis order.

required

Returns:

Type Description
ndarray

Equivalent periodic shifts with maximum overlap on every axis.

Source code in src/merfish3danalysis/utils/multiview_registration.py
def _maximum_overlap_phase_shift_px(
    shift_px: Sequence[float],
    shape: Sequence[int],
) -> np.ndarray:
    """
    Select periodic phase-shift aliases with maximum image overlap.

    FFT phase correlation identifies shifts modulo each axis length. Real-space
    disambiguation can therefore report a shift close to ``+/-axis_size`` that
    overlaps only a small part of the images even when the equivalent
    near-zero shift is physically supported. For equal-shaped local-
    registration images, the representative in
    ``[-axis_size / 2, axis_size / 2)`` retains at least half of every field of
    view.

    Parameters
    ----------
    shift_px : Sequence[float]
        Phase-correlation shift in pixels for each axis.
    shape : Sequence[int]
        Phase-correlation image shape in the same axis order.

    Returns
    -------
    numpy.ndarray
        Equivalent periodic shifts with maximum overlap on every axis.
    """
    shift = np.asarray(shift_px, dtype=np.float64)
    period = np.asarray(shape, dtype=np.float64)
    if shift.ndim != 1 or period.shape != shift.shape:
        raise ValueError("shift_px and shape must be one-dimensional and equal length.")
    if np.any(period <= 0):
        raise ValueError("All image dimensions must be positive.")
    return np.remainder(shift + period / 2.0, period) - period / 2.0

_overlap_slices_after_translation(shape, translation_px)

Return output slices whose translated coordinates stay inside the input.

Parameters:

Name Type Description Default
shape Sequence[int]

Image shape.

required
translation_px Sequence[float]

Translation used by cupyx.scipy.ndimage.affine_transform. An output coordinate p samples input coordinate p + translation_px.

required

Returns:

Type Description
tuple[slice, ...] or None

Valid overlap slices, or None if the translation leaves no overlap.

Source code in src/merfish3danalysis/utils/multiview_registration.py
def _overlap_slices_after_translation(
    shape: Sequence[int],
    translation_px: Sequence[float],
) -> tuple[slice, ...] | None:
    """
    Return output slices whose translated coordinates stay inside the input.

    Parameters
    ----------
    shape : Sequence[int]
        Image shape.
    translation_px : Sequence[float]
        Translation used by ``cupyx.scipy.ndimage.affine_transform``. An output
        coordinate ``p`` samples input coordinate ``p + translation_px``.

    Returns
    -------
    tuple[slice, ...] or None
        Valid overlap slices, or None if the translation leaves no overlap.
    """
    slices = []
    for axis_size, axis_translation_px in zip(shape, translation_px, strict=True):
        start = int(np.ceil(max(0.0, -float(axis_translation_px))))
        stop = int(
            np.floor(min(float(axis_size), float(axis_size) - axis_translation_px))
        )
        if stop <= start:
            return None
        slices.append(slice(start, stop))
    return tuple(slices)

_overlap_weighted_translation_score(fixed, moving, *, pull_shift_px, array_module)

Score a pull translation using correlation and retained image overlap.

Parameters:

Name Type Description Default
fixed Any

Fixed image array.

required
moving Any

Moving image array with the same shape as fixed.

required
pull_shift_px Sequence[float]

Candidate fixed-to-moving pull translation in pixels.

required
array_module Any

NumPy-compatible module implementing array arithmetic. Runtime GPU registration supplies CuPy; CPU tests supply NumPy.

required

Returns:

Type Description
float

Pearson correlation multiplied by the fraction of fixed-image pixels retained in the overlap. Invalid or constant overlaps score negative infinity.

Source code in src/merfish3danalysis/utils/multiview_registration.py
def _overlap_weighted_translation_score(
    fixed: Any,
    moving: Any,
    *,
    pull_shift_px: Sequence[float],
    array_module: Any,
) -> float:
    """
    Score a pull translation using correlation and retained image overlap.

    Parameters
    ----------
    fixed : Any
        Fixed image array.
    moving : Any
        Moving image array with the same shape as ``fixed``.
    pull_shift_px : Sequence[float]
        Candidate fixed-to-moving pull translation in pixels.
    array_module : Any
        NumPy-compatible module implementing array arithmetic. Runtime GPU
        registration supplies CuPy; CPU tests supply NumPy.

    Returns
    -------
    float
        Pearson correlation multiplied by the fraction of fixed-image pixels
        retained in the overlap. Invalid or constant overlaps score negative
        infinity.
    """
    overlap = _translation_overlap_slices(fixed.shape, pull_shift_px)
    if overlap is None:
        return float("-inf")
    fixed_slices, moving_slices = overlap
    fixed_values = fixed[fixed_slices].astype(array_module.float32, copy=False)
    moving_values = moving[moving_slices].astype(array_module.float32, copy=False)
    fixed_centered = fixed_values - array_module.mean(fixed_values)
    moving_centered = moving_values - array_module.mean(moving_values)
    denominator = array_module.sqrt(
        array_module.sum(fixed_centered * fixed_centered)
        * array_module.sum(moving_centered * moving_centered)
    )
    denominator_value = float(denominator)
    if not np.isfinite(denominator_value) or denominator_value <= 0:
        return float("-inf")
    correlation = float(
        array_module.sum(fixed_centered * moving_centered) / denominator
    )
    if not np.isfinite(correlation):
        return float("-inf")
    overlap_fraction = float(fixed_values.size) / float(fixed.size)
    return correlation * overlap_fraction

_select_phase_correlation_pull_shift_px(fixed, moving, *, phase_cross_correlation, array_module, to_numpy, diagnostics=False)

Select a reliable phase-correlation translation candidate.

Phase normalization can amplify decorrelated high-frequency noise in fiducial images and produce a strong but physically unsupported peak near half an image period. This function evaluates phase-normalized, unnormalized, and identity candidates in real space. It selects the candidate with the best overlap-weighted Pearson correlation after mapping every periodic shift to its maximum-overlap representative.

Parameters:

Name Type Description Default
fixed Any

Fixed image array.

required
moving Any

Moving image array with the same shape as fixed.

required
phase_cross_correlation Any

NumPy-compatible phase-correlation callable.

required
array_module Any

NumPy-compatible array module used for real-space scoring.

required
to_numpy Any

Callable converting a phase-correlation result to a NumPy array.

required
diagnostics bool

If True, print candidate shifts and scores.

False

Returns:

Type Description
ndarray

Selected fixed-to-moving pull translation in pixels.

Source code in src/merfish3danalysis/utils/multiview_registration.py
def _select_phase_correlation_pull_shift_px(
    fixed: Any,
    moving: Any,
    *,
    phase_cross_correlation: Any,
    array_module: Any,
    to_numpy: Any,
    diagnostics: bool = False,
) -> np.ndarray:
    """
    Select a reliable phase-correlation translation candidate.

    Phase normalization can amplify decorrelated high-frequency noise in
    fiducial images and produce a strong but physically unsupported peak near
    half an image period. This function evaluates phase-normalized,
    unnormalized, and identity candidates in real space. It selects the
    candidate with the best overlap-weighted Pearson correlation after mapping
    every periodic shift to its maximum-overlap representative.

    Parameters
    ----------
    fixed : Any
        Fixed image array.
    moving : Any
        Moving image array with the same shape as ``fixed``.
    phase_cross_correlation : Any
        NumPy-compatible phase-correlation callable.
    array_module : Any
        NumPy-compatible array module used for real-space scoring.
    to_numpy : Any
        Callable converting a phase-correlation result to a NumPy array.
    diagnostics : bool, default=False
        If True, print candidate shifts and scores.

    Returns
    -------
    numpy.ndarray
        Selected fixed-to-moving pull translation in pixels.
    """
    if fixed.shape != moving.shape:
        raise ValueError(
            "Phase-correlation candidate images must have matching shapes, got "
            f"{fixed.shape!r} and {moving.shape!r}."
        )

    candidates: list[tuple[str, np.ndarray]] = [
        ("identity", np.zeros(fixed.ndim, dtype=np.float32))
    ]
    for normalization in ("phase", None):
        push_shift_px = phase_cross_correlation(
            fixed,
            moving,
            upsample_factor=10,
            disambiguate=False,
            normalization=normalization,
        )[0]
        pull_shift_px = _maximum_overlap_phase_shift_px(
            -np.asarray(to_numpy(push_shift_px), dtype=np.float64),
            fixed.shape,
        ).astype(np.float32)
        if not any(
            np.allclose(pull_shift_px, existing_shift)
            for _label, existing_shift in candidates
        ):
            label = "phase" if normalization == "phase" else "unnormalized"
            candidates.append((label, pull_shift_px))

    scores = [
        _overlap_weighted_translation_score(
            fixed,
            moving,
            pull_shift_px=pull_shift_px,
            array_module=array_module,
        )
        for _label, pull_shift_px in candidates
    ]
    selected_index = int(np.argmax(scores))
    if diagnostics:
        details = ", ".join(
            f"{label}:pull_px={tuple(float(v) for v in shift)}:score={score:.6f}"
            for (label, shift), score in zip(candidates, scores, strict=True)
        )
        _diag(
            f"phase_candidates {details} selected={candidates[selected_index][0]}",
            enabled=True,
        )
    return candidates[selected_index][1].copy()

_translation_overlap_slices(shape, pull_shift_px)

Return fixed and moving slices for an integerized pull translation.

Parameters:

Name Type Description Default
shape Sequence[int]

Equal fixed and moving image shape.

required
pull_shift_px Sequence[float]

Translation for which output coordinate p samples moving coordinate p + pull_shift_px.

required

Returns:

Type Description
tuple[tuple[slice, ...], tuple[slice, ...]] or None

Matching fixed and moving overlap slices, or None when there is no overlap.

Source code in src/merfish3danalysis/utils/multiview_registration.py
def _translation_overlap_slices(
    shape: Sequence[int],
    pull_shift_px: Sequence[float],
) -> tuple[tuple[slice, ...], tuple[slice, ...]] | None:
    """
    Return fixed and moving slices for an integerized pull translation.

    Parameters
    ----------
    shape : Sequence[int]
        Equal fixed and moving image shape.
    pull_shift_px : Sequence[float]
        Translation for which output coordinate ``p`` samples moving
        coordinate ``p + pull_shift_px``.

    Returns
    -------
    tuple[tuple[slice, ...], tuple[slice, ...]] or None
        Matching fixed and moving overlap slices, or ``None`` when there is no
        overlap.
    """
    fixed_slices = []
    moving_slices = []
    for axis_size, axis_shift_px in zip(shape, pull_shift_px, strict=True):
        integer_shift = int(np.rint(float(axis_shift_px)))
        fixed_start = max(0, -integer_shift)
        fixed_stop = min(int(axis_size), int(axis_size) - integer_shift)
        if fixed_stop <= fixed_start:
            return None
        fixed_slices.append(slice(fixed_start, fixed_stop))
        moving_slices.append(
            slice(fixed_start + integer_shift, fixed_stop + integer_shift)
        )
    return tuple(fixed_slices), tuple(moving_slices)

register_pair_to_fixed(fixed, moving, *, spacing_zyx_um, diagnostics=False)

Register a moving image to a fixed image with staged GPU phase correlation.

The input arrays are interpreted as Z, Y, X images with physical spacing in microns. The registration first estimates lateral translation from maximum Z projections, warps the moving volume by that lateral estimate, then runs phase correlation on the full volume to estimate the residual translation. At both stages, phase-normalized, unnormalized, and identity candidates are scored in real space so decorrelated noise cannot promote a large half-period displacement. The returned affine maps fixed/reference physical coordinates to moving-image physical coordinates, matching the convention expected by :func:warp_array_to_reference_gpu.

Parameters:

Name Type Description Default
fixed ndarray

Reference image in Z, Y, X order.

required
moving ndarray

Image to align to fixed, in Z, Y, X order.

required
spacing_zyx_um Sequence[float]

Physical voxel spacing in microns in Z, Y, X order.

required
diagnostics bool

If True, print detailed timing diagnostics.

False

Returns:

Type Description
ndarray

Homogeneous 4x4 affine transform in physical Z, Y, X coordinates. The transform maps coordinates in the fixed reference space to coordinates sampled from the moving image, matching the convention expected by :func:warp_array_to_reference_gpu.

Source code in src/merfish3danalysis/utils/multiview_registration.py
def register_pair_to_fixed(
    fixed: np.ndarray,
    moving: np.ndarray,
    *,
    spacing_zyx_um: Sequence[float],
    diagnostics: bool = False,
) -> np.ndarray:
    """
    Register a moving image to a fixed image with staged GPU phase correlation.

    The input arrays are interpreted as Z, Y, X images with physical spacing in
    microns. The registration first estimates lateral translation from maximum
    Z projections, warps the moving volume by that lateral estimate, then runs
    phase correlation on the full volume to estimate the residual translation.
    At both stages, phase-normalized, unnormalized, and identity candidates are
    scored in real space so decorrelated noise cannot promote a large
    half-period displacement. The returned affine maps fixed/reference
    physical coordinates to moving-image physical coordinates, matching the
    convention expected by :func:`warp_array_to_reference_gpu`.

    Parameters
    ----------
    fixed : numpy.ndarray
        Reference image in Z, Y, X order.
    moving : numpy.ndarray
        Image to align to ``fixed``, in Z, Y, X order.
    spacing_zyx_um : Sequence[float]
        Physical voxel spacing in microns in Z, Y, X order.
    diagnostics : bool, default=False
        If True, print detailed timing diagnostics.

    Returns
    -------
    numpy.ndarray
        Homogeneous 4x4 affine transform in physical Z, Y, X coordinates. The
        transform maps coordinates in the fixed reference space to coordinates
        sampled from the moving image, matching the convention expected by
        :func:`warp_array_to_reference_gpu`.
    """
    import cupy as cp
    from cucim.skimage.registration import phase_cross_correlation

    _diag(
        "register_pair_to_fixed_start "
        f"fixed_shape={tuple(int(v) for v in fixed.shape)} "
        f"moving_shape={tuple(int(v) for v in moving.shape)} "
        f"spacing_zyx_um={tuple(float(v) for v in spacing_zyx_um)}",
        enabled=diagnostics,
    )
    if fixed.shape != moving.shape or fixed.ndim != 3:
        raise ValueError(
            "register_pair_to_fixed expects fixed and moving 3D arrays with "
            f"matching shapes, got {fixed.shape!r} and {moving.shape!r}."
        )

    start_time = timeit.default_timer()
    spacing = round_spacing_um(spacing_zyx_um).astype(np.float32)
    fixed_projection = _max_z_projection_gpu(fixed, cp)
    moving_projection = _max_z_projection_gpu(moving, cp)
    xy_pull_shift_px = _select_phase_correlation_pull_shift_px(
        fixed_projection,
        moving_projection,
        phase_cross_correlation=phase_cross_correlation,
        array_module=cp,
        to_numpy=cp.asnumpy,
        diagnostics=diagnostics,
    )
    del fixed_projection, moving_projection
    _clear_cupy_memory(cp)

    xy_transform = np.eye(4, dtype=np.float32)
    xy_transform[1, 3] = float(xy_pull_shift_px[0]) * float(spacing[1])
    xy_transform[2, 3] = float(xy_pull_shift_px[1]) * float(spacing[2])
    moving_xy_registered = warp_array_to_reference_gpu(
        moving,
        transform_zyx_um=xy_transform,
        spacing_zyx_um=spacing,
        reference_shape=fixed.shape,
        order=1,
        diagnostics=diagnostics,
    )

    overlap_slices = _overlap_slices_after_translation(
        fixed.shape,
        (0.0, float(xy_pull_shift_px[0]), float(xy_pull_shift_px[1])),
    )
    if overlap_slices is None:
        residual_pull_shift_px = np.zeros(3, dtype=np.float32)
    else:
        fixed_overlap = cp.asarray(fixed[overlap_slices], dtype=cp.float32)
        moving_overlap = cp.asarray(
            moving_xy_registered[overlap_slices],
            dtype=cp.float32,
        )
        residual_pull_shift_px = _select_phase_correlation_pull_shift_px(
            fixed_overlap,
            moving_overlap,
            phase_cross_correlation=phase_cross_correlation,
            array_module=cp,
            to_numpy=cp.asnumpy,
            diagnostics=diagnostics,
        )
        del fixed_overlap, moving_overlap
    del moving_xy_registered
    total_shift_px = residual_pull_shift_px.copy()
    total_shift_px[1] += xy_pull_shift_px[0]
    total_shift_px[2] += xy_pull_shift_px[1]
    total_shift_px = _maximum_overlap_phase_shift_px(
        total_shift_px,
        fixed.shape,
    ).astype(np.float32)

    transform = np.eye(4, dtype=np.float32)
    transform[:3, 3] = total_shift_px * spacing
    _diag(
        "register_pair_to_fixed_done "
        f"xy_pull_shift_px=(0.000, {float(xy_pull_shift_px[0]):.3f}, {float(xy_pull_shift_px[1]):.3f}) "
        f"residual_pull_shift_px={tuple(float(v) for v in residual_pull_shift_px)} "
        f"total_pull_shift_px={tuple(float(v) for v in total_shift_px)} "
        f"elapsed_s={timeit.default_timer() - start_time:.2f}",
        enabled=diagnostics,
    )
    _clear_cupy_memory(cp)
    return transform

warp_array_to_reference_gpu(image, *, transform_zyx_um, spacing_zyx_um, reference_shape, reference_origin_zyx_um=(0.0, 0.0, 0.0), mode='constant', cval=0.0, order=1, gpu_id=0, z_batch_size=4, diagnostics=False)

Warp an image into a reference ZYX grid using CuPy affine interpolation.

The physical transform convention matches the local registration adapter: the 4x4 matrix maps output/reference physical coordinates to input/moving physical coordinates. The matrix is converted to the pixel-coordinate convention expected by cupyx.scipy.ndimage.affine_transform.

Parameters:

Name Type Description Default
image ndarray

Moving image in Z, Y, X order.

required
transform_zyx_um ndarray

Homogeneous 4x4 affine transform in physical Z, Y, X coordinates. The transform maps output/reference coordinates to input/moving coordinates.

required
spacing_zyx_um Sequence[float]

Physical voxel spacing in microns in Z, Y, X order for both input and output grids.

required
reference_shape Sequence[int]

Output grid shape in Z, Y, X order.

required
reference_origin_zyx_um Sequence[float]

Physical output origin in microns in Z, Y, X order. The moving image is assumed to use the same origin convention as the reference grid.

(0.0, 0.0, 0.0)
mode str

Boundary mode passed to cupyx.scipy.ndimage.affine_transform.

"constant"
cval float

Constant fill value used when mode="constant". This matches the old SimpleITK registration path, which filled samples outside the moving image with background.

0.0
order int

Interpolation order passed to cupyx.scipy.ndimage.affine_transform.

1
gpu_id int

CUDA device ID to use.

0
z_batch_size int

Number of output z planes to process per GPU batch. Keeping this small avoids allocating full-volume coordinate grids for large tiles.

4
diagnostics bool

If True, print detailed timing diagnostics.

False

Returns:

Type Description
ndarray

Warped image sampled on the reference grid.

Source code in src/merfish3danalysis/utils/multiview_registration.py
def warp_array_to_reference_gpu(
    image: np.ndarray,
    *,
    transform_zyx_um: np.ndarray,
    spacing_zyx_um: Sequence[float],
    reference_shape: Sequence[int],
    reference_origin_zyx_um: Sequence[float] = (0.0, 0.0, 0.0),
    mode: str = "constant",
    cval: float = 0.0,
    order: int = 1,
    gpu_id: int = 0,
    z_batch_size: int = 4,
    diagnostics: bool = False,
) -> np.ndarray:
    """
    Warp an image into a reference ZYX grid using CuPy affine interpolation.

    The physical transform convention matches the local registration adapter:
    the 4x4 matrix maps output/reference physical coordinates to input/moving
    physical coordinates. The matrix is converted to the pixel-coordinate
    convention expected by ``cupyx.scipy.ndimage.affine_transform``.

    Parameters
    ----------
    image : numpy.ndarray
        Moving image in Z, Y, X order.
    transform_zyx_um : numpy.ndarray
        Homogeneous 4x4 affine transform in physical Z, Y, X coordinates. The
        transform maps output/reference coordinates to input/moving
        coordinates.
    spacing_zyx_um : Sequence[float]
        Physical voxel spacing in microns in Z, Y, X order for both input and
        output grids.
    reference_shape : Sequence[int]
        Output grid shape in Z, Y, X order.
    reference_origin_zyx_um : Sequence[float], default=(0.0, 0.0, 0.0)
        Physical output origin in microns in Z, Y, X order. The moving image is
        assumed to use the same origin convention as the reference grid.
    mode : str, default="constant"
        Boundary mode passed to ``cupyx.scipy.ndimage.affine_transform``.
    cval : float, default=0.0
        Constant fill value used when ``mode="constant"``. This matches the
        old SimpleITK registration path, which filled samples outside the
        moving image with background.
    order : int, default=1
        Interpolation order passed to ``cupyx.scipy.ndimage.affine_transform``.
    gpu_id : int, default=0
        CUDA device ID to use.
    z_batch_size : int, default=4
        Number of output z planes to process per GPU batch. Keeping this small
        avoids allocating full-volume coordinate grids for large tiles.
    diagnostics : bool, default=False
        If True, print detailed timing diagnostics.

    Returns
    -------
    numpy.ndarray
        Warped image sampled on the reference grid.
    """
    import cupy as cp
    from cupyx.scipy import ndimage

    cp.cuda.Device(gpu_id).use()

    spacing = round_spacing_um(spacing_zyx_um).astype(np.float32)
    origin = np.asarray(reference_origin_zyx_um, dtype=np.float32)
    transform = np.asarray(transform_zyx_um, dtype=np.float32)
    linear_um = transform[:3, :3]
    translation_um = transform[:3, 3]

    matrix_px = (linear_um * spacing[np.newaxis, :]) / spacing[:, np.newaxis]
    offset_px = (linear_um @ origin + translation_um - origin) / spacing

    _diag(
        "warp_array_to_reference_gpu_start "
        f"image_shape={tuple(int(v) for v in image.shape)} "
        f"reference_shape={tuple(int(v) for v in reference_shape)} "
        f"spacing_zyx_um={tuple(float(v) for v in spacing_zyx_um)} "
        f"mode={mode} "
        f"cval={float(cval)} "
        f"order={order} "
        f"gpu_id={gpu_id}",
        enabled=diagnostics,
    )
    start_time = timeit.default_timer()
    image_gpu = cp.asarray(image)
    warped_gpu = ndimage.affine_transform(
        image_gpu,
        matrix=cp.asarray(matrix_px),
        offset=cp.asarray(offset_px),
        output_shape=tuple(int(v) for v in reference_shape),
        order=order,
        mode=mode,
        cval=float(cval),
    )
    warped = cp.asnumpy(warped_gpu)
    del image_gpu, warped_gpu
    cp.cuda.Stream.null.synchronize()
    cp.get_default_memory_pool().free_all_blocks()
    cp.get_default_pinned_memory_pool().free_all_blocks()
    _diag(
        "warp_array_to_reference_gpu_done "
        f"elapsed_s={timeit.default_timer() - start_time:.2f}",
        enabled=diagnostics,
    )
    return np.asarray(warped)

warp_array_to_reference_with_affine_and_sofima_flow_gpu(image, *, transform_zyx_um, spacing_zyx_um, reference_shape, sofima_flow_field_xyz_px, flow_field_stride_zyx_px, flow_field_box_start_xyz_px, reference_origin_zyx_um=(0.0, 0.0, 0.0), mode='constant', cval=0.0, order=1, gpu_id=0, z_batch_size=4, diagnostics=False)

Warp an image with a stored affine transform and SOFIMA flow field.

The image is sampled exactly once. The SOFIMA flow field is interpolated in reference pixel space, composed with the stored affine transform, and the original moving image is sampled at the composed source coordinates.

Deformable-field convention

sofima_flow_field_xyz_px has channel-first shape (3, z, y, x). Channels are ordered X, Y, Z and spatial axes are ordered Z, Y, X. Each vector is a relative displacement in reference pixels from a reference-grid coordinate toward the affine-initialized moving image. The first map sample is located at flow_field_box_start_xyz_px in X, Y, Z pixel coordinates. SOFIMA estimates patch-centered vectors, so fields produced by :func:estimate_sofima_flow_field_xyz_px use half the patch size as this origin. The map stride is stored separately in Z, Y, X order.

Parameters:

Name Type Description Default
image ndarray

Moving image in native Z, Y, X order.

required
transform_zyx_um ndarray

Homogeneous 4x4 physical transform mapping reference Z, Y, X coordinates to moving native Z, Y, X coordinates.

required
spacing_zyx_um Sequence[float]

Voxel spacing in microns in Z, Y, X order.

required
reference_shape Sequence[int]

Output shape in Z, Y, X order.

required
sofima_flow_field_xyz_px ndarray

Relative SOFIMA flow field with channels X, Y, Z and spatial axes Z, Y, X. It maps reference pixels toward affine-initialized moving pixels.

required
flow_field_stride_zyx_px Sequence[float]

Flow-field sampling stride in reference pixels in Z, Y, X order.

required
flow_field_box_start_xyz_px Sequence[float]

Reference pixel coordinate of the first flow sample in X, Y, Z order.

required
reference_origin_zyx_um Sequence[float]

Physical origin for the reference and moving local grids.

(0.0, 0.0, 0.0)
mode str

Boundary mode for flow-field interpolation and image sampling.

"constant"
cval float

Constant fill value used when sampling outside the flow field or moving image.

0.0
order int

Interpolation order for the final image sampling.

1
gpu_id int

CUDA device ID to use.

0
z_batch_size int

Number of output z planes to process per GPU batch.

4
diagnostics bool

If True, print detailed timing diagnostics.

False

Returns:

Type Description
ndarray

Warped image on the reference grid.

Source code in src/merfish3danalysis/utils/multiview_registration.py
def warp_array_to_reference_with_affine_and_sofima_flow_gpu(
    image: np.ndarray,
    *,
    transform_zyx_um: np.ndarray,
    spacing_zyx_um: Sequence[float],
    reference_shape: Sequence[int],
    sofima_flow_field_xyz_px: np.ndarray,
    flow_field_stride_zyx_px: Sequence[float],
    flow_field_box_start_xyz_px: Sequence[float],
    reference_origin_zyx_um: Sequence[float] = (0.0, 0.0, 0.0),
    mode: str = "constant",
    cval: float = 0.0,
    order: int = 1,
    gpu_id: int = 0,
    z_batch_size: int = 4,
    diagnostics: bool = False,
) -> np.ndarray:
    """
    Warp an image with a stored affine transform and SOFIMA flow field.

    The image is sampled exactly once. The SOFIMA flow field is interpolated in
    reference pixel space, composed with the stored affine transform, and the
    original moving image is sampled at the composed source coordinates.

    Deformable-field convention
    ---------------------------
    ``sofima_flow_field_xyz_px`` has channel-first shape ``(3, z, y, x)``.
    Channels are ordered ``X, Y, Z`` and spatial axes are ordered ``Z, Y, X``.
    Each vector is a relative displacement in reference pixels from a
    reference-grid coordinate toward the affine-initialized moving image. The
    first map sample is located at ``flow_field_box_start_xyz_px`` in ``X, Y,
    Z`` pixel coordinates. SOFIMA estimates patch-centered vectors, so fields
    produced by :func:`estimate_sofima_flow_field_xyz_px` use half the patch
    size as this origin. The map stride is stored separately in ``Z, Y, X``
    order.

    Parameters
    ----------
    image : numpy.ndarray
        Moving image in native Z, Y, X order.
    transform_zyx_um : numpy.ndarray
        Homogeneous 4x4 physical transform mapping reference Z, Y, X
        coordinates to moving native Z, Y, X coordinates.
    spacing_zyx_um : Sequence[float]
        Voxel spacing in microns in Z, Y, X order.
    reference_shape : Sequence[int]
        Output shape in Z, Y, X order.
    sofima_flow_field_xyz_px : numpy.ndarray
        Relative SOFIMA flow field with channels X, Y, Z and spatial axes Z, Y,
        X. It maps reference pixels toward affine-initialized moving pixels.
    flow_field_stride_zyx_px : Sequence[float]
        Flow-field sampling stride in reference pixels in Z, Y, X order.
    flow_field_box_start_xyz_px : Sequence[float]
        Reference pixel coordinate of the first flow sample in X, Y, Z order.
    reference_origin_zyx_um : Sequence[float], default=(0.0, 0.0, 0.0)
        Physical origin for the reference and moving local grids.
    mode : str, default="constant"
        Boundary mode for flow-field interpolation and image sampling.
    cval : float, default=0.0
        Constant fill value used when sampling outside the flow field or moving
        image.
    order : int, default=1
        Interpolation order for the final image sampling.
    gpu_id : int, default=0
        CUDA device ID to use.
    z_batch_size : int, default=4
        Number of output z planes to process per GPU batch.
    diagnostics : bool, default=False
        If True, print detailed timing diagnostics.

    Returns
    -------
    numpy.ndarray
        Warped image on the reference grid.
    """
    import cupy as cp
    from cupyx.scipy import ndimage

    if image.ndim != 3:
        raise ValueError(f"Expected a 3D image, got shape {image.shape!r}.")
    if len(reference_shape) != 3:
        raise ValueError("reference_shape must have three ZYX elements.")

    cp.cuda.Device(gpu_id).use()

    ref_shape = tuple(int(v) for v in reference_shape)
    spacing = cp.asarray(round_spacing_um(spacing_zyx_um), dtype=cp.float32)
    origin = cp.asarray(reference_origin_zyx_um, dtype=cp.float32)
    transform = cp.asarray(transform_zyx_um, dtype=cp.float32)
    flow_field = cp.asarray(sofima_flow_field_xyz_px, dtype=cp.float32)
    if flow_field.ndim != 4:
        raise ValueError("sofima_flow_field_xyz_px must have channel plus ZYX axes.")
    if flow_field.shape[0] != 3 and flow_field.shape[-1] == 3:
        flow_field = cp.moveaxis(flow_field, -1, 0)
    if flow_field.shape[0] != 3:
        raise ValueError("SOFIMA flow field must have three XYZ channels.")

    stride_zyx = cp.asarray(flow_field_stride_zyx_px, dtype=cp.float32)
    box_start_xyz = cp.asarray(flow_field_box_start_xyz_px, dtype=cp.float32)
    box_start_zyx = box_start_xyz[[2, 1, 0]]

    _diag(
        "warp_array_to_reference_with_affine_and_sofima_flow_gpu_start "
        f"image_shape={tuple(int(v) for v in image.shape)} "
        f"reference_shape={ref_shape} "
        f"flow_field_shape={tuple(int(v) for v in flow_field.shape)} "
        f"mode={mode} "
        f"cval={float(cval)} "
        f"order={order} "
        f"gpu_id={gpu_id} "
        f"z_batch_size={int(z_batch_size)}",
        enabled=diagnostics,
    )
    start_time = timeit.default_timer()

    image_gpu = cp.asarray(image)
    warped = np.empty(ref_shape, dtype=np.asarray(image).dtype)
    z_batch_size = max(1, int(z_batch_size))
    y_indices = cp.arange(ref_shape[1], dtype=cp.float32)
    x_indices = cp.arange(ref_shape[2], dtype=cp.float32)
    grid_y, grid_x = cp.meshgrid(y_indices, x_indices, indexing="ij")

    for z_start in range(0, ref_shape[0], z_batch_size):
        z_stop = min(z_start + z_batch_size, ref_shape[0])
        z_indices = cp.arange(z_start, z_stop, dtype=cp.float32)
        grid_z = cp.broadcast_to(
            z_indices[:, cp.newaxis, cp.newaxis],
            (z_stop - z_start, ref_shape[1], ref_shape[2]),
        )
        batch_grid_y = cp.broadcast_to(
            grid_y[cp.newaxis, :, :],
            (z_stop - z_start, ref_shape[1], ref_shape[2]),
        )
        batch_grid_x = cp.broadcast_to(
            grid_x[cp.newaxis, :, :],
            (z_stop - z_start, ref_shape[1], ref_shape[2]),
        )
        flow_coords = cp.stack(
            [
                (grid_z - box_start_zyx[0]) / stride_zyx[0],
                (batch_grid_y - box_start_zyx[1]) / stride_zyx[1],
                (batch_grid_x - box_start_zyx[2]) / stride_zyx[2],
            ],
            axis=0,
        )

        affine_initialized_xyz = []
        for channel_index, identity_channel in enumerate(
            (batch_grid_x, batch_grid_y, grid_z)
        ):
            flow_component = ndimage.map_coordinates(
                flow_field[channel_index],
                flow_coords,
                order=1,
                mode=mode,
                cval=float(cval),
            )
            affine_initialized_xyz.append(identity_channel + flow_component)

        physical_z = affine_initialized_xyz[2] * spacing[0] + origin[0]
        physical_y = affine_initialized_xyz[1] * spacing[1] + origin[1]
        physical_x = affine_initialized_xyz[0] * spacing[2] + origin[2]

        moving_z = (
            transform[0, 0] * physical_z
            + transform[0, 1] * physical_y
            + transform[0, 2] * physical_x
            + transform[0, 3]
        )
        moving_y = (
            transform[1, 0] * physical_z
            + transform[1, 1] * physical_y
            + transform[1, 2] * physical_x
            + transform[1, 3]
        )
        moving_x = (
            transform[2, 0] * physical_z
            + transform[2, 1] * physical_y
            + transform[2, 2] * physical_x
            + transform[2, 3]
        )
        source_coords = cp.stack(
            [
                (moving_z - origin[0]) / spacing[0],
                (moving_y - origin[1]) / spacing[1],
                (moving_x - origin[2]) / spacing[2],
            ],
            axis=0,
        )
        warped_batch = ndimage.map_coordinates(
            image_gpu,
            source_coords,
            order=order,
            mode=mode,
            cval=float(cval),
        )
        warped[z_start:z_stop] = cp.asnumpy(warped_batch)
        del (
            grid_z,
            flow_coords,
            affine_initialized_xyz,
            physical_z,
            physical_y,
            physical_x,
            moving_z,
            moving_y,
            moving_x,
            source_coords,
            warped_batch,
        )

    del (
        image_gpu,
        flow_field,
        grid_y,
        grid_x,
    )
    cp.cuda.Stream.null.synchronize()
    cp.get_default_memory_pool().free_all_blocks()
    cp.get_default_pinned_memory_pool().free_all_blocks()

    _diag(
        "warp_array_to_reference_with_affine_and_sofima_flow_gpu_done "
        f"elapsed_s={timeit.default_timer() - start_time:.2f}",
        enabled=diagnostics,
    )
    return np.asarray(warped)

SOFIMA deformable registration

SOFIMA flow-field estimation utilities.

Classes:

Name Description
SofimaRegistrationConfig

Explicit SOFIMA deformable registration parameters.

Functions:

Name Description
estimate_sofima_flow_field_xyz_px

Estimate the production SOFIMA flow field after affine initialization.

SofimaRegistrationConfig dataclass

Explicit SOFIMA deformable registration parameters.

Methods:

Name Description
as_metadata

Return JSON-compatible config metadata.

Source code in src/merfish3danalysis/utils/sofima_registration.py
@dataclass(frozen=True)
class SofimaRegistrationConfig:
    """Explicit SOFIMA deformable registration parameters."""

    residual_iterations: int = 2
    patch_size_zyx: tuple[int, int, int] = (10, 32, 32)
    minimum_patch_size_px: int = 4
    step_divisor: int = 2
    peak_min_distance: int = 2
    peak_radius: int = 8
    batch_size: int = 32
    max_masked: float = 0.75
    min_peak_ratio: float = 1.2
    min_peak_sharpness: float = 1.2
    max_magnitude: float = 30.0
    max_deviation: float = 5.0
    max_local_z_displacement_px: float = 5.0
    subpixel_offsets: tuple[float, ...] = (-0.5, 0.0, 0.5)
    subpixel_batch_size: int = 32
    normalization_epsilon: float = 1e-6
    mesh_dt: float = 0.001
    mesh_gamma: float = 0.0
    mesh_k0: float = 1.0
    mesh_k: float = 0.01
    mesh_num_iters: int = 1000
    mesh_max_iters: int = 20000
    mesh_stop_v_max: float = 0.001
    mesh_dt_max: float = 100.0
    mesh_start_cap: float = 0.1
    mesh_final_cap: float = 10.0

    def as_metadata(self) -> dict[str, Any]:
        """Return JSON-compatible config metadata."""
        metadata = asdict(self)
        metadata["patch_size_zyx"] = [int(v) for v in self.patch_size_zyx]
        metadata["subpixel_offsets"] = [float(v) for v in self.subpixel_offsets]
        return metadata

as_metadata()

Return JSON-compatible config metadata.

Source code in src/merfish3danalysis/utils/sofima_registration.py
def as_metadata(self) -> dict[str, Any]:
    """Return JSON-compatible config metadata."""
    metadata = asdict(self)
    metadata["patch_size_zyx"] = [int(v) for v in self.patch_size_zyx]
    metadata["subpixel_offsets"] = [float(v) for v in self.subpixel_offsets]
    return metadata

_compose_flow_fields_same_grid(base_flow_xyz, residual_flow_xyz, *, stride_zyx, box_start_xyz)

Compose two SOFIMA flow fields sampled on the same reference grid.

Parameters:

Name Type Description Default
base_flow_xyz ndarray

Existing flow field in (3, z, y, x) order. It maps reference-grid coordinates into the original affine-initialized moving image.

required
residual_flow_xyz ndarray

Residual flow field in (3, z, y, x) order. It maps reference-grid coordinates into the image already warped by base_flow_xyz.

required
stride_zyx tuple[float, float, float]

Flow-grid spacing in reference-image pixels.

required
box_start_xyz tuple[float, float, float]

Reference pixel coordinate of the first flow sample in X, Y, Z order.

required

Returns:

Type Description
ndarray

Composed flow field mapping reference-grid coordinates directly into the original affine-initialized moving image.

Source code in src/merfish3danalysis/utils/sofima_registration.py
def _compose_flow_fields_same_grid(
    base_flow_xyz: np.ndarray,
    residual_flow_xyz: np.ndarray,
    *,
    stride_zyx: tuple[float, float, float],
    box_start_xyz: tuple[float, float, float],
) -> np.ndarray:
    """
    Compose two SOFIMA flow fields sampled on the same reference grid.

    Parameters
    ----------
    base_flow_xyz : numpy.ndarray
        Existing flow field in ``(3, z, y, x)`` order. It maps reference-grid
        coordinates into the original affine-initialized moving image.
    residual_flow_xyz : numpy.ndarray
        Residual flow field in ``(3, z, y, x)`` order. It maps reference-grid
        coordinates into the image already warped by ``base_flow_xyz``.
    stride_zyx : tuple[float, float, float]
        Flow-grid spacing in reference-image pixels.
    box_start_xyz : tuple[float, float, float]
        Reference pixel coordinate of the first flow sample in X, Y, Z order.

    Returns
    -------
    numpy.ndarray
        Composed flow field mapping reference-grid coordinates directly into
        the original affine-initialized moving image.
    """
    import jax
    import jax.numpy as jnp
    from jax.scipy.ndimage import map_coordinates

    base_flow = jnp.asarray(base_flow_xyz, dtype=jnp.float32)
    residual_flow = jnp.asarray(residual_flow_xyz, dtype=jnp.float32)
    z_grid, y_grid, x_grid = jnp.indices(base_flow.shape[1:], dtype=jnp.float32)
    shifted_zyx = (
        box_start_xyz[2] + z_grid * stride_zyx[0] + residual_flow[2],
        box_start_xyz[1] + y_grid * stride_zyx[1] + residual_flow[1],
        box_start_xyz[0] + x_grid * stride_zyx[2] + residual_flow[0],
    )
    coords = jnp.stack(
        [
            (shifted_zyx[0] - box_start_xyz[2]) / stride_zyx[0],
            (shifted_zyx[1] - box_start_xyz[1]) / stride_zyx[1],
            (shifted_zyx[2] - box_start_xyz[0]) / stride_zyx[2],
        ],
        axis=0,
    )
    sampled_base = jnp.stack(
        [
            map_coordinates(
                base_flow[channel_index],
                coords,
                order=1,
                mode="nearest",
            )
            for channel_index in range(3)
        ],
        axis=0,
    )
    composed = residual_flow + sampled_base
    return np.asarray(jax.device_get(composed), dtype=np.float32)

_estimate_sofima_flow_field_xyz_px_impl(fixed_zyx, moving_affine_initialized_zyx, *, config, single_residual_pass=False)

Estimate a SOFIMA flow field after affine initialization.

The returned field follows the package deformable-registration convention:

  • Array shape is (3, z, y, x).
  • Channel order is X, Y, Z because this is SOFIMA's flow-component order.
  • Spatial map axes are Z, Y, X to match image arrays.
  • Values are relative displacements in reference-image pixels. Adding the interpolated field to a reference-grid coordinate gives the coordinate in the affine-initialized moving image.
  • map_box_start_xyz_px is the reference-grid coordinate of the first flow sample in X, Y, Z order. SOFIMA estimates patch-centered displacements, so this origin is half the patch size, not the image corner.

Parameters:

Name Type Description Default
fixed_zyx ndarray

Reference round001 fiducial image in Z, Y, X order.

required
moving_affine_initialized_zyx ndarray

Moving fiducial image already rendered into the reference grid by the stored affine transform.

required
single_residual_pass bool

Internal recursion control. Public callers always use the fixed production path with two residual passes.

False
config SofimaRegistrationConfig

Explicit SOFIMA parameter set.

required

Returns:

Type Description
tuple[ndarray, dict[str, Any]]

Relative SOFIMA flow field with XYZ channels and metadata describing the map spacing/origin. The metadata is sufficient to save the field as OME-Zarr and later reproduce the same warp from the reloaded field.

Notes

SOFIMA's masked cross-correlation estimator computes integer local flow vectors from post_image to pre_image. Passing the affine-initialized moving image as pre_image and round001 as post_image gives the residual displacement from reference pixels into affine-initialized moving pixels. The local measurements are then relaxed with SOFIMA's 3D elastic mesh solver to produce the smooth float-valued field expected by :func:merfish3danalysis.utils.multiview_registration.warp_array_to_reference_with_affine_and_sofima_flow_gpu.

Source code in src/merfish3danalysis/utils/sofima_registration.py
def _estimate_sofima_flow_field_xyz_px_impl(
    fixed_zyx: np.ndarray,
    moving_affine_initialized_zyx: np.ndarray,
    *,
    config: SofimaRegistrationConfig,
    single_residual_pass: bool = False,
) -> tuple[np.ndarray, dict[str, Any]]:
    """
    Estimate a SOFIMA flow field after affine initialization.

    The returned field follows the package deformable-registration convention:

    - Array shape is ``(3, z, y, x)``.
    - Channel order is ``X, Y, Z`` because this is SOFIMA's flow-component
      order.
    - Spatial map axes are ``Z, Y, X`` to match image arrays.
    - Values are relative displacements in reference-image pixels. Adding the
      interpolated field to a reference-grid coordinate gives the coordinate in
      the affine-initialized moving image.
    - ``map_box_start_xyz_px`` is the reference-grid coordinate of the first
      flow sample in ``X, Y, Z`` order. SOFIMA estimates patch-centered
      displacements, so this origin is half the patch size, not the image
      corner.

    Parameters
    ----------
    fixed_zyx : numpy.ndarray
        Reference round001 fiducial image in Z, Y, X order.
    moving_affine_initialized_zyx : numpy.ndarray
        Moving fiducial image already rendered into the reference grid by the
        stored affine transform.
    single_residual_pass : bool, default=False
        Internal recursion control. Public callers always use the fixed
        production path with two residual passes.
    config : SofimaRegistrationConfig
        Explicit SOFIMA parameter set.

    Returns
    -------
    tuple[numpy.ndarray, dict[str, Any]]
        Relative SOFIMA flow field with XYZ channels and metadata describing
        the map spacing/origin. The metadata is sufficient to save the field
        as OME-Zarr and later reproduce the same warp from the reloaded field.

    Notes
    -----
    SOFIMA's masked cross-correlation estimator computes integer local flow
    vectors from ``post_image`` to ``pre_image``. Passing the affine-initialized
    moving image as ``pre_image`` and round001 as ``post_image`` gives the
    residual displacement from reference pixels into affine-initialized moving
    pixels. The local measurements are then relaxed with SOFIMA's 3D elastic
    mesh solver to produce the smooth float-valued field expected by
    :func:`merfish3danalysis.utils.multiview_registration.warp_array_to_reference_with_affine_and_sofima_flow_gpu`.
    """
    residual_iterations = 1 if single_residual_pass else int(config.residual_iterations)
    if residual_iterations > 1:
        from merfish3danalysis.utils.multiview_registration import (
            warp_array_to_reference_with_affine_and_sofima_flow_gpu,
        )

        total_flow, total_metadata = _estimate_sofima_flow_field_xyz_px_impl(
            fixed_zyx,
            moving_affine_initialized_zyx,
            config=config,
            single_residual_pass=True,
        )
        corrected = warp_array_to_reference_with_affine_and_sofima_flow_gpu(
            moving_affine_initialized_zyx,
            transform_zyx_um=np.eye(4, dtype=np.float32),
            spacing_zyx_um=(1.0, 1.0, 1.0),
            reference_shape=fixed_zyx.shape,
            sofima_flow_field_xyz_px=total_flow,
            flow_field_stride_zyx_px=total_metadata["map_stride_zyx_px"],
            flow_field_box_start_xyz_px=total_metadata["map_box_start_xyz_px"],
            mode="nearest",
        ).astype(np.float32, copy=False)
        completed_iterations = 1
        for _iteration in range(1, residual_iterations):
            residual_flow, residual_metadata = _estimate_sofima_flow_field_xyz_px_impl(
                fixed_zyx,
                corrected,
                config=config,
                single_residual_pass=True,
            )
            if residual_metadata["status"] != "ok":
                break
            total_flow = _compose_flow_fields_same_grid(
                total_flow,
                residual_flow,
                stride_zyx=tuple(float(v) for v in total_metadata["map_stride_zyx_px"]),
                box_start_xyz=tuple(
                    float(v) for v in total_metadata["map_box_start_xyz_px"]
                ),
            )
            total_flow, axial_metadata = _stabilize_axial_flow_component(
                total_flow,
                config,
            )
            total_metadata.update(axial_metadata)
            total_metadata["valid_flow_vectors"] = int(
                total_metadata["valid_flow_vectors"]
            ) + int(residual_metadata["valid_flow_vectors"])
            total_metadata["mesh_iterations"] = int(
                total_metadata["mesh_iterations"]
            ) + int(residual_metadata["mesh_iterations"])
            completed_iterations += 1
            if _iteration + 1 < residual_iterations:
                corrected = warp_array_to_reference_with_affine_and_sofima_flow_gpu(
                    moving_affine_initialized_zyx,
                    transform_zyx_um=np.eye(4, dtype=np.float32),
                    spacing_zyx_um=(1.0, 1.0, 1.0),
                    reference_shape=fixed_zyx.shape,
                    sofima_flow_field_xyz_px=total_flow,
                    flow_field_stride_zyx_px=total_metadata["map_stride_zyx_px"],
                    flow_field_box_start_xyz_px=total_metadata["map_box_start_xyz_px"],
                    mode="nearest",
                ).astype(np.float32, copy=False)
        total_metadata["residual_iterations"] = completed_iterations
        total_flow, axial_metadata = _stabilize_axial_flow_component(
            total_flow,
            config,
        )
        total_metadata.update(axial_metadata)
        corrected = warp_array_to_reference_with_affine_and_sofima_flow_gpu(
            moving_affine_initialized_zyx,
            transform_zyx_um=np.eye(4, dtype=np.float32),
            spacing_zyx_um=(1.0, 1.0, 1.0),
            reference_shape=fixed_zyx.shape,
            sofima_flow_field_xyz_px=total_flow,
            flow_field_stride_zyx_px=total_metadata["map_stride_zyx_px"],
            flow_field_box_start_xyz_px=total_metadata["map_box_start_xyz_px"],
            mode="nearest",
        ).astype(np.float32, copy=False)
        affine_error = _normalized_mean_squared_error(
            fixed_zyx,
            moving_affine_initialized_zyx,
        )
        sofima_error = _normalized_mean_squared_error(fixed_zyx, corrected)
        total_metadata["affine_normalized_mse"] = affine_error
        total_metadata["sofima_normalized_mse"] = sofima_error
        if sofima_error >= affine_error:
            total_metadata["status"] = "identity_fallback_no_error_improvement"
            return np.zeros_like(total_flow, dtype=np.float32), total_metadata
        return total_flow.astype(np.float32, copy=False), total_metadata

    from sofima import flow_field, flow_utils

    if fixed_zyx.shape != moving_affine_initialized_zyx.shape:
        raise ValueError(
            "fixed_zyx and moving_affine_initialized_zyx must have matching "
            f"shapes, got {fixed_zyx.shape!r} and "
            f"{moving_affine_initialized_zyx.shape!r}."
        )

    shape_zyx = tuple(int(v) for v in fixed_zyx.shape)
    patch_size, step = _resolve_patch_and_step(shape_zyx, config)

    calculator = flow_field.JAXMaskedXCorrWithStatsCalculator(
        mean=None,
        peak_min_distance=int(config.peak_min_distance),
        peak_radius=int(config.peak_radius),
    )
    flow = calculator.flow_field(
        moving_affine_initialized_zyx.astype(np.float32, copy=False),
        fixed_zyx.astype(np.float32, copy=False),
        patch_size=patch_size,
        step=step,
        batch_size=int(config.batch_size),
        max_masked=float(config.max_masked),
    )
    cleaned_flow = flow_utils.clean_flow(
        flow,
        min_peak_ratio=float(config.min_peak_ratio),
        min_peak_sharpness=float(config.min_peak_sharpness),
        max_magnitude=float(config.max_magnitude),
        max_deviation=float(config.max_deviation),
        dim=3,
    )
    cleaned_flow, subpixel_refined_vectors = _refine_flow_vectors_subpixel(
        cleaned_flow,
        moving_affine_initialized_zyx.astype(np.float32, copy=False),
        fixed_zyx.astype(np.float32, copy=False),
        patch_size_zyx=patch_size,
        step_zyx=step,
        config=config,
    )
    valid_flow_mask = np.isfinite(cleaned_flow[0])
    valid_flow_vectors = int(np.sum(valid_flow_mask))
    if valid_flow_vectors == 0:
        sofima_flow_field = np.zeros_like(cleaned_flow, dtype=np.float32)
        flow_status = "identity_fallback_no_valid_vectors"
        relaxation_metadata = {
            "mesh_relaxation": False,
            "mesh_iterations": 0,
            "mesh_final_kinetic_energy": 0.0,
        }
    else:
        initial_flow_field = _median_initial_flow_field(cleaned_flow)
        sofima_flow_field, relaxation_metadata = _relax_flow_field(
            cleaned_flow,
            initial_flow_field,
            step,
            config,
        )
        sofima_flow_field, axial_metadata = _stabilize_axial_flow_component(
            sofima_flow_field,
            config,
        )
        flow_status = "ok"
    map_stride_zyx_px = [float(v) for v in step]

    metadata = {
        "status": flow_status,
        "valid_flow_vectors": valid_flow_vectors,
        "subpixel_refined_vectors": subpixel_refined_vectors,
        "residual_iterations": 1,
        "map_stride_zyx_px": map_stride_zyx_px,
        "map_box_start_xyz_px": [
            float(patch_size[2]) / 2.0,
            float(patch_size[1]) / 2.0,
            float(patch_size[0]) / 2.0,
        ],
        "map_box_size_xyz_px": [
            float((sofima_flow_field.shape[3] - 1) * map_stride_zyx_px[2] + 1),
            float((sofima_flow_field.shape[2] - 1) * map_stride_zyx_px[1] + 1),
            float((sofima_flow_field.shape[1] - 1) * map_stride_zyx_px[0] + 1),
        ],
        "mesh_initializer": "median_valid_flow",
        "sofima_config": config.as_metadata(),
    }
    metadata.update(relaxation_metadata)
    if valid_flow_vectors > 0:
        metadata.update(axial_metadata)
    return sofima_flow_field.astype(np.float32, copy=False), metadata

_median_initial_flow_field(cleaned_flow_xyz)

Build a dense SOFIMA mesh initializer from valid local flow vectors.

SOFIMA's elastic relaxation needs a dense initial mesh, but the cleaned local flow map is sparse. Invalid nodes are initialized to the robust median vector, while valid measured nodes keep their measured displacement. This avoids the expensive CPU scattered interpolation in sofima.map_utils.fill_missing while preserving the local information available before relaxation.

Parameters:

Name Type Description Default
cleaned_flow_xyz ndarray

Cleaned SOFIMA flow field in (3, z, y, x) order. Invalid vectors are encoded as NaN.

required

Returns:

Type Description
ndarray

Dense initial flow field in (3, z, y, x) order.

Source code in src/merfish3danalysis/utils/sofima_registration.py
def _median_initial_flow_field(cleaned_flow_xyz: np.ndarray) -> np.ndarray:
    """
    Build a dense SOFIMA mesh initializer from valid local flow vectors.

    SOFIMA's elastic relaxation needs a dense initial mesh, but the cleaned
    local flow map is sparse. Invalid nodes are initialized to the robust
    median vector, while valid measured nodes keep their measured displacement.
    This avoids the expensive CPU scattered interpolation in
    ``sofima.map_utils.fill_missing`` while preserving the local information
    available before relaxation.

    Parameters
    ----------
    cleaned_flow_xyz : numpy.ndarray
        Cleaned SOFIMA flow field in ``(3, z, y, x)`` order. Invalid vectors
        are encoded as NaN.

    Returns
    -------
    numpy.ndarray
        Dense initial flow field in ``(3, z, y, x)`` order.
    """
    initial_flow = np.zeros_like(cleaned_flow_xyz, dtype=np.float32)
    valid_mask = np.all(np.isfinite(cleaned_flow_xyz), axis=0)
    if not np.any(valid_mask):
        return initial_flow

    for channel_index in range(3):
        channel = cleaned_flow_xyz[channel_index]
        median = float(np.median(channel[valid_mask]))
        initial_flow[channel_index, ...] = median
        initial_flow[channel_index][valid_mask] = channel[valid_mask]
    return initial_flow

_normalized_mean_squared_error(fixed_zyx, moving_zyx)

Return normalized mean-squared error between two images.

Parameters:

Name Type Description Default
fixed_zyx ndarray

Fixed reference image.

required
moving_zyx ndarray

Moving image sampled on the reference grid.

required

Returns:

Type Description
float

Mean-squared error after robust intensity normalization.

Source code in src/merfish3danalysis/utils/sofima_registration.py
def _normalized_mean_squared_error(
    fixed_zyx: np.ndarray,
    moving_zyx: np.ndarray,
) -> float:
    """
    Return normalized mean-squared error between two images.

    Parameters
    ----------
    fixed_zyx : numpy.ndarray
        Fixed reference image.
    moving_zyx : numpy.ndarray
        Moving image sampled on the reference grid.

    Returns
    -------
    float
        Mean-squared error after robust intensity normalization.
    """
    fixed = np.asarray(fixed_zyx, dtype=np.float32)
    moving = np.asarray(moving_zyx, dtype=np.float32)
    epsilon = np.finfo(np.float32).eps
    fixed_scale = max(float(np.std(fixed)), epsilon)
    moving_scale = max(float(np.std(moving)), epsilon)
    fixed_normalized = (fixed - float(np.mean(fixed))) / fixed_scale
    moving_normalized = (moving - float(np.mean(moving))) / moving_scale
    return float(np.mean((fixed_normalized - moving_normalized) ** 2))

_refine_flow_vectors_subpixel(flow_xyz, pre_image_zyx, post_image_zyx, *, patch_size_zyx, step_zyx, config)

Refine valid SOFIMA integer vectors by local fractional patch matching.

SOFIMA's local cross-correlation reports integer-pixel peak locations. This function keeps SOFIMA's accepted vectors and only searches a small fractional neighborhood around each one. Candidate shifts are scored by normalized sum-of-squared differences between the post patch and a subpixel-sampled pre patch.

Parameters:

Name Type Description Default
flow_xyz ndarray

Cleaned SOFIMA flow field in (3, z, y, x) order.

required
pre_image_zyx ndarray

Moving image in Z, Y, X order. This is SOFIMA's pre_image.

required
post_image_zyx ndarray

Fixed image in Z, Y, X order. This is SOFIMA's post_image.

required
patch_size_zyx tuple[int, int, int]

Patch size used for SOFIMA flow estimation.

required
step_zyx tuple[int, int, int]

Flow-grid step used for SOFIMA flow estimation.

required
config SofimaRegistrationConfig

Explicit SOFIMA parameter set.

required

Returns:

Type Description
tuple[ndarray, int]

Refined flow field and the number of vectors that were refined.

Source code in src/merfish3danalysis/utils/sofima_registration.py
def _refine_flow_vectors_subpixel(
    flow_xyz: np.ndarray,
    pre_image_zyx: np.ndarray,
    post_image_zyx: np.ndarray,
    *,
    patch_size_zyx: tuple[int, int, int],
    step_zyx: tuple[int, int, int],
    config: SofimaRegistrationConfig,
) -> tuple[np.ndarray, int]:
    """
    Refine valid SOFIMA integer vectors by local fractional patch matching.

    SOFIMA's local cross-correlation reports integer-pixel peak locations. This
    function keeps SOFIMA's accepted vectors and only searches a small
    fractional neighborhood around each one. Candidate shifts are scored by
    normalized sum-of-squared differences between the post patch and a
    subpixel-sampled pre patch.

    Parameters
    ----------
    flow_xyz : numpy.ndarray
        Cleaned SOFIMA flow field in ``(3, z, y, x)`` order.
    pre_image_zyx : numpy.ndarray
        Moving image in Z, Y, X order. This is SOFIMA's ``pre_image``.
    post_image_zyx : numpy.ndarray
        Fixed image in Z, Y, X order. This is SOFIMA's ``post_image``.
    patch_size_zyx : tuple[int, int, int]
        Patch size used for SOFIMA flow estimation.
    step_zyx : tuple[int, int, int]
        Flow-grid step used for SOFIMA flow estimation.
    config : SofimaRegistrationConfig
        Explicit SOFIMA parameter set.

    Returns
    -------
    tuple[numpy.ndarray, int]
        Refined flow field and the number of vectors that were refined.
    """
    import itertools

    import jax
    import jax.numpy as jnp
    from jax.scipy.ndimage import map_coordinates

    offsets = np.asarray(config.subpixel_offsets, dtype=np.float32)

    valid_indices = np.argwhere(np.isfinite(flow_xyz[0]))
    if valid_indices.size == 0:
        return flow_xyz.copy(), 0

    patch_grid_zyx = jnp.asarray(np.indices(patch_size_zyx, dtype=np.float32))
    image_shape = np.asarray(pre_image_zyx.shape, dtype=np.float32)
    patch_size_array = np.asarray(patch_size_zyx, dtype=np.float32)
    step_array = np.asarray(step_zyx, dtype=np.float32)
    vector_xyz = np.stack(
        [
            flow_xyz[0][tuple(valid_indices.T)],
            flow_xyz[1][tuple(valid_indices.T)],
            flow_xyz[2][tuple(valid_indices.T)],
        ],
        axis=1,
    ).astype(np.float32, copy=False)
    vector_zyx = vector_xyz[:, [2, 1, 0]]
    post_starts_zyx = valid_indices.astype(np.float32) * step_array
    base_pre_starts_zyx = post_starts_zyx + vector_zyx
    interior_mask = np.all(base_pre_starts_zyx >= 1.0, axis=1) & np.all(
        base_pre_starts_zyx + patch_size_array <= image_shape - 2.0,
        axis=1,
    )
    if not np.any(interior_mask):
        return flow_xyz.copy(), 0

    valid_indices = valid_indices[interior_mask]
    vector_xyz = vector_xyz[interior_mask]
    offset_grid_zyx = np.asarray(
        list(itertools.product(offsets, offsets, offsets)),
        dtype=np.float32,
    )
    batch_size = int(config.subpixel_batch_size)
    normalization_epsilon = jnp.float32(config.normalization_epsilon)
    pre_image = jnp.asarray(pre_image_zyx, dtype=jnp.float32)
    post_image = jnp.asarray(post_image_zyx, dtype=jnp.float32)
    candidate_offsets = jnp.asarray(offset_grid_zyx, dtype=jnp.float32)
    step = jnp.asarray(step_zyx, dtype=jnp.float32)

    @jax.jit
    def _refine_batch(
        pre_image: jnp.ndarray,
        post_image: jnp.ndarray,
        patch_grid_zyx: jnp.ndarray,
        candidate_offsets: jnp.ndarray,
        step: jnp.ndarray,
        batch_indices_zyx: jnp.ndarray,
        batch_vectors_xyz: jnp.ndarray,
    ) -> jnp.ndarray:
        post_starts = batch_indices_zyx.astype(jnp.float32) * step
        batch_vectors_zyx = batch_vectors_xyz[:, [2, 1, 0]]
        base_pre_starts = post_starts + batch_vectors_zyx

        post_coords = (
            patch_grid_zyx[:, jnp.newaxis, ...]
            + post_starts.T[:, :, jnp.newaxis, jnp.newaxis, jnp.newaxis]
        )
        post_patch = map_coordinates(
            post_image,
            post_coords,
            order=1,
            mode="constant",
            cval=0.0,
        )
        post_patch = post_patch - jnp.mean(post_patch, axis=(1, 2, 3), keepdims=True)
        post_patch = post_patch / jnp.maximum(
            jnp.sqrt(jnp.mean(post_patch**2, axis=(1, 2, 3), keepdims=True)),
            normalization_epsilon,
        )

        pre_starts = (
            base_pre_starts[:, jnp.newaxis, :] + candidate_offsets[jnp.newaxis, :, :]
        )
        pre_coords = (
            patch_grid_zyx[:, jnp.newaxis, jnp.newaxis, ...]
            + jnp.moveaxis(pre_starts, -1, 0)[
                :, :, :, jnp.newaxis, jnp.newaxis, jnp.newaxis
            ]
        )
        pre_patch = map_coordinates(
            pre_image,
            pre_coords,
            order=1,
            mode="constant",
            cval=0.0,
        )
        pre_patch = pre_patch - jnp.mean(
            pre_patch,
            axis=(2, 3, 4),
            keepdims=True,
        )
        pre_patch = pre_patch / jnp.maximum(
            jnp.sqrt(jnp.mean(pre_patch**2, axis=(2, 3, 4), keepdims=True)),
            normalization_epsilon,
        )
        scores = jnp.mean(
            (pre_patch - post_patch[:, jnp.newaxis, ...]) ** 2,
            axis=(2, 3, 4),
        )
        best_indices = jnp.argmin(scores, axis=1)
        best_offsets_zyx = candidate_offsets[best_indices]
        return batch_vectors_xyz + best_offsets_zyx[:, [2, 1, 0]]

    refined = flow_xyz.copy()
    refined_vectors = 0
    for start in range(0, valid_indices.shape[0], batch_size):
        stop = min(start + batch_size, valid_indices.shape[0])
        batch_indices = valid_indices[start:stop]
        batch_vectors = vector_xyz[start:stop]
        refined_batch = np.asarray(
            jax.device_get(
                _refine_batch(
                    pre_image,
                    post_image,
                    patch_grid_zyx,
                    candidate_offsets,
                    step,
                    jnp.asarray(batch_indices, dtype=jnp.int32),
                    jnp.asarray(batch_vectors, dtype=jnp.float32),
                )
            ),
            dtype=np.float32,
        )
        refined[0][tuple(batch_indices.T)] = refined_batch[:, 0]
        refined[1][tuple(batch_indices.T)] = refined_batch[:, 1]
        refined[2][tuple(batch_indices.T)] = refined_batch[:, 2]
        refined_vectors += batch_indices.shape[0]

    return refined.astype(np.float32, copy=False), int(refined_vectors)

_relax_flow_field(cleaned_flow_xyz, initial_flow_xyz, step_zyx, config)

Relax a sparse SOFIMA flow field with SOFIMA's elastic mesh solver.

SOFIMA's patch cross-correlation returns integer local flow vectors. The elastic mesh converts those measurements into the smooth float-valued coordinate map expected by downstream warping. Valid measured vectors pull mesh nodes through zero-length springs; invalid nodes are governed by the internal 3D mesh elasticity.

Parameters:

Name Type Description Default
cleaned_flow_xyz ndarray

Cleaned SOFIMA flow field in (3, z, y, x) order. Invalid vectors are encoded as NaN.

required
initial_flow_xyz ndarray

Initial dense flow field in (3, z, y, x) order.

required
step_zyx tuple[int, int, int]

Flow-grid spacing in image pixels in Z, Y, X order.

required
config SofimaRegistrationConfig

Explicit SOFIMA parameter set.

required

Returns:

Type Description
tuple[ndarray, dict[str, Any]]

Relaxed float-valued flow field and metadata for the mesh solve.

Source code in src/merfish3danalysis/utils/sofima_registration.py
def _relax_flow_field(
    cleaned_flow_xyz: np.ndarray,
    initial_flow_xyz: np.ndarray,
    step_zyx: tuple[int, int, int],
    config: SofimaRegistrationConfig,
) -> tuple[np.ndarray, dict[str, Any]]:
    """
    Relax a sparse SOFIMA flow field with SOFIMA's elastic mesh solver.

    SOFIMA's patch cross-correlation returns integer local flow vectors. The
    elastic mesh converts those measurements into the smooth float-valued
    coordinate map expected by downstream warping. Valid measured vectors pull
    mesh nodes through zero-length springs; invalid nodes are governed by the
    internal 3D mesh elasticity.

    Parameters
    ----------
    cleaned_flow_xyz : numpy.ndarray
        Cleaned SOFIMA flow field in ``(3, z, y, x)`` order. Invalid vectors
        are encoded as NaN.
    initial_flow_xyz : numpy.ndarray
        Initial dense flow field in ``(3, z, y, x)`` order.
    step_zyx : tuple[int, int, int]
        Flow-grid spacing in image pixels in Z, Y, X order.
    config : SofimaRegistrationConfig
        Explicit SOFIMA parameter set.

    Returns
    -------
    tuple[numpy.ndarray, dict[str, Any]]
        Relaxed float-valued flow field and metadata for the mesh solve.
    """
    import jax
    import jax.numpy as jnp
    from sofima import mesh

    stride_xyz = (float(step_zyx[2]), float(step_zyx[1]), float(step_zyx[0]))
    mesh_config = mesh.IntegrationConfig(
        dt=float(config.mesh_dt),
        gamma=float(config.mesh_gamma),
        k0=float(config.mesh_k0),
        k=float(config.mesh_k),
        stride=stride_xyz,
        num_iters=int(config.mesh_num_iters),
        max_iters=int(config.mesh_max_iters),
        stop_v_max=float(config.mesh_stop_v_max),
        dt_max=float(config.mesh_dt_max),
        prefer_orig_order=False,
        start_cap=float(config.mesh_start_cap),
        final_cap=float(config.mesh_final_cap),
        remove_drift=False,
    )
    relaxed_flow, kinetic_energy, iterations = mesh.relax_mesh(
        jnp.asarray(initial_flow_xyz, dtype=jnp.float32),
        jnp.asarray(cleaned_flow_xyz, dtype=jnp.float32),
        mesh_config,
        mesh_force=mesh.elastic_mesh_3d,
    )
    relaxed_flow = np.asarray(jax.device_get(relaxed_flow), dtype=np.float32)
    metadata = {
        "mesh_iterations": int(iterations),
        "mesh_final_kinetic_energy": (
            float(kinetic_energy[-1]) if len(kinetic_energy) > 0 else 0.0
        ),
        "mesh_relaxation": True,
    }
    return relaxed_flow, metadata

_resolve_patch_and_step(shape_zyx, config)

Resolve SOFIMA patch size and stride for one fixed/moving volume pair.

Parameters:

Name Type Description Default
shape_zyx tuple[int, int, int]

Shape of the fixed and moving volumes in Z, Y, X order.

required
config SofimaRegistrationConfig

Explicit SOFIMA parameter set.

required

Returns:

Type Description
tuple[tuple[int, int, int], tuple[int, int, int]]

Patch size and step in Z, Y, X order, clipped to the image shape.

Source code in src/merfish3danalysis/utils/sofima_registration.py
def _resolve_patch_and_step(
    shape_zyx: tuple[int, int, int],
    config: SofimaRegistrationConfig,
) -> tuple[tuple[int, int, int], tuple[int, int, int]]:
    """
    Resolve SOFIMA patch size and stride for one fixed/moving volume pair.

    Parameters
    ----------
    shape_zyx : tuple[int, int, int]
        Shape of the fixed and moving volumes in Z, Y, X order.
    config : SofimaRegistrationConfig
        Explicit SOFIMA parameter set.

    Returns
    -------
    tuple[tuple[int, int, int], tuple[int, int, int]]
        Patch size and step in Z, Y, X order, clipped to the image shape.
    """
    patch_size = tuple(
        max(config.minimum_patch_size_px, min(axis_size, patch_size))
        for axis_size, patch_size in zip(
            shape_zyx,
            config.patch_size_zyx,
            strict=False,
        )
    )
    patch_size = tuple(int(v) for v in patch_size)
    step = tuple(max(1, size // int(config.step_divisor)) for size in patch_size)
    return patch_size, step

_stabilize_axial_flow_component(flow_xyz, config)

Clip unstable local axial residuals in a SOFIMA flow field.

The affine registration step handles the bulk Z displacement between rounds. The SOFIMA field is used for residual deformable correction, but local axial flow is much less well constrained than lateral flow in these anisotropic volumes. Large local Z excursions can map valid reference planes outside the moving image and produce black slabs in warped data.

Parameters:

Name Type Description Default
flow_xyz ndarray

SOFIMA flow field with channels ordered X, Y, Z and spatial axes Z, Y, X.

required
config SofimaRegistrationConfig

Explicit SOFIMA parameter set.

required

Returns:

Type Description
tuple[ndarray, dict[str, Any]]

Flow field with the Z channel clipped around its robust median, and metadata describing the clipping.

Source code in src/merfish3danalysis/utils/sofima_registration.py
def _stabilize_axial_flow_component(
    flow_xyz: np.ndarray,
    config: SofimaRegistrationConfig,
) -> tuple[np.ndarray, dict[str, Any]]:
    """
    Clip unstable local axial residuals in a SOFIMA flow field.

    The affine registration step handles the bulk Z displacement between
    rounds. The SOFIMA field is used for residual deformable correction, but
    local axial flow is much less well constrained than lateral flow in these
    anisotropic volumes. Large local Z excursions can map valid reference
    planes outside the moving image and produce black slabs in warped data.

    Parameters
    ----------
    flow_xyz : numpy.ndarray
        SOFIMA flow field with channels ordered X, Y, Z and spatial axes Z, Y,
        X.
    config : SofimaRegistrationConfig
        Explicit SOFIMA parameter set.

    Returns
    -------
    tuple[numpy.ndarray, dict[str, Any]]
        Flow field with the Z channel clipped around its robust median, and
        metadata describing the clipping.
    """
    stabilized = np.asarray(flow_xyz, dtype=np.float32).copy()
    if stabilized.shape[0] != 3:
        raise ValueError("SOFIMA flow field must have three XYZ channels.")

    axial_flow = stabilized[2]
    finite_mask = np.isfinite(axial_flow)
    if not np.any(finite_mask):
        stabilized[2] = 0.0
        return stabilized, {
            "axial_flow_stabilized": True,
            "axial_flow_valid_vectors": 0,
            "axial_flow_median_px": 0.0,
            "axial_flow_max_local_displacement_px": (
                float(config.max_local_z_displacement_px)
            ),
            "axial_flow_clipped_vectors": int(axial_flow.size),
        }

    finite_values = axial_flow[finite_mask]
    median_z = float(np.median(finite_values))
    lower = median_z - float(config.max_local_z_displacement_px)
    upper = median_z + float(config.max_local_z_displacement_px)
    clipped = np.clip(axial_flow, lower, upper)
    clipped = np.where(finite_mask, clipped, median_z)
    clipped_count = int(
        np.sum(np.abs(clipped - axial_flow) > config.normalization_epsilon)
    )
    stabilized[2] = clipped.astype(np.float32, copy=False)

    return stabilized, {
        "axial_flow_stabilized": True,
        "axial_flow_valid_vectors": int(np.sum(finite_mask)),
        "axial_flow_median_px": median_z,
        "axial_flow_max_local_displacement_px": (
            float(config.max_local_z_displacement_px)
        ),
        "axial_flow_clipped_vectors": clipped_count,
        "axial_flow_preclip_min_px": float(np.min(finite_values)),
        "axial_flow_preclip_max_px": float(np.max(finite_values)),
    }

estimate_sofima_flow_field_xyz_px(fixed_zyx, moving_affine_initialized_zyx, *, config=None)

Estimate the production SOFIMA flow field after affine initialization.

Parameters:

Name Type Description Default
fixed_zyx ndarray

Reference round001 fiducial image in Z, Y, X order.

required
moving_affine_initialized_zyx ndarray

Moving fiducial image already rendered into the reference grid by the stored affine transform.

required
config SofimaRegistrationConfig or None

Explicit SOFIMA parameter set. If omitted, use SofimaRegistrationConfig().

None

Returns:

Type Description
tuple[ndarray, dict[str, Any]]

Relative SOFIMA flow field with XYZ channels and metadata describing the map spacing/origin.

Source code in src/merfish3danalysis/utils/sofima_registration.py
def estimate_sofima_flow_field_xyz_px(
    fixed_zyx: np.ndarray,
    moving_affine_initialized_zyx: np.ndarray,
    *,
    config: SofimaRegistrationConfig | None = None,
) -> tuple[np.ndarray, dict[str, Any]]:
    """
    Estimate the production SOFIMA flow field after affine initialization.

    Parameters
    ----------
    fixed_zyx : numpy.ndarray
        Reference round001 fiducial image in Z, Y, X order.
    moving_affine_initialized_zyx : numpy.ndarray
        Moving fiducial image already rendered into the reference grid by the
        stored affine transform.
    config : SofimaRegistrationConfig or None, optional
        Explicit SOFIMA parameter set. If omitted, use
        ``SofimaRegistrationConfig()``.

    Returns
    -------
    tuple[numpy.ndarray, dict[str, Any]]
        Relative SOFIMA flow field with XYZ channels and metadata describing
        the map spacing/origin.
    """
    if config is None:
        config = SofimaRegistrationConfig()

    return _estimate_sofima_flow_field_xyz_px_impl(
        fixed_zyx,
        moving_affine_initialized_zyx,
        config=config,
        single_residual_pass=False,
    )