diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index d64403d03..58b4dda4b 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -23,7 +23,7 @@ jobs: - name: Doing editable install shell: bash -l {0} run: | - test -f setup.py && pip install -e ".[dev]" + pip install -e ".[dev]" - name: Check we are starting with clean git checkout shell: bash -l {0} @@ -37,27 +37,27 @@ jobs: - name: Trying to strip out notebooks shell: bash -l {0} run: | - nbdev_clean + nbdev-clean git status -s # display the status to see which nbs need cleaning up if [[ `git status --porcelain -uno` ]]; then git status -uno - echo -e "!!! Detected unstripped out notebooks\n!!!Remember to run nbdev_install_hooks" + echo -e "!!! Detected unstripped out notebooks\n!!!Remember to run nbdev-install_hooks" echo -e "This error can also happen if you are using an older version of nbdev relative to what is in CI. Please try to upgrade nbdev with the command `pip install -U nbdev`" false fi - - name: Run nbdev_export + - name: Run nbdev-export shell: bash -l {0} run: | - nbdev_export + nbdev-export if [[ `git status --porcelain -uno` ]]; then - echo "::error::Notebooks and library are not in sync. Please run nbdev_export." + echo "::error::Notebooks and library are not in sync. Please run nbdev-export." git status -uno git diff exit 1; fi - - name: Run nbdev_test + - name: Run nbdev-test shell: bash -l {0} run: | - nbdev_test + nbdev-test diff --git a/diffdrr/data.py b/diffdrr/data.py index 41456b4fc..60685cd96 100644 --- a/diffdrr/data.py +++ b/diffdrr/data.py @@ -1,6 +1,6 @@ # AUTOGENERATED! DO NOT EDIT! File to edit: ../notebooks/api/03_data.ipynb. -# %% ../notebooks/api/03_data.ipynb 3 +# %% ../notebooks/api/03_data.ipynb #cec0581f from __future__ import annotations from pathlib import Path @@ -12,10 +12,10 @@ from torchio import LabelMap, ScalarImage, Subject from torchio.transforms import Resample -# %% auto 0 +# %% auto #0 __all__ = ['load_example_ct', 'read'] -# %% ../notebooks/api/03_data.ipynb 5 +# %% ../notebooks/api/03_data.ipynb #126d05d3 def load_example_ct( labels=None, orientation="AP", @@ -37,7 +37,7 @@ def load_example_ct( **kwargs, ) -# %% ../notebooks/api/03_data.ipynb 6 +# %% ../notebooks/api/03_data.ipynb #9eda997b-6e88-4bb1-bb9f-313302fe3c1c from .pose import RigidTransform @@ -96,7 +96,7 @@ def read( dtype=torch.float32, ) elif orientation == "PA": - # Rotates the C-arm about the x-axis by 90 degrees + # Rotates the C-arm about the x-axis by 90 degrees # Reverses the direction of the y-axis reorient = torch.tensor( [ @@ -180,7 +180,7 @@ def read( return subject -# %% ../notebooks/api/03_data.ipynb 7 +# %% ../notebooks/api/03_data.ipynb #e20ad014-b2ae-41b9-8b65-e4ca865e19a2 from diffdrr.pose import RigidTransform @@ -210,7 +210,7 @@ def canonicalize(subject): subject.fiducials = affine(subject.fiducials) return subject -# %% ../notebooks/api/03_data.ipynb 8 +# %% ../notebooks/api/03_data.ipynb #ba2941e0-cb0d-44c7-9b00-4dad1ced447d def transform_hu_to_density(volume, bone_attenuation_multiplier): # volume can be loaded as int16, need to convert to float32 to use float bone_attenuation_multiplier volume = volume.to(torch.float32) diff --git a/diffdrr/detector.py b/diffdrr/detector.py index 1bed607d5..9344f786b 100644 --- a/diffdrr/detector.py +++ b/diffdrr/detector.py @@ -1,16 +1,16 @@ # AUTOGENERATED! DO NOT EDIT! File to edit: ../notebooks/api/02_detector.ipynb. -# %% ../notebooks/api/02_detector.ipynb 3 +# %% ../notebooks/api/02_detector.ipynb #b758e4f3 from __future__ import annotations import torch from fastcore.basics import patch from torch.nn.functional import normalize -# %% auto 0 +# %% auto #0 __all__ = ['Detector', 'get_focal_length', 'get_principal_point', 'parse_intrinsic_matrix', 'make_intrinsic_matrix'] -# %% ../notebooks/api/02_detector.ipynb 5 +# %% ../notebooks/api/02_detector.ipynb #529b92a4-2f71-4d40-a25f-03cc4bc3eb6b from .pose import RigidTransform @@ -93,7 +93,7 @@ def intrinsic(self): """The 3x3 intrinsic matrix.""" return make_intrinsic_matrix(self).to(self.source) -# %% ../notebooks/api/02_detector.ipynb 6 +# %% ../notebooks/api/02_detector.ipynb #b8ad63f4-0e38-4ea2-87b0-f298639dc9a9 @patch def _initialize_carm(self: Detector): """Initialize the default position for the source and detector plane.""" @@ -137,7 +137,7 @@ def _initialize_carm(self: Detector): self.subsamples.append(sample.tolist()) return source, target -# %% ../notebooks/api/02_detector.ipynb 7 +# %% ../notebooks/api/02_detector.ipynb #063d06c3-2618-4282-accd-8fe0ab4d3faa from .pose import RigidTransform @@ -153,7 +153,7 @@ def forward(self: Detector, extrinsic: RigidTransform, calibration: RigidTransfo target = pose(target) return source, target -# %% ../notebooks/api/02_detector.ipynb 9 +# %% ../notebooks/api/02_detector.ipynb #4c0f02f6-c27e-4bdc-a204-31ba5c9f73de def get_focal_length( intrinsic, # Intrinsic matrix (3 x 3 tensor) delx: float, # X-direction spacing (in units length) @@ -163,7 +163,7 @@ def get_focal_length( fy = intrinsic[1, 1] return abs((fx * delx) + (fy * dely)).item() / 2.0 -# %% ../notebooks/api/02_detector.ipynb 10 +# %% ../notebooks/api/02_detector.ipynb #a3535bdf-b819-4c42-9624-00d101b29ded def get_principal_point( intrinsic, # Intrinsic matrix (3 x 3 tensor) height: int, # Y-direction length (in units pixels) @@ -175,7 +175,7 @@ def get_principal_point( y0 = dely * (intrinsic[1, 2] - height / 2) return x0.item(), y0.item() -# %% ../notebooks/api/02_detector.ipynb 11 +# %% ../notebooks/api/02_detector.ipynb #750cb0fe-c96a-4c76-a2cd-51a74fdc6b05 def parse_intrinsic_matrix( intrinsic, # Intrinsic matrix (3 x 3 tensor) height: int, # Y-direction length (in units pixels) @@ -187,7 +187,7 @@ def parse_intrinsic_matrix( x0, y0 = get_principal_point(intrinsic, height, width, delx, dely) return focal_length, x0, y0 -# %% ../notebooks/api/02_detector.ipynb 12 +# %% ../notebooks/api/02_detector.ipynb #4e9f01cb-1dbc-4818-8521-e6785c101a82 def make_intrinsic_matrix(detector: Detector): fx = detector.sdd / detector.delx fy = detector.sdd / detector.dely diff --git a/diffdrr/drr.py b/diffdrr/drr.py index 63c8c3b14..6b188e590 100644 --- a/diffdrr/drr.py +++ b/diffdrr/drr.py @@ -1,6 +1,6 @@ # AUTOGENERATED! DO NOT EDIT! File to edit: ../notebooks/api/00_drr.ipynb. -# %% ../notebooks/api/00_drr.ipynb 3 +# %% ../notebooks/api/00_drr.ipynb #ce95e1ac-413e-407b-87ad-0c7db3a945de from __future__ import annotations import numpy as np @@ -11,10 +11,10 @@ from .detector import Detector from .renderers import Siddon, Trilinear -# %% auto 0 +# %% auto #0 __all__ = ['DRR'] -# %% ../notebooks/api/00_drr.ipynb 7 +# %% ../notebooks/api/00_drr.ipynb #97297d06-6772-4dc7-8af5-d1ea7b379d8d from torchio import Subject from .pose import RigidTransform @@ -138,7 +138,7 @@ def device(self): def dtype(self): return self.density.dtype -# %% ../notebooks/api/00_drr.ipynb 8 +# %% ../notebooks/api/00_drr.ipynb #6513c593-32b8-4676-83d4-e9e7dcf0630a def reshape_subsampled_drr(img: torch.Tensor, detector: Detector, batch_size: int): n_points = detector.height * detector.width drr = torch.zeros(batch_size, n_points).to(img) @@ -146,7 +146,7 @@ def reshape_subsampled_drr(img: torch.Tensor, detector: Detector, batch_size: in drr = drr.view(batch_size, 1, detector.height, detector.width) return drr -# %% ../notebooks/api/00_drr.ipynb 10 +# %% ../notebooks/api/00_drr.ipynb #27b19dfc-6a15-4896-9faa-20faee84dc1f from torch.utils.checkpoint import checkpoint from .pose import RigidTransform, convert @@ -168,12 +168,7 @@ def forward( if parameterization is None: pose = args[0] else: - pose = convert( - *args, - parameterization=parameterization, - convention=convention, - degrees=degrees, - ) + pose = convert(*args, parameterization=parameterization, convention=convention, degrees=degrees) # Create the source / target points and render the image source, target = self.detector(pose, calibration) @@ -231,7 +226,7 @@ def render( return img -# %% ../notebooks/api/00_drr.ipynb 11 +# %% ../notebooks/api/00_drr.ipynb #d17edb1b-d3b4-4f31-b110-f5811ec6c183 @patch def set_intrinsics_( self: DRR, @@ -259,7 +254,7 @@ def set_intrinsics_( reverse_x_axis if reverse_x_axis is not None else self.detector.reverse_x_axis, ).to(self.density) -# %% ../notebooks/api/00_drr.ipynb 12 +# %% ../notebooks/api/00_drr.ipynb #5d96d0d0-d76f-406e-8bc8-9023cf0f3e01 @patch def rescale_detector_(self: DRR, scale: float): """Rescale the detector plane (inplace).""" @@ -270,7 +265,7 @@ def rescale_detector_(self: DRR, scale: float): dely=float(self.detector.dely / scale), ) -# %% ../notebooks/api/00_drr.ipynb 13 +# %% ../notebooks/api/00_drr.ipynb #93a94ef3-5449-45dc-aa62-9fcf6fad643d @patch def perspective_projection( self: DRR, @@ -294,7 +289,7 @@ def perspective_projection( return x[..., :2] -# %% ../notebooks/api/00_drr.ipynb 14 +# %% ../notebooks/api/00_drr.ipynb #802ba874-eef8-4524-be5c-bd250e5639d7 from torch.nn.functional import pad diff --git a/diffdrr/metrics.py b/diffdrr/metrics.py index a75265289..588e5a4a9 100644 --- a/diffdrr/metrics.py +++ b/diffdrr/metrics.py @@ -1,15 +1,15 @@ # AUTOGENERATED! DO NOT EDIT! File to edit: ../notebooks/api/05_metrics.ipynb. -# %% ../notebooks/api/05_metrics.ipynb 3 +# %% ../notebooks/api/05_metrics.ipynb #86ff7dae from __future__ import annotations import torch -# %% auto 0 +# %% auto #0 __all__ = ['NormalizedCrossCorrelation2d', 'MultiscaleNormalizedCrossCorrelation2d', 'GradientNormalizedCrossCorrelation2d', 'MutualInformation', 'LogGeodesicSE3', 'DoubleGeodesicSE3'] -# %% ../notebooks/api/05_metrics.ipynb 6 +# %% ../notebooks/api/05_metrics.ipynb #a77b3608-8d2a-43b6-b902-9f905877dd40 from einops import rearrange @@ -17,7 +17,7 @@ def to_patches(x, patch_size): x = x.unfold(2, patch_size, step=1).unfold(3, patch_size, step=1).contiguous() return rearrange(x, "b c p1 p2 h w -> b (c p1 p2) h w") -# %% ../notebooks/api/05_metrics.ipynb 7 +# %% ../notebooks/api/05_metrics.ipynb #28930479-d8e6-4859-b5de-38a5350f510b class NormalizedCrossCorrelation2d(torch.nn.Module): """Compute Normalized Cross Correlation between two batches of images.""" @@ -43,7 +43,7 @@ def norm(self, x): std = var.sqrt() return (x - mu) / std -# %% ../notebooks/api/05_metrics.ipynb 8 +# %% ../notebooks/api/05_metrics.ipynb #8d06c00d-830c-48d9-b394-07cc83c1ed2b class MultiscaleNormalizedCrossCorrelation2d(torch.nn.Module): """Compute Normalized Cross Correlation between two batches of images at multiple scales.""" @@ -62,7 +62,7 @@ def forward(self, x1, x2): scores.append(weight * ncc(x1, x2)) return torch.stack(scores, dim=0).sum(dim=0) -# %% ../notebooks/api/05_metrics.ipynb 9 +# %% ../notebooks/api/05_metrics.ipynb #a6317e99-8a0a-4dce-959f-904c21595d71 from torchvision.transforms.functional import gaussian_blur @@ -92,7 +92,7 @@ def forward(self, img): x = self.filter(x) return x -# %% ../notebooks/api/05_metrics.ipynb 10 +# %% ../notebooks/api/05_metrics.ipynb #fc39dd1d-ab40-4f7b-926d-dff305b9ab69 class GradientNormalizedCrossCorrelation2d(NormalizedCrossCorrelation2d): """Compute Normalized Cross Correlation between the image gradients of two batches of images.""" @@ -103,7 +103,7 @@ def __init__(self, patch_size=None, sigma=1.0, **kwargs): def forward(self, x1, x2): return super().forward(self.sobel(x1), self.sobel(x2)) -# %% ../notebooks/api/05_metrics.ipynb 11 +# %% ../notebooks/api/05_metrics.ipynb #6e422fc1-8100-4120-b226-f6f54602fe3c from kornia.enhance.histogram import marginal_pdf, joint_pdf @@ -118,7 +118,7 @@ def __init__(self, sigma=0.1, num_bins=256, epsilon=1e-10, normalize=True): self.normalize = normalize def forward(self, x1, x2): - assert x1.shape == x2.shape + assert(x1.shape == x2.shape) B, C, H, W = x1.shape x1 = x1.view(B, H * W, C) @@ -138,7 +138,7 @@ def forward(self, x1, x2): return mutual_information -# %% ../notebooks/api/05_metrics.ipynb 15 +# %% ../notebooks/api/05_metrics.ipynb #b691875b-c136-4ea5-8551-fab45530e315 from .pose import RigidTransform, convert @@ -157,7 +157,7 @@ def forward( ) -> Float[torch.Tensor, "b"]: return pose_2.compose(pose_1.inverse()).get_se3_log().norm(dim=1) -# %% ../notebooks/api/05_metrics.ipynb 18 +# %% ../notebooks/api/05_metrics.ipynb #5ac6d6f4-462b-47ea-b25b-8a2518749e6f from .pose import so3_log_map @@ -175,9 +175,7 @@ def __init__( self.sdr = sdd / 2 self.eps = eps - self.rot_geo = lambda r1, r2: self.sdr * so3_log_map( - r1.transpose(-1, -2) @ r2 - ).norm(dim=-1) + self.rot_geo = lambda r1, r2: self.sdr * so3_log_map(r1.transpose(-1, -2) @ r2).norm(dim=-1) self.xyz_geo = lambda t1, t2: (t1 - t2).norm(dim=-1) def forward(self, pose_1: RigidTransform, pose_2: RigidTransform): diff --git a/diffdrr/pose.py b/diffdrr/pose.py index 92c9dcb2a..576b1344e 100644 --- a/diffdrr/pose.py +++ b/diffdrr/pose.py @@ -1,10 +1,10 @@ # AUTOGENERATED! DO NOT EDIT! File to edit: ../notebooks/api/06_pose.ipynb. -# %% auto 0 +# %% auto #0 __all__ = ['RigidTransform', 'convert', 'rotation_9d_to_matrix', 'matrix_to_rotation_9d', 'rotation_10d_to_quaternion', 'quaternion_to_rotation_10d', 'quaternion_adjugate_to_quaternion', 'quaternion_to_quaternion_adjugate'] -# %% ../notebooks/api/06_pose.ipynb 6 +# %% ../notebooks/api/06_pose.ipynb #189c53d0-7f38-432a-ae02-fd68743eac97 import torch from einops import rearrange @@ -25,7 +25,7 @@ def __new__(cls, matrix, eps=1e-6): def __init__(self, matrix, eps=1e-6): if isinstance(matrix, type(self)): - return + return super().__init__() if matrix.dim() == 2: @@ -104,7 +104,7 @@ def convert(self, parameterization, convention=None, degrees=False): def get_se3_log(self): return se3_log_map(self.matrix.mT) -# %% ../notebooks/api/06_pose.ipynb 7 +# %% ../notebooks/api/06_pose.ipynb #5caffa4c-ed95-4a1e-ac56-3f940f21bbb5 def make_matrix(R, t): assert (batch_size := len(R)) == len(t) matrix = torch.zeros(batch_size, 4, 4).to(R) @@ -113,7 +113,7 @@ def make_matrix(R, t): matrix[..., -1, -1] = 1.0 return matrix -# %% ../notebooks/api/06_pose.ipynb 8 +# %% ../notebooks/api/06_pose.ipynb #a42096a1-442e-43e8-9897-f97cdc1b6df5 from scipy.spatial.transform import Rotation @@ -123,7 +123,7 @@ def random_rigid_transform(batch_size=1): t = 100 * torch.randn((batch_size, 3)) return RigidTransform(make_matrix(R, t)) -# %% ../notebooks/api/06_pose.ipynb 10 +# %% ../notebooks/api/06_pose.ipynb #911693bb-11dd-440c-8ef4-8631cff7a952 PARAMETERIZATIONS = [ "axis_angle", "euler_angles", @@ -136,7 +136,7 @@ def random_rigid_transform(batch_size=1): "se3_log_map", ] -# %% ../notebooks/api/06_pose.ipynb 11 +# %% ../notebooks/api/06_pose.ipynb #95670bf3-d3db-4f1f-8a5a-b0fd7a927a17 def convert(*args, parameterization, convention=None, degrees=False) -> RigidTransform: if parameterization == "euler_angles" and convention is None: raise ValueError( @@ -189,7 +189,7 @@ def convert(*args, parameterization, convention=None, degrees=False) -> RigidTra return convert(matrix, parameterization="matrix") -# %% ../notebooks/api/06_pose.ipynb 13 +# %% ../notebooks/api/06_pose.ipynb #33b440f5-3058-44ec-8a17-e3a4d043659d def rotation_9d_to_matrix(rotation: torch.Tensor) -> torch.Tensor: """Convert a 9-vector to a symmetrically orthogonalized rotation matrix via SVD.""" m = rotation.view(-1, 3, 3) @@ -204,7 +204,7 @@ def rotation_9d_to_matrix(rotation: torch.Tensor) -> torch.Tensor: def matrix_to_rotation_9d(matrix: torch.Tensor) -> torch.Tensor: return matrix.flatten(start_dim=1) -# %% ../notebooks/api/06_pose.ipynb 15 +# %% ../notebooks/api/06_pose.ipynb #c5842328-b7f5-44c5-b305-f7834f44ac9b def _10vec_to_4x4symmetric(vec): """Convert a 10-vector to a symmetric 4x4 matrix.""" b = len(vec) @@ -214,7 +214,7 @@ def _10vec_to_4x4symmetric(vec): A[..., jdx, idx] = vec return A -# %% ../notebooks/api/06_pose.ipynb 16 +# %% ../notebooks/api/06_pose.ipynb #bb2bb147-aae8-4fef-b409-969c014351bf def rotation_10d_to_quaternion(rotation: torch.Tensor) -> torch.Tensor: """ Convert a 10-vector into a symmetric matrix, whose eigenvector corresponding @@ -231,7 +231,7 @@ def quaternion_to_rotation_10d(q: torch.Tensor) -> torch.Tensor: idx, jdx = torch.triu_indices(4, 4) return A[..., idx, jdx] -# %% ../notebooks/api/06_pose.ipynb 17 +# %% ../notebooks/api/06_pose.ipynb #6826c42d-6a45-4344-a76b-0c159070a68a def quaternion_adjugate_to_quaternion(rotation: torch.Tensor) -> torch.Tensor: """ Convert a 10-vector in the quaternion adjugate, a symmetric matrix whose @@ -252,7 +252,7 @@ def quaternion_to_quaternion_adjugate(q: torch.Tensor) -> torch.Tensor: idx, jdx = torch.triu_indices(4, 4) return A[..., idx, jdx] -# %% ../notebooks/api/06_pose.ipynb 20 +# %% ../notebooks/api/06_pose.ipynb #a6b1c2a9-172a-40bc-ab3a-a3c15d65b6a1 # pytorch3d/transforms/rotation_conversions.py from typing import Optional, Union @@ -779,7 +779,7 @@ def matrix_to_rotation_6d(matrix: torch.Tensor) -> torch.Tensor: batch_dim = matrix.size()[:-2] return matrix[..., :2, :].clone().reshape(batch_dim + (6,)) -# %% ../notebooks/api/06_pose.ipynb 21 +# %% ../notebooks/api/06_pose.ipynb #23baee04-2c2d-4d14-a2f3-74b6df8212c7 # pytorch3d/transforms/math.py from typing import Tuple @@ -857,7 +857,7 @@ def _dacos_dx(x: float) -> float: """ return (-1.0) / math.sqrt(1.0 - x * x) -# %% ../notebooks/api/06_pose.ipynb 22 +# %% ../notebooks/api/06_pose.ipynb #78bf64af-dcd0-4e5e-b734-ee60c0778a18 # pytorch3d/transforms/so3.py import warnings @@ -1117,7 +1117,7 @@ def hat(v: torch.Tensor) -> torch.Tensor: return h -# %% ../notebooks/api/06_pose.ipynb 23 +# %% ../notebooks/api/06_pose.ipynb #ca7500ca-80a2-4fe0-a63f-5af82e640ef7 # pytorch3d/transforms/se3.py diff --git a/diffdrr/registration.py b/diffdrr/registration.py index 0f7f7f76b..1bdc25229 100644 --- a/diffdrr/registration.py +++ b/diffdrr/registration.py @@ -1,9 +1,9 @@ # AUTOGENERATED! DO NOT EDIT! File to edit: ../notebooks/api/08_registration.ipynb. -# %% auto 0 +# %% auto #0 __all__ = ['Registration', 'PoseRegressor'] -# %% ../notebooks/api/08_registration.ipynb 4 +# %% ../notebooks/api/08_registration.ipynb #0139ca12-d4c1-4ff4-8b96-0e34b4d4fa62 import torch import torch.nn as nn @@ -49,7 +49,7 @@ def rotation(self): def translation(self): return self._translation -# %% ../notebooks/api/08_registration.ipynb 6 +# %% ../notebooks/api/08_registration.ipynb #63567398-0630-4b95-9d76-6d6c9cc85581 import timm from .pose import RigidTransform @@ -97,7 +97,7 @@ def forward(self, x): rot, xyz, convention=self.convention, parameterization=self.parameterization ) -# %% ../notebooks/api/08_registration.ipynb 7 +# %% ../notebooks/api/08_registration.ipynb #0ab6767f-d830-4889-a41c-a6efd60390df N_ANGULAR_COMPONENTS = { "axis_angle": 3, "euler_angles": 3, diff --git a/diffdrr/renderers.py b/diffdrr/renderers.py index 14b3ebbde..7d9338cc6 100644 --- a/diffdrr/renderers.py +++ b/diffdrr/renderers.py @@ -1,13 +1,13 @@ # AUTOGENERATED! DO NOT EDIT! File to edit: ../notebooks/api/01_renderers.ipynb. -# %% auto 0 +# %% auto #0 __all__ = ['Siddon', 'Trilinear'] -# %% ../notebooks/api/01_renderers.ipynb 3 +# %% ../notebooks/api/01_renderers.ipynb #05efb54f import torch from torch.nn.functional import grid_sample -# %% ../notebooks/api/01_renderers.ipynb 7 +# %% ../notebooks/api/01_renderers.ipynb #47f1b4c7 class Siddon(torch.nn.Module): """Differentiable X-ray renderer implemented with Siddon's method for exact raytracing.""" @@ -62,9 +62,7 @@ def forward( # Use torch.nn.functional.grid_sample to lookup the values of each intersected voxel if self.stop_gradients_through_grid_sample: with torch.no_grad(): - img = _get_voxel( - volume, xyzs, img, self.mode, align_corners=align_corners - ) + img = _get_voxel(volume, xyzs, img, self.mode, align_corners=align_corners) else: img = _get_voxel(volume, xyzs, img, self.mode, align_corners=align_corners) @@ -92,10 +90,8 @@ def forward( return img -# %% ../notebooks/api/01_renderers.ipynb 8 -def _get_alphas( - source, target, dims, voxel_shift, eps, filter_intersections_outside_volume -): +# %% ../notebooks/api/01_renderers.ipynb #08f6261c +def _get_alphas(source, target, dims, voxel_shift, eps, filter_intersections_outside_volume): """Calculates the parametric intersections of each ray with the planes of the CT volume.""" # Parameterize the parallel XYZ planes that comprise the CT volumes alphax = torch.arange(dims[0] + 1).to(source) - voxel_shift @@ -131,7 +127,7 @@ def _get_alpha_minmax(source, target, dims, voxel_shift, eps): min_plane = torch.zeros(3).to(source) - voxel_shift max_plane = (dims + 1).to(source) - voxel_shift - + alpha0 = (min_plane - source) / sdd alpha1 = (max_plane - source) / sdd alphas = torch.stack([alpha0, alpha1]) @@ -172,7 +168,7 @@ def _get_voxel(volume, xyzs, img, mode, align_corners): img = voxels return img -# %% ../notebooks/api/01_renderers.ipynb 9 +# %% ../notebooks/api/01_renderers.ipynb #08a29306 from typing import Callable @@ -186,7 +182,7 @@ def reduce(img, reducefn): else: raise ValueError(f"Only supports reducefn 'sum' or 'max', not {reducefn}") -# %% ../notebooks/api/01_renderers.ipynb 11 +# %% ../notebooks/api/01_renderers.ipynb #8ed6005d class Trilinear(torch.nn.Module): """Differentiable X-ray renderer implemented with trilinear interpolation.""" @@ -222,9 +218,7 @@ def forward( # Sample points along the rays and rescale to [-1, 1] if alphamin is None or alphamax is None: - alphamin, alphamax = _get_alpha_minmax( - source, target, dims, self.voxel_shift, self.eps - ) + alphamin, alphamax = _get_alpha_minmax(source, target, dims, self.voxel_shift, self.eps) alphamin = alphamin.min() alphamax = alphamax.max() alphas = torch.linspace(0, 1, n_points)[None, None].to(volume) @@ -236,7 +230,7 @@ def forward( # Sample the volume with trilinear interpolation img = _get_voxel(volume, xyzs, img, self.mode, align_corners=align_corners) - + # Multiply by the step size to compute the rectangular rule for integration step_size = (alphamax - alphamin) / (n_points - 1) img = img * step_size diff --git a/diffdrr/utils.py b/diffdrr/utils.py index 26db76fc9..d5e5f115d 100644 --- a/diffdrr/utils.py +++ b/diffdrr/utils.py @@ -1,9 +1,9 @@ # AUTOGENERATED! DO NOT EDIT! File to edit: ../notebooks/api/07_utils.ipynb. -# %% auto 0 +# %% auto #0 __all__ = ['resample', 'get_pinhole_camera'] -# %% ../notebooks/api/07_utils.ipynb 4 +# %% ../notebooks/api/07_utils.ipynb #ae47ec2a-0ce5-4317-8de4-47948bb4fb3f import torch from kornia.geometry.transform import center_crop, resize, translate @@ -52,7 +52,7 @@ def resample( return x -# %% ../notebooks/api/07_utils.ipynb 6 +# %% ../notebooks/api/07_utils.ipynb #1206af06-0f4f-40f3-a426-253e3032fcb7 from kornia.geometry.camera.pinhole import PinholeCamera as KorniaPinholeCamera from torchio import Subject @@ -68,6 +68,7 @@ def __init__( width: torch.Tensor, detector: Detector, subject: Subject, + ): super().__init__(intrinsics, extrinsics, height, width) multiplier = -1 if subject.orientation == "PA" else 1 @@ -92,7 +93,7 @@ def pose(self): """Turn c2w matrix in w2c matrix as input to diffdrr.drr.DRR""" return RigidTransform(self.extrinsics).inverse() -# %% ../notebooks/api/07_utils.ipynb 7 +# %% ../notebooks/api/07_utils.ipynb #546d3c18-608a-45d2-9809-2914c74018c9 from copy import deepcopy from kornia.geometry.calibration import solve_pnp_dlt diff --git a/diffdrr/visualization.py b/diffdrr/visualization.py index 305b02745..0aa4cc2bc 100644 --- a/diffdrr/visualization.py +++ b/diffdrr/visualization.py @@ -1,6 +1,6 @@ # AUTOGENERATED! DO NOT EDIT! File to edit: ../notebooks/api/04_visualization.ipynb. -# %% ../notebooks/api/04_visualization.ipynb 3 +# %% ../notebooks/api/04_visualization.ipynb #3b34c0d9 from __future__ import annotations import tempfile @@ -11,10 +11,10 @@ import numpy as np from tqdm import tqdm -# %% auto 0 +# %% auto #0 __all__ = ['plot_drr', 'plot_mask', 'animate', 'drr_to_mesh', 'labelmap_to_mesh', 'img_to_mesh', 'visualize_scene', 'add_image'] -# %% ../notebooks/api/04_visualization.ipynb 5 +# %% ../notebooks/api/04_visualization.ipynb #5bbd598c import torch @@ -55,7 +55,7 @@ def plot_drr( ax.set_yticks([]) return axs -# %% ../notebooks/api/04_visualization.ipynb 6 +# %% ../notebooks/api/04_visualization.ipynb #0999c1ac-0aa0-4f8e-821e-431f6dfb6819 def plot_mask( img: torch.Tensor, axs: matplotlib.axes._axes.Axes, @@ -90,7 +90,7 @@ def plot_mask( if return_masks: return masks -# %% ../notebooks/api/04_visualization.ipynb 7 +# %% ../notebooks/api/04_visualization.ipynb #b1ffbd7c-12eb-48a1-8979-cdf493831a02 def plot_img_and_mask( img: torch.Tensor, title: str | None = None, @@ -100,15 +100,13 @@ def plot_img_and_mask( alpha=0.5, **imshow_kwargs, ): - axs = plot_drr( - img.sum(dim=1, keepdim=True), title, ticks, axs, cmap, **imshow_kwargs - ) + axs = plot_drr(img.sum(dim=1, keepdim=True), title, ticks, axs, cmap, **imshow_kwargs) if len(axs) == 1: axs = axs[0] plot_mask(img[:, 1:], axs=axs, alpha=alpha) return axs -# %% ../notebooks/api/04_visualization.ipynb 8 +# %% ../notebooks/api/04_visualization.ipynb #15b1931b import pathlib import pandas @@ -191,14 +189,14 @@ def make_fig(ground_truth): # Make the animation return iio.imwrite(out, frames, **kwargs) -# %% ../notebooks/api/04_visualization.ipynb 11 +# %% ../notebooks/api/04_visualization.ipynb #b6718de4-26c6-4550-9d88-780fa5b809f6 import pyvista import vtk from torchio import Subject vtk.vtkLogger.SetStderrVerbosity(vtk.vtkLogger.ConvertToVerbosity(-1)) -# %% ../notebooks/api/04_visualization.ipynb 12 +# %% ../notebooks/api/04_visualization.ipynb #94bce480-3f93-49ba-bdd5-8677ac1e8bde def drr_to_mesh( subject: Subject, # torchio.Subject with a `volume` attribute method: str, # Either `surface_nets` or `marching_cubes` @@ -240,8 +238,8 @@ def drr_to_mesh( try: mesh = grid.contour_labels( boundary_style="strict_external", - smoothing=True, - output_mesh_type="quads", + smoothing=True, + output_mesh_type='quads', pad_background=True, orient_faces=True, simplify_output=False, @@ -280,7 +278,7 @@ def drr_to_mesh( mesh.clean(inplace=True, progress_bar=verbose) return mesh -# %% ../notebooks/api/04_visualization.ipynb 13 +# %% ../notebooks/api/04_visualization.ipynb #422595b9-3f28-4c0e-b549-837c8d87430d def labelmap_to_mesh( subject: Subject, # torchio.Subject with a `mask` attribute verbose: bool = True, # Display progress bars for mesh processing steps @@ -294,8 +292,8 @@ def labelmap_to_mesh( grid.point_data["values"] = subject.mask.data[0].numpy().flatten(order="F") mesh = grid.contour_labels( boundary_style="strict_external", - smoothing=True, - output_mesh_type="quads", + smoothing=True, + output_mesh_type='quads', pad_background=True, orient_faces=True, simplify_output=False, @@ -318,7 +316,7 @@ def labelmap_to_mesh( return mesh -# %% ../notebooks/api/04_visualization.ipynb 14 +# %% ../notebooks/api/04_visualization.ipynb #49bec80c-947e-4b66-8205-c540a1342f4a from .pose import RigidTransform @@ -366,7 +364,7 @@ def img_to_mesh( return camera, detector, texture, principal_ray -# %% ../notebooks/api/04_visualization.ipynb 15 +# %% ../notebooks/api/04_visualization.ipynb #fbb0de96-4efa-46ff-a337-3771c9a343e0 import numpy as np @@ -391,7 +389,7 @@ def _make_camera_frustum_mesh(source, target, size=0.125): ) return pyvista.PolyData(vertices, faces) -# %% ../notebooks/api/04_visualization.ipynb 16 +# %% ../notebooks/api/04_visualization.ipynb #ebb5fe6b-886c-415a-8881-5387269f7950 def visualize_scene( drr: DRR, pose: RigidTransform, diff --git a/notebooks/api/01_renderers.ipynb b/notebooks/api/01_renderers.ipynb index 6e062bed1..ebb31274a 100644 --- a/notebooks/api/01_renderers.ipynb +++ b/notebooks/api/01_renderers.ipynb @@ -2,6 +2,7 @@ "cells": [ { "cell_type": "raw", + "id": "9e6d643e", "metadata": {}, "source": [ "---\n", @@ -14,6 +15,7 @@ { "cell_type": "code", "execution_count": null, + "id": "0ebaabf8", "metadata": {}, "outputs": [], "source": [ @@ -23,6 +25,7 @@ { "cell_type": "code", "execution_count": null, + "id": "af843687", "metadata": {}, "outputs": [], "source": [ @@ -33,6 +36,7 @@ { "cell_type": "code", "execution_count": null, + "id": "05efb54f", "metadata": {}, "outputs": [], "source": [ @@ -43,6 +47,7 @@ }, { "cell_type": "markdown", + "id": "7f9cf365", "metadata": {}, "source": [ "## Siddon's Method\n", @@ -69,6 +74,7 @@ }, { "cell_type": "raw", + "id": "355c70d5", "metadata": {}, "source": [ "::: {.callout-note}\n", @@ -79,6 +85,7 @@ }, { "cell_type": "markdown", + "id": "c1f9a61e", "metadata": {}, "source": [ "Siddon's method provides a parametric method to identify the plane intersections $\\{\\alpha_m\\}_{m=1}^M$.\n", @@ -106,6 +113,7 @@ { "cell_type": "code", "execution_count": null, + "id": "47f1b4c7", "metadata": {}, "outputs": [], "source": [ @@ -196,6 +204,7 @@ { "cell_type": "code", "execution_count": null, + "id": "08f6261c", "metadata": {}, "outputs": [], "source": [ @@ -281,6 +290,7 @@ { "cell_type": "code", "execution_count": null, + "id": "08a29306", "metadata": {}, "outputs": [], "source": [ @@ -301,6 +311,7 @@ }, { "cell_type": "markdown", + "id": "14cd691a", "metadata": {}, "source": [ "## Trilinear interpolation\n", @@ -317,6 +328,7 @@ { "cell_type": "code", "execution_count": null, + "id": "8ed6005d", "metadata": {}, "outputs": [], "source": [ @@ -395,6 +407,7 @@ { "cell_type": "code", "execution_count": null, + "id": "be55dc9d", "metadata": {}, "outputs": [], "source": [ @@ -407,6 +420,7 @@ { "cell_type": "code", "execution_count": null, + "id": "5e4d4deb", "metadata": {}, "outputs": [], "source": [] @@ -427,5 +441,5 @@ } }, "nbformat": 4, - "nbformat_minor": 4 + "nbformat_minor": 5 } diff --git a/notebooks/index.ipynb b/notebooks/index.ipynb index 65eda6773..85513b57d 100644 --- a/notebooks/index.ipynb +++ b/notebooks/index.ipynb @@ -2,6 +2,7 @@ "cells": [ { "cell_type": "raw", + "id": "e776a9b4", "metadata": {}, "source": [ "---\n", @@ -12,6 +13,7 @@ }, { "cell_type": "markdown", + "id": "5bc642c5", "metadata": {}, "source": [ "> Auto-differentiable DRR rendering and optimization in PyTorch" @@ -19,6 +21,7 @@ }, { "cell_type": "markdown", + "id": "e233295a", "metadata": {}, "source": [ "[![CI](https://github.com/eigenvivek/DiffDRR/actions/workflows/test.yaml/badge.svg)](https://github.com/eigenvivek/DiffDRR/actions/workflows/test.yaml)\n", @@ -31,6 +34,7 @@ }, { "cell_type": "markdown", + "id": "fdab49cd", "metadata": {}, "source": [ "`DiffDRR` is a PyTorch-based digitally reconstructed radiograph (DRR) generator that provides\n", @@ -44,6 +48,7 @@ }, { "cell_type": "markdown", + "id": "735216bf", "metadata": {}, "source": [ "## Install\n", @@ -64,6 +69,7 @@ }, { "cell_type": "markdown", + "id": "a2963ee4", "metadata": {}, "source": [ "## Hello, World!\n", @@ -74,6 +80,7 @@ { "cell_type": "code", "execution_count": null, + "id": "5d701e5e", "metadata": {}, "outputs": [ { @@ -120,6 +127,7 @@ }, { "cell_type": "markdown", + "id": "784f72cd", "metadata": {}, "source": [ "On a single NVIDIA RTX 2080 Ti GPU, producing such an image takes" @@ -128,6 +136,7 @@ { "cell_type": "code", "execution_count": null, + "id": "07da3296", "metadata": {}, "outputs": [ { @@ -145,6 +154,7 @@ }, { "cell_type": "markdown", + "id": "b03ca035", "metadata": {}, "source": [ "The full example is available at [`introduction.ipynb`](https://vivekg.dev/DiffDRR/tutorials/introduction.html)." @@ -152,6 +162,7 @@ }, { "cell_type": "markdown", + "id": "56cd4950", "metadata": {}, "source": [ "## Usage\n", @@ -214,6 +225,7 @@ }, { "cell_type": "markdown", + "id": "f974abec", "metadata": {}, "source": [ "## Development\n", @@ -246,6 +258,7 @@ }, { "cell_type": "markdown", + "id": "3ed5ba6a", "metadata": {}, "source": [ "## How does `DiffDRR` work?\n", @@ -258,6 +271,7 @@ }, { "cell_type": "markdown", + "id": "19b367d3", "metadata": {}, "source": [ "## Citing `DiffDRR`\n", @@ -297,6 +311,7 @@ { "cell_type": "code", "execution_count": null, + "id": "e56fe186", "metadata": {}, "outputs": [], "source": [] @@ -317,5 +332,5 @@ } }, "nbformat": 4, - "nbformat_minor": 4 + "nbformat_minor": 5 } diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 000000000..25839c104 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,38 @@ +[build-system] +requires = ["setuptools>=64"] +build-backend = "setuptools.build_meta" + +[project] +name = "diffdrr" +dynamic = ["version"] +description = "Auto-differentiable digitally reconstructed radiographs in PyTorch" +readme = "README.md" +requires-python = ">=3.8" +license = {text = "MIT"} +authors = [{name = "Vivek Gopalakrishnan", email = "vivekg@mit.edu"}] +keywords = ['nbdev', 'jupyter', 'notebook', 'python'] +classifiers = ["Natural Language :: English", "Intended Audience :: Developers", "Development Status :: 3 - Alpha", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only"] +dependencies = ['matplotlib', 'seaborn', 'tqdm', 'imageio', 'fastcore', 'pyvista[all]', 'einops', 'torchvision', 'scipy', 'torchio', 'timm', 'numpy', 'kornia', 'roma', 'torch', 'roma'] + +[project.urls] +Repository = "https://github.com/eigenvivek/DiffDRR" +Documentation = "https://vivekg.dev/DiffDRR" + +[project.entry-points.nbdev] +diffdrr = "diffdrr._modidx:d" + +[project.optional-dependencies] +dev = ['nbdev', 'black', 'flake8', 'ipykernel', 'ipywidgets', 'jupyterlab', 'jupyterlab_execute_time', 'jupyterlab-code-formatter', 'isort'] + +[tool.setuptools.dynamic] +version = {attr = "diffdrr.__version__"} + +[tool.setuptools.packages.find] +include = ["diffdrr"] + +[tool.nbdev] +nbs_path = 'notebooks' +tst_flags = 'slow|cuda' +jupyter_hooks = true +custom_sidebar = true +title = 'diffdrr' diff --git a/settings.ini b/settings.ini deleted file mode 100644 index 89fe18984..000000000 --- a/settings.ini +++ /dev/null @@ -1,41 +0,0 @@ -[DEFAULT] -repo = DiffDRR -lib_name = diffdrr -version = 0.6.0 -min_python = 3.8 -license = mit -black_formatting = True -doc_path = _docs -lib_path = diffdrr -nbs_path = notebooks -recursive = True -tst_flags = slow|cuda -put_version_in_init = True -branch = main -custom_sidebar = True -doc_host = https://vivekg.dev -doc_baseurl = /DiffDRR -git_url = https://github.com/eigenvivek/DiffDRR -title = diffdrr -audience = Developers -author = Vivek Gopalakrishnan -author_email = vivekg@mit.edu -copyright = 2022 onwards, Vivek Gopalakrishnan -description = Auto-differentiable digitally reconstructed radiographs in PyTorch -keywords = nbdev jupyter notebook python -language = English -status = 3 -user = eigenvivek -requirements = matplotlib seaborn tqdm imageio fastcore 'pyvista[all]' einops torchvision scipy torchio timm numpy kornia roma -pip_requirements = torch roma -conda_requirements = pytorch -dev_requirements = nbdev black flake8 ipykernel ipywidgets jupyterlab jupyterlab_execute_time jupyterlab-code-formatter isort -readme_nb = index.ipynb -allowed_metadata_keys = -allowed_cell_metadata_keys = -jupyter_hooks = True -clean_ids = True -clear_all = False -cell_number = True -skip_procs = - diff --git a/setup.py b/setup.py deleted file mode 100644 index 08f6537cd..000000000 --- a/setup.py +++ /dev/null @@ -1,83 +0,0 @@ -from pkg_resources import parse_version -from configparser import ConfigParser -import setuptools, shlex - -assert parse_version(setuptools.__version__) >= parse_version("36.2") - -# note: all settings are in settings.ini; edit there, not here -config = ConfigParser(delimiters=["="]) -config.read("settings.ini") -cfg = config["DEFAULT"] - -cfg_keys = "version description keywords author author_email".split() -expected = ( - cfg_keys - + "lib_name user branch license status min_python audience language".split() -) -for o in expected: - assert o in cfg, "missing expected setting: {}".format(o) -setup_cfg = {o: cfg[o] for o in cfg_keys} - -licenses = { - "apache2": ( - "Apache Software License 2.0", - "OSI Approved :: Apache Software License", - ), - "mit": ("MIT License", "OSI Approved :: MIT License"), - "gpl2": ( - "GNU General Public License v2", - "OSI Approved :: GNU General Public License v2 (GPLv2)", - ), - "gpl3": ( - "GNU General Public License v3", - "OSI Approved :: GNU General Public License v3 (GPLv3)", - ), - "bsd3": ("BSD License", "OSI Approved :: BSD License"), -} -statuses = [ - "1 - Planning", - "2 - Pre-Alpha", - "3 - Alpha", - "4 - Beta", - "5 - Production/Stable", - "6 - Mature", - "7 - Inactive", -] -py_versions = "3.8 3.9 3.10 3.11".split() - -requirements = shlex.split(cfg.get("requirements", "")) -if cfg.get("pip_requirements"): - requirements += shlex.split(cfg.get("pip_requirements", "")) -min_python = cfg["min_python"] -lic = licenses.get(cfg["license"].lower(), (cfg["license"], None)) -dev_requirements = (cfg.get("dev_requirements") or "").split() - -setuptools.setup( - name=cfg["lib_name"], - license=lic[0], - classifiers=[ - "Development Status :: " + statuses[int(cfg["status"])], - "Intended Audience :: " + cfg["audience"].title(), - "Natural Language :: " + cfg["language"].title(), - ] - + [ - "Programming Language :: Python :: " + o - for o in py_versions[py_versions.index(min_python) :] - ] - + (["License :: " + lic[1]] if lic[1] else []), - url=cfg["git_url"], - packages=setuptools.find_packages(), - include_package_data=True, - install_requires=requirements, - extras_require={"dev": dev_requirements}, - dependency_links=cfg.get("dep_links", "").split(), - python_requires=">=" + cfg["min_python"], - long_description=open("README.md").read(), - long_description_content_type="text/markdown", - zip_safe=False, - entry_points={ - "console_scripts": cfg.get("console_scripts", "").split(), - "nbdev": [f'{cfg.get("lib_path")}={cfg.get("lib_path")}._modidx:d'], - }, - **setup_cfg, -)