Skip to content

model

xvr.model.augmentations

RandomCenterCrop

RandomCenterCrop(maxcrop: int, p: float = 0.5)

Simulate collimation.

Source code in src/xvr/model/augmentations.py
def __init__(self, maxcrop: int, p: float = 0.5):
    super().__init__(p=p)
    self.maxcrop = maxcrop

xvr.model.inference

predict_pose

predict_pose(
    model: PoseRegressor,
    config: dict,
    img: Float[Tensor, "1 1 H W"],
    sdd: float,
    delx: float,
    dely: float,
    x0: float,
    y0: float,
) -> RigidTransform

Regress the camera pose of an X-ray with a trained pose regressor.

The X-ray is resampled to the intrinsics the model was trained on, center cropped, and normalized before being passed through the network. This is how a checkpoint is used as an initializer for iterative registration.

PARAMETER DESCRIPTION
model

Trained PoseRegressor.

TYPE: PoseRegressor

config

The checkpoint's saved training config, supplying the model's assumed sdd, height, and delx.

TYPE: dict

img

X-ray image tensor of shape (1, 1, H, W).

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

sdd

Source-to-detector distance of the X-ray, in millimeters.

TYPE: float

delx

Pixel spacing of the X-ray along the x-axis, in millimeters.

TYPE: float

dely

Pixel spacing of the X-ray along the y-axis, in millimeters. Must equal delx; non-square pixels are not supported.

TYPE: float

x0

Detector origin offset along the x-axis, in millimeters.

TYPE: float

y0

Detector origin offset along the y-axis, in millimeters.

TYPE: float

RETURNS DESCRIPTION
RigidTransform

The predicted camera pose.

Source code in src/xvr/model/inference.py
def predict_pose(
    model: PoseRegressor,
    config: dict,
    img: Float[torch.Tensor, "1 1 H W"],
    sdd: float,
    delx: float,
    dely: float,
    x0: float,
    y0: float,
) -> RigidTransform:
    """Regress the camera pose of an X-ray with a trained pose regressor.

    The X-ray is resampled to the intrinsics the model was trained on, center
    cropped, and normalized before being passed through the network. This is how
    a checkpoint is used as an initializer for iterative registration.

    Args:
        model: Trained `PoseRegressor`.
        config: The checkpoint's saved training config, supplying the model's
            assumed `sdd`, `height`, and `delx`.
        img: X-ray image tensor of shape ``(1, 1, H, W)``.
        sdd: Source-to-detector distance of the X-ray, in millimeters.
        delx: Pixel spacing of the X-ray along the x-axis, in millimeters.
        dely: Pixel spacing of the X-ray along the y-axis, in millimeters.
            Must equal `delx`; non-square pixels are not supported.
        x0: Detector origin offset along the x-axis, in millimeters.
        y0: Detector origin offset along the y-axis, in millimeters.

    Returns:
        The predicted camera pose.
    """
    # Resample the X-ray image to match the model's assumed intrinsics
    img, height, width = _resample_xray(img, sdd, delx, dely, x0, y0, config)
    height = min(height, width)
    img = center_crop(img, (height, height))

    # Resize the image and normalize pixel intensities
    transforms = XrayTransforms(config["height"])
    img = transforms(img).cuda()

    # Predict pose
    with torch.no_grad():
        init_pose = model(img)

    return init_pose

xvr.model.loss

DiceMetric

forward

forward(y_pred, y_true)

Compute 2D Dice coefficient between to multi-channel labelmaps. Assumes the first channel in each image is background.

Equivalent to monai.metrics.DiceMetric(include_background=False, reduction="none")

Source code in src/xvr/model/loss.py
def forward(self, y_pred, y_true):
    """
    Compute 2D Dice coefficient between to multi-channel labelmaps.
    Assumes the first channel in each image is background.

    Equivalent to monai.metrics.DiceMetric(include_background=False, reduction="none")
    """
    y_pred = y_pred.view(y_pred.shape[0], y_pred.shape[1], -1)
    y_true = y_true.view(y_true.shape[0], y_true.shape[1], -1)

    intersection = (y_pred * y_true).sum(dim=2)
    pred_sum = y_pred.sum(dim=2)
    true_sum = y_true.sum(dim=2)

    dice = (2.0 * intersection) / (pred_sum + true_sum)
    return dice[:, 1:]

HausdorffLoss

HausdorffLoss(alpha: float = 2.0, kernel_size: int = 3, num_iters: int = 10)

Differentiable Hausdorff loss via the distance-transform formulation of Karimi & Salcudean (2019). The distance transform is approximated by iterated morphological erosion (max-pool on the inverted mask) and treated as a fixed spatial weight; gradients flow through (y_pred - y_true)^2.

L = mean( (y_pred - y_true)^2 * (DT(y_true)^a + DT(y_pred)^a) )
Source code in src/xvr/model/loss.py
def __init__(self, alpha: float = 2.0, kernel_size: int = 3, num_iters: int = 10):
    super().__init__()
    self.alpha = alpha
    self.kernel_size = kernel_size
    self.num_iters = num_iters

xvr.model.network

PoseRegressor

PoseRegressor(
    model_name,
    parameterization,
    convention=None,
    pretrained=False,
    height=256,
    unit_conversion_factor=1000.0,
    **kwargs,
)

A PoseRegressor is comprised of a pretrained backbone model that extracts features from an input X-ray and two linear layers that decode these features into rotational and translational camera pose parameters, respectively.

Source code in src/xvr/model/network.py
def __init__(
    self,
    model_name,
    parameterization,
    convention=None,
    pretrained=False,
    height=256,
    unit_conversion_factor=1000.0,
    **kwargs,
):
    super().__init__()

    self.parameterization = parameterization
    self.convention = convention
    n_angular_components = N_ANGULAR_COMPONENTS[parameterization]

    # Handle norm_layer in timm
    norm_layer = kwargs.pop("norm_layer", None)
    if norm_layer not in (None, "default", "batchnorm", "batchnorm2d"):
        kwargs["norm_layer"] = norm_layer

    # Get the size of the output from the backbone
    self.backbone = timm.create_model(
        model_name,
        pretrained,
        num_classes=0,
        in_chans=1,
        **kwargs,
    )
    output = self.backbone(torch.randn(1, 1, height, height)).shape[-1]
    self.xyz_regression = torch.nn.Linear(output, 3)
    self.rot_regression = torch.nn.Linear(output, n_angular_components)

    # E.g., if 1000.0, converts output from meters to millimeters
    self.unit_conversion_factor = unit_conversion_factor

load_model

load_model(ckptpath, meta=False)

Load a pretrained pose regression model

Source code in src/xvr/model/network.py
def load_model(ckptpath, meta=False):
    """Load a pretrained pose regression model"""
    ckpt = torch.load(ckptpath, weights_only=False)
    config = ckpt["config"]

    model_state_dict = ckpt["model_state_dict"]
    model = PoseRegressor(
        model_name=config["model_name"],
        parameterization=config["parameterization"],
        convention=config["convention"],
        norm_layer=config["norm_layer"],
        height=config["height"],
        unit_conversion_factor=config.get("unit_conversion_factor", 1.0),
    ).cuda()
    model.load_state_dict(model_state_dict)
    model.eval()

    if meta:
        return model, config, ckpt["date"]
    else:
        return model, config

xvr.model.sampler

get_random_pose

get_random_pose(
    alphamin,
    alphamax,
    betamin,
    betamax,
    gammamin,
    gammamax,
    txmin,
    txmax,
    tymin,
    tymax,
    tzmin,
    tzmax,
    batch_size,
)

Generate a batch of random poses in SE(3) using specified ranges.

Source code in src/xvr/model/sampler.py
def get_random_pose(
    alphamin,
    alphamax,
    betamin,
    betamax,
    gammamin,
    gammamax,
    txmin,
    txmax,
    tymin,
    tymax,
    tzmin,
    tzmax,
    batch_size,
):
    """Generate a batch of random poses in SE(3) using specified ranges."""
    alpha = uniform(alphamin, alphamax, batch_size, circle_shift=True)
    beta = uniform(betamin, betamax, batch_size, circle_shift=True)
    gamma = uniform(gammamin, gammamax, batch_size, circle_shift=True)
    tx = uniform(txmin, txmax, batch_size)
    ty = uniform(tymin, tymax, batch_size)
    tz = uniform(tzmin, tzmax, batch_size)
    rot = torch.concat([alpha, beta, gamma], dim=1)
    xyz = torch.concat([tx, ty, tz], dim=1)
    return convert(
        rot, xyz, parameterization="euler_angles", convention="ZXY", degrees=True
    )

xvr.model.scheduler

WarmupCosineSchedule

WarmupCosineSchedule(
    optimizer, warmup_steps, t_total, cycles=0.5, last_epoch=-1
)

Linear warmup and then cosine decay. Linearly increases learning rate from 0 to 1 over warmup_steps training steps. Decreases learning rate from 1. to 0. over remaining t_total - warmup_steps steps following a cosine curve. If cycles (default=0.5) is different from default, learning rate follows cosine function after warmup.

Copied from https://github.com/TalSchuster/pytorch-transformers/blob/64fff2a53977ac1caac32c960d2b01f16b7eb913/pytorch_transformers/optimization.py#L64-L81

Source code in src/xvr/model/scheduler.py
def __init__(self, optimizer, warmup_steps, t_total, cycles=0.5, last_epoch=-1):
    self.warmup_steps = warmup_steps
    self.t_total = t_total
    self.cycles = cycles
    super().__init__(optimizer, self.lr_lambda, last_epoch=last_epoch)

xvr.model.trainer

Trainer

Trainer(
    volpath: str,
    maskpath: str | None,
    outpath: str,
    alphamin: float,
    alphamax: float,
    betamin: float,
    betamax: float,
    gammamin: float,
    gammamax: float,
    txmin: float,
    txmax: float,
    tymin: float,
    tymax: float,
    tzmin: float,
    tzmax: float,
    sdd: float,
    height: int,
    delx: float,
    orientation: str = "AP",
    reverse_x_axis: bool = False,
    parameterization: str = "quaternion_adjugate",
    convention: str = "ZXY",
    model_name: str = "resnet18",
    pretrained: bool = True,
    norm_layer: str = "groupnorm",
    unit_conversion_factor: float = 1000.0,
    p_augmentation: float = 0.5,
    lr: float = 0.005,
    weight_ncc: float = 1.0,
    weight_geo: float = 0.01,
    weight_dice: float = 1.0,
    weight_haus: float = 0.1,
    batch_size: int = 116,
    n_total_itrs: int = 1000000,
    n_warmup_itrs: int = 1000,
    n_grad_accum_itrs: int = 4,
    n_save_every_itrs: int = 1000,
    disable_scheduler: bool = False,
    ckptpath: str | None = None,
    reuse_optimizer: bool = False,
    warp: str | None = None,
    invert: bool = False,
    patch_size: tuple[int, int, int] | None = None,
    num_workers: int = 4,
    pin_memory: bool = False,
    weights: list[float] | None = None,
    img_threshold: float = 0.1,
    mask_threshold: float = 0.05,
    n_samples: int = 500,
    geodesic_only: bool = False,
)

Train a pose regression model.

PARAMETER DESCRIPTION
volpath

CT or directory of CTs for pretraining.

TYPE: str

maskpath

Optional labelmaps corresponding to the CTs.

TYPE: str | None

outpath

Directory in which to save model weights.

TYPE: str

alphamin

Minimum primary angle (in degrees).

TYPE: float

alphamax

Maximum primary angle (in degrees).

TYPE: float

betamin

Minimum secondary angle (in degrees).

TYPE: float

betamax

Maximum secondary angle (in degrees).

TYPE: float

gammamin

Minimum tertiary angle (in degrees).

TYPE: float

gammamax

Maximum tertiary angle (in degrees).

TYPE: float

txmin

Minimum x-offset (in millimeters).

TYPE: float

txmax

Maximum x-offset (in millimeters).

TYPE: float

tymin

Minimum y-offset (in millimeters).

TYPE: float

tymax

Maximum y-offset (in millimeters).

TYPE: float

tzmin

Minimum z-offset (in millimeters).

TYPE: float

tzmax

Maximum z-offset (in millimeters).

TYPE: float

sdd

Source-to-detector distance (in millimeters).

TYPE: float

height

DRR height (in pixels).

TYPE: int

delx

DRR pixel size (in millimeters / pixel).

TYPE: float

orientation

Orientation of CT volumes.

TYPE: str DEFAULT: 'AP'

reverse_x_axis

Horizontally flip the rendered DRRs.

TYPE: bool DEFAULT: False

parameterization

Parameterization of SO(3) for regression.

TYPE: str DEFAULT: 'quaternion_adjugate'

convention

If parameterization='euler_angles', specify order.

TYPE: str DEFAULT: 'ZXY'

model_name

Name of model to instantiate from the timm library.

TYPE: str DEFAULT: 'resnet18'

pretrained

Load pretrained ImageNet-1k weights.

TYPE: bool DEFAULT: True

norm_layer

Normalization layer.

TYPE: str DEFAULT: 'groupnorm'

unit_conversion_factor

Scale factor for translation prediction (e.g., from m to mm).

TYPE: float DEFAULT: 1000.0

p_augmentation

Base probability of image augmentations during training.

TYPE: float DEFAULT: 0.5

lr

Maximum learning rate.

TYPE: float DEFAULT: 0.005

weight_ncc

Weight on mNCC loss term.

TYPE: float DEFAULT: 1.0

weight_geo

Weight on geodesic loss term.

TYPE: float DEFAULT: 0.01

weight_dice

Weight on Dice loss term.

TYPE: float DEFAULT: 1.0

weight_haus

Weight on Haussdorff loss term.

TYPE: float DEFAULT: 0.1

batch_size

Number of DRRs per batch.

TYPE: int DEFAULT: 116

n_total_itrs

Number of iterations for training the model.

TYPE: int DEFAULT: 1000000

n_warmup_itrs

Number of iterations for warming up the learning rate.

TYPE: int DEFAULT: 1000

n_grad_accum_itrs

Number of iterations for gradient accumulation.

TYPE: int DEFAULT: 4

n_save_every_itrs

Number of iterations before saving a new model checkpoint.

TYPE: int DEFAULT: 1000

disable_scheduler

Turn off cosine learning rate scheduler.

TYPE: bool DEFAULT: False

ckptpath

Checkpoint of a pretrained pose regressor.

TYPE: str | None DEFAULT: None

reuse_optimizer

Initialize the previous optimizer's state.

TYPE: bool DEFAULT: False

warp

SimpleITK transform to warp input CT to checkpoint's reference frame.

TYPE: str | None DEFAULT: None

invert

Whether to invert the warp or not.

TYPE: bool DEFAULT: False

patch_size

Optional random crop size; if None, return entire volume.

TYPE: tuple[int, int, int] | None DEFAULT: None

num_workers

Number of subprocesses to use in the dataloader.

TYPE: int DEFAULT: 4

pin_memory

Copy volumes into CUDA pinned memory before returning.

TYPE: bool DEFAULT: False

weights

Probability for sampling each volume in volpath.

TYPE: list[float] | None DEFAULT: None

img_threshold

Minimum fraction of foreground pixels to keep a DRR.

TYPE: float DEFAULT: 0.1

mask_threshold

Minimum fraction of mask pixels to keep a DRR.

TYPE: float DEFAULT: 0.05

Source code in src/xvr/model/trainer.py
def __init__(
    self,
    volpath: str,
    maskpath: str | None,
    outpath: str,
    alphamin: float,
    alphamax: float,
    betamin: float,
    betamax: float,
    gammamin: float,
    gammamax: float,
    txmin: float,
    txmax: float,
    tymin: float,
    tymax: float,
    tzmin: float,
    tzmax: float,
    sdd: float,
    height: int,
    delx: float,
    orientation: str = "AP",
    reverse_x_axis: bool = False,
    parameterization: str = "quaternion_adjugate",
    convention: str = "ZXY",
    model_name: str = "resnet18",
    pretrained: bool = True,
    norm_layer: str = "groupnorm",
    unit_conversion_factor: float = 1000.0,
    p_augmentation: float = 0.5,
    lr: float = 5e-3,
    weight_ncc: float = 1e0,
    weight_geo: float = 1e-2,
    weight_dice: float = 1e0,
    weight_haus: float = 1e-1,
    batch_size: int = 116,
    n_total_itrs: int = 1_000_000,
    n_warmup_itrs: int = 1_000,
    n_grad_accum_itrs: int = 4,
    n_save_every_itrs: int = 1_000,
    disable_scheduler: bool = False,
    ckptpath: str | None = None,
    reuse_optimizer: bool = False,
    warp: str | None = None,
    invert: bool = False,
    patch_size: tuple[int, int, int] | None = None,
    num_workers: int = 4,
    pin_memory: bool = False,
    weights: list[float] | None = None,
    img_threshold: float = 0.10,
    mask_threshold: float = 0.05,
    n_samples: int = 500,
    geodesic_only: bool = False,
):
    """Train a pose regression model.

    Args:
        volpath: CT or directory of CTs for pretraining.
        maskpath: Optional labelmaps corresponding to the CTs.
        outpath: Directory in which to save model weights.
        alphamin: Minimum primary angle (in degrees).
        alphamax: Maximum primary angle (in degrees).
        betamin: Minimum secondary angle (in degrees).
        betamax: Maximum secondary angle (in degrees).
        gammamin: Minimum tertiary angle (in degrees).
        gammamax: Maximum tertiary angle (in degrees).
        txmin: Minimum x-offset (in millimeters).
        txmax: Maximum x-offset (in millimeters).
        tymin: Minimum y-offset (in millimeters).
        tymax: Maximum y-offset (in millimeters).
        tzmin: Minimum z-offset (in millimeters).
        tzmax: Maximum z-offset (in millimeters).
        sdd: Source-to-detector distance (in millimeters).
        height: DRR height (in pixels).
        delx: DRR pixel size (in millimeters / pixel).
        orientation: Orientation of CT volumes.
        reverse_x_axis: Horizontally flip the rendered DRRs.
        parameterization: Parameterization of SO(3) for regression.
        convention: If parameterization='euler_angles', specify order.
        model_name: Name of model to instantiate from the timm library.
        pretrained: Load pretrained ImageNet-1k weights.
        norm_layer: Normalization layer.
        unit_conversion_factor: Scale factor for translation prediction (e.g., from m to mm).
        p_augmentation: Base probability of image augmentations during training.
        lr: Maximum learning rate.
        weight_ncc: Weight on mNCC loss term.
        weight_geo: Weight on geodesic loss term.
        weight_dice: Weight on Dice loss term.
        weight_haus: Weight on Haussdorff loss term.
        batch_size: Number of DRRs per batch.
        n_total_itrs: Number of iterations for training the model.
        n_warmup_itrs: Number of iterations for warming up the learning rate.
        n_grad_accum_itrs: Number of iterations for gradient accumulation.
        n_save_every_itrs: Number of iterations before saving a new model checkpoint.
        disable_scheduler: Turn off cosine learning rate scheduler.
        ckptpath: Checkpoint of a pretrained pose regressor.
        reuse_optimizer: Initialize the previous optimizer's state.
        warp: SimpleITK transform to warp input CT to checkpoint's reference frame.
        invert: Whether to invert the warp or not.
        patch_size: Optional random crop size; if None, return entire volume.
        num_workers: Number of subprocesses to use in the dataloader.
        pin_memory: Copy volumes into CUDA pinned memory before returning.
        weights: Probability for sampling each volume in volpath.
        img_threshold: Minimum fraction of foreground pixels to keep a DRR.
        mask_threshold: Minimum fraction of mask pixels to keep a DRR.
    """

    # Record all hyperparameters to be checkpointed
    self.config = locals()
    del self.config["self"]

    # Initialize a lazy list of all 3D volumes
    self.subjects, self.single_subject = initialize_subjects(
        volpath,
        maskpath,
        orientation,
        patch_size,
        n_total_itrs,
        num_workers,
        pin_memory,
        weights,
    )

    # Initialize all deep learning modules
    (
        self.model,
        self.drr,
        self.transforms,
        self.optimizer,
        self.scheduler,
        self.start_itr,
        self.model_number,
    ) = initialize_modules(
        model_name,
        pretrained,
        parameterization,
        convention,
        norm_layer,
        unit_conversion_factor,
        sdd,
        height,
        delx,
        orientation,
        reverse_x_axis,
        lr,
        n_total_itrs,
        n_warmup_itrs,
        n_grad_accum_itrs,
        self.subjects if self.single_subject else None,
        disable_scheduler,
        ckptpath,
        reuse_optimizer,
    )

    # Initialize the loss function
    lossfn = PoseRegressionLoss(sdd, weight_ncc, weight_geo, weight_dice, weight_haus)
    self.lossfn = torch.compile(lossfn)

    # Set up augmentations
    self.contrast_distribution = torch.distributions.Uniform(1.0, 10.0)
    self.augmentations = XrayAugmentations(p_augmentation)

    # Define the pose distribution
    self.pose_distribution = dict(
        alphamin=alphamin,
        alphamax=alphamax,
        betamin=betamin,
        betamax=betamax,
        gammamin=gammamin,
        gammamax=gammamax,
        txmin=txmin,
        txmax=txmax,
        tymin=tymin,
        tymax=tymax,
        tzmin=tzmin,
        tzmax=tzmax,
        batch_size=batch_size,
    )

    # Initialize a conversion between the template and canonical frames of reference
    self.reframe = initialize_coordinate_frame(warp, volpath, invert)

    # Save training config
    self.n_total_itrs = n_total_itrs
    self.n_grad_accum_itrs = n_grad_accum_itrs
    self.n_save_every_itrs = n_save_every_itrs
    self.img_threshold = img_threshold
    self.mask_threshold = mask_threshold
    self.n_samples = n_samples
    self.geodesic_only = geodesic_only
    self.outpath = outpath

xvr.model.utils

initialize_subjects

initialize_subjects(
    volpath: str,
    maskpath: str | None,
    orientation: str | None,
    patch_size: tuple | None,
    num_samples: int,
    num_workers: int,
    pin_memory: bool,
    weights: tuple[float, ...] | None = None,
    replacement: bool = True,
) -> tuple[Subject | SubjectsLoader, bool]

Load the CT (or CTs) to render training DRRs from.

A single volume is read eagerly. A directory is loaded lazily into a weighted-sampling dataloader, optionally serving random patches rather than whole volumes. Volumes and masks are matched by filename across the two directories.

RETURNS DESCRIPTION
subjects

A single Subject if volpath is one file, else a loader yielding subjects or patches.

TYPE: Subject | SubjectsLoader

single_subject

True if volpath was a single volume, which tells the trainer whether the DRR module can hold the volume itself.

TYPE: bool

Source code in src/xvr/model/utils.py
def initialize_subjects(
    volpath: str,  # A single CT or a directory with multiple volumes
    maskpath: str | None,  # Optional labelmaps corresponding to the CTs
    orientation: str | None,  # "AP", "PA", or None
    patch_size: tuple | None,  # Tuple for random crop sizes (h, w, d)
    num_samples: int,  # Total number of training iterations
    num_workers: int,  # Number of workers for the dataloader
    pin_memory: bool,  # Pin memory for the dataloader
    weights: tuple[float, ...] | None = None,  # Sampling probability for each volume
    replacement: bool = True,  # Sample with replacement
) -> tuple[Subject | SubjectsLoader, bool]:
    """Load the CT (or CTs) to render training DRRs from.

    A single volume is read eagerly. A directory is loaded lazily into a
    weighted-sampling dataloader, optionally serving random patches rather than
    whole volumes. Volumes and masks are matched by filename across the two
    directories.

    Returns:
        subjects: A single `Subject` if `volpath` is one file, else a loader
            yielding subjects or patches.
        single_subject: True if `volpath` was a single volume, which tells the
            trainer whether the DRR module can hold the volume itself.
    """
    # If only a single subject is passed, load it and return
    single_subject = False
    if Path(volpath).is_file():
        single_subject = True
        subject = read(volpath, maskpath, orientation=orientation)
        return subject, single_subject

    # Else, construct a list of all volumes and masks
    # We assume volumes and masks have the same name, but are in different folders
    volumes = sorted(Path(volpath).glob("[!.]*.nii.gz"))
    masks = sorted(Path(maskpath).glob("[!.]*.nii.gz")) if maskpath is not None else []
    itr = zip_longest(volumes, masks)
    pbar = tqdm(itr, desc="Lazily loading CTs...", total=len(volumes), ncols=200)

    # Lazily load a list of all subjects in the dataset
    subjects = []
    for volpath, maskpath in pbar:
        volume = ScalarImage(volpath)
        mask = LabelMap(maskpath) if maskpath is not None else None
        subject = Subject(volume=volume, mask=mask)
        subjects.append(subject)

    # Construct a dataloader with efficient IO
    subjects = SubjectsDataset(subjects)
    if weights is None:
        weights = [1 / len(volumes) for _ in range(len(volumes))]
    subject_sampler = WeightedRandomSampler(
        weights, replacement=replacement, num_samples=num_samples
    )

    # Return entire volumes
    if patch_size is None:
        subjects = SubjectsLoader(
            subjects,
            sampler=subject_sampler,
            num_workers=num_workers,
            pin_memory=pin_memory,
        )
        return subjects, single_subject

    # Return random crops
    patch_sampler = UniformSampler(patch_size)
    patches_queue = Queue(
        subjects,
        max_length=64,
        samples_per_volume=4,
        sampler=patch_sampler,
        subject_sampler=subject_sampler,
        shuffle_subjects=False,
        num_workers=num_workers,
    )

    subjects = SubjectsLoader(
        patches_queue,
        batch_size=1,
        num_workers=0,
        pin_memory=pin_memory,
    )

    return subjects, single_subject

initialize_modules

initialize_modules(
    model_name,
    pretrained,
    parameterization,
    convention,
    norm_layer,
    unit_conversion_factor,
    sdd,
    height,
    delx,
    orientation,
    reverse_x_axis,
    lr,
    n_total_itrs,
    n_warmup_itrs,
    n_grad_accum_itrs,
    subject,
    disable_scheduler,
    ckptpath,
    reuse_optimizer,
) -> tuple[
    PoseRegressor,
    DRR,
    Compose,
    Optimizer,
    IdentitySchedule | WarmupCosineSchedule,
    int,
    int,
]

Construct the model, renderer, and optimizer for a training run.

Restores model weights from ckptpath when given, and the optimizer and scheduler state as well when reuse_optimizer is set. With more than one subject the DRR module is built against a dummy CT, since each step swaps in its own volume.

RETURNS DESCRIPTION
model

The pose regressor.

TYPE: PoseRegressor

drr

The differentiable renderer.

TYPE: DRR

transforms

X-ray preprocessing shared with inference.

TYPE: Compose

optimizer

The optimizer.

TYPE: Optimizer

scheduler

The learning rate scheduler.

TYPE: IdentitySchedule | WarmupCosineSchedule

start_itr

Iteration to resume from, 0 unless reusing an optimizer.

TYPE: int

model_number

Checkpoint number to resume from.

TYPE: int

Source code in src/xvr/model/utils.py
def initialize_modules(
    model_name,
    pretrained,
    parameterization,
    convention,
    norm_layer,
    unit_conversion_factor,
    sdd,
    height,
    delx,
    orientation,
    reverse_x_axis,
    lr,
    n_total_itrs,
    n_warmup_itrs,
    n_grad_accum_itrs,
    subject,
    disable_scheduler,
    ckptpath,
    reuse_optimizer,
) -> tuple[
    PoseRegressor,
    DRR,
    Compose,
    Optimizer,
    IdentitySchedule | WarmupCosineSchedule,
    int,
    int,
]:
    """Construct the model, renderer, and optimizer for a training run.

    Restores model weights from `ckptpath` when given, and the optimizer and
    scheduler state as well when `reuse_optimizer` is set. With more than one
    subject the DRR module is built against a dummy CT, since each step swaps in
    its own volume.

    Returns:
        model: The pose regressor.
        drr: The differentiable renderer.
        transforms: X-ray preprocessing shared with inference.
        optimizer: The optimizer.
        scheduler: The learning rate scheduler.
        start_itr: Iteration to resume from, 0 unless reusing an optimizer.
        model_number: Checkpoint number to resume from.
    """
    # Initialize the pose regression model
    model = PoseRegressor(
        model_name=model_name,
        pretrained=pretrained,
        parameterization=parameterization,
        convention=convention,
        norm_layer=norm_layer,
        height=height,
        unit_conversion_factor=unit_conversion_factor,
    ).cuda()

    # If a checkpoint is passed, reload the model state
    ckpt, start_itr, model_number = _load_checkpoint(ckptpath, reuse_optimizer)
    if ckpt is not None:
        print("Loading previous model weights...")
        model.load_state_dict(ckpt["model_state_dict"])

    # Initialize the optimizer and learning rate scheduler
    optimizer = torch.optim.Adam(model.parameters(), lr=lr)
    if disable_scheduler:
        scheduler = IdentitySchedule(optimizer)
    else:
        warmup_itrs = n_warmup_itrs / n_grad_accum_itrs
        total_itrs = n_total_itrs / n_grad_accum_itrs
        scheduler = WarmupCosineSchedule(optimizer, warmup_itrs, total_itrs)

    # Optionally, reload the optimizer and scheduler
    if ckpt is not None and reuse_optimizer:
        print("Reinitializing optimizer...")
        optimizer.load_state_dict(ckpt["optimizer_state_dict"])
        scheduler.load_state_dict(ckpt["scheduler_state_dict"])
    model.train()

    # If more than one subject is provided, initialize the DRR module with a dummy CT
    drr = DRR(
        subject if subject is not None else load_example_ct(orientation=orientation),
        sdd=sdd,
        height=height,
        delx=delx,
        reverse_x_axis=reverse_x_axis,
        renderer="trilinear",
    )
    drr.density = None  # Unload the precomputed density map to free up memory
    if not hasattr(drr, "mask"):
        drr.mask = None
    transforms = XrayTransforms(height)

    # If a single subject was provided, expose their volume and isocenter in the DRR module
    if subject is not None:
        drr.register_buffer("volume", subject.volume.data.squeeze())
        drr.register_buffer("center", torch.tensor(subject.volume.get_center())[None])
    drr = drr.cuda().to(torch.float32)

    return model, drr, transforms, optimizer, scheduler, start_itr, model_number

initialize_coordinate_frame

initialize_coordinate_frame(
    warp: str | Path | None, img: str | Path, invert: bool
) -> RigidTransform | None

Read the rigid transform reframing a CT into a checkpoint's frame.

PARAMETER DESCRIPTION
warp

Path to a SimpleITK transform, or None for no reframing.

TYPE: str | Path | None

img

Path to the CT the transform is defined against.

TYPE: str | Path

invert

If True, invert the transform.

TYPE: bool

RETURNS DESCRIPTION
RigidTransform | None

The transform, or None if warp is None.

Source code in src/xvr/model/utils.py
def initialize_coordinate_frame(
    warp: str | Path | None,
    img: str | Path,
    invert: bool,
) -> RigidTransform | None:
    """Read the rigid transform reframing a CT into a checkpoint's frame.

    Args:
        warp: Path to a SimpleITK transform, or None for no reframing.
        img: Path to the CT the transform is defined against.
        invert: If True, invert the transform.

    Returns:
        The transform, or None if `warp` is None.
    """
    if warp is None:
        return None
    return read_rigid_transform(warp, img, invert).cuda()