Skip to content

Commit 50f5919

Browse files
authored
Update FluxNormaliser for Flex geometry (#2347)
* Update FluxNormaliser for Flex geometry * simplify tests * projection_index as an alias for now deprecated angles * Update changelog
1 parent 56f0a89 commit 50f5919

3 files changed

Lines changed: 78 additions & 38 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
- LSQR algorithm added to the CIL algorithm class (#1975)
44
- Add `VolumeShrinker` tool to reduce the size of the reconstruction volume from an `AcquisitionData` (#2221)
55
- LaminographyGeometryCorrector tool added to processors (#2259)
6+
- `FluxNormaliser` can be used on `Cone3D_Flex` data (#2347)
67
- Bug fixes:
78
- `CentreOfRotationCorrector.image_sharpness` data is now correctly smoothed to reduce aliasing artefacts and improve robustness. (#2202)
89
- `PaganinProcessor` now correctly applies scaling with magnification for cone-beam geometry (#2225)

Wrappers/Python/cil/processors/FluxNormaliser.py

Lines changed: 44 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -116,10 +116,7 @@ def check_input(self, dataset):
116116
if not (type(dataset), AcquisitionData):
117117
raise TypeError("Expected AcquistionData, found {}"
118118
.format(type(dataset)))
119-
120-
if dataset.geometry.geom_type & AcquisitionType.CONE_FLEX:
121-
raise NotImplementedError("FluxNormaliser does not yet support CONE3D_FLEX data")
122-
119+
123120
image_axes = 0
124121
if 'vertical' in dataset.dimension_labels:
125122
self.v_axis = dataset.get_dimension_axis('vertical')
@@ -248,7 +245,7 @@ def _calculate_target(self):
248245
raise TypeError("Target must be string or a number, found {}"
249246
.format(type(self.target)))
250247

251-
def preview_configuration(self, angle=None, channel=None, log=False):
248+
def preview_configuration(self, projection_index=None, channel=None, log=False, **kwargs):
252249
'''
253250
Preview the FluxNormalisation processor configuration for roi mode.
254251
Plots the region of interest on the image and the mean, maximum and
@@ -258,8 +255,8 @@ def preview_configuration(self, angle=None, channel=None, log=False):
258255
259256
Parameters:
260257
-----------
261-
angle: float, optional
262-
Index of the angle to plot, default=None displays the data with the
258+
projection_index: int, optional
259+
Index of the projection to plot, default=None displays the data with the
263260
minimum and maximum pixel values in the roi. For 2D data, the roi is
264261
plotted on the sinogram.
265262
@@ -270,12 +267,26 @@ def preview_configuration(self, angle=None, channel=None, log=False):
270267
log: bool, default=False
271268
If True, plot the image with a log scale, default is False
272269
270+
**kwargs:
271+
angle: int, optional
272+
Deprecated alias for `projection_index`. Use `projection_index` instead.
273+
273274
Returns:
274275
--------
275276
matplotlib.figure.Figure
276277
The figure object created to plot the configuration
277278
'''
278279
import matplotlib.pyplot as plt
280+
281+
if 'angle' in kwargs:
282+
if projection_index is not None:
283+
raise TypeError("Both projection_index and angle were specified; angle is deprecated, use projection_index instead")
284+
projection_index = kwargs.pop('angle')
285+
warnings.warn(
286+
"The 'angle' keyword argument is deprecated and will be removed in a future version; use 'projection_index' instead.",
287+
DeprecationWarning, stacklevel=2)
288+
if kwargs:
289+
raise TypeError(f"preview_configuration() got unexpected keyword arguments {list(kwargs)}")
279290

280291
self._calculate_flux()
281292

@@ -307,48 +318,49 @@ def preview_configuration(self, angle=None, channel=None, log=False):
307318

308319
plt.figure(figsize=(8,8))
309320
if data.geometry.dimension == '3D':
310-
if angle is None:
311-
if 'angle' in data.dimension_labels:
321+
if projection_index is None:
322+
if 'angle' in data.dimension_labels or 'projection' in data.dimension_labels:
312323
self._plot_slice_roi(angle_index=numpy.argmin(min), channel_index=channel, log=log, ax=221)
313324
self._plot_slice_roi(angle_index=numpy.argmax(max), channel_index=channel, log=log, ax=222)
314325
else:
315326
self._plot_slice_roi(log=log, channel_index=channel, ax=211)
316327
else:
317-
if 'angle' in data.dimension_labels:
318-
self._plot_slice_roi(angle_index=angle, channel_index=channel, log=log, ax=211)
328+
if 'angle' in data.dimension_labels or 'projection' in data.dimension_labels:
329+
self._plot_slice_roi(angle_index=projection_index, channel_index=channel, log=log, ax=211)
319330
else:
320331
self._plot_slice_roi(log=log, channel_index=channel, ax=211)
321332

322333
# if data is 2D plot roi on all angles
323334
elif data.geometry.dimension == '2D':
324-
if angle is None:
335+
if projection_index is None:
325336
self._plot_slice_roi(channel_index=channel, log=log, ax=211)
326337
else:
327-
raise ValueError("Cannot plot ROI for a single angle on 2D data, please specify angle=None to plot ROI on the sinogram")
338+
raise ValueError("Cannot plot ROI for a single projection on 2D data, please specify projection_index=None to plot ROI on the sinogram")
328339

329340
plt.subplot(212)
330341
if data.geometry.num_projections==1:
331342
plt.plot(0, flux_array, '.r', label='Mean')
332343
plt.plot(0, min,'.k', label='Minimum')
333344
plt.plot(0, max,'.k', label='Maximum')
334345
else:
335-
indices = range(data.get_dimension_size('angle'))
346+
indices = range(data.geometry.num_projections)
336347
plt.plot(indices, flux_array, 'r', label='Mean')
337348
plt.plot(indices, min,'--k', label='Minimum')
338349
plt.plot(indices, max,'--k', label='Maximum')
339350

340351
plt.legend()
341-
plt.xlabel('angle index')
352+
plt.xlabel('projection index')
342353
plt.ylabel('Intensity in roi')
343354
plt.grid()
344355

345356
ax1 = plt.gca()
346-
ax2 = ax1.twiny()
347-
valid_ticks = [int(tick) for tick in ax1.get_xticks() if 0 <= tick < data.geometry.num_projections]
348-
ax2.set_xticks(valid_ticks)
349-
ax2.set_xbound(ax1.get_xbound())
350-
ax2.set_xticklabels([data.geometry.angles[tick] for tick in valid_ticks])
351-
ax2.set_xlabel('angle')
357+
if 'angle' in data.geometry.dimension_labels:
358+
ax2 = ax1.twiny()
359+
valid_ticks = [int(tick) for tick in ax1.get_xticks() if 0 <= tick < data.geometry.num_projections]
360+
ax2.set_xticks(valid_ticks)
361+
ax2.set_xbound(ax1.get_xbound())
362+
ax2.set_xticklabels([data.geometry.angles[tick] for tick in valid_ticks])
363+
ax2.set_xlabel('angle')
352364

353365
plt.tight_layout()
354366

@@ -363,7 +375,7 @@ def _plot_slice_roi(self, angle_index=None, channel_index=None, log=False, ax=11
363375
Parameters:
364376
-----------
365377
angle_index: int, optional
366-
Index of the angle to plot
378+
Index of the projection to plot
367379
channel_index: int, optional
368380
Index of the channel to plot
369381
log: bool, optional
@@ -376,14 +388,16 @@ def _plot_slice_roi(self, angle_index=None, channel_index=None, log=False, ax=11
376388
data = self.get_input()
377389
if angle_index is not None and 'angle' in data.dimension_labels:
378390
data_slice = data.get_slice(angle=angle_index)
391+
elif angle_index is not None and 'projection' in data.dimension_labels:
392+
data_slice = data.get_slice(projection=angle_index)
379393
else:
380394
data_slice = data
381395

382396
if 'channel' in data.dimension_labels:
383397
data_slice = data_slice.get_slice(channel=channel_index)
384398

385399
if len(data_slice.shape) != 2:
386-
raise ValueError("Data shape not compatible with preview_configuration(), data must have at least two of 'horizontal', 'vertical' and 'angle'")
400+
raise ValueError("Data shape not compatible with preview_configuration(), data must have at least two of 'horizontal', 'vertical' and 'angle'/'projection'")
387401

388402
# if horizontal and vertical are not specified in the roi, get the
389403
# min and max extent from the full size of the dimension
@@ -409,14 +423,14 @@ def _plot_slice_roi(self, angle_index=None, channel_index=None, log=False, ax=11
409423
v = data_slice.dimension_labels[0]
410424

411425
# get the box to plot from the roi
412-
if h == 'angle':
426+
if h == 'angle' or h == 'projection':
413427
h_min = min_angle
414428
h_max = max_angle
415429
else:
416430
h_min = self.roi[h][0]
417431
h_max = self.roi[h][1]
418432

419-
if v == 'angle':
433+
if v == 'angle' or v == 'projection':
420434
v_min = min_angle
421435
v_max = max_angle
422436
else:
@@ -431,8 +445,12 @@ def _plot_slice_roi(self, angle_index=None, channel_index=None, log=False, ax=11
431445
ax1.plot([h_max, h_max],[v_min, v_max],'--r')
432446

433447
title = 'ROI'
434-
if angle_index is not None:
448+
if angle_index is not None and 'angle' in data_slice.dimension_labels:
435449
title += ' angle = ' + str(data.geometry.angles[angle_index])
450+
451+
if angle_index is not None and 'projection' in data_slice.dimension_labels:
452+
title += ' projection = ' + str(angle_index)
453+
436454
if channel_index is not None:
437455
title += ' channel = ' + str(channel_index)
438456
ax1.set_title(title)

Wrappers/Python/test/test_DataProcessor.py

Lines changed: 33 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3588,15 +3588,15 @@ def setUp(self):
35883588
.set_panel([3,3])
35893589
arr = numpy.array([[[1,2,3],[1,2,3],[1,2,3]],
35903590
[[4,5,6],[4,5,6],[4,5,6]],
3591-
[[7,8,9],[7,8,9],[7,8,9]]])
3591+
[[7,8,9],[7,8,9],[7,8,9]]],dtype=numpy.float32)
35923592
self.data_simple = AcquisitionData(arr, geometry=ag)
35933593

35943594
source_position_set=[[0,-100000,0]]*3
35953595
detector_position_set=[[0,0,0]]*3
35963596
detector_direction_x_set=[[1, 0, 0]]*3
35973597
detector_direction_y_set=[[0, 0, 1]]*3
3598-
cone_flex_ag = AcquisitionGeometry.create_Cone3D_Flex(source_position_set, detector_position_set, detector_direction_x_set, detector_direction_y_set).set_panel([3,3])
3599-
self.cone_flex = AcquisitionData(arr, geometry=cone_flex_ag)
3598+
cone_flex_ag = AcquisitionGeometry.create_Cone3D_Flex(source_position_set, detector_position_set, detector_direction_x_set, detector_direction_y_set).set_panel([128,128])
3599+
self.cone_flex = cone_flex_ag.allocate(1)
36003600

36013601
def error_message(self,processor, test_parameter):
36023602
return "Failed with processor " + str(processor) + " on test parameter " + test_parameter
@@ -3623,11 +3623,6 @@ def test_check_input(self):
36233623
with self.assertRaises(ValueError):
36243624
processor.check_input(self.data_cone)
36253625

3626-
# check there's a not implemented error if cone flex geom is used:
3627-
processor = FluxNormaliser(flux=[1,2,3])
3628-
with self.assertRaises(NotImplementedError):
3629-
processor.check_input(self.cone_flex)
3630-
36313626
def test_calculate_flux(self):
36323627
# check there is an error if flux array size is not equal to the number of angles in data
36333628
processor = FluxNormaliser(flux = [1,2,3])
@@ -3814,7 +3809,8 @@ def test_preview_configuration(self, mock_show):
38143809

38153810
# Test no error with preview_configuration with different data shapes
38163811
for data in [self.data_cone, self.data_parallel, self.data_multichannel,
3817-
self.data_slice, self.data_reorder, self.data_single_angle]:
3812+
self.data_slice, self.data_reorder, self.data_single_angle,
3813+
self.cone_flex]:
38183814
mock_show.reset_mock()
38193815

38203816
roi = {'horizontal':(25,40)}
@@ -3826,15 +3822,15 @@ def test_preview_configuration(self, mock_show):
38263822

38273823
# for 3D, check no error specifying a single angle to plot
38283824
if data.geometry.dimension == '3D':
3829-
processor.preview_configuration(angle=1)
3825+
processor.preview_configuration(projection_index=1)
38303826
# if 2D, attempt to plot single angle should cause error
38313827
else:
38323828
with self.assertRaises(ValueError):
3833-
processor.preview_configuration(angle=1)
3829+
processor.preview_configuration(projection_index=1)
38343830

38353831
# if data is multichannel, check no error specifying a single channel to plot
38363832
if 'channel' in data.dimension_labels:
3837-
processor.preview_configuration(angle=1, channel=1)
3833+
processor.preview_configuration(projection_index=1, channel=1)
38383834
processor.preview_configuration(channel=1)
38393835
# if single channel, check specifying channel causes an error
38403836
else:
@@ -3844,6 +3840,25 @@ def test_preview_configuration(self, mock_show):
38443840
# Re-enable logging
38453841
logging.disable(logging.NOTSET)
38463842

3843+
@unittest.skipIf(not has_matplotlib, "matplotlib not installed")
3844+
@patch('matplotlib.pyplot.show')
3845+
def test_preview_configuration_deprecated_angle(self, mock_show):
3846+
logging.disable(logging.CRITICAL)
3847+
3848+
roi = {'horizontal':(25,40)}
3849+
processor = FluxNormaliser(roi=roi)
3850+
processor.set_input(self.data_cone)
3851+
3852+
with self.assertWarns(DeprecationWarning):
3853+
processor.preview_configuration(angle=1)
3854+
3855+
processor.preview_configuration(projection_index=1)
3856+
3857+
with self.assertRaises(TypeError):
3858+
processor.preview_configuration(projection_index=1, angle=1)
3859+
3860+
logging.disable(logging.NOTSET)
3861+
38473862
def test_FluxNormaliser(self, accelerated=False):
38483863

38493864
# Suppress backround range warning
@@ -3855,6 +3870,12 @@ def test_FluxNormaliser(self, accelerated=False):
38553870
data_norm = processor.get_output()
38563871
numpy.testing.assert_allclose(data_norm.array, self.data_cone.array)
38573872

3873+
#Test flux with no target on ConeFlex
3874+
processor = FluxNormaliser(flux=1, accelerated=accelerated)
3875+
processor.set_input(self.cone_flex)
3876+
data_norm = processor.get_output()
3877+
numpy.testing.assert_allclose(data_norm.array, self.cone_flex.array)
3878+
38583879
#Test flux with target
38593880
processor = FluxNormaliser(flux=10, target=5.0, accelerated=accelerated)
38603881
processor.set_input(self.data_cone)

0 commit comments

Comments
 (0)