Skip to content

register

xvr.register.context

XrayContext

Everything a protocol may need to initialize a pose estimate.

xvr.register.initializers

PoseInitializer

Strategy for computing the initial pose estimate before optimization.

Concrete implementations expose two pieces of metadata read by Register during setup, plus a callable that produces a RigidTransform given the current X-ray context.

ATTRIBUTE DESCRIPTION
orientation

Patient orientation for the DRR.

TYPE: str | None

reverse_x_axis

Horizontally flip the rendered DRRs.

TYPE: bool

__call__

__call__(ctx: XrayContext) -> RigidTransform

Compute an initial pose estimate from the X-ray context.

Source code in src/xvr/register/initializers.py
def __call__(self, ctx: XrayContext) -> RigidTransform:
    """Compute an initial pose estimate from the X-ray context."""
    ...

FixedPose

Initial pose specified manually as Euler angles and a translation.

Useful when an approximate pose is already known, e.g., from a prior scan, a clinical estimate, or a hand-tuned guess.

ATTRIBUTE DESCRIPTION
rot

Rotation angles in degrees as (rx, ry, rz), ZXY convention.

TYPE: tuple[float, float, float]

xyz

Translation in mm as (x, y, z).

TYPE: tuple[float, float, float]

eps

Small value added to inputs to avoid degenerate gradients.

TYPE: float

ModelPose

Initial pose predicted by a neural network from the X-ray image.

The checkpoint dictates orientation and reverse_x_axis; users do not set these directly.

ATTRIBUTE DESCRIPTION
ckpt

Path to the model checkpoint.

TYPE: str

volume

Path to the CT image the warp is defined against (Register's imagepath).

TYPE: str | None

warp

SimpleITK transform reframing the predicted pose into the CT's frame. None for patient-specific models already in the CT's frame.

TYPE: str | None

antipodal

Initialize from the antipode of the predicted pose.

TYPE: bool

DicomPose

Initial pose parsed from pose parameters in the DICOM metadata.

RestartPose

Initial pose loaded from a previous registration run's final pose.

ATTRIBUTE DESCRIPTION
ckpt

Path to a .pth saved by a previous registration run.

TYPE: str

xvr.register.logging

OptimizationLogger dataclass

OptimizationLogger(
    losses: list[float],
    scales: list[float],
    rescale_factors: list[float],
    times: list[float],
    rots: Float[Tensor, "N 3"],
    xyzs: Float[Tensor, "N 3"],
)

Log the intermediates of a multiscale optimization run.

RegistrationResult

RegistrationResult(
    drr: DRR,
    pose: Pose,
    init_pose: RigidTransform,
    gt: Float[Tensor, "1 1 H W"],
    log: OptimizationLogger | None = None,
)

The result of a registration run.

PARAMETER DESCRIPTION
drr

The DRR renderer with detector geometry.

TYPE: DRR

pose

The optimized pose model.

TYPE: Pose

init_pose

The initial pose estimate.

TYPE: RigidTransform

gt

The preprocessed ground truth X-ray.

TYPE: Float[Tensor, '1 1 H W']

log

The optimization log.

TYPE: OptimizationLogger | None DEFAULT: None

Source code in src/xvr/register/logging.py
def __init__(
    self,
    drr: DRR,
    pose: Pose,
    init_pose: RigidTransform,
    gt: Float[torch.Tensor, "1 1 H W"],
    log: OptimizationLogger | None = None,
) -> None:
    self.drr = drr
    self.init_pose = init_pose
    self.final_pose = pose()
    self.gt = gt
    self.log = log

save

save(path: str | Path) -> None

Save the registration result to a checkpoint file.

PARAMETER DESCRIPTION
path

Output file path.

TYPE: str | Path

Source code in src/xvr/register/logging.py
def save(self, path: str | Path) -> None:
    """Save the registration result to a checkpoint file.

    Args:
        path: Output file path.
    """
    path = Path(path)
    path.parent.mkdir(parents=True, exist_ok=True)
    torch.save(
        {
            "detector": {
                "sdd": self.drr.detector.sdd,
                "height": self.drr.detector.height,
                "width": self.drr.detector.width,
                "delx": self.drr.detector.delx,
                "dely": self.drr.detector.dely,
                "reverse_x_axis": self.drr.detector.reverse_x_axis,
            },
            "runtime": sum(self.log.times) if self.log is not None else 0.0,
            "init_pose": self.init_pose.matrix.cpu(),
            "final_pose": self.final_pose.matrix.cpu(),
            "gt": self.gt.cpu(),
            "log": {
                "losses": self.log.losses,
                "scales": self.log.scales,
                "rescale_factors": self.log.rescale_factors,
                "times": self.log.times,
                "rots": self.log.rots.cpu(),
                "xyzs": self.log.xyzs.cpu(),
            }
            if self.log is not None
            else None,
        },
        path,
    )

xvr.register.loss

load_loss_function

load_loss_function(loss: str | Module, **kwargs: Any) -> Module

Initialize a loss function for 2D/3D registration.

PARAMETER DESCRIPTION
loss

Either the name of a loss function in METRICS or a callable Module whose forward method implements the loss.

TYPE: str | Module

**kwargs

Optional arguments for the constructor of the loss function.

TYPE: Any DEFAULT: {}

Source code in src/xvr/register/loss.py
def load_loss_function(loss: str | torch.nn.Module, **kwargs: Any) -> torch.nn.Module:
    """Initialize a loss function for 2D/3D registration.

    Args:
        loss: Either the name of a loss function in `METRICS` or a callable
            Module whose forward method implements the loss.
        **kwargs: Optional arguments for the constructor of the loss function.
    """
    if isinstance(loss, torch.nn.Module):
        return loss
    try:
        return METRICS[loss](**kwargs)
    except KeyError:
        raise ValueError(f"Unknown metric '{loss}'. Available: {list(METRICS.keys())}")

xvr.register.losses.gmncc

GradientMultiscaleNormalizedCrossCorrelation2d

GradientMultiscaleNormalizedCrossCorrelation2d(
    mncc_patch_size: int = 9,
    gncc_patch_size: int = 11,
    sigma: float = 0.0,
    beta: float = 0.5,
)

A weighted average of gradient and multiscale NCC.

Source code in src/xvr/register/losses/gmncc.py
def __init__(
    self,
    mncc_patch_size: int = 9,
    gncc_patch_size: int = 11,
    sigma: float = 0.0,
    beta: float = 0.5,
):
    """A weighted average of gradient and multiscale NCC."""
    super().__init__()
    self.mncc = MultiscaleNormalizedCrossCorrelation2d([None, mncc_patch_size], [0.5, 0.5])
    self.gncc = GradientNormalizedCrossCorrelation2d(gncc_patch_size, sigma)
    self.beta = beta

xvr.register.plot

RegistrationOutput

RegistrationOutput(data: bytes, fmt: str, width: int)

Image output that renders inline in Jupyter and can be saved to disk.

Source code in src/xvr/register/plot.py
def __init__(self, data: bytes, fmt: str, width: int):
    self._data = data
    self._fmt = fmt
    self._width = width

save

save(path: str) -> None

Save the image to disk.

PARAMETER DESCRIPTION
path

Output file path, e.g. "result.png" or "result.gif".

TYPE: str

Source code in src/xvr/register/plot.py
def save(self, path: str) -> None:
    """Save the image to disk.

    Args:
        path: Output file path, e.g. "result.png" or "result.gif".
    """
    with open(path, "wb") as f:
        f.write(self._data)

gif

gif(
    logger: OptimizationLogger, width: int = 600, duration: int = 500
) -> RegistrationOutput

Animate between GT and predictions as an infinitely looping GIF.

PARAMETER DESCRIPTION
logger

OptimizationLogger with .gt, .init_pose, .final_pose, and .drr.

TYPE: OptimizationLogger

width

Display width in pixels when rendered in Jupyter.

TYPE: int DEFAULT: 600

duration

Time per frame in milliseconds.

TYPE: int DEFAULT: 500

RETURNS DESCRIPTION
RegistrationOutput

RegistrationOutput that renders inline in Jupyter and can be saved to disk.

Source code in src/xvr/register/plot.py
def gif(
    logger: OptimizationLogger,
    width: int = 600,
    duration: int = 500,
) -> RegistrationOutput:
    """Animate between GT and predictions as an infinitely looping GIF.

    Args:
        logger: OptimizationLogger with .gt, .init_pose, .final_pose, and .drr.
        width: Display width in pixels when rendered in Jupyter.
        duration: Time per frame in milliseconds.

    Returns:
        RegistrationOutput that renders inline in Jupyter and can be saved to disk.
    """
    with torch.inference_mode():
        img_init = logger.drr(logger.init_pose)
        img_final = logger.drr(logger.final_pose)

    gt_pil = _to_pil(logger.gt)
    init_pil = _to_pil(img_init)
    fin_pil = _to_pil(img_final)

    frame1 = Image.fromarray(np.hstack([np.array(gt_pil), np.array(gt_pil)]))
    frame2 = Image.fromarray(np.hstack([np.array(init_pil), np.array(fin_pil)]))

    buf = BytesIO()
    frame1.save(buf, format="GIF", save_all=True, append_images=[frame2], loop=0, duration=duration)
    buf.seek(0)
    return RegistrationOutput(buf.read(), fmt="gif", width=width)

plot

plot(
    logger: OptimizationLogger,
    method: VizMethod = "edges",
    histeq_strength: float = 0.5,
    width: int = 900,
    **kwargs: Any,
) -> RegistrationOutput

Visualise registration results (GT | GT vs initial pred | GT vs final pred).

PARAMETER DESCRIPTION
logger

OptimizationLogger with .gt, .init_pose, .final_pose, and .drr.

TYPE: OptimizationLogger

method

Comparison method, one of "overlay", "checkerboard", "edges".

TYPE: VizMethod DEFAULT: 'edges'

histeq_strength

Histogram equalisation strength (0.0 = off, 1.0 = full).

TYPE: float DEFAULT: 0.5

width

Display width in pixels when rendered in Jupyter.

TYPE: int DEFAULT: 900

**kwargs

Forwarded to the chosen method function. "checkerboard" accepts n_patches (int, default 8); "edges" accepts sigma (float, default 2.0).

TYPE: Any DEFAULT: {}

RETURNS DESCRIPTION
RegistrationOutput

RegistrationOutput that renders inline in Jupyter and can be saved to disk.

Source code in src/xvr/register/plot.py
def plot(
    logger: OptimizationLogger,
    method: VizMethod = "edges",
    histeq_strength: float = 0.5,
    width: int = 900,
    **kwargs: Any,
) -> RegistrationOutput:
    """Visualise registration results (GT | GT vs initial pred | GT vs final pred).

    Args:
        logger: OptimizationLogger with .gt, .init_pose, .final_pose, and .drr.
        method: Comparison method, one of "overlay", "checkerboard", "edges".
        histeq_strength: Histogram equalisation strength (0.0 = off, 1.0 = full).
        width: Display width in pixels when rendered in Jupyter.
        **kwargs: Forwarded to the chosen method function. "checkerboard" accepts
            n_patches (int, default 8); "edges" accepts sigma (float, default 2.0).

    Returns:
        RegistrationOutput that renders inline in Jupyter and can be saved to disk.
    """
    if method not in {"overlay", "checkerboard", "edges"}:
        raise ValueError(f"method must be 'overlay', 'checkerboard', or 'edges'; got {method!r}")

    with torch.inference_mode():
        img_init = logger.drr(logger.init_pose)
        img_final = logger.drr(logger.final_pose)

    gt_pil, init_pil, fin_pil = _histeq_joint(
        [_to_pil(t) for t in [logger.gt, img_init, img_final]],
        strength=histeq_strength,
    )

    viz_fn = {"overlay": _overlay, "checkerboard": _checkerboard, "edges": _edges}[method]

    _dpi = 100  # internal only — cancels out in figsize, controls rendering sharpness
    h, w = logger.gt.squeeze().shape[-2:]
    fig, axs = plt.subplots(ncols=3, figsize=(3 * w / _dpi, h / _dpi), dpi=_dpi)

    plot_drr(logger.gt, axs=axs[0], ticks=False)
    axs[0].set_aspect("auto")

    for ax, pred_pil in zip(axs[1:], [init_pil, fin_pil]):
        ax.imshow(viz_fn(pred_pil, gt_pil, **kwargs), aspect="auto")
        ax.set_xticks([])
        ax.set_yticks([])

    fig.subplots_adjust(wspace=0, hspace=0, top=1, bottom=0, left=0, right=1)
    buf = BytesIO()
    fig.savefig(buf, format="png", dpi=_dpi)
    plt.close(fig)
    buf.seek(0)
    return RegistrationOutput(buf.read(), fmt="png", width=width)

xvr.register.pose

Pose

Pose(init: RigidTransform, parameterization: str, convention: str | None)

Optimizable pose defined on the global chart.

Source code in src/xvr/register/pose.py
def __init__(
    self,
    init: RigidTransform,
    parameterization: str,
    convention: str | None,
):
    super().__init__()
    rot, xyz = init.convert(parameterization, convention)
    self._rot = torch.nn.Parameter(rot)
    self._xyz = torch.nn.Parameter(xyz)
    self.parameterization = parameterization
    self.convention = convention

xvr.register.register

Register

2D/3D image registration.

Performs multiscale gradient-based registration of a CT volume to an X-ray image by optimizing a differentiable image similarity metric over the pose parameters of a DRR renderer.

The strategy used to compute the initial pose estimate is supplied via the initializer argument. See PoseInitializer for the contract; built-in implementations include FixedPose and ModelPose.

ATTRIBUTE DESCRIPTION
imagepath

Path to the CT image.

TYPE: str

initializer

Strategy for computing the initial pose before optimization. Also supplies the CT orientation and DRR reverse_x_axis flag.

TYPE: PoseInitializer

labelpath

Path to the segmentation label map. If None, uses the full image.

TYPE: str | None

labels

Label indices to include in the DRR. If None, uses all labels.

TYPE: list[int] | None

metric

Image similarity metric, either a string key or a custom nn.Module.

TYPE: str | Module

metric_kwargs

Additional keyword arguments passed to the loss function.

TYPE: dict

scales

Downsampling scale(s) for multiscale registration.

TYPE: list[float]

n_itrs

Number of optimization iterations per scale.

TYPE: list[int]

lr_rot

Learning rate for rotation parameters.

TYPE: float

lr_xyz

Learning rate for translation parameters.

TYPE: float

lr_reduce_factor

Factor by which to reduce the learning rate on plateau.

TYPE: float

patience

Number of steps without improvement before reducing the learning rate.

TYPE: list[int] | int

threshold

Minimum change to qualify as an improvement.

TYPE: float

max_n_plateaus

Number of learning rate reductions before early stopping.

TYPE: int

device

Torch device to run on.

TYPE: str

__call__

__call__(
    filename: str,
    crop: int = 0,
    linearize: bool = True,
    subtract_background: bool = False,
    equalize: bool = False,
    reducefn: str | int | Callable = "max",
    parameterization: str = "euler_angles",
    convention: str | None = "ZXY",
    init_only: bool = False,
    savepath: Path | str | None = None,
    saveplot: bool = False,
) -> RegistrationResult

Run registration on an X-ray image.

Preprocesses the X-ray, computes an initial pose estimate via self.initializer, and runs multiscale gradient-based optimization to refine the pose.

PARAMETER DESCRIPTION
filename

Path to the X-ray image.

TYPE: str

crop

Number of pixels to crop from the image border.

TYPE: int DEFAULT: 0

linearize

Convert image to linear attenuation values.

TYPE: bool DEFAULT: True

subtract_background

Subtract background from the image.

TYPE: bool DEFAULT: False

equalize

Apply histogram equalization during optimization.

TYPE: bool DEFAULT: False

reducefn

Reduction function for multi-frame images.

TYPE: str | int | Callable DEFAULT: 'max'

parameterization

Representation of SO(3) for pose optimization.

TYPE: str DEFAULT: 'euler_angles'

convention

If parameterization='euler_angles', specify order.

TYPE: str | None DEFAULT: 'ZXY'

init_only

Return initial pose estimate result without optimization.

TYPE: bool DEFAULT: False

savepath

Location to save the registration results.

TYPE: Path | str | None DEFAULT: None

saveplot

Save plots of registration results.

TYPE: bool DEFAULT: False

RETURNS DESCRIPTION
RegistrationResult

A RegistrationResult with the optimized pose and full optimization log.

Source code in src/xvr/register/register.py
def __call__(
    self,
    filename: str,
    crop: int = 0,
    linearize: bool = True,
    subtract_background: bool = False,
    equalize: bool = False,
    reducefn: str | int | Callable = "max",
    parameterization: str = "euler_angles",
    convention: str | None = "ZXY",
    init_only: bool = False,
    savepath: Path | str | None = None,
    saveplot: bool = False,
) -> RegistrationResult:
    """Run registration on an X-ray image.

    Preprocesses the X-ray, computes an initial pose estimate via
    `self.initializer`, and runs multiscale gradient-based optimization
    to refine the pose.

    Args:
        filename: Path to the X-ray image.
        crop: Number of pixels to crop from the image border.
        linearize: Convert image to linear attenuation values.
        subtract_background: Subtract background from the image.
        equalize: Apply histogram equalization during optimization.
        reducefn: Reduction function for multi-frame images.
        parameterization: Representation of SO(3) for pose optimization.
        convention: If `parameterization='euler_angles'`, specify order.
        init_only: Return initial pose estimate result without optimization.
        savepath: Location to save the registration results.
        saveplot: Save plots of registration results.

    Returns:
        A RegistrationResult with the optimized pose and full optimization log.
    """
    # Read and preprocess the ground truth X-ray
    gt, intrinsics, _ = read_xray(filename, crop, subtract_background, linearize, reducefn)
    *_, height, width = gt.shape

    # Bundle per-call facts for the initializer
    ctx = XrayContext(filename=Path(filename), img=gt, intrinsics=intrinsics)

    # Get the initial pose estimate
    with torch.no_grad():
        init_pose = self.initializer(ctx).to(self.device)

    # Build the DRR renderer and pose model
    intrinsics.x0 = -intrinsics.x0
    drr = DRR(
        subject=self.subject,
        height=height,
        width=width,
        reverse_x_axis=self.initializer.reverse_x_axis,
        renderer="trilinear",
        voxel_shift=0.0,
        **intrinsics,
    ).to(self.device)
    pose = Pose(init_pose, parameterization, convention).to(self.device)

    # Optionally perform multiscale registration
    log = None
    if not init_only:
        log = self._run_multiscale(drr, pose, gt, equalize, crop)

    # Save the registration results
    result = RegistrationResult(drr, pose, init_pose, gt, log)
    if savepath is not None:
        savepath = Path(savepath) / Path(filename).stem
        result.save(savepath.with_suffix(".pth"))
        if saveplot:
            plot(result).save(savepath.with_suffix(".png"))
            gif(result).save(savepath.with_suffix(".gif"))

    return result

parse_scales

parse_scales(scales: list[float], crop: int, height: int) -> list[float]

Convert absolute downscale factors to sequential rescale ratios for an image pyramid.

PARAMETER DESCRIPTION
scales

Absolute downscale factors relative to the original image (e.g. [8, 4, 2, 1]). A scale of 1.0 snaps back to full cropped resolution.

TYPE: list[float]

crop

Total pixels cropped from the original image (crop/2 from each side).

TYPE: int

height

Height of the cropped image in pixels.

TYPE: int

RETURNS DESCRIPTION
list[float]

Sequential rescale ratios between consecutive pyramid levels.

Source code in src/xvr/register/register.py
def parse_scales(scales: list[float], crop: int, height: int) -> list[float]:
    """Convert absolute downscale factors to sequential rescale ratios for an image pyramid.

    Args:
        scales: Absolute downscale factors relative to the original image (e.g. [8, 4, 2, 1]).
            A scale of 1.0 snaps back to full cropped resolution.
        crop: Total pixels cropped from the original image (crop/2 from each side).
        height: Height of the cropped image in pixels.

    Returns:
        Sequential rescale ratios between consecutive pyramid levels.
    """
    pyramid = [1.0] + [1.0 if x == 1.0 else x * (height / (height + crop)) for x in scales]
    return [pyramid[idx] / pyramid[idx + 1] for idx in range(len(pyramid) - 1)]