Source code for neuroreg.transforms.regdat

from __future__ import annotations

from dataclasses import dataclass
from pathlib import Path

import numpy as np

from ..image.geometry import vox2tkras_from_volume_info
from .lta import LTA, _AnyHeader, _header_info, _header_to_vol_info, affine_from_volume_info

_VALID_FLOAT2INT = {"tkregister", "round", "floor"}


[docs] @dataclass(slots=True) class RegisterDat: """FreeSurfer tkregister-style volumetric registration file.""" matrix: np.ndarray subject: str = "subject-unknown" inplane_resolution: float = 1.0 between_plane_resolution: float = 1.0 intensity: float = 0.1 float2int: str = "round" def __post_init__(self) -> None: self.matrix = np.asarray(self.matrix, dtype=float).reshape(4, 4) if self.float2int not in _VALID_FLOAT2INT: raise ValueError(f"float2int must be one of {_VALID_FLOAT2INT}, got {self.float2int!r}")
[docs] @classmethod def read(cls, filename: str | Path) -> RegisterDat: """Read a FreeSurfer ``register.dat`` / tkregister transform file. Parameters ---------- filename : str or Path Path to a ``register.dat`` file. Returns ------- RegisterDat Parsed register.dat wrapper. Raises ------ ValueError If the file is shorter than the required header-plus-matrix layout. """ path = Path(filename) lines = [line.strip() for line in path.read_text().splitlines() if line.strip()] if len(lines) < 8: raise ValueError(f"{path}: register.dat file is too short") subject = lines[0] inplane = float(lines[1]) between = float(lines[2]) intensity = float(lines[3]) matrix = np.array([[float(v) for v in row.split()] for row in lines[4:8]], dtype=float) float2int = lines[8] if len(lines) >= 9 else "tkregister" return cls( matrix=matrix, subject=subject, inplane_resolution=inplane, between_plane_resolution=between, intensity=intensity, float2int=float2int, )
[docs] @classmethod def from_lta( cls, lta: LTA, *, subject: str | None = None, intensity: float | None = None, float2int: str = "round", ) -> RegisterDat: """Create a register.dat transform from a canonical LTA. Parameters ---------- lta : LTA Canonical scanner-RAS transform mapping moving to reference space. subject : str or None, optional Subject name to store in the output file. When omitted, the subject metadata from ``lta`` is used when available. intensity : float or None, optional Intensity scale to store in the output file. When omitted, ``lta`` metadata is used when available. float2int : {'tkregister', 'round', 'floor'}, default='round' Float-to-int conversion mode written to the output file. Returns ------- RegisterDat Wrapper containing the equivalent tkregister transform. """ src_affine = affine_from_volume_info(lta.src) dst_affine = affine_from_volume_info(lta.dst) src_vox2tkras = vox2tkras_from_volume_info(lta.src) dst_vox2tkras = vox2tkras_from_volume_info(lta.dst) src_ras2tkras = src_vox2tkras @ np.linalg.inv(src_affine) dst_tkras2ras = dst_affine @ np.linalg.inv(dst_vox2tkras) reg_matrix = src_ras2tkras @ np.linalg.inv(lta.r2r()) @ dst_tkras2ras subject_name = subject if subject is not None else (lta.subject or "subject-unknown") reg_intensity = intensity if intensity is not None else (0.1 if lta.fscale is None else float(lta.fscale)) return cls( matrix=reg_matrix, subject=subject_name, inplane_resolution=float(lta.src["voxelsize"][0]), between_plane_resolution=float(lta.src["voxelsize"][2]), intensity=reg_intensity, float2int=float2int, )
[docs] def to_lta( self, *, src_fname: str, src_img: _AnyHeader, dst_fname: str, dst_img: _AnyHeader, ) -> LTA: """Convert the tkregister transform to canonical RAS-to-RAS LTA. Parameters ---------- src_fname, dst_fname : str Source and destination filenames stored in the output LTA metadata. src_img, dst_img : header-like Source and destination image headers used to recover tkregister geometry. Returns ------- LTA Canonical scanner-RAS transform wrapper. Notes ----- ``src`` is the moving/input volume and ``dst`` is the reference/target volume. """ src = _header_to_vol_info(_header_info(src_img), src_fname) dst = _header_to_vol_info(_header_info(dst_img), dst_fname) src_affine = affine_from_volume_info(src) dst_affine = affine_from_volume_info(dst) src_vox2tkras = vox2tkras_from_volume_info(src) dst_vox2tkras = vox2tkras_from_volume_info(dst) src_ras2tkras = src_vox2tkras @ np.linalg.inv(src_affine) dst_tkras2ras = dst_affine @ np.linalg.inv(dst_vox2tkras) matrix = dst_tkras2ras @ np.linalg.inv(self.matrix) @ src_ras2tkras return LTA(matrix, 1, src, dst, subject=self.subject, fscale=self.intensity)
[docs] def write(self, filename: str | Path) -> None: """Write the transform in ``register.dat`` format. Parameters ---------- filename : str or Path Output transform path. Returns ------- None Writes the transform to ``filename``. """ path = Path(filename) with path.open("w") as f: f.write(f"{self.subject or 'subject-unknown'}\n") f.write(f"{float(self.inplane_resolution):.6f}\n") f.write(f"{float(self.between_plane_resolution):.6f}\n") f.write(f"{float(self.intensity):.6f}\n") for row_idx, row in enumerate(self.matrix, start=1): if row_idx < 4: f.write(" ".join(f"{float(v):.15e}" for v in row) + " \n") else: f.write(" ".join(f"{float(v):.15g}" for v in row) + "\n") f.write(f"{self.float2int}\n")