diffct 2.2.0__tar.gz → 2.2.1__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-2.2.0 → diffct-2.2.1}/CHANGELOG.md +24 -0
- {diffct-2.2.0 → diffct-2.2.1}/CITATION.cff +2 -2
- {diffct-2.2.0 → diffct-2.2.1}/PKG-INFO +3 -3
- {diffct-2.2.0 → diffct-2.2.1}/README.md +2 -2
- {diffct-2.2.0 → diffct-2.2.1}/diffct/__init__.py +13 -1
- {diffct-2.2.0 → diffct-2.2.1}/diffct/analytical.py +151 -6
- {diffct-2.2.0 → diffct-2.2.1}/diffct/kernels/__init__.py +8 -0
- diffct-2.2.1/diffct/kernels/analytical_transpose.py +295 -0
- diffct-2.2.1/diffct/reconstruction.py +399 -0
- {diffct-2.2.0 → diffct-2.2.1}/examples/README.md +22 -0
- {diffct-2.2.0 → diffct-2.2.1}/pyproject.toml +1 -1
- diffct-2.2.1/tests/test_analytical_autograd.py +313 -0
- {diffct-2.2.0 → diffct-2.2.1}/tests/test_block_storage.py +7 -1
- diffct-2.2.1/tests/test_reconstruction_api.py +872 -0
- {diffct-2.2.0 → diffct-2.2.1}/.gitignore +0 -0
- {diffct-2.2.0 → diffct-2.2.1}/LICENSE +0 -0
- {diffct-2.2.0 → diffct-2.2.1}/NOTICE +0 -0
- {diffct-2.2.0 → diffct-2.2.1}/diffct/chunking.py +0 -0
- {diffct-2.2.0 → diffct-2.2.1}/diffct/constants.py +0 -0
- {diffct-2.2.0 → diffct-2.2.1}/diffct/differentiable.py +0 -0
- {diffct-2.2.0 → diffct-2.2.1}/diffct/geometry.py +0 -0
- {diffct-2.2.0 → diffct-2.2.1}/diffct/kernels/cone_beam.py +0 -0
- {diffct-2.2.0 → diffct-2.2.1}/diffct/kernels/fan_beam.py +0 -0
- {diffct-2.2.0 → diffct-2.2.1}/diffct/kernels/geometry_vjp.py +0 -0
- {diffct-2.2.0 → diffct-2.2.1}/diffct/kernels/parallel_beam.py +0 -0
- {diffct-2.2.0 → diffct-2.2.1}/diffct/operators.py +0 -0
- {diffct-2.2.0 → diffct-2.2.1}/diffct/pipeline.py +0 -0
- {diffct-2.2.0 → diffct-2.2.1}/diffct/projectors.py +0 -0
- {diffct-2.2.0 → diffct-2.2.1}/diffct/storage.py +0 -0
- {diffct-2.2.0 → diffct-2.2.1}/diffct/streaming.py +0 -0
- {diffct-2.2.0 → diffct-2.2.1}/diffct/surfaces.py +0 -0
- {diffct-2.2.0 → diffct-2.2.1}/diffct/utils.py +0 -0
- {diffct-2.2.0 → diffct-2.2.1}/docs/video/README.md +0 -0
- {diffct-2.2.0 → diffct-2.2.1}/examples/data/NOTICE +0 -0
- {diffct-2.2.0 → diffct-2.2.1}/formal/README.md +0 -0
- {diffct-2.2.0 → diffct-2.2.1}/pytest.ini +0 -0
- {diffct-2.2.0 → diffct-2.2.1}/tests/__init__.py +0 -0
- {diffct-2.2.0 → diffct-2.2.1}/tests/_phantoms.py +0 -0
- {diffct-2.2.0 → diffct-2.2.1}/tests/benchmarks/README.md +0 -0
- {diffct-2.2.0 → diffct-2.2.1}/tests/benchmarks/__init__.py +0 -0
- {diffct-2.2.0 → diffct-2.2.1}/tests/benchmarks/_common.py +0 -0
- {diffct-2.2.0 → diffct-2.2.1}/tests/benchmarks/test_bench_cone.py +0 -0
- {diffct-2.2.0 → diffct-2.2.1}/tests/benchmarks/test_bench_fan.py +0 -0
- {diffct-2.2.0 → diffct-2.2.1}/tests/benchmarks/test_bench_parallel.py +0 -0
- {diffct-2.2.0 → diffct-2.2.1}/tests/distributed_projector_check.py +0 -0
- {diffct-2.2.0 → diffct-2.2.1}/tests/test_adjoint_inner_product.py +0 -0
- {diffct-2.2.0 → diffct-2.2.1}/tests/test_allocator_capacity.py +0 -0
- {diffct-2.2.0 → diffct-2.2.1}/tests/test_analytical_regressions.py +0 -0
- {diffct-2.2.0 → diffct-2.2.1}/tests/test_axis_aligned_chords.py +0 -0
- {diffct-2.2.0 → diffct-2.2.1}/tests/test_batched_surfaces.py +0 -0
- {diffct-2.2.0 → diffct-2.2.1}/tests/test_block_cgls.py +0 -0
- {diffct-2.2.0 → diffct-2.2.1}/tests/test_cgls_state_admission.py +0 -0
- {diffct-2.2.0 → diffct-2.2.1}/tests/test_chunked_projector.py +0 -0
- {diffct-2.2.0 → diffct-2.2.1}/tests/test_completed_transfer_reuse.py +0 -0
- {diffct-2.2.0 → diffct-2.2.1}/tests/test_cone_projector_autograd.py +0 -0
- {diffct-2.2.0 → diffct-2.2.1}/tests/test_cuda_exit_deallocations.py +0 -0
- {diffct-2.2.0 → diffct-2.2.1}/tests/test_cuda_sink_lifetimes.py +0 -0
- {diffct-2.2.0 → diffct-2.2.1}/tests/test_cuda_smoke.py +0 -0
- {diffct-2.2.0 → diffct-2.2.1}/tests/test_detector_surface_dtypes.py +0 -0
- {diffct-2.2.0 → diffct-2.2.1}/tests/test_detector_surface_mapping.py +0 -0
- {diffct-2.2.0 → diffct-2.2.1}/tests/test_detector_surface_units.py +0 -0
- {diffct-2.2.0 → diffct-2.2.1}/tests/test_detector_surfaces.py +0 -0
- {diffct-2.2.0 → diffct-2.2.1}/tests/test_example_psnr.py +0 -0
- {diffct-2.2.0 → diffct-2.2.1}/tests/test_execution_buffers.py +0 -0
- {diffct-2.2.0 → diffct-2.2.1}/tests/test_execution_schedules.py +0 -0
- {diffct-2.2.0 → diffct-2.2.1}/tests/test_fbp_fan_accuracy.py +0 -0
- {diffct-2.2.0 → diffct-2.2.1}/tests/test_fbp_fan_offsets.py +0 -0
- {diffct-2.2.0 → diffct-2.2.1}/tests/test_fbp_parallel_accuracy.py +0 -0
- {diffct-2.2.0 → diffct-2.2.1}/tests/test_fdk_cone_accuracy.py +0 -0
- {diffct-2.2.0 → diffct-2.2.1}/tests/test_fdk_cone_offsets.py +0 -0
- {diffct-2.2.0 → diffct-2.2.1}/tests/test_geometry_autograd.py +0 -0
- {diffct-2.2.0 → diffct-2.2.1}/tests/test_geometry_gradients.py +0 -0
- {diffct-2.2.0 → diffct-2.2.1}/tests/test_gradcheck.py +0 -0
- {diffct-2.2.0 → diffct-2.2.1}/tests/test_multi_gpu_exit.py +0 -0
- {diffct-2.2.0 → diffct-2.2.1}/tests/test_output_pipeline_overlap.py +0 -0
- {diffct-2.2.0 → diffct-2.2.1}/tests/test_projector_api.py +0 -0
- {diffct-2.2.0 → diffct-2.2.1}/tests/test_projector_degenerate_geometry.py +0 -0
- {diffct-2.2.0 → diffct-2.2.1}/tests/test_projector_geometry_devices.py +0 -0
- {diffct-2.2.0 → diffct-2.2.1}/tests/test_ramp_filter_windows.py +0 -0
- {diffct-2.2.0 → diffct-2.2.1}/tests/test_release_2_0_1.py +0 -0
- {diffct-2.2.0 → diffct-2.2.1}/tests/test_spatial_partition.py +0 -0
- {diffct-2.2.0 → diffct-2.2.1}/tests/test_transfer_pipeline.py +0 -0
- {diffct-2.2.0 → diffct-2.2.1}/tests/test_transfer_retirement_lifetimes.py +0 -0
- {diffct-2.2.0 → diffct-2.2.1}/tests/test_weights.py +0 -0
|
@@ -7,6 +7,30 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [2.2.1] - 2026-10-13
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- Public differentiable `fbp`, `fdk` and approximate slice-windowed `helical_fdk`
|
|
15
|
+
pipelines, with replaceable projection, angular and frequency-response tensors.
|
|
16
|
+
Low-precision measurements run in float32 and keep gradients in their own dtype.
|
|
17
|
+
- Dedicated CUDA transposes of the analytical parallel/fan/cone gathers,
|
|
18
|
+
including sinogram gradients and higher derivatives with fixed geometry.
|
|
19
|
+
- Circular short-scan checks for `short_scan=True`: orbit, detector-u handedness,
|
|
20
|
+
angular span, and constant detector distance and offset. Tolerances follow the
|
|
21
|
+
physical geometry scale and the original trajectory precision.
|
|
22
|
+
- Public helical view weights and weighted gather. Slice centers and angular
|
|
23
|
+
quadrature use float64, so large angle or height offsets keep full-turn coverage.
|
|
24
|
+
- Examples `learnable_reconstruction.py` and `helical_fdk.py`, and the
|
|
25
|
+
analytical learning guide.
|
|
26
|
+
|
|
27
|
+
### Changed
|
|
28
|
+
|
|
29
|
+
- `parallel_weighted_backproject`, `fan_weighted_backproject` and
|
|
30
|
+
`cone_weighted_backproject` support sinogram autograd. Geometry and isocenter
|
|
31
|
+
gradients raise an error.
|
|
32
|
+
- The FDK baselines in `examples/_common.py` and `walnut_reconstruction.py` call `fdk`.
|
|
33
|
+
|
|
10
34
|
## [2.2.0] - 2026-10-11
|
|
11
35
|
|
|
12
36
|
### Added
|
|
@@ -13,8 +13,8 @@ authors:
|
|
|
13
13
|
given-names: Yipeng
|
|
14
14
|
email: yipeng.sun@fau.de
|
|
15
15
|
alias: sypsyp97
|
|
16
|
-
version: 2.2.
|
|
17
|
-
date-released: "2026-10-
|
|
16
|
+
version: 2.2.1
|
|
17
|
+
date-released: "2026-10-13"
|
|
18
18
|
license: Apache-2.0
|
|
19
19
|
repository-code: "https://github.com/sypsyp97/diffct"
|
|
20
20
|
url: "https://sypsyp97.github.io/diffct/"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: diffct
|
|
3
|
-
Version: 2.2.
|
|
3
|
+
Version: 2.2.1
|
|
4
4
|
Summary: Differentiable CUDA projectors for CT: arbitrary trajectories, multi-GPU and multi-node execution, geometry gradients
|
|
5
5
|
Project-URL: Homepage, https://github.com/sypsyp97/diffct
|
|
6
6
|
Project-URL: Documentation, https://sypsyp97.github.io/diffct/
|
|
@@ -77,7 +77,7 @@ To our knowledge, diffct is the only open-source GPU CT library that combines ar
|
|
|
77
77
|
- **Matched operators.** `project()` and `backproject()` form an exact adjoint pair for the cell-constant Siddon model. Both support PyTorch autograd, with volume/sinogram gradients and Hessian-vector products.
|
|
78
78
|
- **Large volumes.** Automatic spatial, view and pixel blocks with bounded transfer buffers control working memory. `project_into`/`backproject_into` accept caller-owned tensor or disk stores; tensor autograd keeps complete data on its input device. See [chunked execution](https://github.com/sypsyp97/diffct/blob/main/docs/REFERENCE.md#chunked-execution).
|
|
79
79
|
- **Multi-GPU and multi-node.** Use `devices=[0, 1, 2, 3]` in one process, or torchrun and NCCL across ranks. Default `partition="views"` shards projections and replicates volumes. `partition="space"` gives each rank an owned volume slab and combines ray contributions. Speedup depends on workload and communication costs.
|
|
80
|
-
- **
|
|
80
|
+
- **Differentiable FBP and FDK.** `fbp`, `fdk` and approximate `helical_fdk` compose the public filters, weights and analytical backprojectors. Their backward kernels transpose the actual interpolation stencil; custom filters and weights can be learned. See the [analytical guide](docs/source/analytical_learning.rst).
|
|
81
81
|
|
|
82
82
|
Capabilities, limits and isocenter rules: [docs/REFERENCE.md](https://github.com/sypsyp97/diffct/blob/main/docs/REFERENCE.md#capabilities-and-limits).
|
|
83
83
|
|
|
@@ -220,7 +220,7 @@ Walnut data: Meaney 2022, Zenodo 6986012, CC BY 4.0; see [examples/data/NOTICE](
|
|
|
220
220
|
|
|
221
221
|
## Examples
|
|
222
222
|
|
|
223
|
-
`quickstart.py`, `curved_detector.py`, `chunked_reconstruction.py`, `analytical_reconstruction.py`, `iterative_reconstruction.py`, `walnut_reconstruction.py`, `geometry_calibration.py`, `benchmark_projector.py`, `plot_trajectory.py`, and the Slurm template `slurm/multi_node.sbatch`. Launch modes and distributed-loss rules are in [examples/README.md](https://github.com/sypsyp97/diffct/blob/main/examples/README.md).
|
|
223
|
+
`quickstart.py`, `curved_detector.py`, `chunked_reconstruction.py`, `disk_reconstruction.py`, `benchmark_execution.py`, `analytical_reconstruction.py`, `learnable_reconstruction.py`, `helical_fdk.py`, `iterative_reconstruction.py`, `walnut_reconstruction.py`, `geometry_calibration.py`, `benchmark_projector.py`, `benchmark_scaling.py`, `plot_trajectory.py`, and the Slurm template `slurm/multi_node.sbatch`. Launch modes and distributed-loss rules are in [examples/README.md](https://github.com/sypsyp97/diffct/blob/main/examples/README.md).
|
|
224
224
|
|
|
225
225
|
## Documentation
|
|
226
226
|
|
|
@@ -46,7 +46,7 @@ To our knowledge, diffct is the only open-source GPU CT library that combines ar
|
|
|
46
46
|
- **Matched operators.** `project()` and `backproject()` form an exact adjoint pair for the cell-constant Siddon model. Both support PyTorch autograd, with volume/sinogram gradients and Hessian-vector products.
|
|
47
47
|
- **Large volumes.** Automatic spatial, view and pixel blocks with bounded transfer buffers control working memory. `project_into`/`backproject_into` accept caller-owned tensor or disk stores; tensor autograd keeps complete data on its input device. See [chunked execution](https://github.com/sypsyp97/diffct/blob/main/docs/REFERENCE.md#chunked-execution).
|
|
48
48
|
- **Multi-GPU and multi-node.** Use `devices=[0, 1, 2, 3]` in one process, or torchrun and NCCL across ranks. Default `partition="views"` shards projections and replicates volumes. `partition="space"` gives each rank an owned volume slab and combines ray contributions. Speedup depends on workload and communication costs.
|
|
49
|
-
- **
|
|
49
|
+
- **Differentiable FBP and FDK.** `fbp`, `fdk` and approximate `helical_fdk` compose the public filters, weights and analytical backprojectors. Their backward kernels transpose the actual interpolation stencil; custom filters and weights can be learned. See the [analytical guide](docs/source/analytical_learning.rst).
|
|
50
50
|
|
|
51
51
|
Capabilities, limits and isocenter rules: [docs/REFERENCE.md](https://github.com/sypsyp97/diffct/blob/main/docs/REFERENCE.md#capabilities-and-limits).
|
|
52
52
|
|
|
@@ -189,7 +189,7 @@ Walnut data: Meaney 2022, Zenodo 6986012, CC BY 4.0; see [examples/data/NOTICE](
|
|
|
189
189
|
|
|
190
190
|
## Examples
|
|
191
191
|
|
|
192
|
-
`quickstart.py`, `curved_detector.py`, `chunked_reconstruction.py`, `analytical_reconstruction.py`, `iterative_reconstruction.py`, `walnut_reconstruction.py`, `geometry_calibration.py`, `benchmark_projector.py`, `plot_trajectory.py`, and the Slurm template `slurm/multi_node.sbatch`. Launch modes and distributed-loss rules are in [examples/README.md](https://github.com/sypsyp97/diffct/blob/main/examples/README.md).
|
|
192
|
+
`quickstart.py`, `curved_detector.py`, `chunked_reconstruction.py`, `disk_reconstruction.py`, `benchmark_execution.py`, `analytical_reconstruction.py`, `learnable_reconstruction.py`, `helical_fdk.py`, `iterative_reconstruction.py`, `walnut_reconstruction.py`, `geometry_calibration.py`, `benchmark_projector.py`, `benchmark_scaling.py`, `plot_trajectory.py`, and the Slurm template `slurm/multi_node.sbatch`. Launch modes and distributed-loss rules are in [examples/README.md](https://github.com/sypsyp97/diffct/blob/main/examples/README.md).
|
|
193
193
|
|
|
194
194
|
## Documentation
|
|
195
195
|
|
|
@@ -17,6 +17,13 @@ from .projectors import (
|
|
|
17
17
|
from .operators import Projector
|
|
18
18
|
from .storage import TensorStore, NpyStore
|
|
19
19
|
from .surfaces import ParameterizedSurface
|
|
20
|
+
from .reconstruction import (
|
|
21
|
+
fbp,
|
|
22
|
+
fdk,
|
|
23
|
+
helical_fdk,
|
|
24
|
+
helical_view_weights,
|
|
25
|
+
helical_weighted_backproject,
|
|
26
|
+
)
|
|
20
27
|
|
|
21
28
|
from .geometry import (
|
|
22
29
|
circular_trajectory_3d,
|
|
@@ -51,13 +58,18 @@ try:
|
|
|
51
58
|
except ImportError:
|
|
52
59
|
pass
|
|
53
60
|
|
|
54
|
-
__version__ = '2.2.
|
|
61
|
+
__version__ = '2.2.1'
|
|
55
62
|
|
|
56
63
|
__all__ = [
|
|
57
64
|
'Projector',
|
|
58
65
|
'TensorStore',
|
|
59
66
|
'NpyStore',
|
|
60
67
|
'ParameterizedSurface',
|
|
68
|
+
'fbp',
|
|
69
|
+
'fdk',
|
|
70
|
+
'helical_fdk',
|
|
71
|
+
'helical_view_weights',
|
|
72
|
+
'helical_weighted_backproject',
|
|
61
73
|
'ParallelProjectorFunction',
|
|
62
74
|
'ParallelBackprojectorFunction',
|
|
63
75
|
'FanProjectorFunction',
|
|
@@ -40,6 +40,9 @@ from .kernels import (
|
|
|
40
40
|
_parallel_2d_fbp_backproject_kernel,
|
|
41
41
|
_fan_2d_fbp_backproject_kernel,
|
|
42
42
|
_cone_3d_fdk_backproject_kernel,
|
|
43
|
+
_parallel_2d_fbp_transpose_kernel,
|
|
44
|
+
_fan_2d_fbp_transpose_kernel,
|
|
45
|
+
_cone_3d_fdk_transpose_kernel,
|
|
43
46
|
)
|
|
44
47
|
|
|
45
48
|
|
|
@@ -367,8 +370,8 @@ def _as_contig_f32(t, device):
|
|
|
367
370
|
|
|
368
371
|
|
|
369
372
|
@_on_device_of("sinogram")
|
|
370
|
-
def
|
|
371
|
-
|
|
373
|
+
def _parallel_weighted_backproject_raw(sinogram, ray_dir, det_origin, det_u_vec,
|
|
374
|
+
detector_spacing, H, W, voxel_spacing=1.0):
|
|
372
375
|
"""Voxel-driven parallel-beam FBP backprojection with analytical constant.
|
|
373
376
|
|
|
374
377
|
Runs the dedicated parallel-beam FBP gather kernel (one thread per
|
|
@@ -451,8 +454,8 @@ def _analytical_geometry(src_pos, det_center, n, isocenter):
|
|
|
451
454
|
|
|
452
455
|
|
|
453
456
|
@_on_device_of("sinogram")
|
|
454
|
-
def
|
|
455
|
-
|
|
457
|
+
def _fan_weighted_backproject_raw(sinogram, src_pos, det_center, det_u_vec,
|
|
458
|
+
detector_spacing, H, W, voxel_spacing=1.0, *, isocenter=None):
|
|
456
459
|
"""Voxel-driven fan-beam FBP backprojection with analytical constant.
|
|
457
460
|
|
|
458
461
|
Runs the dedicated fan-beam FBP gather kernel (one thread per output
|
|
@@ -507,8 +510,8 @@ def fan_weighted_backproject(sinogram, src_pos, det_center, det_u_vec,
|
|
|
507
510
|
|
|
508
511
|
|
|
509
512
|
@_on_device_of("sinogram")
|
|
510
|
-
def
|
|
511
|
-
|
|
513
|
+
def _cone_weighted_backproject_raw(sinogram, src_pos, det_center, det_u_vec, det_v_vec,
|
|
514
|
+
D, H, W, du, dv, voxel_spacing=1.0, *, isocenter=None):
|
|
512
515
|
"""Voxel-driven cone-beam FDK backprojection with analytical constant.
|
|
513
516
|
|
|
514
517
|
Runs the dedicated cone-beam FDK gather kernel (one thread per output
|
|
@@ -571,3 +574,145 @@ def cone_weighted_backproject(sinogram, src_pos, det_center, det_u_vec, det_v_ve
|
|
|
571
574
|
# Convert back to (D, H, W) layout for the user.
|
|
572
575
|
reco = reco_perm.permute(2, 1, 0).contiguous() * scale
|
|
573
576
|
return reco
|
|
577
|
+
|
|
578
|
+
|
|
579
|
+
def _validate_analytical_inputs(sinogram, geometry, ndim, isocenter=None):
|
|
580
|
+
if not isinstance(sinogram, torch.Tensor) or sinogram.ndim != ndim:
|
|
581
|
+
raise ValueError(f"sinogram must have rank {ndim}")
|
|
582
|
+
if min(sinogram.shape[1:]) < 2:
|
|
583
|
+
raise ValueError("Analytical backprojection requires at least two detector bins on each axis")
|
|
584
|
+
for component in geometry:
|
|
585
|
+
if not isinstance(component, torch.Tensor) or component.shape != (sinogram.shape[0], ndim):
|
|
586
|
+
raise ValueError(f"Every geometry tensor must have shape (n_views, {ndim})")
|
|
587
|
+
if component.requires_grad:
|
|
588
|
+
raise ValueError("Analytical backprojection does not support geometry gradients")
|
|
589
|
+
if isinstance(isocenter, torch.Tensor) and isocenter.requires_grad:
|
|
590
|
+
raise ValueError("Analytical backprojection does not support isocenter gradients")
|
|
591
|
+
if not sinogram.is_cuda:
|
|
592
|
+
raise ValueError("sinogram must be on a CUDA device")
|
|
593
|
+
|
|
594
|
+
|
|
595
|
+
def _analytical_gather_raw(sinogram, beam, geometry, parameters, isocenter):
|
|
596
|
+
if beam == "parallel":
|
|
597
|
+
return _parallel_weighted_backproject_raw(sinogram, *geometry, *parameters)
|
|
598
|
+
if beam == "fan":
|
|
599
|
+
return _fan_weighted_backproject_raw(sinogram, *geometry, *parameters, isocenter=isocenter)
|
|
600
|
+
return _cone_weighted_backproject_raw(sinogram, *geometry, *parameters, isocenter=isocenter)
|
|
601
|
+
|
|
602
|
+
|
|
603
|
+
@_on_device_of("image")
|
|
604
|
+
def _analytical_transpose_raw(image, beam, geometry, parameters, isocenter, sinogram_shape):
|
|
605
|
+
device = image.device
|
|
606
|
+
geometry = tuple(_as_contig_f32(component, device) for component in geometry)
|
|
607
|
+
image = _as_contig_f32(image, device)
|
|
608
|
+
sinogram = torch.zeros(sinogram_shape, device=device, dtype=torch.float32)
|
|
609
|
+
d_sino = TorchCUDABridge.tensor_to_cuda_array(sinogram)
|
|
610
|
+
d_geometry = tuple(TorchCUDABridge.tensor_to_cuda_array(component) for component in geometry)
|
|
611
|
+
stream = _get_numba_external_stream_for(torch.cuda.current_stream())
|
|
612
|
+
|
|
613
|
+
scale = 1.0 / (2.0 * math.pi)
|
|
614
|
+
iso = ()
|
|
615
|
+
if beam != "parallel":
|
|
616
|
+
source, detector, u = geometry[:3]
|
|
617
|
+
normal = (torch.stack((-u[:, 1], u[:, 0]), dim=1) if beam == "fan"
|
|
618
|
+
else torch.cross(u, geometry[3], dim=1))
|
|
619
|
+
isocenter, sid_mean, sdd_mean = _analytical_geometry(source, detector, normal, isocenter)
|
|
620
|
+
scale = sdd_mean / (2.0 * math.pi * sid_mean)
|
|
621
|
+
iso = tuple(_DTYPE(value.item() / parameters[-1]) for value in isocenter)
|
|
622
|
+
image = image * scale
|
|
623
|
+
|
|
624
|
+
if beam == "cone":
|
|
625
|
+
D, H, W, du, dv, spacing = parameters
|
|
626
|
+
image = image.permute(2, 1, 0).contiguous()
|
|
627
|
+
d_image = TorchCUDABridge.tensor_to_cuda_array(image)
|
|
628
|
+
grid, tpb = _grid_3d(D, H, W)
|
|
629
|
+
_cone_3d_fdk_transpose_kernel[grid, tpb, stream](
|
|
630
|
+
d_sino, *sinogram_shape, d_image, W, H, D, _DTYPE(du), _DTYPE(dv),
|
|
631
|
+
*d_geometry, _DTYPE(W * 0.5), _DTYPE(H * 0.5), _DTYPE(D * 0.5),
|
|
632
|
+
_DTYPE(spacing), *iso,
|
|
633
|
+
)
|
|
634
|
+
else:
|
|
635
|
+
pitch, H, W, spacing = parameters
|
|
636
|
+
d_image = TorchCUDABridge.tensor_to_cuda_array(image)
|
|
637
|
+
grid, tpb = _grid_2d(W, H)
|
|
638
|
+
kernel = (_parallel_2d_fbp_transpose_kernel if beam == "parallel"
|
|
639
|
+
else _fan_2d_fbp_transpose_kernel)
|
|
640
|
+
kernel[grid, tpb, stream](
|
|
641
|
+
d_sino, *sinogram_shape, d_image, W, H, _DTYPE(pitch), *d_geometry,
|
|
642
|
+
_DTYPE(W * 0.5), _DTYPE(H * 0.5), _DTYPE(spacing), *iso,
|
|
643
|
+
)
|
|
644
|
+
return sinogram
|
|
645
|
+
|
|
646
|
+
|
|
647
|
+
class _AnalyticalGather(torch.autograd.Function):
|
|
648
|
+
@staticmethod
|
|
649
|
+
def forward(ctx, sinogram, beam, geometry, parameters, isocenter):
|
|
650
|
+
# Backward uses the exact geometry observed by forward, even if the
|
|
651
|
+
# caller edits its fixed trajectory or isocenter afterwards.
|
|
652
|
+
geometry = tuple(component.detach().clone() for component in geometry)
|
|
653
|
+
if isocenter is not None:
|
|
654
|
+
isocenter = torch.as_tensor(isocenter, device=sinogram.device,
|
|
655
|
+
dtype=torch.float32).detach().clone()
|
|
656
|
+
ctx.save_for_backward(*geometry, isocenter)
|
|
657
|
+
ctx.beam, ctx.parameters = beam, parameters
|
|
658
|
+
ctx.input_shape, ctx.input_dtype, ctx.input_device = sinogram.shape, sinogram.dtype, sinogram.device
|
|
659
|
+
return _analytical_gather_raw(sinogram, beam, geometry, parameters, isocenter)
|
|
660
|
+
|
|
661
|
+
@staticmethod
|
|
662
|
+
def backward(ctx, grad_output):
|
|
663
|
+
*geometry, isocenter = ctx.saved_tensors
|
|
664
|
+
grad_input = _AnalyticalTranspose.apply(
|
|
665
|
+
grad_output, ctx.beam, tuple(geometry), ctx.parameters, isocenter, ctx.input_shape,
|
|
666
|
+
).to(device=ctx.input_device, dtype=ctx.input_dtype)
|
|
667
|
+
return grad_input, None, None, None, None
|
|
668
|
+
|
|
669
|
+
|
|
670
|
+
class _AnalyticalTranspose(torch.autograd.Function):
|
|
671
|
+
@staticmethod
|
|
672
|
+
def forward(ctx, image, beam, geometry, parameters, isocenter, sinogram_shape):
|
|
673
|
+
ctx.beam, ctx.geometry, ctx.parameters, ctx.isocenter = beam, geometry, parameters, isocenter
|
|
674
|
+
ctx.input_dtype, ctx.input_device = image.dtype, image.device
|
|
675
|
+
return _analytical_transpose_raw(image, beam, geometry, parameters, isocenter, sinogram_shape)
|
|
676
|
+
|
|
677
|
+
@staticmethod
|
|
678
|
+
def backward(ctx, grad_output):
|
|
679
|
+
grad_input = _AnalyticalGather.apply(
|
|
680
|
+
grad_output, ctx.beam, ctx.geometry, ctx.parameters, ctx.isocenter,
|
|
681
|
+
).to(device=ctx.input_device, dtype=ctx.input_dtype)
|
|
682
|
+
return grad_input, None, None, None, None, None
|
|
683
|
+
|
|
684
|
+
|
|
685
|
+
def _analytical_backproject(sinogram, beam, geometry, parameters, isocenter=None):
|
|
686
|
+
_validate_analytical_inputs(sinogram, geometry, 3 if beam == "cone" else 2, isocenter)
|
|
687
|
+
if torch.is_grad_enabled() and sinogram.requires_grad:
|
|
688
|
+
return _AnalyticalGather.apply(sinogram, beam, geometry, parameters, isocenter)
|
|
689
|
+
return _analytical_gather_raw(sinogram, beam, geometry, parameters, isocenter)
|
|
690
|
+
|
|
691
|
+
|
|
692
|
+
def parallel_weighted_backproject(sinogram, ray_dir, det_origin, det_u_vec,
|
|
693
|
+
detector_spacing, H, W, voxel_spacing=1.0):
|
|
694
|
+
return _analytical_backproject(
|
|
695
|
+
sinogram, "parallel", (ray_dir, det_origin, det_u_vec),
|
|
696
|
+
(detector_spacing, H, W, voxel_spacing),
|
|
697
|
+
)
|
|
698
|
+
|
|
699
|
+
|
|
700
|
+
def fan_weighted_backproject(sinogram, src_pos, det_center, det_u_vec,
|
|
701
|
+
detector_spacing, H, W, voxel_spacing=1.0, *, isocenter=None):
|
|
702
|
+
return _analytical_backproject(
|
|
703
|
+
sinogram, "fan", (src_pos, det_center, det_u_vec),
|
|
704
|
+
(detector_spacing, H, W, voxel_spacing), isocenter,
|
|
705
|
+
)
|
|
706
|
+
|
|
707
|
+
|
|
708
|
+
def cone_weighted_backproject(sinogram, src_pos, det_center, det_u_vec, det_v_vec,
|
|
709
|
+
D, H, W, du, dv, voxel_spacing=1.0, *, isocenter=None):
|
|
710
|
+
return _analytical_backproject(
|
|
711
|
+
sinogram, "cone", (src_pos, det_center, det_u_vec, det_v_vec),
|
|
712
|
+
(D, H, W, du, dv, voxel_spacing), isocenter,
|
|
713
|
+
)
|
|
714
|
+
|
|
715
|
+
|
|
716
|
+
parallel_weighted_backproject.__doc__ = _parallel_weighted_backproject_raw.__doc__
|
|
717
|
+
fan_weighted_backproject.__doc__ = _fan_weighted_backproject_raw.__doc__
|
|
718
|
+
cone_weighted_backproject.__doc__ = _cone_weighted_backproject_raw.__doc__
|
|
@@ -26,6 +26,11 @@ from .geometry_vjp import (
|
|
|
26
26
|
_fan_2d_geometry_vjp_kernel,
|
|
27
27
|
_cone_3d_geometry_vjp_kernel,
|
|
28
28
|
)
|
|
29
|
+
from .analytical_transpose import (
|
|
30
|
+
_parallel_2d_fbp_transpose_kernel,
|
|
31
|
+
_fan_2d_fbp_transpose_kernel,
|
|
32
|
+
_cone_3d_fdk_transpose_kernel,
|
|
33
|
+
)
|
|
29
34
|
|
|
30
35
|
__all__ = [
|
|
31
36
|
'_parallel_2d_forward_kernel',
|
|
@@ -40,4 +45,7 @@ __all__ = [
|
|
|
40
45
|
'_parallel_2d_geometry_vjp_kernel',
|
|
41
46
|
'_fan_2d_geometry_vjp_kernel',
|
|
42
47
|
'_cone_3d_geometry_vjp_kernel',
|
|
48
|
+
'_parallel_2d_fbp_transpose_kernel',
|
|
49
|
+
'_fan_2d_fbp_transpose_kernel',
|
|
50
|
+
'_cone_3d_fdk_transpose_kernel',
|
|
43
51
|
]
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
"""Discrete transposes of the analytical FBP/FDK gather kernels.
|
|
2
|
+
|
|
3
|
+
Geometry arithmetic and interpolation bounds mirror the gather kernels;
|
|
4
|
+
only the final sample operation changes to an atomic detector-bin scatter.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import math
|
|
8
|
+
|
|
9
|
+
import numpy as np
|
|
10
|
+
from numba import cuda
|
|
11
|
+
|
|
12
|
+
from ..constants import _EPSILON, _FDK_ACCURACY_DECORATOR, _HALF, _ONE, _ZERO
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@_FDK_ACCURACY_DECORATOR
|
|
16
|
+
def _parallel_2d_fbp_transpose_kernel(
|
|
17
|
+
d_sino, n_views, n_det,
|
|
18
|
+
d_image, Nx, Ny,
|
|
19
|
+
det_spacing, d_ray_dir, d_det_origin, d_det_u_vec,
|
|
20
|
+
cx, cy, voxel_spacing
|
|
21
|
+
):
|
|
22
|
+
"""Scatter image cotangents through the parallel analytical gather stencil."""
|
|
23
|
+
ix, iy = cuda.grid(2)
|
|
24
|
+
if ix >= Nx or iy >= Ny:
|
|
25
|
+
return
|
|
26
|
+
if n_det < 2:
|
|
27
|
+
return
|
|
28
|
+
|
|
29
|
+
# Sample at voxel centres, matching the cell-constant Siddon projector.
|
|
30
|
+
x_v = np.float32(ix) + _HALF - cx
|
|
31
|
+
y_v = np.float32(iy) + _HALF - cy
|
|
32
|
+
|
|
33
|
+
det_spacing_v = det_spacing / voxel_spacing
|
|
34
|
+
half_u = (np.float32(n_det) - _ONE) * _HALF
|
|
35
|
+
|
|
36
|
+
for iview in range(n_views):
|
|
37
|
+
dox = d_det_origin[iview, 0] / voxel_spacing
|
|
38
|
+
doy = d_det_origin[iview, 1] / voxel_spacing
|
|
39
|
+
|
|
40
|
+
ux = d_det_u_vec[iview, 0]
|
|
41
|
+
uy = d_det_u_vec[iview, 1]
|
|
42
|
+
|
|
43
|
+
rx = x_v - dox
|
|
44
|
+
ry = y_v - doy
|
|
45
|
+
|
|
46
|
+
u_det = rx * ux + ry * uy
|
|
47
|
+
|
|
48
|
+
fu = u_det / det_spacing_v + half_u
|
|
49
|
+
if fu < _ZERO or fu > (np.float32(n_det) - _ONE):
|
|
50
|
+
continue
|
|
51
|
+
|
|
52
|
+
iu0 = int(math.floor(fu))
|
|
53
|
+
if iu0 >= n_det - 1:
|
|
54
|
+
iu0 = n_det - 2
|
|
55
|
+
if iu0 < 0:
|
|
56
|
+
iu0 = 0
|
|
57
|
+
tu = fu - np.float32(iu0)
|
|
58
|
+
if tu < _ZERO:
|
|
59
|
+
tu = _ZERO
|
|
60
|
+
elif tu > _ONE:
|
|
61
|
+
tu = _ONE
|
|
62
|
+
|
|
63
|
+
value = d_image[iy, ix]
|
|
64
|
+
cuda.atomic.add(d_sino, (iview, iu0), value * (_ONE - tu))
|
|
65
|
+
cuda.atomic.add(d_sino, (iview, iu0 + 1), value * tu)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@_FDK_ACCURACY_DECORATOR
|
|
69
|
+
def _fan_2d_fbp_transpose_kernel(
|
|
70
|
+
d_sino, n_views, n_det,
|
|
71
|
+
d_image, Nx, Ny,
|
|
72
|
+
det_spacing, d_src_pos, d_det_center, d_det_u_vec,
|
|
73
|
+
cx, cy, voxel_spacing, iso_x, iso_y
|
|
74
|
+
):
|
|
75
|
+
"""Scatter image cotangents through the fan analytical gather stencil."""
|
|
76
|
+
ix, iy = cuda.grid(2)
|
|
77
|
+
if ix >= Nx or iy >= Ny:
|
|
78
|
+
return
|
|
79
|
+
if n_det < 2:
|
|
80
|
+
return
|
|
81
|
+
|
|
82
|
+
# Sample at voxel centres, matching the cell-constant Siddon projector.
|
|
83
|
+
x_v = np.float32(ix) + _HALF - cx
|
|
84
|
+
y_v = np.float32(iy) + _HALF - cy
|
|
85
|
+
|
|
86
|
+
det_spacing_v = det_spacing / voxel_spacing
|
|
87
|
+
half_u = (np.float32(n_det) - _ONE) * _HALF
|
|
88
|
+
|
|
89
|
+
for iview in range(n_views):
|
|
90
|
+
sx = d_src_pos[iview, 0] / voxel_spacing
|
|
91
|
+
sy = d_src_pos[iview, 1] / voxel_spacing
|
|
92
|
+
|
|
93
|
+
dcx = d_det_center[iview, 0] / voxel_spacing
|
|
94
|
+
dcy = d_det_center[iview, 1] / voxel_spacing
|
|
95
|
+
|
|
96
|
+
ux = d_det_u_vec[iview, 0]
|
|
97
|
+
uy = d_det_u_vec[iview, 1]
|
|
98
|
+
|
|
99
|
+
# Detector normal in 2D: rotate u_vec 90 degrees. Choose the
|
|
100
|
+
# orientation that points from the source toward the detector,
|
|
101
|
+
# so that ``U_n`` is positive for voxels on the detector side.
|
|
102
|
+
nx = -uy
|
|
103
|
+
ny = ux
|
|
104
|
+
# Align normal so that (det_center - src) . n > 0.
|
|
105
|
+
align = (dcx - sx) * nx + (dcy - sy) * ny
|
|
106
|
+
if align < _ZERO:
|
|
107
|
+
nx = -nx
|
|
108
|
+
ny = -ny
|
|
109
|
+
|
|
110
|
+
# Perpendicular distance from source to voxel along normal.
|
|
111
|
+
px = x_v - sx
|
|
112
|
+
py = y_v - sy
|
|
113
|
+
U_n = px * nx + py * ny
|
|
114
|
+
if U_n <= _EPSILON:
|
|
115
|
+
continue
|
|
116
|
+
|
|
117
|
+
sdd_n = (dcx - sx) * nx + (dcy - sy) * ny
|
|
118
|
+
if sdd_n <= _EPSILON:
|
|
119
|
+
continue
|
|
120
|
+
mag = sdd_n / U_n
|
|
121
|
+
|
|
122
|
+
# Source-to-isocenter distance, independent of coordinate origin.
|
|
123
|
+
sid_n = (iso_x - sx) * nx + (iso_y - sy) * ny
|
|
124
|
+
if sid_n <= _EPSILON:
|
|
125
|
+
continue
|
|
126
|
+
|
|
127
|
+
hx = sx + mag * px
|
|
128
|
+
hy = sy + mag * py
|
|
129
|
+
|
|
130
|
+
rx = hx - dcx
|
|
131
|
+
ry = hy - dcy
|
|
132
|
+
u_det = rx * ux + ry * uy
|
|
133
|
+
|
|
134
|
+
fu = u_det / det_spacing_v + half_u
|
|
135
|
+
if fu < _ZERO or fu > (np.float32(n_det) - _ONE):
|
|
136
|
+
continue
|
|
137
|
+
|
|
138
|
+
iu0 = int(math.floor(fu))
|
|
139
|
+
if iu0 >= n_det - 1:
|
|
140
|
+
iu0 = n_det - 2
|
|
141
|
+
if iu0 < 0:
|
|
142
|
+
iu0 = 0
|
|
143
|
+
tu = fu - np.float32(iu0)
|
|
144
|
+
if tu < _ZERO:
|
|
145
|
+
tu = _ZERO
|
|
146
|
+
elif tu > _ONE:
|
|
147
|
+
tu = _ONE
|
|
148
|
+
|
|
149
|
+
w_ratio = sid_n / U_n
|
|
150
|
+
w = w_ratio * w_ratio
|
|
151
|
+
value = d_image[iy, ix] * w
|
|
152
|
+
cuda.atomic.add(d_sino, (iview, iu0), value * (_ONE - tu))
|
|
153
|
+
cuda.atomic.add(d_sino, (iview, iu0 + 1), value * tu)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
@_FDK_ACCURACY_DECORATOR
|
|
157
|
+
def _cone_3d_fdk_transpose_kernel(
|
|
158
|
+
d_sino, n_views, n_u, n_v,
|
|
159
|
+
d_vol, Nx, Ny, Nz,
|
|
160
|
+
du, dv, d_src_pos, d_det_center, d_det_u_vec, d_det_v_vec,
|
|
161
|
+
cx, cy, cz, voxel_spacing, iso_x, iso_y, iso_z
|
|
162
|
+
):
|
|
163
|
+
"""Scatter image cotangents through the cone analytical gather stencil."""
|
|
164
|
+
iz, iy, ix = cuda.grid(3)
|
|
165
|
+
if ix >= Nx or iy >= Ny or iz >= Nz:
|
|
166
|
+
return
|
|
167
|
+
if n_u < 2 or n_v < 2:
|
|
168
|
+
return
|
|
169
|
+
|
|
170
|
+
# Voxel position in voxel-unit, origin-centred coordinates.
|
|
171
|
+
# Sample at voxel centres, matching the cell-constant Siddon projector.
|
|
172
|
+
x_v = np.float32(ix) + _HALF - cx
|
|
173
|
+
y_v = np.float32(iy) + _HALF - cy
|
|
174
|
+
z_v = np.float32(iz) + _HALF - cz
|
|
175
|
+
|
|
176
|
+
du_v = du / voxel_spacing
|
|
177
|
+
dv_v = dv / voxel_spacing
|
|
178
|
+
|
|
179
|
+
half_u = (np.float32(n_u) - _ONE) * _HALF
|
|
180
|
+
half_v = (np.float32(n_v) - _ONE) * _HALF
|
|
181
|
+
|
|
182
|
+
for iview in range(n_views):
|
|
183
|
+
# Source position in voxel units.
|
|
184
|
+
sx = d_src_pos[iview, 0] / voxel_spacing
|
|
185
|
+
sy = d_src_pos[iview, 1] / voxel_spacing
|
|
186
|
+
sz = d_src_pos[iview, 2] / voxel_spacing
|
|
187
|
+
|
|
188
|
+
# Detector centre in voxel units.
|
|
189
|
+
dcx = d_det_center[iview, 0] / voxel_spacing
|
|
190
|
+
dcy = d_det_center[iview, 1] / voxel_spacing
|
|
191
|
+
dcz = d_det_center[iview, 2] / voxel_spacing
|
|
192
|
+
|
|
193
|
+
# Detector orientation (unit vectors, ratio-free).
|
|
194
|
+
ux = d_det_u_vec[iview, 0]
|
|
195
|
+
uy = d_det_u_vec[iview, 1]
|
|
196
|
+
uz = d_det_u_vec[iview, 2]
|
|
197
|
+
vx = d_det_v_vec[iview, 0]
|
|
198
|
+
vy = d_det_v_vec[iview, 1]
|
|
199
|
+
vz = d_det_v_vec[iview, 2]
|
|
200
|
+
|
|
201
|
+
# Detector normal n = u x v. Normalized defensively in case the
|
|
202
|
+
# caller supplied non-orthonormal u/v axes.
|
|
203
|
+
nx = uy * vz - uz * vy
|
|
204
|
+
ny = uz * vx - ux * vz
|
|
205
|
+
nz = ux * vy - uy * vx
|
|
206
|
+
n_len = math.sqrt(nx * nx + ny * ny + nz * nz)
|
|
207
|
+
if n_len < _EPSILON:
|
|
208
|
+
continue
|
|
209
|
+
nx /= n_len
|
|
210
|
+
ny /= n_len
|
|
211
|
+
nz /= n_len
|
|
212
|
+
|
|
213
|
+
# Orient the normal so it points from the source toward the
|
|
214
|
+
# detector. Without this flip, user-supplied (u_vec, v_vec) pairs
|
|
215
|
+
# whose cross product happens to point the other way would make
|
|
216
|
+
# every voxel's U_n negative and get silently skipped, producing
|
|
217
|
+
# a zero reconstruction. The circular_trajectory_3d helper already
|
|
218
|
+
# produces the right-handed convention so this is a no-op there.
|
|
219
|
+
align = (dcx - sx) * nx + (dcy - sy) * ny + (dcz - sz) * nz
|
|
220
|
+
if align < _ZERO:
|
|
221
|
+
nx = -nx
|
|
222
|
+
ny = -ny
|
|
223
|
+
nz = -nz
|
|
224
|
+
|
|
225
|
+
# Signed distance from source to voxel along the detector normal
|
|
226
|
+
# (this is the U in the (sid/U)^2 FDK weight).
|
|
227
|
+
px = x_v - sx
|
|
228
|
+
py = y_v - sy
|
|
229
|
+
pz = z_v - sz
|
|
230
|
+
U_n = px * nx + py * ny + pz * nz
|
|
231
|
+
if U_n <= _EPSILON:
|
|
232
|
+
continue
|
|
233
|
+
|
|
234
|
+
# Source-to-detector distance along the normal.
|
|
235
|
+
sdd_n = (dcx - sx) * nx + (dcy - sy) * ny + (dcz - sz) * nz
|
|
236
|
+
if sdd_n <= _EPSILON:
|
|
237
|
+
continue
|
|
238
|
+
|
|
239
|
+
# Source-to-isocenter distance, independent of coordinate origin.
|
|
240
|
+
sid_n = (iso_x - sx) * nx + (iso_y - sy) * ny + (iso_z - sz) * nz
|
|
241
|
+
if sid_n <= _EPSILON:
|
|
242
|
+
continue
|
|
243
|
+
|
|
244
|
+
mag = sdd_n / U_n
|
|
245
|
+
|
|
246
|
+
# Hit point on the detector plane.
|
|
247
|
+
hx = sx + mag * px
|
|
248
|
+
hy = sy + mag * py
|
|
249
|
+
hz = sz + mag * pz
|
|
250
|
+
|
|
251
|
+
# Detector coordinates relative to detector centre.
|
|
252
|
+
rx = hx - dcx
|
|
253
|
+
ry = hy - dcy
|
|
254
|
+
rz = hz - dcz
|
|
255
|
+
|
|
256
|
+
u_det = rx * ux + ry * uy + rz * uz
|
|
257
|
+
v_det = rx * vx + ry * vy + rz * vz
|
|
258
|
+
|
|
259
|
+
# Convert (u, v) to fractional bin index.
|
|
260
|
+
fu = u_det / du_v + half_u
|
|
261
|
+
fv = v_det / dv_v + half_v
|
|
262
|
+
|
|
263
|
+
if fu < _ZERO or fu > (np.float32(n_u) - _ONE) or fv < _ZERO or fv > (np.float32(n_v) - _ONE):
|
|
264
|
+
continue
|
|
265
|
+
|
|
266
|
+
iu0 = int(math.floor(fu))
|
|
267
|
+
iv0 = int(math.floor(fv))
|
|
268
|
+
if iu0 >= n_u - 1:
|
|
269
|
+
iu0 = n_u - 2
|
|
270
|
+
if iv0 >= n_v - 1:
|
|
271
|
+
iv0 = n_v - 2
|
|
272
|
+
if iu0 < 0:
|
|
273
|
+
iu0 = 0
|
|
274
|
+
if iv0 < 0:
|
|
275
|
+
iv0 = 0
|
|
276
|
+
tu = fu - np.float32(iu0)
|
|
277
|
+
tv = fv - np.float32(iv0)
|
|
278
|
+
if tu < _ZERO:
|
|
279
|
+
tu = _ZERO
|
|
280
|
+
elif tu > _ONE:
|
|
281
|
+
tu = _ONE
|
|
282
|
+
if tv < _ZERO:
|
|
283
|
+
tv = _ZERO
|
|
284
|
+
elif tv > _ONE:
|
|
285
|
+
tv = _ONE
|
|
286
|
+
|
|
287
|
+
omtu = _ONE - tu
|
|
288
|
+
omtv = _ONE - tv
|
|
289
|
+
w_ratio = sid_n / U_n
|
|
290
|
+
w = w_ratio * w_ratio
|
|
291
|
+
value = d_vol[ix, iy, iz] * w
|
|
292
|
+
cuda.atomic.add(d_sino, (iview, iu0, iv0), value * omtu * omtv)
|
|
293
|
+
cuda.atomic.add(d_sino, (iview, iu0 + 1, iv0), value * tu * omtv)
|
|
294
|
+
cuda.atomic.add(d_sino, (iview, iu0, iv0 + 1), value * omtu * tv)
|
|
295
|
+
cuda.atomic.add(d_sino, (iview, iu0 + 1, iv0 + 1), value * tu * tv)
|