Skip to content

Commit 822fbd0

Browse files
author
Dimitar Tasev
authored
Merge pull request #634 from mantidproject/475_iterations_refine_window
2 parents 3a76338 + 07c7a86 commit 822fbd0

File tree

9 files changed

+431
-122
lines changed

9 files changed

+431
-122
lines changed
Lines changed: 49 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
# Copyright (C) 2020 ISIS Rutherford Appleton Laboratory UKRI
22
# SPDX - License - Identifier: GPL-3.0-or-later
3-
3+
from dataclasses import replace
44
from logging import getLogger
5+
from typing import Union
56

67
from mantidimaging.core.data import Images
78
from mantidimaging.core.reconstruct import get_reconstructor_for
@@ -10,48 +11,83 @@
1011

1112
LOG = getLogger(__name__)
1213

14+
INIT_ITERS_CENTRE_VALUE = 100
15+
INIT_ITERS_STEP = 50
16+
1317

1418
class CORInspectionDialogModel(object):
15-
def __init__(self, images: Images, slice_idx: int, initial_cor: ScalarCoR, recon_params: ReconstructionParameters):
19+
def __init__(self, images: Images, slice_idx: int, initial_cor: ScalarCoR, recon_params: ReconstructionParameters,
20+
iters_mode: bool):
1621
self.image_width = images.width
1722
self.sino = images.sino(slice_idx)
1823

1924
# Initial parameters
20-
self.centre_cor = initial_cor.value
21-
self.cor_step = self.image_width * 0.05
25+
if iters_mode:
26+
self.centre_value: Union[int, float] = INIT_ITERS_CENTRE_VALUE
27+
self.step = INIT_ITERS_STEP
28+
self.initial_cor = initial_cor
29+
self._recon_preview = self._recon_iters_preview
30+
self._divide_step = self._divide_iters_step
31+
else:
32+
self.centre_value = initial_cor.value
33+
self.step = self.image_width * 0.05
34+
self._recon_preview = self._recon_cor_preview
35+
self._divide_step = self._divide_cor_step
2236

2337
# Cache projection angles
2438
self.proj_angles = images.projection_angles(recon_params.max_projection_angle)
2539
self.recon_params = recon_params
2640
self.reconstructor = get_reconstructor_for(recon_params.algorithm)
2741

28-
def adjust_cor(self, image):
42+
def _divide_iters_step(self):
43+
self.step = self.step // 2
44+
45+
def _divide_cor_step(self):
46+
self.step /= 2
47+
48+
def adjust(self, image):
2949
"""
30-
Adjusts the rotation centre and step after an image is selected as the
50+
Adjusts the rotation centre/number of iterations and step after an image is selected as the
3151
optimal of an iteration.
3252
"""
3353
if image == ImageType.LESS:
34-
self.centre_cor -= self.cor_step
54+
self.centre_value -= self.step
3555
elif image == ImageType.MORE:
36-
self.centre_cor += self.cor_step
56+
self.centre_value += self.step
3757
elif image == ImageType.CURRENT:
38-
self.cor_step /= 2
58+
self._divide_step()
3959

4060
def cor(self, image):
4161
"""
4262
Gets the rotation centre for a given image in the current iteration.
4363
"""
4464
if image == ImageType.LESS:
45-
return max(self.cor_extents[0], self.centre_cor - self.cor_step)
65+
return max(self.cor_extents[0], self.centre_value - self.step)
4666
elif image == ImageType.CURRENT:
47-
return self.centre_cor
67+
return self.centre_value
4868
elif image == ImageType.MORE:
49-
return min(self.cor_extents[1], self.centre_cor + self.cor_step)
69+
return min(self.cor_extents[1], self.centre_value + self.step)
5070

51-
def recon_preview(self, image):
71+
def iterations(self, image):
72+
if image == ImageType.LESS:
73+
return max(1, self.centre_value - self.step)
74+
elif image == ImageType.CURRENT:
75+
return self.centre_value
76+
elif image == ImageType.MORE:
77+
return self.centre_value + self.step
78+
79+
def _recon_cor_preview(self, image):
5280
cor = ScalarCoR(self.cor(image))
5381
return self.reconstructor.single_sino(self.sino, cor, self.proj_angles, self.recon_params)
5482

83+
def _recon_iters_preview(self, image):
84+
iters = self.iterations(image)
85+
new_params = replace(self.recon_params, num_iter=iters)
86+
return self.reconstructor.single_sino(self.sino, self.initial_cor, self.proj_angles, new_params)
87+
88+
def recon_preview(self, image):
89+
return self._recon_preview(image)
90+
5591
@property
5692
def cor_extents(self):
5793
return 0, self.sino.shape[1] - 1

mantidimaging/gui/dialogs/cor_inspection/presenter.py

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,15 @@ class CORInspectionDialogPresenter(BasePresenter):
3535
view: 'CORInspectionDialogView'
3636

3737
def __init__(self, view, images: Images, slice_index: int, initial_cor: ScalarCoR,
38-
recon_params: ReconstructionParameters):
38+
recon_params: ReconstructionParameters, iters_mode: bool):
3939
super().__init__(view)
4040

41-
self.model = CORInspectionDialogModel(images, slice_index, initial_cor, recon_params)
41+
if iters_mode:
42+
self.get_title = self._make_iters_title
43+
else:
44+
self.get_title = self._make_cor_title
45+
46+
self.model = CORInspectionDialogModel(images, slice_index, initial_cor, recon_params, iters_mode)
4247

4348
def notify(self, signal):
4449
try:
@@ -66,32 +71,41 @@ def on_load(self):
6671
def on_select_image(self, img):
6772
LOG.debug('Image selected: {}'.format(img))
6873

69-
# Adjust COR step
70-
self.model.adjust_cor(img)
74+
# Adjust COR/iterations step
75+
self.model.adjust(img)
7176

7277
if img != ImageType.CURRENT:
7378
# Update UI
7479
self.do_refresh()
7580
else:
7681
self.do_refresh([ImageType.LESS, ImageType.MORE])
7782

83+
def _make_cor_title(self, image) -> str:
84+
return 'COR: {}'.format(self.model.cor(image))
85+
86+
def _make_iters_title(self, image) -> str:
87+
return 'Iterations: {}'.format(self.model.iterations(image))
88+
7889
def do_refresh(self, images=None):
7990
if images is None:
8091
images = ImageType
8192
# Parameters
82-
self.view.step_size = self.model.cor_step
93+
self.view.step_size = self.model.step
8394

8495
# Images
85-
for i in ImageType:
86-
title = 'COR: {}'.format(self.model.cor(i))
87-
self.view.set_image(i, self.model.recon_preview(i), title)
96+
for i in images:
97+
self.view.set_image(i, self.model.recon_preview(i), self.get_title(i))
8898

8999
def do_update_ui_parameters(self):
90-
self.model.cor_step = self.view.step_size
100+
self.model.step = self.view.step_size
91101

92102
# Update UI
93103
self.notify(Notification.FULL_UPDATE)
94104

95105
@property
96106
def optimal_rotation_centre(self) -> ScalarCoR:
97-
return ScalarCoR(self.model.centre_cor)
107+
return ScalarCoR(self.model.centre_value)
108+
109+
@property
110+
def optimal_iterations(self):
111+
return self.model.centre_value

mantidimaging/gui/dialogs/cor_inspection/test/model_test.py

Lines changed: 80 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -2,59 +2,115 @@
22
# SPDX - License - Identifier: GPL-3.0-or-later
33

44
import unittest
5+
from unittest.mock import Mock, patch
56

67
import numpy.testing as npt
78

89
from mantidimaging.core.utility.data_containers import ScalarCoR, ReconstructionParameters
910
from mantidimaging.gui.dialogs.cor_inspection import CORInspectionDialogModel
11+
from mantidimaging.gui.dialogs.cor_inspection.model import INIT_ITERS_CENTRE_VALUE, INIT_ITERS_STEP
1012
from mantidimaging.gui.dialogs.cor_inspection.types import ImageType
1113
from mantidimaging.test_helpers.unit_test_helper import generate_images
1214

1315

1416
class CORInspectionDialogModelTest(unittest.TestCase):
1517
def test_construct(self):
1618
images = generate_images()
17-
m = CORInspectionDialogModel(images, 5, ScalarCoR(20), ReconstructionParameters('FBP_CUDA', 'ram-lak'))
19+
m = CORInspectionDialogModel(images, 5, ScalarCoR(20), ReconstructionParameters('FBP_CUDA', 'ram-lak'), False)
1820
npt.assert_equal(m.sino, images.sino(5))
1921
self.assertEqual(m.cor_extents, (0, 9))
2022
self.assertEqual(m.proj_angles.value.shape, (10, ))
2123

2224
def test_start_cor_step(self):
2325
images = generate_images()
24-
m = CORInspectionDialogModel(images, 5, ScalarCoR(20), ReconstructionParameters('FBP_CUDA', 'ram-lak'))
25-
self.assertEqual(images.width * 0.05, m.cor_step)
26+
m = CORInspectionDialogModel(images, 5, ScalarCoR(20), ReconstructionParameters('FBP_CUDA', 'ram-lak'), False)
27+
self.assertEqual(images.width * 0.05, m.step)
2628

2729
def test_current_cor(self):
2830
images = generate_images()
29-
m = CORInspectionDialogModel(images, 5, ScalarCoR(20), ReconstructionParameters('FBP_CUDA', 'ram-lak'))
30-
m.centre_cor = 5
31-
m.cor_step = 1
31+
m = CORInspectionDialogModel(images, 5, ScalarCoR(20), ReconstructionParameters('FBP_CUDA', 'ram-lak'), False)
32+
m.centre_value = 5
33+
m.step = 1
3234
self.assertEqual(m.cor(ImageType.LESS), 4)
3335
self.assertEqual(m.cor(ImageType.CURRENT), 5)
3436
self.assertEqual(m.cor(ImageType.MORE), 6)
3537

3638
def test_adjust_cor(self):
3739
images = generate_images()
38-
m = CORInspectionDialogModel(images, 5, ScalarCoR(20), ReconstructionParameters('FBP_CUDA', 'ram-lak'))
39-
m.centre_cor = 5
40-
m.cor_step = 1
40+
m = CORInspectionDialogModel(images, 5, ScalarCoR(20), ReconstructionParameters('FBP_CUDA', 'ram-lak'), False)
41+
m.centre_value = 5
42+
m.step = 1
4143

42-
m.adjust_cor(ImageType.CURRENT)
43-
self.assertEqual(m.centre_cor, 5)
44-
self.assertEqual(m.cor_step, 0.5)
44+
m.adjust(ImageType.CURRENT)
45+
self.assertEqual(m.centre_value, 5)
46+
self.assertEqual(m.step, 0.5)
4547

46-
m.adjust_cor(ImageType.LESS)
47-
self.assertEqual(m.centre_cor, 4.5)
48-
self.assertEqual(m.cor_step, 0.5)
48+
m.adjust(ImageType.LESS)
49+
self.assertEqual(m.centre_value, 4.5)
50+
self.assertEqual(m.step, 0.5)
4951

50-
m.adjust_cor(ImageType.CURRENT)
51-
self.assertEqual(m.centre_cor, 4.5)
52-
self.assertEqual(m.cor_step, 0.25)
52+
m.adjust(ImageType.CURRENT)
53+
self.assertEqual(m.centre_value, 4.5)
54+
self.assertEqual(m.step, 0.25)
5355

54-
m.adjust_cor(ImageType.MORE)
55-
self.assertEqual(m.centre_cor, 4.75)
56-
self.assertEqual(m.cor_step, 0.25)
56+
m.adjust(ImageType.MORE)
57+
self.assertEqual(m.centre_value, 4.75)
58+
self.assertEqual(m.step, 0.25)
5759

58-
m.adjust_cor(ImageType.CURRENT)
59-
self.assertEqual(m.centre_cor, 4.75)
60-
self.assertEqual(m.cor_step, 0.125)
60+
m.adjust(ImageType.CURRENT)
61+
self.assertEqual(m.centre_value, 4.75)
62+
self.assertEqual(m.step, 0.125)
63+
64+
def test_iters_mode_init(self):
65+
images = generate_images()
66+
m = CORInspectionDialogModel(images, 5, ScalarCoR(20), ReconstructionParameters('FBP_CUDA', 'ram-lak'), True)
67+
68+
self.assertEqual(m.centre_value, INIT_ITERS_CENTRE_VALUE)
69+
self.assertEqual(m.step, INIT_ITERS_STEP)
70+
self.assertEqual(m._recon_preview, m._recon_iters_preview)
71+
self.assertEqual(m._divide_step, m._divide_iters_step)
72+
73+
def test_cor_mode_init(self):
74+
images = generate_images()
75+
initial_cor = ScalarCoR(20)
76+
m = CORInspectionDialogModel(images, 5, initial_cor, ReconstructionParameters('FBP_CUDA', 'ram-lak'), False)
77+
78+
self.assertEqual(m.centre_value, initial_cor.value)
79+
self.assertEqual(m.step, images.width * 0.05)
80+
self.assertEqual(m._recon_preview, m._recon_cor_preview)
81+
self.assertEqual(m._divide_step, m._divide_cor_step)
82+
83+
def test_divide_iters_step(self):
84+
images = generate_images()
85+
m = CORInspectionDialogModel(images, 5, ScalarCoR(20), ReconstructionParameters('FBP_CUDA', 'ram-lak'), True)
86+
m.step = 11
87+
88+
m._divide_step()
89+
self.assertEqual(m.step, 5)
90+
91+
def test_iterations(self):
92+
images = generate_images()
93+
m = CORInspectionDialogModel(images, 5, ScalarCoR(20), ReconstructionParameters('FBP_CUDA', 'ram-lak'), True)
94+
95+
m.adjust(ImageType.LESS)
96+
self.assertEqual(m.centre_value, 50)
97+
self.assertEqual(m.step, 50)
98+
99+
m.adjust(ImageType.CURRENT)
100+
self.assertEqual(m.centre_value, 50)
101+
self.assertEqual(m.step, 25)
102+
103+
m.adjust(ImageType.MORE)
104+
self.assertEqual(m.centre_value, 75)
105+
self.assertEqual(m.step, 25)
106+
107+
@patch('mantidimaging.gui.dialogs.cor_inspection.model.replace')
108+
def test_recon_iters_preview(self, replace_mock):
109+
images = generate_images()
110+
m = CORInspectionDialogModel(images, 5, ScalarCoR(20), ReconstructionParameters('FBP_CUDA', 'ram-lak'), True)
111+
112+
m.reconstructor = Mock()
113+
m.recon_preview(ImageType.CURRENT)
114+
replace_mock.assert_called_once_with(m.recon_params, num_iter=100)
115+
m.reconstructor.single_sino.assert_called_once_with(m.sino, m.initial_cor, m.proj_angles,
116+
replace_mock.return_value)

mantidimaging/gui/dialogs/cor_inspection/view.py

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from typing import List
55

66
import numpy as np
7-
from PyQt5.QtWidgets import QPushButton, QDoubleSpinBox
7+
from PyQt5.QtWidgets import QPushButton, QDoubleSpinBox, QSpinBox, QStackedWidget
88

99
from mantidimaging.core.data import Images
1010
from mantidimaging.core.utility.data_containers import ScalarCoR, ReconstructionParameters
@@ -19,20 +19,35 @@ class CORInspectionDialogView(BaseDialogView):
1919
lessButton: QPushButton
2020
currentButton: QPushButton
2121
moreButton: QPushButton
22-
step: QDoubleSpinBox
22+
stepCOR: QDoubleSpinBox
23+
stepIterations: QSpinBox
24+
stepStackedWidget: QStackedWidget
25+
instructionStackedWidget: QStackedWidget
2326

2427
def __init__(self, parent, images: Images, slice_index: int, initial_cor: ScalarCoR,
25-
recon_params: ReconstructionParameters):
28+
recon_params: ReconstructionParameters, iters_mode: bool):
2629
super().__init__(parent, 'gui/ui/cor_inspection_dialog.ui')
27-
self.presenter = CORInspectionDialogPresenter(self, images, slice_index, initial_cor, recon_params)
30+
self.iters_mode = iters_mode
31+
32+
if self.iters_mode:
33+
self.spin_box = self.stepIterations
34+
else:
35+
self.spin_box = self.stepCOR
36+
37+
self.presenter = CORInspectionDialogPresenter(self, images, slice_index, initial_cor, recon_params, iters_mode)
38+
39+
self.stepCOR.editingFinished.connect(lambda: self.presenter.do_update_ui_parameters())
40+
self.stepIterations.editingFinished.connect(lambda: self.presenter.do_update_ui_parameters())
2841

29-
self.step.editingFinished.connect(lambda: self.presenter.do_update_ui_parameters())
3042
self.lessButton.clicked.connect(lambda: self.presenter.on_select_image(ImageType.LESS))
3143
self.currentButton.clicked.connect(lambda: self.presenter.on_select_image(ImageType.CURRENT))
3244
self.moreButton.clicked.connect(lambda: self.presenter.on_select_image(ImageType.MORE))
3345

3446
self.finishButton.clicked.connect(self.accept)
3547

48+
self.stepStackedWidget.setCurrentIndex(int(iters_mode))
49+
self.instructionStackedWidget.setCurrentIndex(int(iters_mode))
50+
3651
self.image_canvas = CompareSlicesView(self)
3752
self.imagePlotLayout.addWidget(self.image_canvas)
3853

@@ -46,21 +61,25 @@ def set_maximum_cor(self, cor):
4661
"""
4762
Set the maximum valid rotation centre.
4863
"""
49-
self.step.setMaximum(cor)
64+
self.stepCOR.setMaximum(cor)
5065

5166
@property
5267
def step_size(self):
53-
return self.step.value()
68+
return self.spin_box.value()
5469

5570
@step_size.setter
5671
def step_size(self, value):
57-
with BlockQtSignals([self.step]):
58-
self.step.setValue(value)
72+
with BlockQtSignals([self.spin_box]):
73+
self.spin_box.setValue(value)
5974

6075
@property
6176
def optimal_rotation_centre(self) -> ScalarCoR:
6277
return self.presenter.optimal_rotation_centre
6378

79+
@property
80+
def optimal_iterations(self) -> int:
81+
return self.presenter.optimal_iterations
82+
6483
def mark_best_recon(self, diffs):
6584
best = diffs.index(max(diffs))
6685
buttons: List[QPushButton] = [self.lessButton, self.currentButton, self.moreButton]

0 commit comments

Comments
 (0)