nanodrr 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. nanodrr-0.1.0/PKG-INFO +56 -0
  2. nanodrr-0.1.0/README.md +41 -0
  3. nanodrr-0.1.0/pyproject.toml +48 -0
  4. nanodrr-0.1.0/src/nanodrr/__init__.py +0 -0
  5. nanodrr-0.1.0/src/nanodrr/camera/.ipynb_checkpoints/extrinsics-checkpoint.py +70 -0
  6. nanodrr-0.1.0/src/nanodrr/camera/.ipynb_checkpoints/intrinsics-checkpoint.py +55 -0
  7. nanodrr-0.1.0/src/nanodrr/camera/.ipynb_checkpoints/matrices-checkpoint.py +174 -0
  8. nanodrr-0.1.0/src/nanodrr/camera/__init__.py +4 -0
  9. nanodrr-0.1.0/src/nanodrr/camera/extrinsics.py +70 -0
  10. nanodrr-0.1.0/src/nanodrr/camera/intrinsics.py +57 -0
  11. nanodrr-0.1.0/src/nanodrr/data/.ipynb_checkpoints/__init__-checkpoint.py +4 -0
  12. nanodrr-0.1.0/src/nanodrr/data/.ipynb_checkpoints/demo-checkpoint.py +22 -0
  13. nanodrr-0.1.0/src/nanodrr/data/.ipynb_checkpoints/io-checkpoint.py +125 -0
  14. nanodrr-0.1.0/src/nanodrr/data/.ipynb_checkpoints/lac-checkpoint.py +10 -0
  15. nanodrr-0.1.0/src/nanodrr/data/.ipynb_checkpoints/preprocess-checkpoint.py +39 -0
  16. nanodrr-0.1.0/src/nanodrr/data/__init__.py +4 -0
  17. nanodrr-0.1.0/src/nanodrr/data/demo.py +23 -0
  18. nanodrr-0.1.0/src/nanodrr/data/io.py +125 -0
  19. nanodrr-0.1.0/src/nanodrr/data/preprocess.py +39 -0
  20. nanodrr-0.1.0/src/nanodrr/drr/.ipynb_checkpoints/__init__-checkpoint.py +4 -0
  21. nanodrr-0.1.0/src/nanodrr/drr/.ipynb_checkpoints/drr-checkpoint.py +50 -0
  22. nanodrr-0.1.0/src/nanodrr/drr/.ipynb_checkpoints/render-checkpoint.py +97 -0
  23. nanodrr-0.1.0/src/nanodrr/drr/__init__.py +4 -0
  24. nanodrr-0.1.0/src/nanodrr/drr/drr.py +70 -0
  25. nanodrr-0.1.0/src/nanodrr/drr/render.py +97 -0
  26. nanodrr-0.1.0/src/nanodrr/geometry/.ipynb_checkpoints/transform-checkpoint.py +29 -0
  27. nanodrr-0.1.0/src/nanodrr/geometry/__init__.py +4 -0
  28. nanodrr-0.1.0/src/nanodrr/geometry/se3.py +254 -0
  29. nanodrr-0.1.0/src/nanodrr/geometry/transform.py +28 -0
  30. nanodrr-0.1.0/src/nanodrr/metrics/.ipynb_checkpoints/geo-checkpoint.py +45 -0
  31. nanodrr-0.1.0/src/nanodrr/metrics/__init__.py +13 -0
  32. nanodrr-0.1.0/src/nanodrr/metrics/geo.py +45 -0
  33. nanodrr-0.1.0/src/nanodrr/metrics/ncc.py +133 -0
  34. nanodrr-0.1.0/src/nanodrr/plot/__init__.py +3 -0
  35. nanodrr-0.1.0/src/nanodrr/plot/plot.py +167 -0
  36. nanodrr-0.1.0/src/nanodrr/py.typed +0 -0
  37. nanodrr-0.1.0/src/nanodrr/registration/.ipynb_checkpoints/registration-checkpoint.py +55 -0
  38. nanodrr-0.1.0/src/nanodrr/registration/__init__.py +3 -0
  39. nanodrr-0.1.0/src/nanodrr/registration/registration.py +52 -0
  40. nanodrr-0.1.0/src/nanodrr/scene/__init__.py +3 -0
  41. nanodrr-0.1.0/src/nanodrr/scene/camera.py +76 -0
  42. nanodrr-0.1.0/src/nanodrr/scene/scene.py +62 -0
  43. nanodrr-0.1.0/src/nanodrr/scene/surface.py +12 -0
  44. nanodrr-0.1.0/src/nanodrr/scene/utils.py +22 -0
nanodrr-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,56 @@
1
+ Metadata-Version: 2.3
2
+ Name: nanodrr
3
+ Version: 0.1.0
4
+ Summary: Blazing fast differentiable DRR rendering in modern PyTorch
5
+ Requires-Dist: jaxtyping>=0.3.0
6
+ Requires-Dist: matplotlib>=3.0.0
7
+ Requires-Dist: roma>=1.5.6
8
+ Requires-Dist: torch>=2.4.0
9
+ Requires-Dist: torchio>=0.21.0
10
+ Requires-Dist: pyvista[all]>=0.47.0 ; extra == 'scene'
11
+ Requires-Dist: vtk>=9.6.0 ; extra == 'scene'
12
+ Requires-Python: >=3.10
13
+ Provides-Extra: scene
14
+ Description-Content-Type: text/markdown
15
+
16
+ # nanodrr
17
+
18
+ A performance-oriented reimplementation of [`DiffDRR`](https://github.com/eigenvivek/DiffDRR) with the following improvements:
19
+
20
+ - Optimized, pure PyTorch implementation (**~5× faster than `DiffDRR` at baseline**)
21
+ - Modular design (freely swap subjects, extrinsics, and intrinsics during rendering)
22
+ - Compatibility with `torch.compile` and mixed precision
23
+ - Extensive type hints with `jaxtyping`
24
+ - Standard Python package structure managed with `uv`
25
+
26
+ All projective geometry is implemented internally using the standard [Hartley and Zisserman](https://www.cambridge.org/core/books/multiple-view-geometry-in-computer-vision/0B6F289C78B2B23F596CAA76D3D43F7A) pinhole camera formulation.
27
+
28
+ ## Installation
29
+
30
+ > [!NOTE]
31
+ >
32
+ > On `pytorch<2.9`, `torch.compile` with `bfloat16` is slower than eager due to a CUDA graph capture issue (see [Benchmarks](#benchmarks)). Use `pytorch>=2.9` (Triton ≥3.5) for best results.
33
+
34
+ ```
35
+ pip install "git+https://github.com/eigenvivek/nanodrr.git"
36
+ ```
37
+
38
+ ## Benchmarks
39
+
40
+ > [!IMPORTANT]
41
+ > - **~5× faster** than [`DiffDRR`](https://github.com/eigenvivek/DiffDRR) out of the box, without compilation (946 FPS vs 213 FPS)
42
+ > - **~8× faster** with `torch.compile` and `bfloat16` on `pytorch>=2.9` (1,650 FPS vs 213 FPS)
43
+ > - **~2.5× less memory** than `DiffDRR` (516 MB vs 1,344 MB peak reserved with `bfloat16` + compile)
44
+
45
+ ![Benchmarking runtime, FPS, and memory usage.](tests/benchmark/benchmark.png "benchmark")
46
+
47
+ > *Mean ± std. dev. of 10 runs, 100 loops each. Benchmarked by rendering 200×200 DRRs on an NVIDIA RTX 6000 Ada (48 GB) with Python 3.12. Compile represents `torch.compile(mode="reduce-overhead", fullgraph=True)`. Full experiment at [`tests/benchmark/`](tests/benchmark/).*
48
+
49
+ ## Roadmap
50
+
51
+ - [x] Implement a fully optimized renderer
52
+ - [x] Port strictly necessary modules from `DiffDRR` (e.g., SE(3) utilities, loss functions, and 2D plotting)
53
+ - [x] Migrate 3D plotting functions to an optional module
54
+ - [ ] Integrate with [`xvr`](https://github.com/eigenvivek/xvr) to speed up network training and registration
55
+ - [ ] Integrate with [`polypose`](https://github.com/eigenvivek/polypose) to speed up registration
56
+ - [ ] Release as `v1.0.0` of `DiffDRR`!
@@ -0,0 +1,41 @@
1
+ # nanodrr
2
+
3
+ A performance-oriented reimplementation of [`DiffDRR`](https://github.com/eigenvivek/DiffDRR) with the following improvements:
4
+
5
+ - Optimized, pure PyTorch implementation (**~5× faster than `DiffDRR` at baseline**)
6
+ - Modular design (freely swap subjects, extrinsics, and intrinsics during rendering)
7
+ - Compatibility with `torch.compile` and mixed precision
8
+ - Extensive type hints with `jaxtyping`
9
+ - Standard Python package structure managed with `uv`
10
+
11
+ All projective geometry is implemented internally using the standard [Hartley and Zisserman](https://www.cambridge.org/core/books/multiple-view-geometry-in-computer-vision/0B6F289C78B2B23F596CAA76D3D43F7A) pinhole camera formulation.
12
+
13
+ ## Installation
14
+
15
+ > [!NOTE]
16
+ >
17
+ > On `pytorch<2.9`, `torch.compile` with `bfloat16` is slower than eager due to a CUDA graph capture issue (see [Benchmarks](#benchmarks)). Use `pytorch>=2.9` (Triton ≥3.5) for best results.
18
+
19
+ ```
20
+ pip install "git+https://github.com/eigenvivek/nanodrr.git"
21
+ ```
22
+
23
+ ## Benchmarks
24
+
25
+ > [!IMPORTANT]
26
+ > - **~5× faster** than [`DiffDRR`](https://github.com/eigenvivek/DiffDRR) out of the box, without compilation (946 FPS vs 213 FPS)
27
+ > - **~8× faster** with `torch.compile` and `bfloat16` on `pytorch>=2.9` (1,650 FPS vs 213 FPS)
28
+ > - **~2.5× less memory** than `DiffDRR` (516 MB vs 1,344 MB peak reserved with `bfloat16` + compile)
29
+
30
+ ![Benchmarking runtime, FPS, and memory usage.](tests/benchmark/benchmark.png "benchmark")
31
+
32
+ > *Mean ± std. dev. of 10 runs, 100 loops each. Benchmarked by rendering 200×200 DRRs on an NVIDIA RTX 6000 Ada (48 GB) with Python 3.12. Compile represents `torch.compile(mode="reduce-overhead", fullgraph=True)`. Full experiment at [`tests/benchmark/`](tests/benchmark/).*
33
+
34
+ ## Roadmap
35
+
36
+ - [x] Implement a fully optimized renderer
37
+ - [x] Port strictly necessary modules from `DiffDRR` (e.g., SE(3) utilities, loss functions, and 2D plotting)
38
+ - [x] Migrate 3D plotting functions to an optional module
39
+ - [ ] Integrate with [`xvr`](https://github.com/eigenvivek/xvr) to speed up network training and registration
40
+ - [ ] Integrate with [`polypose`](https://github.com/eigenvivek/polypose) to speed up registration
41
+ - [ ] Release as `v1.0.0` of `DiffDRR`!
@@ -0,0 +1,48 @@
1
+ [project]
2
+ name = "nanodrr"
3
+ version = "0.1.0"
4
+ description = "Blazing fast differentiable DRR rendering in modern PyTorch"
5
+ readme = "README.md"
6
+ requires-python = ">=3.10"
7
+ dependencies = [
8
+ "jaxtyping>=0.3.0",
9
+ "matplotlib>=3.0.0",
10
+ "roma>=1.5.6",
11
+ "torch>=2.4.0",
12
+ "torchio>=0.21.0",
13
+ ]
14
+
15
+ [project.optional-dependencies]
16
+ scene = [
17
+ "pyvista[all]>=0.47.0",
18
+ "vtk>=9.6.0",
19
+ ]
20
+
21
+ [build-system]
22
+ requires = ["uv_build>=0.10.2,<0.11.0"]
23
+ build-backend = "uv_build"
24
+
25
+ [tool.ruff]
26
+ line-length = 120
27
+
28
+ [tool.ruff.lint]
29
+ ignore = [
30
+ "F722", # Forward annotation false positive from jaxtyping
31
+ "F821", # Forward annotation false positive from jaxtyping
32
+ ]
33
+
34
+ [dependency-groups]
35
+ dev = [
36
+ "prek>=0.3.2",
37
+ ]
38
+ docs = [
39
+ "griffe>=2.0.0",
40
+ "mkdocs-callouts>=1.16.0",
41
+ "mkdocs-gen-files>=0.6.0",
42
+ "mkdocs-jupyter>=0.25.1",
43
+ "mkdocs-literate-nav>=0.6.2",
44
+ "mkdocs-material>=9.7.1",
45
+ "mkdocs-same-dir>=0.1.3",
46
+ "mkdocs-section-index>=0.3.10",
47
+ "mkdocstrings[python]>=1.0.3",
48
+ ]
File without changes
@@ -0,0 +1,70 @@
1
+ import torch
2
+ from jaxtyping import Float
3
+
4
+ from ..geometry import convert
5
+
6
+ _ORIENTATION_MATRICES = {
7
+ "AP": [
8
+ [-1, 0, 0, 0],
9
+ [ 0, 0, -1, 0],
10
+ [ 0,-1, 0, 0],
11
+ [ 0, 0, 0, 1],
12
+ ],
13
+ "PA": [
14
+ [-1, 0, 0, 0],
15
+ [ 0, 0, 1, 0],
16
+ [ 0,-1, 0, 0],
17
+ [ 0, 0, 0, 1],
18
+ ],
19
+ None: [
20
+ [-1, 0, 0, 0],
21
+ [ 0,-1, 0, 0],
22
+ [ 0, 0, 1, 0],
23
+ [ 0, 0, 0, 1],
24
+ ],
25
+ }
26
+
27
+
28
+ def make_rt_inv(
29
+ rotation: Float[torch.Tensor, "B 3"],
30
+ translation: Float[torch.Tensor, "B 3"],
31
+ orientation: str | None = "AP",
32
+ isocenter: Float[torch.Tensor, "3"] | None = None,
33
+ ) -> Float[torch.Tensor, "B 4 4"]:
34
+ """Create 4x4 camera-to-world (extrinsic inverse) matrices.
35
+
36
+ Composes pose and reorientation as ``extrinsic_inv = pose @ reorient``
37
+ so that *translation* is applied in the pre-reoriented frame.
38
+
39
+ Args:
40
+ rotation: (B, 3) Euler angles (z, x, y) in degrees, ZXY convention.
41
+ translation: (B, 3) camera position in mm, relative to *isocenter*
42
+ (or world origin when isocenter is ``None``).
43
+ orientation: ``"AP"``, ``"PA"``, or ``None``.
44
+ isocenter: Optional (3,) volume centre in world coordinates.
45
+
46
+ Returns:
47
+ (B, 4, 4) camera-to-world transformation matrices.
48
+ """
49
+ if orientation not in _ORIENTATION_MATRICES:
50
+ raise ValueError(f"Unknown orientation: {orientation}. Use 'AP', 'PA', or None")
51
+
52
+ device = rotation.device
53
+ dtype = rotation.dtype
54
+
55
+ if isocenter is None:
56
+ isocenter = torch.zeros(3, device=device, dtype=dtype)
57
+
58
+ pose = convert(rotation, translation, "euler", convention="ZXY", isocenter=isocenter)
59
+ orientation_matrix = _get_orientation_matrix(orientation, device, dtype)
60
+
61
+ return pose @ orientation_matrix
62
+
63
+
64
+ def _get_orientation_matrix(
65
+ orientation: str | None,
66
+ device: torch.device,
67
+ dtype: torch.dtype,
68
+ ) -> Float[torch.Tensor, "4 4"]:
69
+ """Return the combined orientation + Rz(180°) matrix."""
70
+ return torch.tensor(_ORIENTATION_MATRICES[orientation], device=device, dtype=dtype)
@@ -0,0 +1,55 @@
1
+ import torch
2
+ from jaxtyping import Float
3
+
4
+
5
+ def make_k_inv(
6
+ sdd: float,
7
+ delx: float,
8
+ dely: float,
9
+ x0: float,
10
+ y0: float,
11
+ height: int,
12
+ width: int,
13
+ dtype: torch.dtype | None = None,
14
+ device: torch.device | None = None,
15
+ ) -> Float[torch.Tensor, "1 3 3"]:
16
+ """Build the inverse intrinsic matrix K⁻¹ for a cone-beam projector.
17
+
18
+ Focal lengths and principal point are derived from the physical geometry:
19
+
20
+ fx = sdd / delx cy = y0 / dely + height / 2
21
+ fy = sdd / dely cx = x0 / delx + width / 2
22
+
23
+ The returned matrix is the analytical inverse of:
24
+
25
+ K = [[fx, 0, cx],
26
+ [0, fy, cy],
27
+ [0, 0, 1]]
28
+
29
+ Args:
30
+ sdd: Source-to-detector distance (mm).
31
+ delx, dely: Pixel spacing in x and y (mm/px).
32
+ x0, y0: Principal-point offset from detector centre (mm).
33
+ height, width: Detector dimensions in pixels.
34
+ dtype: Optional tensor dtype.
35
+ device: Optional tensor device.
36
+
37
+ Returns:
38
+ (1, 3, 3) inverse intrinsic matrix.
39
+ """
40
+ fx = sdd / delx
41
+ fy = sdd / dely
42
+ cx = x0 / delx + width / 2.0
43
+ cy = y0 / dely + height / 2.0
44
+
45
+ return torch.tensor(
46
+ [
47
+ [
48
+ [1.0 / fx, 0.0, -cx / fx],
49
+ [0.0, 1.0 / fy, -cy / fy],
50
+ [0.0, 0.0, 1.0],
51
+ ]
52
+ ],
53
+ dtype=dtype,
54
+ device=device,
55
+ )
@@ -0,0 +1,174 @@
1
+ import torch
2
+
3
+
4
+ def make_k_inv(
5
+ sdd: float,
6
+ delx: float,
7
+ dely: float,
8
+ x0: float,
9
+ y0: float,
10
+ height: int,
11
+ width: int,
12
+ ) -> torch.Tensor:
13
+ fx = sdd / delx
14
+ fy = sdd / dely
15
+ cx = x0 / delx + width / 2.0
16
+ cy = y0 / dely + height / 2.0
17
+
18
+ fx_inv = 1.0 / fx
19
+ fy_inv = 1.0 / fy
20
+
21
+ return torch.tensor(
22
+ [
23
+ [
24
+ [fx_inv, 0.0, -cx * fx_inv],
25
+ [0.0, fy_inv, -cy * fy_inv],
26
+ [0.0, 0.0, 1.0],
27
+ ]
28
+ ]
29
+ )
30
+
31
+
32
+ def make_rt_inv(
33
+ rotation: torch.Tensor,
34
+ translation: torch.Tensor,
35
+ orientation: str | None = "AP",
36
+ isocenter: torch.Tensor | None = None,
37
+ ) -> torch.Tensor:
38
+ """Create 4x4 camera-to-world (extrinsic inverse) transformation matrix.
39
+
40
+ Composes the pose and reorientation to match DiffDRR's behavior:
41
+ extrinsic_inv = pose @ reorient
42
+
43
+ This order means the translation is applied in the pre-reoriented frame,
44
+ so translation=(0, 850, 0) with AP orientation places the source at Y=850
45
+ in world coordinates (behind the patient for AP imaging).
46
+
47
+ When isocenter is provided, the translation is interpreted as relative to
48
+ the isocenter rather than world origin.
49
+
50
+ Args:
51
+ rotation: (batch, 3) Euler angles (angle_z, angle_x, angle_y) in degrees, ZXY convention
52
+ translation: (batch, 3) camera position (mm). If isocenter is provided,
53
+ this is relative to isocenter; otherwise relative to world origin.
54
+ orientation: "AP", "PA", or None for frame-of-reference
55
+ isocenter: Optional (3,) volume isocenter in world coordinates.
56
+ When provided, the translation is relative to this point.
57
+
58
+ Returns:
59
+ (batch, 4, 4) camera-to-world transformation matrices
60
+ """
61
+ if orientation not in (None, "AP", "PA"):
62
+ raise ValueError(f"Unknown orientation: {orientation}. Use 'AP', 'PA', or None")
63
+
64
+ batch_size = rotation.shape[0]
65
+ device = rotation.device
66
+ dtype = rotation.dtype
67
+
68
+ # Default isocenter to origin
69
+ if isocenter is None:
70
+ isocenter = torch.zeros(3, device=device, dtype=dtype)
71
+
72
+ # Get rotation matrices from Euler angles
73
+ R = euler_to_matrix(rotation) # (batch, 3, 3)
74
+
75
+ # Compute camera center: R @ translation + isocenter
76
+ # bij,bj->bi : batched matrix-vector multiply
77
+ camera_center = torch.einsum("bij,bj->bi", R, translation)
78
+ camera_center = camera_center + isocenter
79
+
80
+ # Build 4x4 pose matrices [R | camera_center]
81
+ pose = torch.zeros(batch_size, 4, 4, device=device, dtype=dtype)
82
+ pose[:, :3, :3] = R
83
+ pose[:, :3, 3] = camera_center
84
+ pose[:, 3, 3] = 1.0
85
+
86
+ # Apply orientation (pose @ combined)
87
+ # bij,jk->bik : batched matrix times single matrix
88
+ orientation_matrix = get_orientation_matrix(orientation, device, dtype)
89
+ out = torch.einsum("bij,jk->bik", pose, orientation_matrix)
90
+
91
+ return out
92
+
93
+
94
+ def euler_to_matrix(rotation: torch.Tensor) -> torch.Tensor:
95
+ """Convert ZXY Euler angles (degrees) to rotation matrices.
96
+
97
+ Args:
98
+ rotation: Euler angles (angle_z, angle_x, angle_y) in degrees, shape (batch, 3)
99
+
100
+ Returns:
101
+ Rotation matrices of shape (batch, 3, 3)
102
+ """
103
+ angles = torch.deg2rad(rotation)
104
+ z, x, y = angles[:, 0], angles[:, 1], angles[:, 2]
105
+
106
+ cz, sz = torch.cos(z), torch.sin(z)
107
+ cx, sx = torch.cos(x), torch.sin(x)
108
+ cy, sy = torch.cos(y), torch.sin(y)
109
+
110
+ # ZXY Euler rotation matrix
111
+ R = torch.stack(
112
+ [
113
+ torch.stack(
114
+ [cy * cz - sx * sy * sz, -cx * sz, cz * sy + cy * sx * sz], dim=1
115
+ ),
116
+ torch.stack(
117
+ [cy * sz + cz * sx * sy, cx * cz, sy * sz - cy * cz * sx], dim=1
118
+ ),
119
+ torch.stack([-cx * sy, sx, cx * cy], dim=1),
120
+ ],
121
+ dim=1,
122
+ )
123
+
124
+ return R
125
+
126
+
127
+ def get_orientation_matrix(
128
+ orientation: str | None, device: torch.device, dtype: torch.dtype
129
+ ) -> torch.Tensor:
130
+ """Get the combined orientation + Rz(180°) matrix.
131
+
132
+ Args:
133
+ orientation: "AP", "PA", or None
134
+ device: torch device
135
+ dtype: torch dtype
136
+
137
+ Returns:
138
+ 4x4 transformation matrix
139
+ """
140
+ if orientation == "AP":
141
+ combined = torch.tensor(
142
+ [
143
+ [-1.0, 0.0, 0.0, 0.0],
144
+ [0.0, 0.0, -1.0, 0.0],
145
+ [0.0, -1.0, 0.0, 0.0],
146
+ [0.0, 0.0, 0.0, 1.0],
147
+ ],
148
+ device=device,
149
+ dtype=dtype,
150
+ )
151
+ elif orientation == "PA":
152
+ combined = torch.tensor(
153
+ [
154
+ [-1.0, 0.0, 0.0, 0.0],
155
+ [0.0, 0.0, -1.0, 0.0],
156
+ [0.0, -1.0, 0.0, 0.0],
157
+ [0.0, 0.0, 0.0, 1.0],
158
+ ],
159
+ device=device,
160
+ dtype=dtype,
161
+ )
162
+ else: # None - just Rz180
163
+ combined = torch.tensor(
164
+ [
165
+ [-1.0, 0.0, 0.0, 0.0],
166
+ [0.0, -1.0, 0.0, 0.0],
167
+ [0.0, 0.0, 1.0, 0.0],
168
+ [0.0, 0.0, 0.0, 1.0],
169
+ ],
170
+ device=device,
171
+ dtype=dtype,
172
+ )
173
+
174
+ return combined
@@ -0,0 +1,4 @@
1
+ from .intrinsics import make_k_inv
2
+ from .extrinsics import make_rt_inv
3
+
4
+ __all__ = ["make_k_inv", "make_rt_inv"]
@@ -0,0 +1,70 @@
1
+ import torch
2
+ from jaxtyping import Float
3
+
4
+ from ..geometry import convert
5
+
6
+ _ORIENTATION_MATRICES = {
7
+ "AP": [
8
+ [-1, 0, 0, 0],
9
+ [0, 0, -1, 0],
10
+ [0, -1, 0, 0],
11
+ [0, 0, 0, 1],
12
+ ],
13
+ "PA": [
14
+ [-1, 0, 0, 0],
15
+ [0, 0, 1, 0],
16
+ [0, -1, 0, 0],
17
+ [0, 0, 0, 1],
18
+ ],
19
+ None: [
20
+ [-1, 0, 0, 0],
21
+ [0, -1, 0, 0],
22
+ [0, 0, 1, 0],
23
+ [0, 0, 0, 1],
24
+ ],
25
+ }
26
+
27
+
28
+ def make_rt_inv(
29
+ rotation: Float[torch.Tensor, "B 3"],
30
+ translation: Float[torch.Tensor, "B 3"],
31
+ orientation: str | None = "AP",
32
+ isocenter: Float[torch.Tensor, "3"] | None = None,
33
+ ) -> Float[torch.Tensor, "B 4 4"]:
34
+ """Create 4x4 camera-to-world (extrinsic inverse) matrices.
35
+
36
+ Composes pose and reorientation as ``extrinsic_inv = pose @ reorient``
37
+ so that *translation* is applied in the pre-reoriented frame.
38
+
39
+ Args:
40
+ rotation: (B, 3) Euler angles (z, x, y) in degrees, ZXY convention.
41
+ translation: (B, 3) camera position in mm, relative to *isocenter*
42
+ (or world origin when isocenter is ``None``).
43
+ orientation: ``"AP"``, ``"PA"``, or ``None``.
44
+ isocenter: Optional (3,) volume centre in world coordinates.
45
+
46
+ Returns:
47
+ (B, 4, 4) camera-to-world transformation matrices.
48
+ """
49
+ if orientation not in _ORIENTATION_MATRICES:
50
+ raise ValueError(f"Unknown orientation: {orientation}. Use 'AP', 'PA', or None")
51
+
52
+ device = rotation.device
53
+ dtype = rotation.dtype
54
+
55
+ if isocenter is None:
56
+ isocenter = torch.zeros(3, device=device, dtype=dtype)
57
+
58
+ pose = convert(rotation, translation, "euler", convention="ZXY", isocenter=isocenter)
59
+ orientation_matrix = _get_orientation_matrix(orientation, device, dtype)
60
+
61
+ return pose @ orientation_matrix
62
+
63
+
64
+ def _get_orientation_matrix(
65
+ orientation: str | None,
66
+ device: torch.device,
67
+ dtype: torch.dtype,
68
+ ) -> Float[torch.Tensor, "4 4"]:
69
+ """Return the combined orientation + Rz(180°) matrix."""
70
+ return torch.tensor(_ORIENTATION_MATRICES[orientation], device=device, dtype=dtype)
@@ -0,0 +1,57 @@
1
+ import torch
2
+ from jaxtyping import Float
3
+
4
+
5
+ def make_k_inv(
6
+ sdd: float,
7
+ delx: float,
8
+ dely: float,
9
+ x0: float,
10
+ y0: float,
11
+ height: int,
12
+ width: int,
13
+ dtype: torch.dtype | None = None,
14
+ device: torch.device | None = None,
15
+ ) -> Float[torch.Tensor, "1 3 3"]:
16
+ """Build the inverse intrinsic matrix K⁻¹ for a cone-beam projector.
17
+
18
+ Focal lengths and principal point are derived from the physical geometry:
19
+
20
+ fx = sdd / delx cy = y0 / dely + height / 2
21
+ fy = sdd / dely cx = x0 / delx + width / 2
22
+
23
+ The returned matrix is the analytical inverse of:
24
+
25
+ K = [[fx, 0, cx],
26
+ [0, fy, cy],
27
+ [0, 0, 1]]
28
+
29
+ Args:
30
+ sdd: Source-to-detector distance (mm).
31
+ delx: Pixel spacing in x (mm/px).
32
+ dely: Pixel spacing in y (mm/px).
33
+ x0: Principal-point offset from detector centre in x (mm).
34
+ y0: Principal-point offset from detector centre in y (mm).
35
+ height: Detector height in pixels.
36
+ width: Detector width in pixels.
37
+ dtype: Optional tensor dtype.
38
+ device: Optional tensor device.
39
+ Returns:
40
+ (1, 3, 3) inverse intrinsic matrix.
41
+ """
42
+ fx = sdd / delx
43
+ fy = sdd / dely
44
+ cx = x0 / delx + width / 2.0
45
+ cy = y0 / dely + height / 2.0
46
+
47
+ return torch.tensor(
48
+ [
49
+ [
50
+ [1.0 / fx, 0.0, -cx / fx],
51
+ [0.0, 1.0 / fy, -cy / fy],
52
+ [0.0, 0.0, 1.0],
53
+ ]
54
+ ],
55
+ dtype=dtype,
56
+ device=device,
57
+ )
@@ -0,0 +1,4 @@
1
+ from .demo import download_deepfluoro
2
+ from .io import Subject
3
+
4
+ __all__ = ["download_deepfluoro", "Subject"]
@@ -0,0 +1,22 @@
1
+ import os
2
+ import torch
3
+ from platformdirs import user_cache_dir
4
+
5
+ CACHE_DIR = user_cache_dir("nanodrr")
6
+
7
+ def download_deepfluoro(subject: int = 1) -> tuple[str, str]:
8
+ """Download a subject from the DeepFluoro dataset."""
9
+ subject = f"subject{subject:02d}"
10
+ base_url = f"https://huggingface.co/datasets/eigenvivek/xvr-data/resolve/main/deepfluoro/{subject}"
11
+ imagepath = os.path.join(CACHE_DIR, "deepfluoro", subject, "volume.nii.gz")
12
+ labelpath = os.path.join(CACHE_DIR, "deepfluoro", subject, "mask.nii.gz")
13
+
14
+ for url, local_path in [
15
+ (f"{base_url}/volume.nii.gz", imagepath),
16
+ (f"{base_url}/mask.nii.gz", labelpath),
17
+ ]:
18
+ if not os.path.exists(local_path):
19
+ os.makedirs(os.path.dirname(local_path), exist_ok=True)
20
+ torch.hub.download_url_to_file(url, local_path)
21
+
22
+ return imagepath, labelpath