diffct 1.3.3__tar.gz → 1.3.4__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.
- {diffct-1.3.3 → diffct-1.3.4}/CHANGELOG.md +31 -0
- {diffct-1.3.3 → diffct-1.3.4}/PKG-INFO +1 -1
- {diffct-1.3.3 → diffct-1.3.4}/diffct/differentiable.py +78 -258
- {diffct-1.3.3 → diffct-1.3.4}/examples/iterative_reco_cone.py +12 -14
- {diffct-1.3.3 → diffct-1.3.4}/examples/iterative_reco_fan.py +8 -9
- {diffct-1.3.3 → diffct-1.3.4}/examples/iterative_reco_parallel.py +7 -8
- {diffct-1.3.3 → diffct-1.3.4}/pyproject.toml +1 -1
- {diffct-1.3.3 → diffct-1.3.4}/tests/test_fdk_cone_accuracy.py +6 -4
- {diffct-1.3.3 → diffct-1.3.4}/.git +0 -0
- {diffct-1.3.3 → diffct-1.3.4}/.github/workflows/docs.yml +0 -0
- {diffct-1.3.3 → diffct-1.3.4}/.github/workflows/release.yml +0 -0
- {diffct-1.3.3 → diffct-1.3.4}/.gitignore +0 -0
- {diffct-1.3.3 → diffct-1.3.4}/LICENSE +0 -0
- {diffct-1.3.3 → diffct-1.3.4}/README.md +0 -0
- {diffct-1.3.3 → diffct-1.3.4}/README.zh.md +0 -0
- {diffct-1.3.3 → diffct-1.3.4}/diffct/__init__.py +0 -0
- {diffct-1.3.3 → diffct-1.3.4}/docs/Makefile +0 -0
- {diffct-1.3.3 → diffct-1.3.4}/docs/source/_static/.gitkeep +0 -0
- {diffct-1.3.3 → diffct-1.3.4}/docs/source/api.rst +0 -0
- {diffct-1.3.3 → diffct-1.3.4}/docs/source/conf.py +0 -0
- {diffct-1.3.3 → diffct-1.3.4}/docs/source/examples.rst +0 -0
- {diffct-1.3.3 → diffct-1.3.4}/docs/source/fbp_fan_example.rst +0 -0
- {diffct-1.3.3 → diffct-1.3.4}/docs/source/fbp_parallel_example.rst +0 -0
- {diffct-1.3.3 → diffct-1.3.4}/docs/source/fdk_cone_example.rst +0 -0
- {diffct-1.3.3 → diffct-1.3.4}/docs/source/getting_started.rst +0 -0
- {diffct-1.3.3 → diffct-1.3.4}/docs/source/index.rst +0 -0
- {diffct-1.3.3 → diffct-1.3.4}/docs/source/iterative_reco_cone_example.rst +0 -0
- {diffct-1.3.3 → diffct-1.3.4}/docs/source/iterative_reco_fan_example.rst +0 -0
- {diffct-1.3.3 → diffct-1.3.4}/docs/source/iterative_reco_parallel_example.rst +0 -0
- {diffct-1.3.3 → diffct-1.3.4}/examples/data/NOTICE +0 -0
- {diffct-1.3.3 → diffct-1.3.4}/examples/data/preprocess_walnut.py +0 -0
- {diffct-1.3.3 → diffct-1.3.4}/examples/data/walnut_cone.npz +0 -0
- {diffct-1.3.3 → diffct-1.3.4}/examples/data/walnut_reco.png +0 -0
- {diffct-1.3.3 → diffct-1.3.4}/examples/fbp_fan.py +0 -0
- {diffct-1.3.3 → diffct-1.3.4}/examples/fbp_parallel.py +0 -0
- {diffct-1.3.3 → diffct-1.3.4}/examples/fdk_cone.py +0 -0
- {diffct-1.3.3 → diffct-1.3.4}/examples/realdata_fbp_fan.py +0 -0
- {diffct-1.3.3 → diffct-1.3.4}/examples/realdata_fbp_parallel.py +0 -0
- {diffct-1.3.3 → diffct-1.3.4}/examples/realdata_fdk_cone.py +0 -0
- {diffct-1.3.3 → diffct-1.3.4}/examples/realdata_walnut_fdk.py +0 -0
- {diffct-1.3.3 → diffct-1.3.4}/pytest.ini +0 -0
- {diffct-1.3.3 → diffct-1.3.4}/requirements.txt +0 -0
- {diffct-1.3.3 → diffct-1.3.4}/tests/benchmarks/README.md +0 -0
- {diffct-1.3.3 → diffct-1.3.4}/tests/benchmarks/__init__.py +0 -0
- {diffct-1.3.3 → diffct-1.3.4}/tests/benchmarks/_common.py +0 -0
- {diffct-1.3.3 → diffct-1.3.4}/tests/benchmarks/test_bench_cone.py +0 -0
- {diffct-1.3.3 → diffct-1.3.4}/tests/benchmarks/test_bench_fan.py +0 -0
- {diffct-1.3.3 → diffct-1.3.4}/tests/benchmarks/test_bench_parallel.py +0 -0
- {diffct-1.3.3 → diffct-1.3.4}/tests/test_adjoint_inner_product.py +0 -0
- {diffct-1.3.3 → diffct-1.3.4}/tests/test_cone_projector_autograd.py +0 -0
- {diffct-1.3.3 → diffct-1.3.4}/tests/test_cuda_smoke.py +0 -0
- {diffct-1.3.3 → diffct-1.3.4}/tests/test_fbp_fan_accuracy.py +0 -0
- {diffct-1.3.3 → diffct-1.3.4}/tests/test_fbp_fan_offsets.py +0 -0
- {diffct-1.3.3 → diffct-1.3.4}/tests/test_fbp_parallel_accuracy.py +0 -0
- {diffct-1.3.3 → diffct-1.3.4}/tests/test_fdk_cone_offsets.py +0 -0
- {diffct-1.3.3 → diffct-1.3.4}/tests/test_gradcheck.py +0 -0
- {diffct-1.3.3 → diffct-1.3.4}/tests/test_ramp_filter_windows.py +0 -0
- {diffct-1.3.3 → diffct-1.3.4}/tests/test_weights.py +0 -0
|
@@ -5,6 +5,37 @@ All notable changes to this project are documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [1.3.4] - 2026-04-23
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
- **Siddon projectors switched to cell-constant integration.** The 2D
|
|
12
|
+
parallel, 2D fan, and 3D cone Siddon forward/backward kernels in
|
|
13
|
+
`diffct/differentiable.py` now integrate each ray by accumulating
|
|
14
|
+
`d_image[iy, ix] * seg_len` on the traversed pixel/voxel instead of
|
|
15
|
+
sampling the bi-/tri-linear interpolant at the segment midpoint.
|
|
16
|
+
This simplifies the inner loop, removes the interpolation weight
|
|
17
|
+
bookkeeping, and keeps the forward / adjoint pair matched by
|
|
18
|
+
construction (still verified by
|
|
19
|
+
`tests/test_adjoint_inner_product.py`). Kernel docstrings and the
|
|
20
|
+
`"siddon"` backend blurbs in `examples/iterative_reco_*.py` have
|
|
21
|
+
been updated to say *cell-constant Siddon*.
|
|
22
|
+
- **Iterative examples clamp the reconstruction in-place** instead of
|
|
23
|
+
wrapping the forward pass in `nn.ReLU`. `examples/iterative_reco_parallel.py`,
|
|
24
|
+
`iterative_reco_fan.py`, and `iterative_reco_cone.py` now call
|
|
25
|
+
`self.model.reco.clamp_(min=0.0)` under `torch.no_grad()` after each
|
|
26
|
+
optimizer step, so gradients flow through the unclamped volume while
|
|
27
|
+
the stored state stays non-negative.
|
|
28
|
+
- **`examples/iterative_reco_fan.py` default backend** is now
|
|
29
|
+
`"siddon"` (was `"sf"`), matching the parallel and cone iterative
|
|
30
|
+
examples.
|
|
31
|
+
|
|
32
|
+
### Fixed
|
|
33
|
+
- **FDK cone accuracy test tolerance.** `tests/test_fdk_cone_accuracy.py`
|
|
34
|
+
raises the maximum central z-profile error bound from `0.6` to `0.65`
|
|
35
|
+
to reflect the slightly sharper one-sample transition error at
|
|
36
|
+
ellipsoid boundaries under cell-constant forward projection; global
|
|
37
|
+
FDK RMSE is unchanged.
|
|
38
|
+
|
|
8
39
|
## [1.3.3] - 2026-04-23
|
|
9
40
|
|
|
10
41
|
### Fixed
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: diffct
|
|
3
|
-
Version: 1.3.
|
|
3
|
+
Version: 1.3.4
|
|
4
4
|
Summary: A CUDA-based library for computed tomography (CT) projection and reconstruction with differentiable operators
|
|
5
5
|
Project-URL: Homepage, https://github.com/sypsyp97/diffct
|
|
6
6
|
Author-email: Yipeng Sun <yipeng.sun@fau.de>
|
|
@@ -618,7 +618,7 @@ def _parallel_2d_forward_kernel(
|
|
|
618
618
|
):
|
|
619
619
|
"""Compute the 2D parallel beam forward projection.
|
|
620
620
|
|
|
621
|
-
This CUDA kernel implements
|
|
621
|
+
This CUDA kernel implements cell-constant Siddon ray tracing for
|
|
622
622
|
2D parallel beam forward projection.
|
|
623
623
|
|
|
624
624
|
Parameters
|
|
@@ -650,11 +650,10 @@ def _parallel_2d_forward_kernel(
|
|
|
650
650
|
|
|
651
651
|
Notes
|
|
652
652
|
-----
|
|
653
|
-
The Siddon method
|
|
654
|
-
- Calculating ray-volume boundary intersections to define traversal limits.
|
|
655
|
-
- Iterating through voxels along the ray path via parametric equations.
|
|
656
|
-
-
|
|
657
|
-
- Aggregating weighted voxel values based on ray segment lengths.
|
|
653
|
+
The cell-constant Siddon method provides accurate ray-volume intersection by:
|
|
654
|
+
- Calculating ray-volume boundary intersections to define traversal limits.
|
|
655
|
+
- Iterating through voxels along the ray path via parametric equations.
|
|
656
|
+
- Aggregating each traversed pixel value based on ray segment lengths.
|
|
658
657
|
"""
|
|
659
658
|
# CUDA THREAD ORGANIZATION: 2D grid maps directly to ray geometry
|
|
660
659
|
# Each thread processes one ray defined by (projection_angle, detector_element) pair
|
|
@@ -731,49 +730,15 @@ def _parallel_2d_forward_kernel(
|
|
|
731
730
|
tx = (np.float32(next_ix) - cx - pnt_x) * inv_dir_x if abs(dir_x) > _EPSILON else _INF
|
|
732
731
|
ty = (np.float32(next_iy) - cy - pnt_y) * inv_dir_y if abs(dir_y) > _EPSILON else _INF
|
|
733
732
|
|
|
734
|
-
# === MAIN RAY TRAVERSAL LOOP ===
|
|
735
|
-
# Step through voxels along ray path, accumulating
|
|
736
|
-
while t < t_max:
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
seg_len
|
|
742
|
-
|
|
743
|
-
if seg_len > _EPSILON: # Only process segments with meaningful length (avoid numerical noise)
|
|
744
|
-
# === BILINEAR INTERPOLATION SAMPLING ===
|
|
745
|
-
# Sample volume at ray segment midpoint for accurate integration
|
|
746
|
-
# Mathematical basis: Midpoint rule for numerical integration along ray segments
|
|
747
|
-
t_mid = t + seg_len * _HALF
|
|
748
|
-
mid_x = pnt_x + t_mid * dir_x + cx # Midpoint x-coordinate in image space
|
|
749
|
-
mid_y = pnt_y + t_mid * dir_y + cy # Midpoint y-coordinate in image space
|
|
750
|
-
|
|
751
|
-
# Convert continuous coordinates to discrete voxel indices and fractional weights
|
|
752
|
-
# Floor operation gives base voxel index, fractional part gives interpolation weights
|
|
753
|
-
ix0, iy0 = int(math.floor(mid_x)), int(math.floor(mid_y)) # Base voxel indices (bottom-left corner)
|
|
754
|
-
dx = mid_x - np.float32(ix0) # Fractional parts: distance from base voxel center [0,1]
|
|
755
|
-
dy = mid_y - np.float32(iy0)
|
|
756
|
-
|
|
757
|
-
# Clamp indices to stay in-bounds during interpolation
|
|
758
|
-
ix0 = max(0, min(ix0, Nx - 2))
|
|
759
|
-
iy0 = max(0, min(iy0, Ny - 2))
|
|
760
|
-
|
|
761
|
-
# === BILINEAR INTERPOLATION WEIGHT CALCULATION ===
|
|
762
|
-
# Mathematical basis: Bilinear interpolation formula f(x,y) = Σ f(xi,yi) * wi(x,y)
|
|
763
|
-
# where wi(x,y) are the bilinear basis functions for each corner voxel
|
|
764
|
-
# Weights are products of 1D linear interpolation weights: (1-dx) or dx, (1-dy) or dy
|
|
765
|
-
one_minus_dx = _ONE - dx
|
|
766
|
-
one_minus_dy = _ONE - dy
|
|
767
|
-
v00 = d_image[iy0, ix0]
|
|
768
|
-
v10 = d_image[iy0, ix0 + 1]
|
|
769
|
-
v01 = d_image[iy0 + 1, ix0]
|
|
770
|
-
v11 = d_image[iy0 + 1, ix0 + 1]
|
|
771
|
-
row0 = (v00 * one_minus_dx + v10 * dx) * one_minus_dy
|
|
772
|
-
row1 = (v01 * one_minus_dx + v11 * dx) * dy
|
|
773
|
-
val = row0 + row1
|
|
774
|
-
# Accumulate contribution weighted by ray segment length (discrete line integral approximation)
|
|
775
|
-
# This implements the Radon transform: integral of f(x,y) along the ray path
|
|
776
|
-
accum += val * seg_len
|
|
733
|
+
# === MAIN RAY TRAVERSAL LOOP ===
|
|
734
|
+
# Step through voxels along ray path, accumulating cell-constant contributions.
|
|
735
|
+
while t < t_max:
|
|
736
|
+
if 0 <= ix < Nx and 0 <= iy < Ny:
|
|
737
|
+
# Determine next voxel boundary crossing (minimum of x, y boundaries or ray exit)
|
|
738
|
+
t_next = min(tx, ty, t_max)
|
|
739
|
+
seg_len = t_next - t # Length of ray segment within current voxel region
|
|
740
|
+
if seg_len > _EPSILON: # Only process segments with meaningful length (avoid numerical noise)
|
|
741
|
+
accum += d_image[iy, ix] * seg_len
|
|
777
742
|
|
|
778
743
|
# === VOXEL BOUNDARY CROSSING LOGIC ===
|
|
779
744
|
# Advance to next voxel based on which boundary is crossed first
|
|
@@ -797,7 +762,7 @@ def _parallel_2d_backward_kernel(
|
|
|
797
762
|
):
|
|
798
763
|
"""Compute the 2D parallel beam backprojection.
|
|
799
764
|
|
|
800
|
-
This CUDA kernel implements the
|
|
765
|
+
This CUDA kernel implements the adjoint of cell-constant Siddon ray tracing for
|
|
801
766
|
2D parallel beam backprojection.
|
|
802
767
|
|
|
803
768
|
Parameters
|
|
@@ -878,39 +843,14 @@ def _parallel_2d_backward_kernel(
|
|
|
878
843
|
tx = (np.float32(next_ix) - cx - pnt_x) * inv_dir_x if abs(dir_x) > _EPSILON else _INF
|
|
879
844
|
ty = (np.float32(next_iy) - cy - pnt_y) * inv_dir_y if abs(dir_y) > _EPSILON else _INF
|
|
880
845
|
|
|
881
|
-
# === BACKPROJECTION TRAVERSAL LOOP ===
|
|
882
|
-
#
|
|
883
|
-
while t < t_max:
|
|
884
|
-
if 0 <= ix < Nx and 0 <= iy < Ny:
|
|
885
|
-
t_next = min(tx, ty, t_max)
|
|
886
|
-
seg_len = t_next - t
|
|
887
|
-
if seg_len > _EPSILON:
|
|
888
|
-
|
|
889
|
-
t_mid = t + seg_len * _HALF
|
|
890
|
-
mid_x = pnt_x + t_mid * dir_x + cx
|
|
891
|
-
mid_y = pnt_y + t_mid * dir_y + cy
|
|
892
|
-
ix0, iy0 = int(math.floor(mid_x)), int(math.floor(mid_y))
|
|
893
|
-
dx = mid_x - np.float32(ix0)
|
|
894
|
-
dy = mid_y - np.float32(iy0)
|
|
895
|
-
|
|
896
|
-
# Clamp indices to stay in-bounds during interpolation
|
|
897
|
-
ix0 = max(0, min(ix0, Nx - 2))
|
|
898
|
-
iy0 = max(0, min(iy0, Ny - 2))
|
|
899
|
-
|
|
900
|
-
# === ATOMIC BACKPROJECTION WITH BILINEAR WEIGHTS ===
|
|
901
|
-
# Distribute contribution weighted by segment length and interpolation weights
|
|
902
|
-
# CUDA ATOMIC OPERATIONS: Essential for thread safety in backprojection
|
|
903
|
-
# Multiple threads (rays) can write to the same voxel simultaneously, causing race conditions
|
|
904
|
-
# Atomic add operations serialize these writes, ensuring correct accumulation of contributions
|
|
905
|
-
# Performance impact: Atomic operations are slower than regular writes but necessary for correctness
|
|
906
|
-
# Memory access pattern: Global memory atomics with potential bank conflicts, but unavoidable
|
|
907
|
-
cval = val * seg_len # Contribution value for this ray segment
|
|
908
|
-
one_minus_dx = _ONE - dx
|
|
909
|
-
one_minus_dy = _ONE - dy
|
|
910
|
-
cuda.atomic.add(d_image, (iy0, ix0), cval * one_minus_dx * one_minus_dy)
|
|
911
|
-
cuda.atomic.add(d_image, (iy0, ix0 + 1), cval * dx * one_minus_dy)
|
|
912
|
-
cuda.atomic.add(d_image, (iy0 + 1, ix0), cval * one_minus_dx * dy)
|
|
913
|
-
cuda.atomic.add(d_image, (iy0 + 1, ix0 + 1), cval * dx * dy)
|
|
846
|
+
# === BACKPROJECTION TRAVERSAL LOOP ===
|
|
847
|
+
# Adjoint of the cell-constant Siddon forward projection.
|
|
848
|
+
while t < t_max:
|
|
849
|
+
if 0 <= ix < Nx and 0 <= iy < Ny:
|
|
850
|
+
t_next = min(tx, ty, t_max)
|
|
851
|
+
seg_len = t_next - t
|
|
852
|
+
if seg_len > _EPSILON:
|
|
853
|
+
cuda.atomic.add(d_image, (iy, ix), val * seg_len)
|
|
914
854
|
|
|
915
855
|
# Advance to next voxel (identical logic to forward projection)
|
|
916
856
|
if tx <= ty:
|
|
@@ -936,7 +876,7 @@ def _fan_2d_forward_kernel(
|
|
|
936
876
|
):
|
|
937
877
|
"""Compute the 2D fan beam forward projection.
|
|
938
878
|
|
|
939
|
-
This CUDA kernel implements
|
|
879
|
+
This CUDA kernel implements cell-constant Siddon ray tracing for
|
|
940
880
|
2D fan beam forward projection.
|
|
941
881
|
|
|
942
882
|
Parameters
|
|
@@ -1046,35 +986,13 @@ def _fan_2d_forward_kernel(
|
|
|
1046
986
|
tx = (np.float32(next_ix) - cx - src_x) * inv_dir_x if abs(dir_x) > _EPSILON else _INF
|
|
1047
987
|
ty = (np.float32(next_iy) - cy - src_y) * inv_dir_y if abs(dir_y) > _EPSILON else _INF
|
|
1048
988
|
|
|
1049
|
-
# Main traversal loop with
|
|
1050
|
-
while t < t_max:
|
|
1051
|
-
if 0 <= ix < Nx and 0 <= iy < Ny:
|
|
1052
|
-
t_next = min(tx, ty, t_max)
|
|
1053
|
-
seg_len = t_next - t
|
|
1054
|
-
if seg_len > _EPSILON:
|
|
1055
|
-
|
|
1056
|
-
t_mid = t + seg_len * _HALF
|
|
1057
|
-
mid_x = src_x + t_mid * dir_x + cx
|
|
1058
|
-
mid_y = src_y + t_mid * dir_y + cy
|
|
1059
|
-
ix0, iy0 = int(math.floor(mid_x)), int(math.floor(mid_y))
|
|
1060
|
-
dx = mid_x - np.float32(ix0)
|
|
1061
|
-
dy = mid_y - np.float32(iy0)
|
|
1062
|
-
|
|
1063
|
-
# Clamp indices to stay in-bounds during interpolation
|
|
1064
|
-
ix0 = max(0, min(ix0, Nx - 2))
|
|
1065
|
-
iy0 = max(0, min(iy0, Ny - 2))
|
|
1066
|
-
|
|
1067
|
-
# Bilinear interpolation (identical to parallel beam)
|
|
1068
|
-
one_minus_dx = _ONE - dx
|
|
1069
|
-
one_minus_dy = _ONE - dy
|
|
1070
|
-
v00 = d_image[iy0, ix0]
|
|
1071
|
-
v10 = d_image[iy0, ix0 + 1]
|
|
1072
|
-
v01 = d_image[iy0 + 1, ix0]
|
|
1073
|
-
v11 = d_image[iy0 + 1, ix0 + 1]
|
|
1074
|
-
row0 = (v00 * one_minus_dx + v10 * dx) * one_minus_dy
|
|
1075
|
-
row1 = (v01 * one_minus_dx + v11 * dx) * dy
|
|
1076
|
-
val = row0 + row1
|
|
1077
|
-
accum += val * seg_len
|
|
989
|
+
# Main traversal loop with cell-constant Siddon integration.
|
|
990
|
+
while t < t_max:
|
|
991
|
+
if 0 <= ix < Nx and 0 <= iy < Ny:
|
|
992
|
+
t_next = min(tx, ty, t_max)
|
|
993
|
+
seg_len = t_next - t
|
|
994
|
+
if seg_len > _EPSILON:
|
|
995
|
+
accum += d_image[iy, ix] * seg_len
|
|
1078
996
|
|
|
1079
997
|
# Voxel boundary crossing logic (identical to parallel beam)
|
|
1080
998
|
if tx <= ty:
|
|
@@ -1098,7 +1016,7 @@ def _fan_2d_backward_kernel(
|
|
|
1098
1016
|
):
|
|
1099
1017
|
"""Pure adjoint ``P^T`` of the fan-beam forward projector.
|
|
1100
1018
|
|
|
1101
|
-
Siddon ray-driven scatter with
|
|
1019
|
+
Cell-constant Siddon ray-driven scatter with no
|
|
1102
1020
|
distance weighting. Used by ``FanProjectorFunction.backward`` and
|
|
1103
1021
|
``FanBackprojectorFunction.forward``. The analytical FBP path lives
|
|
1104
1022
|
in ``_fan_2d_fbp_backproject_kernel`` / ``fan_weighted_backproject``
|
|
@@ -1165,37 +1083,14 @@ def _fan_2d_backward_kernel(
|
|
|
1165
1083
|
tx = (np.float32(next_ix) - cx - src_x) * inv_dir_x if abs(dir_x) > _EPSILON else _INF
|
|
1166
1084
|
ty = (np.float32(next_iy) - cy - src_y) * inv_dir_y if abs(dir_y) > _EPSILON else _INF
|
|
1167
1085
|
|
|
1168
|
-
# === FAN BEAM BACKPROJECTION TRAVERSAL LOOP ===
|
|
1169
|
-
#
|
|
1170
|
-
while t < t_max:
|
|
1171
|
-
if 0 <= ix < Nx and 0 <= iy < Ny:
|
|
1172
|
-
t_next = min(tx, ty, t_max)
|
|
1173
|
-
seg_len = t_next - t
|
|
1174
|
-
if seg_len > _EPSILON:
|
|
1175
|
-
|
|
1176
|
-
t_mid = t + seg_len * _HALF
|
|
1177
|
-
mid_x = src_x + t_mid * dir_x + cx
|
|
1178
|
-
mid_y = src_y + t_mid * dir_y + cy
|
|
1179
|
-
ix0, iy0 = int(math.floor(mid_x)), int(math.floor(mid_y))
|
|
1180
|
-
dx = mid_x - np.float32(ix0)
|
|
1181
|
-
dy = mid_y - np.float32(iy0)
|
|
1182
|
-
|
|
1183
|
-
# Clamp indices to stay in-bounds during interpolation
|
|
1184
|
-
ix0 = max(0, min(ix0, Nx - 2))
|
|
1185
|
-
iy0 = max(0, min(iy0, Ny - 2))
|
|
1186
|
-
|
|
1187
|
-
# === ATOMIC BACKPROJECTION WITH BILINEAR WEIGHTS ===
|
|
1188
|
-
# Pure adjoint scatter: contribution is just val * seg_len,
|
|
1189
|
-
# distributed onto the four nearest pixels with bilinear
|
|
1190
|
-
# weights. No distance weighting - that belongs to the
|
|
1191
|
-
# analytical FBP kernel, not this adjoint.
|
|
1192
|
-
cval = val * seg_len
|
|
1193
|
-
one_minus_dx = _ONE - dx
|
|
1194
|
-
one_minus_dy = _ONE - dy
|
|
1195
|
-
cuda.atomic.add(d_image, (iy0, ix0), cval * one_minus_dx * one_minus_dy)
|
|
1196
|
-
cuda.atomic.add(d_image, (iy0, ix0 + 1), cval * dx * one_minus_dy)
|
|
1197
|
-
cuda.atomic.add(d_image, (iy0 + 1, ix0), cval * one_minus_dx * dy)
|
|
1198
|
-
cuda.atomic.add(d_image, (iy0 + 1, ix0 + 1), cval * dx * dy)
|
|
1086
|
+
# === FAN BEAM BACKPROJECTION TRAVERSAL LOOP ===
|
|
1087
|
+
# Adjoint of the cell-constant Siddon forward projection.
|
|
1088
|
+
while t < t_max:
|
|
1089
|
+
if 0 <= ix < Nx and 0 <= iy < Ny:
|
|
1090
|
+
t_next = min(tx, ty, t_max)
|
|
1091
|
+
seg_len = t_next - t
|
|
1092
|
+
if seg_len > _EPSILON:
|
|
1093
|
+
cuda.atomic.add(d_image, (iy, ix), val * seg_len)
|
|
1199
1094
|
|
|
1200
1095
|
# === VOXEL BOUNDARY CROSSING LOGIC ===
|
|
1201
1096
|
# Advance to next voxel based on which boundary is crossed first
|
|
@@ -1533,7 +1428,7 @@ def _cone_3d_forward_kernel(
|
|
|
1533
1428
|
):
|
|
1534
1429
|
"""Compute the 3D cone-beam forward projection.
|
|
1535
1430
|
|
|
1536
|
-
This CUDA kernel implements
|
|
1431
|
+
This CUDA kernel implements cell-constant Siddon ray tracing for
|
|
1537
1432
|
3D cone-beam forward projection.
|
|
1538
1433
|
|
|
1539
1434
|
Parameters
|
|
@@ -1577,9 +1472,9 @@ def _cone_3d_forward_kernel(
|
|
|
1577
1472
|
|
|
1578
1473
|
Notes
|
|
1579
1474
|
-----
|
|
1580
|
-
Cone-beam geometry extends the fan-beam configuration to 3D by employing
|
|
1581
|
-
a 2D detector array and
|
|
1582
|
-
|
|
1475
|
+
Cone-beam geometry extends the fan-beam configuration to 3D by employing
|
|
1476
|
+
a 2D detector array and integrating each ray through piecewise-constant
|
|
1477
|
+
voxel cells.
|
|
1583
1478
|
"""
|
|
1584
1479
|
# Put detector-v on threadIdx.x; d_sino[view, u, v] is row-major with v
|
|
1585
1480
|
# as the stride-1 axis, and adjacent v rays also tend to walk nearby z cells.
|
|
@@ -1673,52 +1568,14 @@ def _cone_3d_forward_kernel(
|
|
|
1673
1568
|
ty = (np.float32(next_iy) - cy - src_y) * inv_dir_y if abs(dir_y) > _EPSILON else _INF
|
|
1674
1569
|
tz = (np.float32(next_iz) - cz - src_z) * inv_dir_z if abs(dir_z) > _EPSILON else _INF
|
|
1675
1570
|
|
|
1676
|
-
# === 3D TRAVERSAL LOOP WITH
|
|
1677
|
-
while t < t_max:
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
seg_len
|
|
1683
|
-
|
|
1684
|
-
# === TRILINEAR INTERPOLATION SAMPLING ===
|
|
1685
|
-
# Sample 3D volume at ray segment midpoint for accurate integration
|
|
1686
|
-
# Mathematical basis: Midpoint rule for numerical integration along 3D ray segments
|
|
1687
|
-
t_mid = t + seg_len * _HALF
|
|
1688
|
-
mid_x = src_x + t_mid * dir_x + cx # Midpoint x-coordinate in volume space
|
|
1689
|
-
mid_y = src_y + t_mid * dir_y + cy # Midpoint y-coordinate in volume space
|
|
1690
|
-
mid_z = src_z + t_mid * dir_z + cz # Midpoint z-coordinate in volume space
|
|
1691
|
-
|
|
1692
|
-
# Convert continuous 3D coordinates to discrete voxel indices and fractional weights
|
|
1693
|
-
ix0, iy0, iz0 = int(math.floor(mid_x)), int(math.floor(mid_y)), int(math.floor(mid_z))
|
|
1694
|
-
dx = mid_x - np.float32(ix0)
|
|
1695
|
-
dy = mid_y - np.float32(iy0)
|
|
1696
|
-
dz = mid_z - np.float32(iz0)
|
|
1697
|
-
|
|
1698
|
-
# Clamp indices to stay in-bounds during interpolation
|
|
1699
|
-
ix0 = max(0, min(ix0, Nx - 2))
|
|
1700
|
-
iy0 = max(0, min(iy0, Ny - 2))
|
|
1701
|
-
iz0 = max(0, min(iz0, Nz - 2))
|
|
1702
|
-
|
|
1703
|
-
# Precompute complements
|
|
1704
|
-
omdx = _ONE - dx
|
|
1705
|
-
omdy = _ONE - dy
|
|
1706
|
-
omdz = _ONE - dz
|
|
1707
|
-
|
|
1708
|
-
# === TRILINEAR INTERPOLATION WEIGHT CALCULATION ===
|
|
1709
|
-
val = (
|
|
1710
|
-
d_vol[ix0, iy0, iz0] * omdx*omdy*omdz +
|
|
1711
|
-
d_vol[ix0 + 1, iy0, iz0] * dx *omdy*omdz +
|
|
1712
|
-
d_vol[ix0, iy0 + 1, iz0] * omdx*dy *omdz +
|
|
1713
|
-
d_vol[ix0, iy0, iz0 + 1] * omdx*omdy*dz +
|
|
1714
|
-
d_vol[ix0 + 1, iy0 + 1, iz0] * dx *dy *omdz +
|
|
1715
|
-
d_vol[ix0 + 1, iy0, iz0 + 1] * dx *omdy*dz +
|
|
1716
|
-
d_vol[ix0, iy0 + 1, iz0 + 1] * omdx*dy *dz +
|
|
1717
|
-
d_vol[ix0 + 1, iy0 + 1, iz0 + 1] * dx *dy *dz
|
|
1718
|
-
)
|
|
1719
|
-
# Accumulate contribution weighted by 3D ray segment length (discrete line integral approximation)
|
|
1720
|
-
# This implements the 3D Radon transform: integral of f(x,y,z) along the ray path
|
|
1721
|
-
accum += val * seg_len
|
|
1571
|
+
# === 3D TRAVERSAL LOOP WITH CELL-CONSTANT SIDDON INTEGRATION ===
|
|
1572
|
+
while t < t_max:
|
|
1573
|
+
if 0 <= ix < Nx and 0 <= iy < Ny and 0 <= iz < Nz:
|
|
1574
|
+
# Determine next 3D voxel boundary crossing (minimum of x, y, z boundaries or ray exit)
|
|
1575
|
+
t_next = min(tx, ty, tz, t_max)
|
|
1576
|
+
seg_len = t_next - t
|
|
1577
|
+
if seg_len > _EPSILON:
|
|
1578
|
+
accum += d_vol[ix, iy, iz] * seg_len
|
|
1722
1579
|
|
|
1723
1580
|
# === 3D VOXEL BOUNDARY CROSSING LOGIC ===
|
|
1724
1581
|
# Advance to next voxel based on which boundary is crossed first in 3D
|
|
@@ -1748,7 +1605,7 @@ def _cone_3d_backward_kernel(
|
|
|
1748
1605
|
):
|
|
1749
1606
|
"""Pure adjoint ``P^T`` of the cone-beam forward projector.
|
|
1750
1607
|
|
|
1751
|
-
Siddon ray-driven scatter with
|
|
1608
|
+
Cell-constant Siddon ray-driven scatter with no
|
|
1752
1609
|
distance weighting. Used by ``ConeProjectorFunction.backward`` and
|
|
1753
1610
|
``ConeBackprojectorFunction.forward``. The analytical FDK path lives
|
|
1754
1611
|
in ``_cone_3d_fdk_backproject_kernel`` / ``cone_weighted_backproject``
|
|
@@ -1836,51 +1693,15 @@ def _cone_3d_backward_kernel(
|
|
|
1836
1693
|
ty = (np.float32(next_iy) - cy - src_y) * inv_dir_y if abs(dir_y) > _EPSILON else _INF
|
|
1837
1694
|
tz = (np.float32(next_iz) - cz - src_z) * inv_dir_z if abs(dir_z) > _EPSILON else _INF
|
|
1838
1695
|
|
|
1839
|
-
# === 3D CONE BEAM BACKPROJECTION TRAVERSAL LOOP ===
|
|
1840
|
-
#
|
|
1841
|
-
while t < t_max:
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
seg_len
|
|
1847
|
-
|
|
1848
|
-
# === TRILINEAR INTERPOLATION SAMPLING ===
|
|
1849
|
-
# Sample 3D volume at ray segment midpoint using source as ray origin
|
|
1850
|
-
t_mid = t + seg_len * _HALF
|
|
1851
|
-
mid_x = src_x + t_mid * dir_x + cx
|
|
1852
|
-
mid_y = src_y + t_mid * dir_y + cy
|
|
1853
|
-
mid_z = src_z + t_mid * dir_z + cz
|
|
1854
|
-
|
|
1855
|
-
# Convert continuous 3D coordinates to voxel indices and interpolation weights
|
|
1856
|
-
ix0, iy0, iz0 = int(math.floor(mid_x)), int(math.floor(mid_y)), int(math.floor(mid_z))
|
|
1857
|
-
dx = mid_x - np.float32(ix0)
|
|
1858
|
-
dy = mid_y - np.float32(iy0)
|
|
1859
|
-
dz = mid_z - np.float32(iz0)
|
|
1860
|
-
|
|
1861
|
-
# Clamp indices to stay in-bounds during interpolation
|
|
1862
|
-
ix0 = max(0, min(ix0, Nx - 2))
|
|
1863
|
-
iy0 = max(0, min(iy0, Ny - 2))
|
|
1864
|
-
iz0 = max(0, min(iz0, Nz - 2))
|
|
1865
|
-
|
|
1866
|
-
# Precompute complements and contribution. Pure adjoint:
|
|
1867
|
-
# cval = g * seg_len, distributed onto the eight nearest
|
|
1868
|
-
# voxels with trilinear weights. Distance weighting lives
|
|
1869
|
-
# in the analytical FDK kernel, not here.
|
|
1870
|
-
omdx = _ONE - dx
|
|
1871
|
-
omdy = _ONE - dy
|
|
1872
|
-
omdz = _ONE - dz
|
|
1873
|
-
cval = g * seg_len
|
|
1874
|
-
|
|
1875
|
-
# === ATOMIC BACKPROJECTION WITH TRILINEAR WEIGHTS ===
|
|
1876
|
-
cuda.atomic.add(d_vol, (ix0, iy0, iz0), cval * omdx*omdy*omdz)
|
|
1877
|
-
cuda.atomic.add(d_vol, (ix0 + 1, iy0, iz0), cval * dx *omdy*omdz)
|
|
1878
|
-
cuda.atomic.add(d_vol, (ix0, iy0 + 1, iz0), cval * omdx*dy *omdz)
|
|
1879
|
-
cuda.atomic.add(d_vol, (ix0, iy0, iz0 + 1), cval * omdx*omdy*dz)
|
|
1880
|
-
cuda.atomic.add(d_vol, (ix0 + 1, iy0 + 1, iz0), cval * dx *dy *omdz)
|
|
1881
|
-
cuda.atomic.add(d_vol, (ix0 + 1, iy0, iz0 + 1), cval * dx *omdy*dz)
|
|
1882
|
-
cuda.atomic.add(d_vol, (ix0, iy0 + 1, iz0 + 1), cval * omdx*dy *dz)
|
|
1883
|
-
cuda.atomic.add(d_vol, (ix0 + 1, iy0 + 1, iz0 + 1), cval * dx *dy *dz)
|
|
1696
|
+
# === 3D CONE BEAM BACKPROJECTION TRAVERSAL LOOP ===
|
|
1697
|
+
# Adjoint of the cell-constant Siddon forward projection.
|
|
1698
|
+
while t < t_max:
|
|
1699
|
+
if 0 <= ix < Nx and 0 <= iy < Ny and 0 <= iz < Nz:
|
|
1700
|
+
# Determine next 3D voxel boundary crossing (minimum of x, y, z boundaries or ray exit)
|
|
1701
|
+
t_next = min(tx, ty, tz, t_max)
|
|
1702
|
+
seg_len = t_next - t
|
|
1703
|
+
if seg_len > _EPSILON:
|
|
1704
|
+
cuda.atomic.add(d_vol, (ix, iy, iz), g * seg_len)
|
|
1884
1705
|
|
|
1885
1706
|
# === 3D VOXEL BOUNDARY CROSSING LOGIC ===
|
|
1886
1707
|
# Advance to next voxel based on which boundary is crossed first in 3D
|
|
@@ -3708,7 +3529,7 @@ class ParallelProjectorFunction(torch.autograd.Function):
|
|
|
3708
3529
|
Notes
|
|
3709
3530
|
-----
|
|
3710
3531
|
Provides a differentiable interface to the CUDA-accelerated Siddon ray-tracing
|
|
3711
|
-
method with
|
|
3532
|
+
method with a cell-constant image basis for parallel beam CT geometry. The forward pass computes
|
|
3712
3533
|
the sinogram from a 2D image using parallel beam geometry. The backward pass
|
|
3713
3534
|
computes gradients using the adjoint backprojection operation. Requires
|
|
3714
3535
|
CUDA-capable hardware and a properly configured CUDA environment; all input
|
|
@@ -3770,7 +3591,7 @@ class ParallelProjectorFunction(torch.autograd.Function):
|
|
|
3770
3591
|
-----
|
|
3771
3592
|
- All input tensors must be on the same CUDA device.
|
|
3772
3593
|
- The operation is fully differentiable and supports autograd.
|
|
3773
|
-
- Uses
|
|
3594
|
+
- Uses cell-constant Siddon ray tracing.
|
|
3774
3595
|
|
|
3775
3596
|
Examples
|
|
3776
3597
|
--------
|
|
@@ -3888,7 +3709,7 @@ class ParallelBackprojectorFunction(torch.autograd.Function):
|
|
|
3888
3709
|
Notes
|
|
3889
3710
|
-----
|
|
3890
3711
|
Provides a differentiable interface to the CUDA-accelerated Siddon ray-tracing
|
|
3891
|
-
method with
|
|
3712
|
+
method with a cell-constant image basis for parallel beam backprojection. The forward pass computes a 2D
|
|
3892
3713
|
reconstruction from sinogram data using parallel beam backprojection, and the
|
|
3893
3714
|
backward pass computes gradients via forward projection as the adjoint operation.
|
|
3894
3715
|
Requires CUDA-capable hardware and consistent device placements.
|
|
@@ -3946,7 +3767,7 @@ class ParallelBackprojectorFunction(torch.autograd.Function):
|
|
|
3946
3767
|
-----
|
|
3947
3768
|
- All input tensors must be on the same CUDA device.
|
|
3948
3769
|
- The operation is fully differentiable and supports autograd.
|
|
3949
|
-
- Uses the
|
|
3770
|
+
- Uses the adjoint of cell-constant Siddon ray tracing.
|
|
3950
3771
|
|
|
3951
3772
|
Examples
|
|
3952
3773
|
--------
|
|
@@ -4070,7 +3891,7 @@ class FanProjectorFunction(torch.autograd.Function):
|
|
|
4070
3891
|
Notes
|
|
4071
3892
|
-----
|
|
4072
3893
|
Provides a differentiable interface to the CUDA-accelerated Siddon ray-tracing
|
|
4073
|
-
method with
|
|
3894
|
+
method with a cell-constant image basis for fan beam geometry, where rays diverge from a point
|
|
4074
3895
|
X-ray source to a linear detector array. The forward pass computes sinograms
|
|
4075
3896
|
using divergent beam geometry. The backward pass returns the **pure adjoint**
|
|
4076
3897
|
``P^T`` (Siddon scatter, no distance weighting) so that it is the correct
|
|
@@ -4112,8 +3933,7 @@ class FanProjectorFunction(torch.autograd.Function):
|
|
|
4112
3933
|
----------
|
|
4113
3934
|
backend : str, optional
|
|
4114
3935
|
Forward projector backend. ``"siddon"`` (default) is the existing
|
|
4115
|
-
ray-driven Siddon kernel
|
|
4116
|
-
midpoints. ``"sf"`` is a prototype separable-footprint (SF-TR)
|
|
3936
|
+
ray-driven cell-constant Siddon kernel. ``"sf"`` is a prototype separable-footprint (SF-TR)
|
|
4117
3937
|
voxel-driven projector that projects each voxel's footprint as a
|
|
4118
3938
|
trapezoid on the detector and integrates it closed-form over each
|
|
4119
3939
|
detector cell; it is mass-conserving and closer to the physical
|
|
@@ -4149,7 +3969,7 @@ class FanProjectorFunction(torch.autograd.Function):
|
|
|
4149
3969
|
- All input tensors must be on the same CUDA device.
|
|
4150
3970
|
- The operation is fully differentiable and supports autograd.
|
|
4151
3971
|
- Fan beam geometry uses divergent rays from a point source to the detector.
|
|
4152
|
-
- Uses
|
|
3972
|
+
- Uses cell-constant Siddon ray tracing.
|
|
4153
3973
|
|
|
4154
3974
|
Examples
|
|
4155
3975
|
--------
|
|
@@ -4301,9 +4121,9 @@ class FanBackprojectorFunction(torch.autograd.Function):
|
|
|
4301
4121
|
Notes
|
|
4302
4122
|
-----
|
|
4303
4123
|
Provides a differentiable interface to the CUDA-accelerated Siddon ray-tracing
|
|
4304
|
-
method with
|
|
4124
|
+
method with a cell-constant image basis for fan beam backprojection. The forward pass runs
|
|
4305
4125
|
the **pure adjoint** ``P^T`` of the fan forward projector (Siddon scatter
|
|
4306
|
-
with
|
|
4126
|
+
with no distance weighting), and the backward pass
|
|
4307
4127
|
computes gradients via forward projection. The forward/backward pair is
|
|
4308
4128
|
therefore a self-consistent autograd adjoint pair. Analytical FBP distance
|
|
4309
4129
|
weighting lives in ``fan_weighted_backproject`` and is *not* applied here.
|
|
@@ -4378,7 +4198,7 @@ class FanBackprojectorFunction(torch.autograd.Function):
|
|
|
4378
4198
|
- All input tensors must be on the same CUDA device.
|
|
4379
4199
|
- The operation is fully differentiable and supports autograd.
|
|
4380
4200
|
- Fan beam geometry uses divergent rays from a point source to the detector.
|
|
4381
|
-
- Uses the
|
|
4201
|
+
- Uses the adjoint of cell-constant Siddon ray tracing.
|
|
4382
4202
|
|
|
4383
4203
|
Examples
|
|
4384
4204
|
--------
|
|
@@ -4530,11 +4350,11 @@ class ConeProjectorFunction(torch.autograd.Function):
|
|
|
4530
4350
|
Notes
|
|
4531
4351
|
-----
|
|
4532
4352
|
Provides a differentiable interface to the CUDA-accelerated Siddon ray-tracing
|
|
4533
|
-
method with
|
|
4353
|
+
method with a cell-constant voxel basis for 3D cone beam geometry. Rays emanate from a point
|
|
4534
4354
|
X-ray source to a 2D detector array capturing volumetric projection data.
|
|
4535
4355
|
The forward pass computes 3D projections. The backward pass returns the
|
|
4536
4356
|
**pure adjoint** ``P^T`` of the forward projector - a Siddon ray-driven
|
|
4537
|
-
scatter with
|
|
4357
|
+
scatter with no distance weighting -
|
|
4538
4358
|
so that ``P^T`` is the mathematically correct gradient of ``y = P(x)``
|
|
4539
4359
|
with respect to ``x``. Analytical FDK distance weighting is handled
|
|
4540
4360
|
separately in ``cone_weighted_backproject``, not here.
|
|
@@ -4621,7 +4441,7 @@ class ConeProjectorFunction(torch.autograd.Function):
|
|
|
4621
4441
|
- All input tensors must be on the same CUDA device.
|
|
4622
4442
|
- The operation is fully differentiable and supports autograd.
|
|
4623
4443
|
- Cone beam geometry uses a point source and a 2D detector array.
|
|
4624
|
-
- Uses
|
|
4444
|
+
- Uses cell-constant Siddon ray tracing.
|
|
4625
4445
|
|
|
4626
4446
|
Examples
|
|
4627
4447
|
--------
|
|
@@ -4814,9 +4634,9 @@ class ConeBackprojectorFunction(torch.autograd.Function):
|
|
|
4814
4634
|
Notes
|
|
4815
4635
|
-----
|
|
4816
4636
|
Provides a differentiable interface to the CUDA-accelerated Siddon ray-tracing
|
|
4817
|
-
method with
|
|
4637
|
+
method with a cell-constant voxel basis for 3D cone beam backprojection. The forward pass
|
|
4818
4638
|
runs the **pure adjoint** ``P^T`` of the cone forward projector: a Siddon
|
|
4819
|
-
ray-driven scatter with
|
|
4639
|
+
ray-driven scatter with no distance
|
|
4820
4640
|
weighting. The backward pass computes gradients via 3D cone beam forward
|
|
4821
4641
|
projection, which is exactly the adjoint of this pure ``P^T`` - so the
|
|
4822
4642
|
forward/backward pair is self-consistent for autograd. Analytical FDK
|
|
@@ -4910,7 +4730,7 @@ class ConeBackprojectorFunction(torch.autograd.Function):
|
|
|
4910
4730
|
- All input tensors must be on the same CUDA device.
|
|
4911
4731
|
- The operation is fully differentiable and supports autograd.
|
|
4912
4732
|
- Cone beam geometry uses a point source and a 2D detector array.
|
|
4913
|
-
- Uses the
|
|
4733
|
+
- Uses the adjoint of cell-constant Siddon ray tracing.
|
|
4914
4734
|
|
|
4915
4735
|
Examples
|
|
4916
4736
|
--------
|
|
@@ -71,11 +71,8 @@ class IterativeRecoModel(nn.Module):
|
|
|
71
71
|
self.dv = dv
|
|
72
72
|
self.sdd = sdd
|
|
73
73
|
self.sid = sid
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
self.relu = nn.ReLU()
|
|
77
|
-
self.voxel_spacing = voxel_spacing
|
|
78
|
-
self.backend = backend
|
|
74
|
+
self.voxel_spacing = voxel_spacing
|
|
75
|
+
self.backend = backend
|
|
79
76
|
|
|
80
77
|
def forward(self, x):
|
|
81
78
|
updated_reco = x + self.reco
|
|
@@ -94,7 +91,7 @@ class IterativeRecoModel(nn.Module):
|
|
|
94
91
|
0.0, 0.0, 0.0, # center_offset_x, y, z
|
|
95
92
|
self.backend,
|
|
96
93
|
)
|
|
97
|
-
return current_sino,
|
|
94
|
+
return current_sino, updated_reco
|
|
98
95
|
|
|
99
96
|
class Pipeline:
|
|
100
97
|
def __init__(self, lr, volume_shape, angles,
|
|
@@ -115,10 +112,12 @@ class Pipeline:
|
|
|
115
112
|
for epoch in range(self.epoches):
|
|
116
113
|
self.optimizer.zero_grad()
|
|
117
114
|
predictions, current_reco = self.model(input)
|
|
118
|
-
loss_value = self.loss(predictions, label)
|
|
119
|
-
loss_value.backward()
|
|
120
|
-
self.optimizer.step()
|
|
121
|
-
|
|
115
|
+
loss_value = self.loss(predictions, label)
|
|
116
|
+
loss_value.backward()
|
|
117
|
+
self.optimizer.step()
|
|
118
|
+
with torch.no_grad():
|
|
119
|
+
self.model.reco.clamp_(min=0.0)
|
|
120
|
+
loss_values.append(loss_value.item())
|
|
122
121
|
|
|
123
122
|
if epoch % 10 == 0:
|
|
124
123
|
print(f"Epoch {epoch}, Loss: {loss_value.item()}")
|
|
@@ -145,10 +144,9 @@ def main():
|
|
|
145
144
|
# byte-for-byte (matched scatter/gather kernel pair, verified by
|
|
146
145
|
# tests/test_adjoint_inner_product.py). Options:
|
|
147
146
|
#
|
|
148
|
-
# "siddon" - 3D ray-driven Siddon
|
|
149
|
-
#
|
|
150
|
-
#
|
|
151
|
-
# bottleneck.
|
|
147
|
+
# "siddon" - 3D ray-driven cell-constant Siddon. Fastest
|
|
148
|
+
# per-iteration step. Good default when
|
|
149
|
+
# iteration count is the bottleneck.
|
|
152
150
|
# "sf_tr" - 3D SF with trapezoidal transaxial and
|
|
153
151
|
# rectangular axial footprint. Mass-
|
|
154
152
|
# conserving per voxel, closed-form cell
|
|
@@ -47,9 +47,6 @@ class IterativeRecoModel(nn.Module):
|
|
|
47
47
|
self.detector_spacing = detector_spacing
|
|
48
48
|
self.sdd = sdd
|
|
49
49
|
self.sid = sid
|
|
50
|
-
# Output clamp for display; the projector uses the unconstrained
|
|
51
|
-
# image so gradients keep flowing through negative updates.
|
|
52
|
-
self.relu = nn.ReLU()
|
|
53
50
|
self.voxel_spacing = voxel_spacing
|
|
54
51
|
self.backend = backend
|
|
55
52
|
|
|
@@ -72,7 +69,7 @@ class IterativeRecoModel(nn.Module):
|
|
|
72
69
|
0.0, # center_offset_y
|
|
73
70
|
self.backend,
|
|
74
71
|
)
|
|
75
|
-
return current_sino,
|
|
72
|
+
return current_sino, updated_reco
|
|
76
73
|
|
|
77
74
|
class Pipeline:
|
|
78
75
|
def __init__(self, lr, volume_shape, angles,
|
|
@@ -96,6 +93,8 @@ class Pipeline:
|
|
|
96
93
|
loss_value = self.loss(predictions, label)
|
|
97
94
|
loss_value.backward()
|
|
98
95
|
self.optimizer.step()
|
|
96
|
+
with torch.no_grad():
|
|
97
|
+
self.model.reco.clamp_(min=0.0)
|
|
99
98
|
loss_values.append(loss_value.item())
|
|
100
99
|
|
|
101
100
|
if epoch % 10 == 0:
|
|
@@ -123,9 +122,9 @@ def main():
|
|
|
123
122
|
# byte-for-byte (guaranteed by the matched scatter/gather kernel
|
|
124
123
|
# pair, verified by tests/test_adjoint_inner_product.py). Options:
|
|
125
124
|
#
|
|
126
|
-
# "siddon" - ray-driven Siddon
|
|
127
|
-
#
|
|
128
|
-
#
|
|
125
|
+
# "siddon" - ray-driven cell-constant Siddon. Fastest.
|
|
126
|
+
# Good default when you want the shortest
|
|
127
|
+
# iteration step.
|
|
129
128
|
# "sf" - voxel-driven separable-footprint projector
|
|
130
129
|
# (Long et al. SF-TR). Mass-conserving per
|
|
131
130
|
# voxel, closed-form cell integral, ~3x
|
|
@@ -133,7 +132,7 @@ def main():
|
|
|
133
132
|
# want a physically-principled cell-
|
|
134
133
|
# integrated forward model and don't mind
|
|
135
134
|
# the per-iteration cost.
|
|
136
|
-
projector_backend = "
|
|
135
|
+
projector_backend = "siddon"
|
|
137
136
|
|
|
138
137
|
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
|
|
139
138
|
phantom_torch = torch.tensor(phantom_cpu, device=device, dtype=torch.float32)
|
|
@@ -185,4 +184,4 @@ def main():
|
|
|
185
184
|
plt.show()
|
|
186
185
|
|
|
187
186
|
if __name__ == "__main__":
|
|
188
|
-
main()
|
|
187
|
+
main()
|
|
@@ -43,15 +43,12 @@ class IterativeRecoModel(nn.Module):
|
|
|
43
43
|
self.num_detectors = num_detectors
|
|
44
44
|
self.detector_spacing = detector_spacing
|
|
45
45
|
self.voxel_spacing = voxel_spacing
|
|
46
|
-
# Output clamp for display; the projector uses the unconstrained
|
|
47
|
-
# image so gradients keep flowing through negative updates.
|
|
48
|
-
self.relu = nn.ReLU()
|
|
49
46
|
|
|
50
47
|
def forward(self, x):
|
|
51
48
|
updated_reco = x + self.reco
|
|
52
49
|
current_sino = ParallelProjectorFunction.apply(updated_reco, self.angles,
|
|
53
50
|
self.num_detectors, self.detector_spacing, self.voxel_spacing)
|
|
54
|
-
return current_sino,
|
|
51
|
+
return current_sino, updated_reco
|
|
55
52
|
|
|
56
53
|
class Pipeline:
|
|
57
54
|
def __init__(self, lr, volume_shape, angles, num_detectors, detector_spacing,
|
|
@@ -69,10 +66,12 @@ class Pipeline:
|
|
|
69
66
|
for epoch in range(self.epoches):
|
|
70
67
|
self.optimizer.zero_grad()
|
|
71
68
|
predictions, current_reco = self.model(input)
|
|
72
|
-
loss_value = self.loss(predictions, label)
|
|
73
|
-
loss_value.backward()
|
|
74
|
-
self.optimizer.step()
|
|
75
|
-
|
|
69
|
+
loss_value = self.loss(predictions, label)
|
|
70
|
+
loss_value.backward()
|
|
71
|
+
self.optimizer.step()
|
|
72
|
+
with torch.no_grad():
|
|
73
|
+
self.model.reco.clamp_(min=0.0)
|
|
74
|
+
loss_values.append(loss_value.item())
|
|
76
75
|
|
|
77
76
|
if epoch % 10 == 0:
|
|
78
77
|
print(f"Epoch {epoch}, Loss: {loss_value.item()}")
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "diffct"
|
|
7
|
-
version = "1.3.
|
|
7
|
+
version = "1.3.4"
|
|
8
8
|
description = "A CUDA-based library for computed tomography (CT) projection and reconstruction with differentiable operators"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
authors = [
|
|
@@ -186,10 +186,12 @@ def test_fdk_cone_shepp_logan_rmse_is_small():
|
|
|
186
186
|
|
|
187
187
|
# z-profile along the central column: catches gross amplitude drift
|
|
188
188
|
# along the axial direction (a common symptom of wrong FDK constants).
|
|
189
|
-
z_profile_err = torch.abs(
|
|
190
|
-
reco_raw[:, Ny // 2, Nx // 2] - phantom_torch[:, Ny // 2, Nx // 2]
|
|
191
|
-
)
|
|
192
|
-
|
|
189
|
+
z_profile_err = torch.abs(
|
|
190
|
+
reco_raw[:, Ny // 2, Nx // 2] - phantom_torch[:, Ny // 2, Nx // 2]
|
|
191
|
+
)
|
|
192
|
+
# Cell-constant forward projection gives a slightly sharper one-sample
|
|
193
|
+
# transition error at ellipsoid boundaries while preserving global FDK RMSE.
|
|
194
|
+
assert z_profile_err.max().item() < 0.65
|
|
193
195
|
|
|
194
196
|
# Amplitude sanity: phantom is clipped to [0, 1]. Real FDK output has
|
|
195
197
|
# a small negative ringing lobe near sharp edges, so we only assert the
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|