Skip to content

Commit 8bcb5de

Browse files
authored
Merge pull request #220 from DiamondLightSource/tomobarpadding
padding parameter for the horizontal detector (tomobar v. 2025.08)
2 parents 5e1c8c5 + 2f07157 commit 8bcb5de

File tree

3 files changed

+181
-23
lines changed

3 files changed

+181
-23
lines changed

httomolibgpu/recon/algorithm.py

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ def FBP2d_astra(
5959
data: np.ndarray,
6060
angles: np.ndarray,
6161
center: Optional[float] = None,
62+
detector_pad: int = 0,
6263
filter_type: str = "ram-lak",
6364
filter_parameter: Optional[float] = None,
6465
filter_d: Optional[float] = None,
@@ -81,6 +82,8 @@ def FBP2d_astra(
8182
An array of angles given in radians.
8283
center : float, optional
8384
The center of rotation (CoR).
85+
detector_pad : int
86+
Detector width padding with edge values to remove circle/arc type artifacts in the reconstruction.
8487
filter_type: str
8588
Type of projection filter, see ASTRA's API for all available options for filters.
8689
filter_parameter: float, optional
@@ -112,7 +115,7 @@ def FBP2d_astra(
112115
recon_size = data_shape[2]
113116

114117
RecTools = _instantiate_direct_recon2d_class(
115-
data, angles, center, recon_size, gpu_id
118+
data, angles, center, detector_pad, recon_size, gpu_id
116119
)
117120

118121
detY_size = data_shape[1]
@@ -140,6 +143,7 @@ def FBP3d_tomobar(
140143
data: cp.ndarray,
141144
angles: np.ndarray,
142145
center: Optional[float] = None,
146+
detector_pad: int = 0,
143147
filter_freq_cutoff: float = 0.35,
144148
recon_size: Optional[int] = None,
145149
recon_mask_radius: Optional[float] = 0.95,
@@ -159,6 +163,8 @@ def FBP3d_tomobar(
159163
An array of angles given in radians.
160164
center : float, optional
161165
The center of rotation (CoR).
166+
detector_pad : int
167+
Detector width padding with edge values to remove circle/arc type artifacts in the reconstruction.
162168
filter_freq_cutoff : float
163169
Cutoff frequency parameter for the SINC filter, the lower values produce better contrast but noisy reconstruction.
164170
recon_size : int, optional
@@ -182,7 +188,7 @@ def FBP3d_tomobar(
182188
data = data_checker(data, verbosity=True, method_name="FBP3d_tomobar")
183189

184190
RecToolsCP = _instantiate_direct_recon_class(
185-
data, angles, center, recon_size, gpu_id
191+
data, angles, center, detector_pad, recon_size, gpu_id
186192
)
187193

188194
reconstruction = RecToolsCP.FBP(
@@ -200,6 +206,7 @@ def LPRec3d_tomobar(
200206
data: cp.ndarray,
201207
angles: np.ndarray,
202208
center: Optional[float] = None,
209+
detector_pad: int = 0,
203210
filter_type: str = "shepp",
204211
filter_freq_cutoff: float = 1.0,
205212
recon_size: Optional[int] = None,
@@ -219,6 +226,8 @@ def LPRec3d_tomobar(
219226
An array of angles given in radians.
220227
center : float, optional
221228
The center of rotation (CoR).
229+
detector_pad : int
230+
Detector width padding with edge values to remove circle/arc type artifacts in the reconstruction.
222231
filter_type : str
223232
Filter type, the accepted strings are: none, ramp, shepp, cosine, cosine2, hamming, hann, parzen.
224233
filter_freq_cutoff : float
@@ -242,7 +251,9 @@ def LPRec3d_tomobar(
242251

243252
data = data_checker(data, verbosity=True, method_name="LPRec3d_tomobar")
244253

245-
RecToolsCP = _instantiate_direct_recon_class(data, angles, center, recon_size, 0)
254+
RecToolsCP = _instantiate_direct_recon_class(
255+
data, angles, center, detector_pad, recon_size, 0
256+
)
246257

247258
reconstruction = RecToolsCP.FOURIER_INV(
248259
_take_neg_log(data) if neglog else data,
@@ -260,6 +271,7 @@ def SIRT3d_tomobar(
260271
data: cp.ndarray,
261272
angles: np.ndarray,
262273
center: Optional[float] = None,
274+
detector_pad: int = 0,
263275
recon_size: Optional[int] = None,
264276
iterations: Optional[int] = 300,
265277
nonnegativity: Optional[bool] = True,
@@ -280,6 +292,8 @@ def SIRT3d_tomobar(
280292
An array of angles given in radians.
281293
center : float, optional
282294
The center of rotation (CoR).
295+
detector_pad : int
296+
Detector width padding with edge values to remove circle/arc type artifacts in the reconstruction.
283297
recon_size : int, optional
284298
The [recon_size, recon_size] shape of the reconstructed slice in pixels.
285299
By default (None), the reconstructed size will be the dimension of the horizontal detector.
@@ -304,6 +318,7 @@ def SIRT3d_tomobar(
304318
data,
305319
angles,
306320
center,
321+
detector_pad,
307322
recon_size,
308323
gpu_id,
309324
datafidelity="LS",
@@ -327,6 +342,7 @@ def CGLS3d_tomobar(
327342
data: cp.ndarray,
328343
angles: np.ndarray,
329344
center: Optional[float] = None,
345+
detector_pad: int = 0,
330346
recon_size: Optional[int] = None,
331347
iterations: Optional[int] = 20,
332348
nonnegativity: Optional[bool] = True,
@@ -347,6 +363,8 @@ def CGLS3d_tomobar(
347363
An array of angles given in radians.
348364
center : float, optional
349365
The center of rotation (CoR).
366+
detector_pad : int
367+
Detector width padding with edge values to remove circle/arc type artifacts in the reconstruction.
350368
recon_size : int, optional
351369
The [recon_size, recon_size] shape of the reconstructed slice in pixels.
352370
By default (None), the reconstructed size will be the dimension of the horizontal detector.
@@ -368,7 +386,7 @@ def CGLS3d_tomobar(
368386
data = data_checker(data, verbosity=True, method_name="CGLS3d_tomobar")
369387

370388
RecToolsCP = _instantiate_iterative_recon_class(
371-
data, angles, center, recon_size, gpu_id, datafidelity="LS"
389+
data, angles, center, detector_pad, recon_size, gpu_id, datafidelity="LS"
372390
)
373391

374392
_data_ = {
@@ -386,6 +404,7 @@ def _instantiate_direct_recon_class(
386404
data: cp.ndarray,
387405
angles: np.ndarray,
388406
center: Optional[float] = None,
407+
detector_pad: int = 0,
389408
recon_size: Optional[int] = None,
390409
gpu_id: int = 0,
391410
) -> Type:
@@ -395,6 +414,7 @@ def _instantiate_direct_recon_class(
395414
data (cp.ndarray): data array
396415
angles (np.ndarray): angles
397416
center (Optional[float], optional): center of recon. Defaults to None.
417+
detector_pad (int): Detector width padding. Defaults to 0.
398418
recon_size (Optional[int], optional): recon_size. Defaults to None.
399419
gpu_id (int, optional): gpu ID. Defaults to 0.
400420
@@ -407,6 +427,7 @@ def _instantiate_direct_recon_class(
407427
recon_size = data.shape[2]
408428
RecToolsCP = RecToolsDIRCuPy(
409429
DetectorsDimH=data.shape[2], # Horizontal detector dimension
430+
DetectorsDimH_pad=detector_pad, # padding for horizontal detector
410431
DetectorsDimV=data.shape[1], # Vertical detector dimension (3D case)
411432
CenterRotOffset=data.shape[2] / 2
412433
- center
@@ -423,6 +444,7 @@ def _instantiate_direct_recon2d_class(
423444
data: np.ndarray,
424445
angles: np.ndarray,
425446
center: Optional[float] = None,
447+
detector_pad: int = 0,
426448
recon_size: Optional[int] = None,
427449
gpu_id: int = 0,
428450
) -> Type:
@@ -432,6 +454,7 @@ def _instantiate_direct_recon2d_class(
432454
data (cp.ndarray): data array
433455
angles (np.ndarray): angles
434456
center (Optional[float], optional): center of recon. Defaults to None.
457+
detector_pad (int): Detector width padding. Defaults to 0.
435458
recon_size (Optional[int], optional): recon_size. Defaults to None.
436459
gpu_id (int, optional): gpu ID. Defaults to 0.
437460
@@ -444,6 +467,7 @@ def _instantiate_direct_recon2d_class(
444467
recon_size = data.shape[2]
445468
RecTools = RecToolsDIR(
446469
DetectorsDimH=data.shape[2], # Horizontal detector dimension
470+
DetectorsDimH_pad=detector_pad, # padding for horizontal detector
447471
DetectorsDimV=None, # 2d case
448472
CenterRotOffset=data.shape[2] / 2
449473
- center
@@ -459,6 +483,7 @@ def _instantiate_iterative_recon_class(
459483
data: cp.ndarray,
460484
angles: np.ndarray,
461485
center: Optional[float] = None,
486+
detector_pad: int = 0,
462487
recon_size: Optional[int] = None,
463488
gpu_id: int = 0,
464489
datafidelity: str = "LS",
@@ -469,6 +494,7 @@ def _instantiate_iterative_recon_class(
469494
data (cp.ndarray): data array
470495
angles (np.ndarray): angles
471496
center (Optional[float], optional): center of recon. Defaults to None.
497+
detector_pad (int): Detector width padding. Defaults to 0.
472498
recon_size (Optional[int], optional): recon_size. Defaults to None.
473499
datafidelity (str, optional): Data fidelity
474500
gpu_id (int, optional): gpu ID. Defaults to 0.
@@ -482,6 +508,7 @@ def _instantiate_iterative_recon_class(
482508
recon_size = data.shape[2]
483509
RecToolsCP = RecToolsIRCuPy(
484510
DetectorsDimH=data.shape[2], # Horizontal detector dimension
511+
DetectorsDimH_pad=detector_pad, # padding for horizontal detector
485512
DetectorsDimV=data.shape[1], # Vertical detector dimension (3D case)
486513
CenterRotOffset=data.shape[2] / 2
487514
- center

tests/test_recon/test_algorithm.py

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ def test_reconstruct_FBP_2d_astra(data, flats, darks, ensure_clean_memory):
2222
cp.asnumpy(normalised_data),
2323
np.linspace(0.0 * np.pi / 180.0, 180.0 * np.pi / 180.0, data.shape[0]),
2424
79.5,
25+
0,
2526
filter_type="shepp-logan",
2627
filter_parameter=None,
2728
filter_d=2.0,
@@ -35,6 +36,28 @@ def test_reconstruct_FBP_2d_astra(data, flats, darks, ensure_clean_memory):
3536
assert recon_data.shape == (recon_size, 128, recon_size)
3637

3738

39+
def test_reconstruct_FBP_2d_astra_pad(data, flats, darks, ensure_clean_memory):
40+
normalised_data = normalize_cupy(data, flats, darks, cutoff=10, minus_log=True)
41+
recon_size = 150
42+
43+
recon_data = FBP2d_astra(
44+
cp.asnumpy(normalised_data),
45+
np.linspace(0.0 * np.pi / 180.0, 180.0 * np.pi / 180.0, data.shape[0]),
46+
79.5,
47+
20,
48+
filter_type="shepp-logan",
49+
filter_parameter=None,
50+
filter_d=2.0,
51+
recon_size=recon_size,
52+
recon_mask_radius=0.9,
53+
)
54+
assert recon_data.flags.c_contiguous
55+
assert_allclose(np.mean(recon_data), 0.0020, atol=1e-04)
56+
assert_allclose(np.mean(recon_data, axis=(0, 2)).sum(), 0.265565, rtol=1e-05)
57+
assert recon_data.dtype == np.float32
58+
assert recon_data.shape == (recon_size, 128, recon_size)
59+
60+
3861
def test_reconstruct_FBP3d_tomobar_1(data, flats, darks, ensure_clean_memory):
3962
recon_data = FBP3d_tomobar(
4063
normalize_cupy(data, flats, darks, cutoff=10, minus_log=True),
@@ -95,6 +118,7 @@ def test_reconstruct_FBP3d_tomobar_3(data, flats, darks, ensure_clean_memory):
95118
normalize_cupy(data, flats, darks, cutoff=20.5, minus_log=False),
96119
np.linspace(5.0 * np.pi / 360.0, 180.0 * np.pi / 360.0, data.shape[0]),
97120
79, # center
121+
0, # detector pad
98122
1.1, # filter_freq_cutoff
99123
210, # recon_size
100124
0.9, # recon_mask_radius
@@ -109,11 +133,48 @@ def test_reconstruct_FBP3d_tomobar_3(data, flats, darks, ensure_clean_memory):
109133
assert recon_data.shape == (210, 128, 210)
110134

111135

136+
def test_reconstruct_FBP3d_tomobar_3_pad(data, flats, darks, ensure_clean_memory):
137+
recon_data = FBP3d_tomobar(
138+
normalize_cupy(data, flats, darks, cutoff=20.5, minus_log=False),
139+
np.linspace(5.0 * np.pi / 360.0, 180.0 * np.pi / 360.0, data.shape[0]),
140+
79, # center
141+
20, # detector pad
142+
1.1, # filter_freq_cutoff
143+
210, # recon_size
144+
0.9, # recon_mask_radius
145+
)
146+
147+
recon_data = recon_data.get()
148+
assert_allclose(np.mean(recon_data), -0.000392, atol=1e-6)
149+
assert_allclose(
150+
np.mean(recon_data, axis=(0, 2)).sum(), -0.050226, rtol=1e-06, atol=1e-5
151+
)
152+
assert recon_data.dtype == np.float32
153+
assert recon_data.shape == (210, 128, 210)
154+
155+
112156
def test_reconstruct_LPRec3d_tomobar_1(data, flats, darks, ensure_clean_memory):
113157
recon_data = LPRec3d_tomobar(
114158
data=normalize_cupy(data, flats, darks, cutoff=10, minus_log=True),
115159
angles=np.linspace(0.0 * np.pi / 180.0, 180.0 * np.pi / 180.0, data.shape[0]),
116160
center=79.5,
161+
detector_pad=0,
162+
recon_size=130,
163+
recon_mask_radius=0.95,
164+
)
165+
assert recon_data.flags.c_contiguous
166+
recon_data = recon_data.get()
167+
assert_allclose(np.mean(recon_data), 0.007, atol=1e-3)
168+
assert recon_data.dtype == np.float32
169+
assert recon_data.shape == (130, 128, 130)
170+
171+
172+
def test_reconstruct_LPRec3d_tomobar_1_pad(data, flats, darks, ensure_clean_memory):
173+
recon_data = LPRec3d_tomobar(
174+
data=normalize_cupy(data, flats, darks, cutoff=10, minus_log=True),
175+
angles=np.linspace(0.0 * np.pi / 180.0, 180.0 * np.pi / 180.0, data.shape[0]),
176+
center=79.5,
177+
detector_pad=10,
117178
recon_size=130,
118179
recon_mask_radius=0.95,
119180
)
@@ -165,10 +226,11 @@ def test_FBP3d_tomobar_performance(ensure_clean_memory):
165226
data = cp.asarray(data_host, dtype=np.float32)
166227
angles = np.linspace(0.0 * np.pi / 180.0, 180.0 * np.pi / 180.0, data.shape[0])
167228
cor = 79.5
229+
det_pad = 0
168230
filter_freq_cutoff = 1.1
169231

170232
# cold run first
171-
FBP3d_tomobar(data, angles, cor, filter_freq_cutoff)
233+
FBP3d_tomobar(data, angles, cor, det_pad, filter_freq_cutoff)
172234
dev.synchronize()
173235

174236
start = time.perf_counter_ns()

0 commit comments

Comments
 (0)