cil-krl 0.3.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.
- cil_krl-0.3.0/LICENSE +21 -0
- cil_krl-0.3.0/PKG-INFO +312 -0
- cil_krl-0.3.0/README.md +254 -0
- cil_krl-0.3.0/pyproject.toml +94 -0
- cil_krl-0.3.0/setup.cfg +4 -0
- cil_krl-0.3.0/src/cil_krl.egg-info/PKG-INFO +312 -0
- cil_krl-0.3.0/src/cil_krl.egg-info/SOURCES.txt +36 -0
- cil_krl-0.3.0/src/cil_krl.egg-info/dependency_links.txt +1 -0
- cil_krl-0.3.0/src/cil_krl.egg-info/requires.txt +15 -0
- cil_krl-0.3.0/src/cil_krl.egg-info/top_level.txt +1 -0
- cil_krl-0.3.0/src/krl/__init__.py +58 -0
- cil_krl-0.3.0/src/krl/algorithms/__init__.py +7 -0
- cil_krl-0.3.0/src/krl/algorithms/lbfgsb.py +177 -0
- cil_krl-0.3.0/src/krl/algorithms/maprl.py +321 -0
- cil_krl-0.3.0/src/krl/algorithms/richardson_lucy.py +190 -0
- cil_krl-0.3.0/src/krl/callbacks.py +185 -0
- cil_krl-0.3.0/src/krl/operators/__init__.py +20 -0
- cil_krl-0.3.0/src/krl/operators/blurring.py +161 -0
- cil_krl-0.3.0/src/krl/operators/directional.py +61 -0
- cil_krl-0.3.0/src/krl/operators/gpu_kernel_operator.py +1241 -0
- cil_krl-0.3.0/src/krl/operators/kernel_operator.py +898 -0
- cil_krl-0.3.0/src/krl/utils.py +163 -0
- cil_krl-0.3.0/tests/test_backend_selection.py +112 -0
- cil_krl-0.3.0/tests/test_blurring.py +99 -0
- cil_krl-0.3.0/tests/test_callbacks.py +183 -0
- cil_krl-0.3.0/tests/test_cuda_fallback.py +164 -0
- cil_krl-0.3.0/tests/test_directional_maprl_krl.py +243 -0
- cil_krl-0.3.0/tests/test_gpu_kernel_operator.py +904 -0
- cil_krl-0.3.0/tests/test_gradient_directional_maprl.py +498 -0
- cil_krl-0.3.0/tests/test_hybrid_freezing.py +458 -0
- cil_krl-0.3.0/tests/test_imports.py +11 -0
- cil_krl-0.3.0/tests/test_integration.py +217 -0
- cil_krl-0.3.0/tests/test_kernel_operator.py +1114 -0
- cil_krl-0.3.0/tests/test_lbfgsb.py +105 -0
- cil_krl-0.3.0/tests/test_mps_kernel_operator.py +143 -0
- cil_krl-0.3.0/tests/test_nifti_io.py +192 -0
- cil_krl-0.3.0/tests/test_preconditioner.py +368 -0
- cil_krl-0.3.0/tests/test_quickstart.py +61 -0
cil_krl-0.3.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Sam Porter
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
cil_krl-0.3.0/PKG-INFO
ADDED
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: cil-krl
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: Kernelised Richardson-Lucy deconvolution for PET with anatomical guidance, built as a CIL plugin
|
|
5
|
+
Author-email: Kjell Erlandsson <k.erlandsson@ucl.ac.uk>, Sam Porter <sam.porter.18@ucl.ac.uk>
|
|
6
|
+
License: MIT License
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2026 Sam Porter
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
+
in the Software without restriction, including without limitation the rights
|
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
+
furnished to do so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
|
18
|
+
copies or substantial portions of the Software.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
+
SOFTWARE.
|
|
27
|
+
|
|
28
|
+
Project-URL: Homepage, https://github.com/samdporter/Kernel_RL
|
|
29
|
+
Project-URL: Repository, https://github.com/samdporter/Kernel_RL
|
|
30
|
+
Project-URL: Issues, https://github.com/samdporter/Kernel_RL/issues
|
|
31
|
+
Keywords: PET,deconvolution,Richardson-Lucy,CIL,medical imaging,image reconstruction,kernel methods
|
|
32
|
+
Classifier: Development Status :: 3 - Alpha
|
|
33
|
+
Classifier: Intended Audience :: Science/Research
|
|
34
|
+
Classifier: Topic :: Scientific/Engineering :: Medical Science Apps.
|
|
35
|
+
Classifier: Topic :: Scientific/Engineering :: Image Processing
|
|
36
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
37
|
+
Classifier: Programming Language :: Python :: 3
|
|
38
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
39
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
40
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
41
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
42
|
+
Requires-Python: >=3.10
|
|
43
|
+
Description-Content-Type: text/markdown
|
|
44
|
+
License-File: LICENSE
|
|
45
|
+
Requires-Dist: numpy>=1.23
|
|
46
|
+
Requires-Dist: scipy>=1.7
|
|
47
|
+
Requires-Dist: nibabel>=3.0
|
|
48
|
+
Requires-Dist: numba>=0.55
|
|
49
|
+
Provides-Extra: gpu
|
|
50
|
+
Requires-Dist: torch>=1.9; extra == "gpu"
|
|
51
|
+
Provides-Extra: dev
|
|
52
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
53
|
+
Requires-Dist: pytest-cov>=3.0; extra == "dev"
|
|
54
|
+
Requires-Dist: ruff>=0.4; extra == "dev"
|
|
55
|
+
Provides-Extra: all
|
|
56
|
+
Requires-Dist: cil-krl[dev,gpu]; extra == "all"
|
|
57
|
+
Dynamic: license-file
|
|
58
|
+
|
|
59
|
+
# cil-krl: Kernelised Richardson-Lucy Deconvolution for PET
|
|
60
|
+
|
|
61
|
+
[](https://github.com/samdporter/Kernel_RL/actions/workflows/ci.yml)
|
|
62
|
+
[](https://www.python.org/downloads/)
|
|
63
|
+
|
|
64
|
+
**cil-krl** is a plugin for the [Core Imaging Library (CIL)](https://github.com/TomographicImaging/CIL)
|
|
65
|
+
implementing anatomically-guided Richardson-Lucy deconvolution for PET imaging:
|
|
66
|
+
|
|
67
|
+
- **KRL** — kernelised Richardson-Lucy: deconvolution steered by an anatomical image (e.g. MRI) via a kernel operator
|
|
68
|
+
- **HKRL** — hybrid KRL mixing emission and anatomical features, with optional kernel freezing
|
|
69
|
+
- **MAP-RL** — maximum-a-posteriori RL with Armijo line search and preconditioning
|
|
70
|
+
- **DTV** — directional total variation regularisation built on CIL's gradient operators
|
|
71
|
+
- **Backends** — numba CPU backend; optional PyTorch backend for CUDA GPUs and Apple MPS
|
|
72
|
+
|
|
73
|
+
Everything is built directly on CIL's optimisation framework: operators subclass
|
|
74
|
+
`cil.optimisation.operators.LinearOperator`, algorithms subclass
|
|
75
|
+
`cil.optimisation.algorithms.Algorithm`, so they compose with the rest of the CIL
|
|
76
|
+
ecosystem (callbacks, functions, block operators, ...).
|
|
77
|
+
|
|
78
|
+
## Installation
|
|
79
|
+
|
|
80
|
+
`cil-krl` is **not published on PyPI**, so `pip install cil-krl` does not work.
|
|
81
|
+
Install CIL first — it is distributed via conda (conda-forge + ccpi channels),
|
|
82
|
+
not PyPI — then install this package from a checkout or from a wheel you build
|
|
83
|
+
yourself.
|
|
84
|
+
|
|
85
|
+
Combinations tested in CI:
|
|
86
|
+
|
|
87
|
+
| Python | CIL |
|
|
88
|
+
|--------|-----|
|
|
89
|
+
| 3.10 | 25.0.0 |
|
|
90
|
+
| 3.11, 3.12, 3.13 | 26.0.0 |
|
|
91
|
+
|
|
92
|
+
The wheel-install gate in CI builds and installs the wheel on Python 3.11 /
|
|
93
|
+
CIL 26.0.0, which resolved to NumPy 2.4 and Numba 0.68 on CPU. The CUDA/torch
|
|
94
|
+
tests are opt-in and skip without a GPU, so they are not covered by that gate.
|
|
95
|
+
|
|
96
|
+
### Linux
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
# 1. Create an environment with CIL (conda or micromamba)
|
|
100
|
+
conda create -n krl -c conda-forge -c ccpi python=3.11 cil=26.0.0 pip
|
|
101
|
+
conda activate krl
|
|
102
|
+
|
|
103
|
+
# 2. Install cil-krl from a checkout
|
|
104
|
+
git clone https://github.com/samdporter/Kernel_RL.git && cd Kernel_RL
|
|
105
|
+
pip install -e ".[dev]" # + pytest, ruff
|
|
106
|
+
# pip install -e ".[gpu]" # optional: adds PyTorch for the torch backend
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Alternatively build a wheel in the checkout with `make build` and install it with
|
|
110
|
+
`pip install dist/*.whl`.
|
|
111
|
+
|
|
112
|
+
### macOS
|
|
113
|
+
|
|
114
|
+
CIL has no `osx-arm64` build, so run the whole thing in a Linux (x86_64)
|
|
115
|
+
container, from the repository root:
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
docker run --rm --platform linux/amd64 -v "$PWD:/repo:ro" --workdir /tmp \
|
|
119
|
+
--entrypoint /bin/bash mambaorg/micromamba:2.3.2 -lc '
|
|
120
|
+
micromamba create -y -q -n krl -c conda-forge -c ccpi python=3.11 cil=26.0.0 pip &&
|
|
121
|
+
cp -R /repo /tmp/krl &&
|
|
122
|
+
micromamba run -n krl python -m pip install -e "/tmp/krl[dev]" &&
|
|
123
|
+
micromamba run -n krl python -c "import krl; print(krl.__version__)"'
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
> **macOS / OpenMP caveat:** importing PyTorch into a process that also uses
|
|
127
|
+
> CIL's native acceleration libraries can abort due to duplicate OpenMP runtimes.
|
|
128
|
+
> On macOS keep to the numba backend — `backend="auto"` resolves to numba there
|
|
129
|
+
> *without importing torch* — or run torch-based work in a separate process.
|
|
130
|
+
|
|
131
|
+
A native Apple Silicon route also works: CIL built from source plus the torch
|
|
132
|
+
backend on MPS (with a documented OpenMP workaround). See
|
|
133
|
+
[macOS / ARM](docs/MACOS-ARM.md) for the verified setup and test commands.
|
|
134
|
+
|
|
135
|
+
## Quickstart
|
|
136
|
+
|
|
137
|
+
A complete, self-contained CPU example: a synthetic emission image, a synthetic
|
|
138
|
+
anatomical image, the observed blurred data and a callback. Every variable is
|
|
139
|
+
defined below, so the snippet runs as-is.
|
|
140
|
+
|
|
141
|
+
```python
|
|
142
|
+
import numpy as np
|
|
143
|
+
from cil.framework import ImageGeometry
|
|
144
|
+
from cil.optimisation.utilities.callbacks import Callback
|
|
145
|
+
|
|
146
|
+
from krl import RichardsonLucy, create_gaussian_blur, get_kernel_operator
|
|
147
|
+
|
|
148
|
+
# 1. Geometry and two aligned synthetic images
|
|
149
|
+
geometry = ImageGeometry(voxel_num_x=32, voxel_num_y=32, voxel_num_z=16)
|
|
150
|
+
|
|
151
|
+
z, y, x = np.indices((16, 32, 32), dtype=np.float32)
|
|
152
|
+
emission = geometry.allocate(0.0)
|
|
153
|
+
emission.fill(50.0 * np.exp(-((z - 8) ** 2 + (y - 12) ** 2 + (x - 14) ** 2) / 12.0))
|
|
154
|
+
mr_image = geometry.allocate(0.0)
|
|
155
|
+
mr_image.fill(np.exp(-((z - 6) ** 2 + (y - 20) ** 2 + (x - 18) ** 2) / 8.0))
|
|
156
|
+
|
|
157
|
+
# 2. Observed data: the emission image blurred by the PSF
|
|
158
|
+
blur_op = create_gaussian_blur(sigma=(1.5, 1.5, 1.5), geometry=geometry, backend="numba")
|
|
159
|
+
observed = blur_op.direct(emission)
|
|
160
|
+
|
|
161
|
+
# 3. Anatomical guidance operator
|
|
162
|
+
kernel_op = get_kernel_operator(
|
|
163
|
+
geometry, backend="numba", num_neighbours=3, sigma_anat=0.5
|
|
164
|
+
)
|
|
165
|
+
kernel_op.set_anatomical_image(mr_image)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
# 4. Callback: records and prints the objective after each iteration
|
|
169
|
+
class ObjectiveCallback(Callback):
|
|
170
|
+
def __init__(self):
|
|
171
|
+
super().__init__()
|
|
172
|
+
self.values = []
|
|
173
|
+
|
|
174
|
+
def __call__(self, algorithm):
|
|
175
|
+
self.values.append(float(algorithm.loss[-1]))
|
|
176
|
+
print(f"iteration {algorithm.iteration}: objective {self.values[-1]:.4f}")
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
# 5. Reconstruct (omit kernel_operator for standard RL)
|
|
180
|
+
callback = ObjectiveCallback()
|
|
181
|
+
algo = RichardsonLucy(
|
|
182
|
+
initial_estimate=observed,
|
|
183
|
+
blurring_operator=blur_op,
|
|
184
|
+
observed_data=observed,
|
|
185
|
+
kernel_operator=kernel_op,
|
|
186
|
+
)
|
|
187
|
+
algo.run(iterations=8, callbacks=[callback])
|
|
188
|
+
|
|
189
|
+
reconstruction = algo.get_output()
|
|
190
|
+
assert np.isfinite(reconstruction.as_array()).all()
|
|
191
|
+
assert (reconstruction.as_array() >= 0).all()
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
Callbacks receive the CIL `Algorithm` object after every iteration. `krl` also
|
|
195
|
+
provides `NRMSECallback` (NRMSE against a ground truth, appended to a CSV file)
|
|
196
|
+
and `SaveIterationCallback` (writes `.nii.gz` snapshots of the reconstruction).
|
|
197
|
+
|
|
198
|
+
Because `KernelOperator` is a plain CIL `LinearOperator`, you can also drop it
|
|
199
|
+
into your own CIL compositions (`CompositionOperator`, custom `Function`s, ...)
|
|
200
|
+
and drive it with any CIL algorithm.
|
|
201
|
+
|
|
202
|
+
## Backends
|
|
203
|
+
|
|
204
|
+
| Backend | Used by | Hardware | Notes |
|
|
205
|
+
|---------|---------|----------|-------|
|
|
206
|
+
| `numba` | kernel + blur | CPU | used by the quickstart above; kernel arithmetic in float64 |
|
|
207
|
+
| `torch` | kernel + blur | CUDA GPU / Apple MPS | optional (`gpu` extra); device falls back cuda → mps → cpu. The kernel operator's MPS path is verified end-to-end against the numba reference; the blur backend's MPS branch is not verified end-to-end |
|
|
208
|
+
| `scipy` | blur only | CPU | last-resort fallback for the blur operator |
|
|
209
|
+
|
|
210
|
+
- `backend="auto"` is the default for both `get_kernel_operator` and
|
|
211
|
+
`create_gaussian_blur`:
|
|
212
|
+
- on **macOS** it resolves to numba **without importing torch** (see the OpenMP
|
|
213
|
+
caveat above);
|
|
214
|
+
- elsewhere it probes for torch and uses it only when CUDA is available,
|
|
215
|
+
otherwise it falls back to numba (blur: torch → numba → scipy).
|
|
216
|
+
- The torch kernel operator's `device="auto"` resolves cuda → mps → cpu
|
|
217
|
+
(same order as the blur backend); when the device resolves to CPU it runs
|
|
218
|
+
through the numba implementation, so installing torch never makes the
|
|
219
|
+
package CUDA-only.
|
|
220
|
+
|
|
221
|
+
## Notes
|
|
222
|
+
|
|
223
|
+
### Boundary conditions
|
|
224
|
+
|
|
225
|
+
- The numba and torch blur backends **zero-pad** at the volume boundary; the
|
|
226
|
+
scipy backend uses **reflection**.
|
|
227
|
+
- The kernel operator uses inclusive mirror reflection of its neighbourhood at
|
|
228
|
+
the volume boundaries on both backends.
|
|
229
|
+
|
|
230
|
+
### Data type vs computational precision
|
|
231
|
+
|
|
232
|
+
- Operator results are written into a clone of the *input* container, so the
|
|
233
|
+
storage dtype of a result follows the image you passed in (a float32 input
|
|
234
|
+
gives a float32 output, even when the computation used float64).
|
|
235
|
+
- The numba backends accumulate in float64. The torch kernel backend computes in
|
|
236
|
+
float32 by default (`dtype="float64"` selects double), and the torch blur
|
|
237
|
+
backend always computes in float32.
|
|
238
|
+
|
|
239
|
+
### Aligned images
|
|
240
|
+
|
|
241
|
+
- The anatomical image must be on exactly the same grid as the emission image:
|
|
242
|
+
same shape (validated, a mismatch raises `ValueError`) and voxel-by-voxel
|
|
243
|
+
correspondence. No resampling is performed, so co-register beforehand.
|
|
244
|
+
|
|
245
|
+
### HKRL (adaptive hybrid) restrictions
|
|
246
|
+
|
|
247
|
+
- The kernel operator supports single-channel 3-D volumes only.
|
|
248
|
+
- With `hybrid=True` the emission reference is taken from the first `direct()`
|
|
249
|
+
call, so `direct()` must run before `adjoint()` — the adjoint raises if the
|
|
250
|
+
reference or the normalisation map has not been initialised.
|
|
251
|
+
- While unfrozen, the reference is refreshed on every forward call and
|
|
252
|
+
`RichardsonLucy` recomputes the sensitivity `A^T 1` each iteration.
|
|
253
|
+
- `freeze_iteration=N` freezes the kernel after the N-th update: from then on
|
|
254
|
+
forward and adjoint share the same frozen reference and the operator no longer
|
|
255
|
+
changes between iterations. Freezing is a modelling choice; no convergence
|
|
256
|
+
rate is claimed for it.
|
|
257
|
+
|
|
258
|
+
### L-BFGS-B
|
|
259
|
+
|
|
260
|
+
- `LBFGSBOptimizer` is a thin wrapper around `scipy.optimize.minimize(method=
|
|
261
|
+
"L-BFGS-B")` operating on flat arrays. It is **not** a CIL `Algorithm`
|
|
262
|
+
subclass: it has its own `run(iterations, callbacks, verbose)` and its
|
|
263
|
+
callbacks receive the optimiser object rather than a CIL algorithm.
|
|
264
|
+
|
|
265
|
+
### Image I/O
|
|
266
|
+
|
|
267
|
+
- `load_image` / `save_image` accept only 3-D `.nii` / `.nii.gz` volumes
|
|
268
|
+
(singleton axes are rejected on load). Saved files record voxel values and
|
|
269
|
+
voxel spacing only — no original NIfTI header or affine is preserved.
|
|
270
|
+
|
|
271
|
+
## Development
|
|
272
|
+
|
|
273
|
+
```bash
|
|
274
|
+
export PYTHON=$(micromamba run -n krl which python) # interpreter with CIL
|
|
275
|
+
make install # editable install + dev tools (uv)
|
|
276
|
+
make test # CPU test suite
|
|
277
|
+
make lint # ruff check
|
|
278
|
+
make build # sdist + wheel
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
GPU tests are opt-in via `KRL_RUN_GPU_TESTS=1`: `make gpu-test` runs the CUDA
|
|
282
|
+
modules, while the MPS module is run explicitly (see
|
|
283
|
+
[macOS / ARM](docs/MACOS-ARM.md)).
|
|
284
|
+
|
|
285
|
+
The research pipelines, benchmark scripts and BrainWeb data preparation used in
|
|
286
|
+
the original study live under [`examples/`](examples/README.md): they are
|
|
287
|
+
historical reference material, not part of the installed package and not
|
|
288
|
+
maintained.
|
|
289
|
+
|
|
290
|
+
## Documentation
|
|
291
|
+
|
|
292
|
+
- [Methods overview](docs/METHODS.md) — RL, KRL, HKRL and DTV via the Python API
|
|
293
|
+
- [Documentation index](docs/README.md)
|
|
294
|
+
|
|
295
|
+
## Citation
|
|
296
|
+
|
|
297
|
+
If you use cil-krl in your research, please cite it and CIL:
|
|
298
|
+
|
|
299
|
+
```bibtex
|
|
300
|
+
@software{krl2025,
|
|
301
|
+
author = {Erlandsson, Kjell and Porter, Sam},
|
|
302
|
+
title = {cil-krl: Kernelised Richardson-Lucy Deconvolution for PET},
|
|
303
|
+
year = {2025},
|
|
304
|
+
url = {https://github.com/samdporter/Kernel_RL}
|
|
305
|
+
}
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
See also the [CIL citation guidelines](https://github.com/TomographicImaging/CIL#citing-cil).
|
|
309
|
+
|
|
310
|
+
## License
|
|
311
|
+
|
|
312
|
+
cil-krl is MIT-licensed — see [LICENSE](LICENSE).
|
cil_krl-0.3.0/README.md
ADDED
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
# cil-krl: Kernelised Richardson-Lucy Deconvolution for PET
|
|
2
|
+
|
|
3
|
+
[](https://github.com/samdporter/Kernel_RL/actions/workflows/ci.yml)
|
|
4
|
+
[](https://www.python.org/downloads/)
|
|
5
|
+
|
|
6
|
+
**cil-krl** is a plugin for the [Core Imaging Library (CIL)](https://github.com/TomographicImaging/CIL)
|
|
7
|
+
implementing anatomically-guided Richardson-Lucy deconvolution for PET imaging:
|
|
8
|
+
|
|
9
|
+
- **KRL** — kernelised Richardson-Lucy: deconvolution steered by an anatomical image (e.g. MRI) via a kernel operator
|
|
10
|
+
- **HKRL** — hybrid KRL mixing emission and anatomical features, with optional kernel freezing
|
|
11
|
+
- **MAP-RL** — maximum-a-posteriori RL with Armijo line search and preconditioning
|
|
12
|
+
- **DTV** — directional total variation regularisation built on CIL's gradient operators
|
|
13
|
+
- **Backends** — numba CPU backend; optional PyTorch backend for CUDA GPUs and Apple MPS
|
|
14
|
+
|
|
15
|
+
Everything is built directly on CIL's optimisation framework: operators subclass
|
|
16
|
+
`cil.optimisation.operators.LinearOperator`, algorithms subclass
|
|
17
|
+
`cil.optimisation.algorithms.Algorithm`, so they compose with the rest of the CIL
|
|
18
|
+
ecosystem (callbacks, functions, block operators, ...).
|
|
19
|
+
|
|
20
|
+
## Installation
|
|
21
|
+
|
|
22
|
+
`cil-krl` is **not published on PyPI**, so `pip install cil-krl` does not work.
|
|
23
|
+
Install CIL first — it is distributed via conda (conda-forge + ccpi channels),
|
|
24
|
+
not PyPI — then install this package from a checkout or from a wheel you build
|
|
25
|
+
yourself.
|
|
26
|
+
|
|
27
|
+
Combinations tested in CI:
|
|
28
|
+
|
|
29
|
+
| Python | CIL |
|
|
30
|
+
|--------|-----|
|
|
31
|
+
| 3.10 | 25.0.0 |
|
|
32
|
+
| 3.11, 3.12, 3.13 | 26.0.0 |
|
|
33
|
+
|
|
34
|
+
The wheel-install gate in CI builds and installs the wheel on Python 3.11 /
|
|
35
|
+
CIL 26.0.0, which resolved to NumPy 2.4 and Numba 0.68 on CPU. The CUDA/torch
|
|
36
|
+
tests are opt-in and skip without a GPU, so they are not covered by that gate.
|
|
37
|
+
|
|
38
|
+
### Linux
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
# 1. Create an environment with CIL (conda or micromamba)
|
|
42
|
+
conda create -n krl -c conda-forge -c ccpi python=3.11 cil=26.0.0 pip
|
|
43
|
+
conda activate krl
|
|
44
|
+
|
|
45
|
+
# 2. Install cil-krl from a checkout
|
|
46
|
+
git clone https://github.com/samdporter/Kernel_RL.git && cd Kernel_RL
|
|
47
|
+
pip install -e ".[dev]" # + pytest, ruff
|
|
48
|
+
# pip install -e ".[gpu]" # optional: adds PyTorch for the torch backend
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Alternatively build a wheel in the checkout with `make build` and install it with
|
|
52
|
+
`pip install dist/*.whl`.
|
|
53
|
+
|
|
54
|
+
### macOS
|
|
55
|
+
|
|
56
|
+
CIL has no `osx-arm64` build, so run the whole thing in a Linux (x86_64)
|
|
57
|
+
container, from the repository root:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
docker run --rm --platform linux/amd64 -v "$PWD:/repo:ro" --workdir /tmp \
|
|
61
|
+
--entrypoint /bin/bash mambaorg/micromamba:2.3.2 -lc '
|
|
62
|
+
micromamba create -y -q -n krl -c conda-forge -c ccpi python=3.11 cil=26.0.0 pip &&
|
|
63
|
+
cp -R /repo /tmp/krl &&
|
|
64
|
+
micromamba run -n krl python -m pip install -e "/tmp/krl[dev]" &&
|
|
65
|
+
micromamba run -n krl python -c "import krl; print(krl.__version__)"'
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
> **macOS / OpenMP caveat:** importing PyTorch into a process that also uses
|
|
69
|
+
> CIL's native acceleration libraries can abort due to duplicate OpenMP runtimes.
|
|
70
|
+
> On macOS keep to the numba backend — `backend="auto"` resolves to numba there
|
|
71
|
+
> *without importing torch* — or run torch-based work in a separate process.
|
|
72
|
+
|
|
73
|
+
A native Apple Silicon route also works: CIL built from source plus the torch
|
|
74
|
+
backend on MPS (with a documented OpenMP workaround). See
|
|
75
|
+
[macOS / ARM](docs/MACOS-ARM.md) for the verified setup and test commands.
|
|
76
|
+
|
|
77
|
+
## Quickstart
|
|
78
|
+
|
|
79
|
+
A complete, self-contained CPU example: a synthetic emission image, a synthetic
|
|
80
|
+
anatomical image, the observed blurred data and a callback. Every variable is
|
|
81
|
+
defined below, so the snippet runs as-is.
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
import numpy as np
|
|
85
|
+
from cil.framework import ImageGeometry
|
|
86
|
+
from cil.optimisation.utilities.callbacks import Callback
|
|
87
|
+
|
|
88
|
+
from krl import RichardsonLucy, create_gaussian_blur, get_kernel_operator
|
|
89
|
+
|
|
90
|
+
# 1. Geometry and two aligned synthetic images
|
|
91
|
+
geometry = ImageGeometry(voxel_num_x=32, voxel_num_y=32, voxel_num_z=16)
|
|
92
|
+
|
|
93
|
+
z, y, x = np.indices((16, 32, 32), dtype=np.float32)
|
|
94
|
+
emission = geometry.allocate(0.0)
|
|
95
|
+
emission.fill(50.0 * np.exp(-((z - 8) ** 2 + (y - 12) ** 2 + (x - 14) ** 2) / 12.0))
|
|
96
|
+
mr_image = geometry.allocate(0.0)
|
|
97
|
+
mr_image.fill(np.exp(-((z - 6) ** 2 + (y - 20) ** 2 + (x - 18) ** 2) / 8.0))
|
|
98
|
+
|
|
99
|
+
# 2. Observed data: the emission image blurred by the PSF
|
|
100
|
+
blur_op = create_gaussian_blur(sigma=(1.5, 1.5, 1.5), geometry=geometry, backend="numba")
|
|
101
|
+
observed = blur_op.direct(emission)
|
|
102
|
+
|
|
103
|
+
# 3. Anatomical guidance operator
|
|
104
|
+
kernel_op = get_kernel_operator(
|
|
105
|
+
geometry, backend="numba", num_neighbours=3, sigma_anat=0.5
|
|
106
|
+
)
|
|
107
|
+
kernel_op.set_anatomical_image(mr_image)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
# 4. Callback: records and prints the objective after each iteration
|
|
111
|
+
class ObjectiveCallback(Callback):
|
|
112
|
+
def __init__(self):
|
|
113
|
+
super().__init__()
|
|
114
|
+
self.values = []
|
|
115
|
+
|
|
116
|
+
def __call__(self, algorithm):
|
|
117
|
+
self.values.append(float(algorithm.loss[-1]))
|
|
118
|
+
print(f"iteration {algorithm.iteration}: objective {self.values[-1]:.4f}")
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
# 5. Reconstruct (omit kernel_operator for standard RL)
|
|
122
|
+
callback = ObjectiveCallback()
|
|
123
|
+
algo = RichardsonLucy(
|
|
124
|
+
initial_estimate=observed,
|
|
125
|
+
blurring_operator=blur_op,
|
|
126
|
+
observed_data=observed,
|
|
127
|
+
kernel_operator=kernel_op,
|
|
128
|
+
)
|
|
129
|
+
algo.run(iterations=8, callbacks=[callback])
|
|
130
|
+
|
|
131
|
+
reconstruction = algo.get_output()
|
|
132
|
+
assert np.isfinite(reconstruction.as_array()).all()
|
|
133
|
+
assert (reconstruction.as_array() >= 0).all()
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Callbacks receive the CIL `Algorithm` object after every iteration. `krl` also
|
|
137
|
+
provides `NRMSECallback` (NRMSE against a ground truth, appended to a CSV file)
|
|
138
|
+
and `SaveIterationCallback` (writes `.nii.gz` snapshots of the reconstruction).
|
|
139
|
+
|
|
140
|
+
Because `KernelOperator` is a plain CIL `LinearOperator`, you can also drop it
|
|
141
|
+
into your own CIL compositions (`CompositionOperator`, custom `Function`s, ...)
|
|
142
|
+
and drive it with any CIL algorithm.
|
|
143
|
+
|
|
144
|
+
## Backends
|
|
145
|
+
|
|
146
|
+
| Backend | Used by | Hardware | Notes |
|
|
147
|
+
|---------|---------|----------|-------|
|
|
148
|
+
| `numba` | kernel + blur | CPU | used by the quickstart above; kernel arithmetic in float64 |
|
|
149
|
+
| `torch` | kernel + blur | CUDA GPU / Apple MPS | optional (`gpu` extra); device falls back cuda → mps → cpu. The kernel operator's MPS path is verified end-to-end against the numba reference; the blur backend's MPS branch is not verified end-to-end |
|
|
150
|
+
| `scipy` | blur only | CPU | last-resort fallback for the blur operator |
|
|
151
|
+
|
|
152
|
+
- `backend="auto"` is the default for both `get_kernel_operator` and
|
|
153
|
+
`create_gaussian_blur`:
|
|
154
|
+
- on **macOS** it resolves to numba **without importing torch** (see the OpenMP
|
|
155
|
+
caveat above);
|
|
156
|
+
- elsewhere it probes for torch and uses it only when CUDA is available,
|
|
157
|
+
otherwise it falls back to numba (blur: torch → numba → scipy).
|
|
158
|
+
- The torch kernel operator's `device="auto"` resolves cuda → mps → cpu
|
|
159
|
+
(same order as the blur backend); when the device resolves to CPU it runs
|
|
160
|
+
through the numba implementation, so installing torch never makes the
|
|
161
|
+
package CUDA-only.
|
|
162
|
+
|
|
163
|
+
## Notes
|
|
164
|
+
|
|
165
|
+
### Boundary conditions
|
|
166
|
+
|
|
167
|
+
- The numba and torch blur backends **zero-pad** at the volume boundary; the
|
|
168
|
+
scipy backend uses **reflection**.
|
|
169
|
+
- The kernel operator uses inclusive mirror reflection of its neighbourhood at
|
|
170
|
+
the volume boundaries on both backends.
|
|
171
|
+
|
|
172
|
+
### Data type vs computational precision
|
|
173
|
+
|
|
174
|
+
- Operator results are written into a clone of the *input* container, so the
|
|
175
|
+
storage dtype of a result follows the image you passed in (a float32 input
|
|
176
|
+
gives a float32 output, even when the computation used float64).
|
|
177
|
+
- The numba backends accumulate in float64. The torch kernel backend computes in
|
|
178
|
+
float32 by default (`dtype="float64"` selects double), and the torch blur
|
|
179
|
+
backend always computes in float32.
|
|
180
|
+
|
|
181
|
+
### Aligned images
|
|
182
|
+
|
|
183
|
+
- The anatomical image must be on exactly the same grid as the emission image:
|
|
184
|
+
same shape (validated, a mismatch raises `ValueError`) and voxel-by-voxel
|
|
185
|
+
correspondence. No resampling is performed, so co-register beforehand.
|
|
186
|
+
|
|
187
|
+
### HKRL (adaptive hybrid) restrictions
|
|
188
|
+
|
|
189
|
+
- The kernel operator supports single-channel 3-D volumes only.
|
|
190
|
+
- With `hybrid=True` the emission reference is taken from the first `direct()`
|
|
191
|
+
call, so `direct()` must run before `adjoint()` — the adjoint raises if the
|
|
192
|
+
reference or the normalisation map has not been initialised.
|
|
193
|
+
- While unfrozen, the reference is refreshed on every forward call and
|
|
194
|
+
`RichardsonLucy` recomputes the sensitivity `A^T 1` each iteration.
|
|
195
|
+
- `freeze_iteration=N` freezes the kernel after the N-th update: from then on
|
|
196
|
+
forward and adjoint share the same frozen reference and the operator no longer
|
|
197
|
+
changes between iterations. Freezing is a modelling choice; no convergence
|
|
198
|
+
rate is claimed for it.
|
|
199
|
+
|
|
200
|
+
### L-BFGS-B
|
|
201
|
+
|
|
202
|
+
- `LBFGSBOptimizer` is a thin wrapper around `scipy.optimize.minimize(method=
|
|
203
|
+
"L-BFGS-B")` operating on flat arrays. It is **not** a CIL `Algorithm`
|
|
204
|
+
subclass: it has its own `run(iterations, callbacks, verbose)` and its
|
|
205
|
+
callbacks receive the optimiser object rather than a CIL algorithm.
|
|
206
|
+
|
|
207
|
+
### Image I/O
|
|
208
|
+
|
|
209
|
+
- `load_image` / `save_image` accept only 3-D `.nii` / `.nii.gz` volumes
|
|
210
|
+
(singleton axes are rejected on load). Saved files record voxel values and
|
|
211
|
+
voxel spacing only — no original NIfTI header or affine is preserved.
|
|
212
|
+
|
|
213
|
+
## Development
|
|
214
|
+
|
|
215
|
+
```bash
|
|
216
|
+
export PYTHON=$(micromamba run -n krl which python) # interpreter with CIL
|
|
217
|
+
make install # editable install + dev tools (uv)
|
|
218
|
+
make test # CPU test suite
|
|
219
|
+
make lint # ruff check
|
|
220
|
+
make build # sdist + wheel
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
GPU tests are opt-in via `KRL_RUN_GPU_TESTS=1`: `make gpu-test` runs the CUDA
|
|
224
|
+
modules, while the MPS module is run explicitly (see
|
|
225
|
+
[macOS / ARM](docs/MACOS-ARM.md)).
|
|
226
|
+
|
|
227
|
+
The research pipelines, benchmark scripts and BrainWeb data preparation used in
|
|
228
|
+
the original study live under [`examples/`](examples/README.md): they are
|
|
229
|
+
historical reference material, not part of the installed package and not
|
|
230
|
+
maintained.
|
|
231
|
+
|
|
232
|
+
## Documentation
|
|
233
|
+
|
|
234
|
+
- [Methods overview](docs/METHODS.md) — RL, KRL, HKRL and DTV via the Python API
|
|
235
|
+
- [Documentation index](docs/README.md)
|
|
236
|
+
|
|
237
|
+
## Citation
|
|
238
|
+
|
|
239
|
+
If you use cil-krl in your research, please cite it and CIL:
|
|
240
|
+
|
|
241
|
+
```bibtex
|
|
242
|
+
@software{krl2025,
|
|
243
|
+
author = {Erlandsson, Kjell and Porter, Sam},
|
|
244
|
+
title = {cil-krl: Kernelised Richardson-Lucy Deconvolution for PET},
|
|
245
|
+
year = {2025},
|
|
246
|
+
url = {https://github.com/samdporter/Kernel_RL}
|
|
247
|
+
}
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
See also the [CIL citation guidelines](https://github.com/TomographicImaging/CIL#citing-cil).
|
|
251
|
+
|
|
252
|
+
## License
|
|
253
|
+
|
|
254
|
+
cil-krl is MIT-licensed — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=64", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "cil-krl"
|
|
7
|
+
version = "0.3.0"
|
|
8
|
+
description = "Kernelised Richardson-Lucy deconvolution for PET with anatomical guidance, built as a CIL plugin"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = {file = "LICENSE"}
|
|
12
|
+
authors = [
|
|
13
|
+
{name = "Kjell Erlandsson", email = "k.erlandsson@ucl.ac.uk"},
|
|
14
|
+
{name = "Sam Porter", email = "sam.porter.18@ucl.ac.uk"},
|
|
15
|
+
]
|
|
16
|
+
keywords = [
|
|
17
|
+
"PET",
|
|
18
|
+
"deconvolution",
|
|
19
|
+
"Richardson-Lucy",
|
|
20
|
+
"CIL",
|
|
21
|
+
"medical imaging",
|
|
22
|
+
"image reconstruction",
|
|
23
|
+
"kernel methods",
|
|
24
|
+
]
|
|
25
|
+
classifiers = [
|
|
26
|
+
"Development Status :: 3 - Alpha",
|
|
27
|
+
"Intended Audience :: Science/Research",
|
|
28
|
+
"Topic :: Scientific/Engineering :: Medical Science Apps.",
|
|
29
|
+
"Topic :: Scientific/Engineering :: Image Processing",
|
|
30
|
+
"License :: OSI Approved :: MIT License",
|
|
31
|
+
"Programming Language :: Python :: 3",
|
|
32
|
+
"Programming Language :: Python :: 3.10",
|
|
33
|
+
"Programming Language :: Python :: 3.11",
|
|
34
|
+
"Programming Language :: Python :: 3.12",
|
|
35
|
+
"Programming Language :: Python :: 3.13",
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
# NOTE: CIL is a runtime requirement but is not pip-installable from PyPI.
|
|
39
|
+
# Install CIL first (conda: `conda install -c conda-forge -c ccpi cil`), then
|
|
40
|
+
# install this package from source (`pip install -e .`) or from a built wheel;
|
|
41
|
+
# it is not published on PyPI either.
|
|
42
|
+
dependencies = [
|
|
43
|
+
"numpy>=1.23",
|
|
44
|
+
"scipy>=1.7",
|
|
45
|
+
"nibabel>=3.0",
|
|
46
|
+
"numba>=0.55",
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
[project.optional-dependencies]
|
|
50
|
+
gpu = [
|
|
51
|
+
"torch>=1.9",
|
|
52
|
+
]
|
|
53
|
+
dev = [
|
|
54
|
+
"pytest>=7.0",
|
|
55
|
+
"pytest-cov>=3.0",
|
|
56
|
+
"ruff>=0.4",
|
|
57
|
+
]
|
|
58
|
+
all = [
|
|
59
|
+
"cil-krl[gpu,dev]",
|
|
60
|
+
]
|
|
61
|
+
|
|
62
|
+
[project.urls]
|
|
63
|
+
Homepage = "https://github.com/samdporter/Kernel_RL"
|
|
64
|
+
Repository = "https://github.com/samdporter/Kernel_RL"
|
|
65
|
+
Issues = "https://github.com/samdporter/Kernel_RL/issues"
|
|
66
|
+
|
|
67
|
+
[tool.setuptools]
|
|
68
|
+
package-dir = {"" = "src"}
|
|
69
|
+
|
|
70
|
+
[tool.setuptools.packages.find]
|
|
71
|
+
where = ["src"]
|
|
72
|
+
include = ["krl*"]
|
|
73
|
+
|
|
74
|
+
[tool.pytest.ini_options]
|
|
75
|
+
testpaths = ["tests"]
|
|
76
|
+
python_files = ["test_*.py"]
|
|
77
|
+
python_classes = ["Test*"]
|
|
78
|
+
python_functions = ["test_*"]
|
|
79
|
+
addopts = "-q --tb=short"
|
|
80
|
+
markers = [
|
|
81
|
+
"gpu: tests requiring an NVIDIA GPU with CUDA (deselected by default)",
|
|
82
|
+
]
|
|
83
|
+
|
|
84
|
+
[tool.ruff]
|
|
85
|
+
line-length = 120
|
|
86
|
+
target-version = "py310"
|
|
87
|
+
exclude = ["examples"]
|
|
88
|
+
|
|
89
|
+
[tool.ruff.lint]
|
|
90
|
+
select = ["E", "F", "W", "I"]
|
|
91
|
+
ignore = [
|
|
92
|
+
"E402", # torch-guarded imports in the GPU operator module
|
|
93
|
+
"F841", # captured intermediate results in tests
|
|
94
|
+
]
|
cil_krl-0.3.0/setup.cfg
ADDED