tomwer 1.4.0rc0__py3-none-any.whl → 1.4.0rc2__py3-none-any.whl
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.
- orangecontrib/tomwer/tutorials/test_cor.ows +3 -3
- orangecontrib/tomwer/widgets/reconstruction/AxisOW.py +6 -14
- orangecontrib/tomwer/widgets/reconstruction/NabuVolumeOW.py +4 -2
- tomwer/app/axis.py +0 -3
- tomwer/app/multipag.py +11 -3
- tomwer/core/process/reconstruction/axis/axis.py +27 -736
- tomwer/core/process/reconstruction/axis/mode.py +86 -24
- tomwer/core/process/reconstruction/axis/params.py +127 -138
- tomwer/core/process/reconstruction/axis/side.py +8 -0
- tomwer/core/process/reconstruction/nabu/nabuscores.py +17 -20
- tomwer/core/process/reconstruction/nabu/nabuslices.py +5 -1
- tomwer/core/process/reconstruction/saaxis/saaxis.py +4 -4
- tomwer/core/process/reconstruction/sadeltabeta/sadeltabeta.py +4 -4
- tomwer/core/process/reconstruction/tests/test_axis.py +1 -1
- tomwer/core/process/reconstruction/tests/test_utils.py +4 -4
- tomwer/core/process/reconstruction/utils/cor.py +8 -4
- tomwer/core/process/tests/test_nabu.py +1 -3
- tomwer/core/scan/nxtomoscan.py +2 -0
- tomwer/core/scan/scanbase.py +4 -4
- tomwer/core/scan/tests/test_process_registration.py +0 -18
- tomwer/gui/fonts.py +5 -0
- tomwer/gui/reconstruction/axis/AxisMainWindow.py +20 -9
- tomwer/gui/reconstruction/axis/AxisOptionsWidget.py +239 -79
- tomwer/gui/reconstruction/axis/AxisSettingsWidget.py +38 -17
- tomwer/gui/reconstruction/axis/AxisWidget.py +16 -8
- tomwer/gui/reconstruction/axis/CalculationWidget.py +40 -200
- tomwer/gui/reconstruction/axis/ControlWidget.py +10 -2
- tomwer/gui/reconstruction/axis/EstimatedCORWidget.py +394 -0
- tomwer/gui/reconstruction/axis/EstimatedCorComboBox.py +118 -0
- tomwer/gui/reconstruction/axis/InputWidget.py +11 -155
- tomwer/gui/reconstruction/saaxis/corrangeselector.py +19 -10
- tomwer/gui/reconstruction/scores/scoreplot.py +5 -2
- tomwer/gui/reconstruction/tests/test_nabu.py +8 -0
- tomwer/gui/stitching/z_stitching/fineestimation.py +1 -1
- tomwer/gui/tests/test_axis_gui.py +31 -15
- tomwer/synctools/stacks/reconstruction/axis.py +5 -23
- tomwer/synctools/stacks/reconstruction/dkrefcopy.py +1 -1
- tomwer/synctools/stacks/reconstruction/nabu.py +2 -2
- tomwer/synctools/stacks/reconstruction/normalization.py +1 -1
- tomwer/synctools/stacks/reconstruction/saaxis.py +1 -1
- tomwer/synctools/stacks/reconstruction/sadeltabeta.py +1 -1
- tomwer/tests/orangecontrib/tomwer/widgets/reconstruction/tests/test_axis.py +0 -16
- tomwer/tests/test_ewoks/test_single_node_execution.py +1 -1
- tomwer/tests/test_ewoks/test_workflows.py +1 -1
- tomwer/version.py +1 -1
- {tomwer-1.4.0rc0.dist-info → tomwer-1.4.0rc2.dist-info}/METADATA +2 -2
- {tomwer-1.4.0rc0.dist-info → tomwer-1.4.0rc2.dist-info}/RECORD +51 -48
- tomwer/core/process/tests/test_axis.py +0 -231
- {tomwer-1.4.0rc0.dist-info → tomwer-1.4.0rc2.dist-info}/LICENSE +0 -0
- {tomwer-1.4.0rc0.dist-info → tomwer-1.4.0rc2.dist-info}/WHEEL +0 -0
- {tomwer-1.4.0rc0.dist-info → tomwer-1.4.0rc2.dist-info}/entry_points.txt +0 -0
- {tomwer-1.4.0rc0.dist-info → tomwer-1.4.0rc2.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,394 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
from silx.gui import qt
|
4
|
+
from processview.gui.DropDownWidget import DropDownWidget
|
5
|
+
from tomwer.synctools.axis import QAxisRP
|
6
|
+
from tomwer.gui.utils.qt_utils import block_signals
|
7
|
+
from tomwer.gui.utils.buttons import PadlockButton
|
8
|
+
from tomwer.gui.reconstruction.axis.EstimatedCorComboBox import EstimatedCorComboBox
|
9
|
+
from tomwer.core.process.reconstruction.axis.side import Side
|
10
|
+
from tomwer.core.process.reconstruction.axis import mode as axis_mode
|
11
|
+
from tomwer.gui.fonts import FONT_SMALL
|
12
|
+
from tomwer.gui.utils.scrollarea import QDoubleSpinBoxIgnoreWheel
|
13
|
+
from pyunitsystem.metricsystem import MetricSystem
|
14
|
+
|
15
|
+
|
16
|
+
class EstimatedCORWidget(qt.QGroupBox):
|
17
|
+
"""
|
18
|
+
Widget to define the estimated center of rotation.
|
19
|
+
(based on the motor offset and the 'x_rotation_axis_pixel_position')
|
20
|
+
"""
|
21
|
+
|
22
|
+
sigValueChanged = qt.Signal()
|
23
|
+
"""Emit when one of the value changed"""
|
24
|
+
sigUpdateXRotAxisPixelPosOnNewScan = qt.Signal()
|
25
|
+
"""Emit when user want to stop / activate x rotation axis pixel position when a new scan arrives"""
|
26
|
+
sigYAxisInvertedChanged = qt.Signal(bool)
|
27
|
+
|
28
|
+
def __init__(self, parent, axis_params: QAxisRP):
|
29
|
+
self._axis_params = axis_params
|
30
|
+
self._imageWidth = None
|
31
|
+
|
32
|
+
super().__init__(parent)
|
33
|
+
self.setLayout(qt.QGridLayout())
|
34
|
+
# estimated cor
|
35
|
+
self._estimatedCORLabel = qt.QLabel("Estimated CoR (relative)", self)
|
36
|
+
self.layout().addWidget(self._estimatedCORLabel, 0, 0, 1, 1)
|
37
|
+
|
38
|
+
self._estimatedCORQCB = EstimatedCorComboBox(self)
|
39
|
+
self.layout().addWidget(self._estimatedCORQCB, 0, 1, 1, 1)
|
40
|
+
|
41
|
+
# offset calibration
|
42
|
+
self._offsetWidgetDropdown = DropDownWidget(
|
43
|
+
parent=self, direction=qt.Qt.LayoutDirection.RightToLeft
|
44
|
+
)
|
45
|
+
self._offsetWidget = _OffsetCalibration(parent=self, axis_params=axis_params)
|
46
|
+
self.layout().addWidget(self._offsetWidgetDropdown, 1, 0, 2, 2)
|
47
|
+
self._offsetWidgetDropdown.setWidget(self._offsetWidget)
|
48
|
+
|
49
|
+
# set up
|
50
|
+
self._offsetWidgetDropdown.setChecked(False)
|
51
|
+
|
52
|
+
# connect signal / slot
|
53
|
+
self._estimatedCORQCB.sigEstimatedCorChanged.connect(self._corChanged)
|
54
|
+
self._offsetWidget.sigOffsetChanged.connect(
|
55
|
+
self._updateEstimatedCorFromMotorOffsetWidget
|
56
|
+
)
|
57
|
+
self._offsetWidget.sigXRotationAxisPixelPositionChanged.connect(
|
58
|
+
self._updateEstimatedCorFromMotorOffsetWidget
|
59
|
+
)
|
60
|
+
self._offsetWidget.sigUpdateXRotAxisPixelPosOnNewScan.connect(
|
61
|
+
self.sigUpdateXRotAxisPixelPosOnNewScan
|
62
|
+
)
|
63
|
+
self._offsetWidget.sigYAxisInvertedChanged.connect(self.sigYAxisInvertedChanged)
|
64
|
+
|
65
|
+
def getEstimatedCor(self):
|
66
|
+
return self._estimatedCORQCB.getCurrentCorValue()
|
67
|
+
|
68
|
+
def setEstimatedCor(
|
69
|
+
self, value: float | Side | str, provided_with_offset: bool = False
|
70
|
+
):
|
71
|
+
|
72
|
+
if isinstance(value, float):
|
73
|
+
if self._offsetWidget.isYAxisInverted():
|
74
|
+
value = -1.0 * value
|
75
|
+
|
76
|
+
if provided_with_offset:
|
77
|
+
value_with_offset = value
|
78
|
+
value_without_offset = value - self._offsetWidget.getOffset()
|
79
|
+
else:
|
80
|
+
value_with_offset = value + self._offsetWidget.getOffset()
|
81
|
+
value_without_offset = value
|
82
|
+
with block_signals(self._offsetWidget):
|
83
|
+
self._offsetWidget.setXRotationAxisPixelPosition(
|
84
|
+
value_without_offset, apply_flip=False
|
85
|
+
)
|
86
|
+
else:
|
87
|
+
# case this is a side
|
88
|
+
value_with_offset = value
|
89
|
+
|
90
|
+
self._estimatedCORQCB.setCurrentCorValue(value_with_offset)
|
91
|
+
|
92
|
+
with block_signals(self._axis_params):
|
93
|
+
self._axis_params.estimated_cor = value_with_offset
|
94
|
+
|
95
|
+
def _corChanged(self, value):
|
96
|
+
assert value is None or isinstance(value, (float, Side, None))
|
97
|
+
with block_signals(self._axis_params):
|
98
|
+
self._axis_params.estimated_cor = value
|
99
|
+
self.sigValueChanged.emit()
|
100
|
+
|
101
|
+
def _updateVisibleSides(self, mode: axis_mode.AxisMode):
|
102
|
+
"""
|
103
|
+
Update the visibility and selection of sides (Left, Center, Right)
|
104
|
+
for the Estimated Center of Rotation (CoR) ComboBox based on the
|
105
|
+
provided axis mode and a calculated CoR guess.
|
106
|
+
|
107
|
+
This method adjusts the available sides and determines the new
|
108
|
+
side (Left, Center, or Right) based on the position of a CoR guess
|
109
|
+
relative to the width of the image. The image is divided into thirds:
|
110
|
+
- Left: From -infinity to the first third.
|
111
|
+
- Center: From the first third to the second third.
|
112
|
+
- Right: From the second third to infinity.
|
113
|
+
|
114
|
+
If the CoR guess is not valid or falls outside the calculated bounds,
|
115
|
+
a default valid side is selected.
|
116
|
+
|
117
|
+
Behavior:
|
118
|
+
---------
|
119
|
+
- By default if the method allows it, the side will be the estimated CoR.
|
120
|
+
- If the method does not allow it, it will update the visible sides in
|
121
|
+
the Estimated CoR ComboBox based on the valid sides defined in the axis
|
122
|
+
mode metadata and dynamically determines the new side (Left, Center, Right)
|
123
|
+
based on the CoR guess position within the image width.
|
124
|
+
|
125
|
+
"""
|
126
|
+
mode = axis_mode.AxisMode.from_value(mode)
|
127
|
+
valid_sides = axis_mode.AXIS_MODE_METADATAS[mode].valid_sides
|
128
|
+
self._estimatedCORQCB.setSidesVisible(valid_sides)
|
129
|
+
first_guess_available = axis_mode.AXIS_MODE_METADATAS[
|
130
|
+
mode
|
131
|
+
].allows_estimated_cor_as_numerical_value
|
132
|
+
self._estimatedCORQCB.setFirstGuessAvailable(first_guess_available)
|
133
|
+
|
134
|
+
if first_guess_available:
|
135
|
+
# if the first guess is valid, when the sides visibility is modify we want to activate it.
|
136
|
+
self._estimatedCORQCB.selectFirstGuess()
|
137
|
+
elif valid_sides:
|
138
|
+
# Proceed only if there are valid sides
|
139
|
+
current_side = self._estimatedCORQCB.getCurrentCorValue()
|
140
|
+
if not isinstance(current_side, Side) or current_side not in valid_sides:
|
141
|
+
with block_signals(self._estimatedCORQCB):
|
142
|
+
cor_guess = self._estimatedCORQCB.getCurrentCorValue()
|
143
|
+
if cor_guess is not None and self._imageWidth is not None:
|
144
|
+
left_boundary = -float("inf")
|
145
|
+
right_boundary = float("inf")
|
146
|
+
middle_left_boundary = (
|
147
|
+
self._imageWidth / 3 - self._imageWidth / 2
|
148
|
+
)
|
149
|
+
middle_right_boundary = (
|
150
|
+
2 * self._imageWidth / 3 - self._imageWidth / 2
|
151
|
+
)
|
152
|
+
|
153
|
+
if (
|
154
|
+
Side.LEFT in valid_sides
|
155
|
+
and left_boundary <= cor_guess < middle_left_boundary
|
156
|
+
):
|
157
|
+
new_side = Side.LEFT
|
158
|
+
elif (
|
159
|
+
Side.CENTER in valid_sides
|
160
|
+
and middle_left_boundary
|
161
|
+
<= cor_guess
|
162
|
+
< middle_right_boundary
|
163
|
+
):
|
164
|
+
new_side = Side.CENTER
|
165
|
+
elif (
|
166
|
+
Side.RIGHT in valid_sides
|
167
|
+
and middle_right_boundary <= cor_guess <= right_boundary
|
168
|
+
):
|
169
|
+
new_side = Side.RIGHT
|
170
|
+
else:
|
171
|
+
# Fallback to the first available valid side
|
172
|
+
new_side = valid_sides[0]
|
173
|
+
else:
|
174
|
+
# If no guess or boundaries are available, fallback to the first valid side
|
175
|
+
new_side = valid_sides[0]
|
176
|
+
self._estimatedCORQCB.setCurrentCorValue(new_side)
|
177
|
+
self._axis_params.estimated_cor = new_side
|
178
|
+
|
179
|
+
def _updateEstimatedCorFromMotorOffsetWidget(self):
|
180
|
+
self._estimatedCORQCB.setCurrentCorValue(self._offsetWidget.getEstimatedCor())
|
181
|
+
self.sigValueChanged.emit()
|
182
|
+
|
183
|
+
def setImageWidth(self, image_width: float | None):
|
184
|
+
self._imageWidth = image_width
|
185
|
+
|
186
|
+
# expose API
|
187
|
+
def updateXRotationAxisPixelPositionOnNewScan(self) -> bool:
|
188
|
+
return self._offsetWidget.updateXRotationAxisPixelPositionOnNewScan()
|
189
|
+
|
190
|
+
def setUpdateXRotationAxisPixelPositionOnNewScan(self, update: bool):
|
191
|
+
self._offsetWidget.setUpdateXRotationAxisPixelPositionOnNewScan(update=update)
|
192
|
+
|
193
|
+
def setPixelSize(self, pixel_size_m: float | None) -> None:
|
194
|
+
self._offsetWidget.setPixelSize(pixel_size_m=pixel_size_m)
|
195
|
+
|
196
|
+
def isYAxisInverted(self) -> bool:
|
197
|
+
return self._offsetWidget.isYAxisInverted()
|
198
|
+
|
199
|
+
def setYAxisInverted(self, checked: bool):
|
200
|
+
return self._offsetWidget.setYAxisInverted(checked=checked)
|
201
|
+
|
202
|
+
|
203
|
+
class _OffsetCalibration(qt.QGroupBox):
|
204
|
+
|
205
|
+
sigOffsetChanged = qt.Signal()
|
206
|
+
sigXRotationAxisPixelPositionChanged = qt.Signal()
|
207
|
+
sigUpdateXRotAxisPixelPosOnNewScan = qt.Signal()
|
208
|
+
sigYAxisInvertedChanged = qt.Signal(bool)
|
209
|
+
|
210
|
+
def __init__(self, parent, axis_params: QAxisRP):
|
211
|
+
super().__init__(parent)
|
212
|
+
self._axis_params = axis_params
|
213
|
+
|
214
|
+
self.setLayout(qt.QVBoxLayout())
|
215
|
+
|
216
|
+
# x_rotation_axis_pixel_position
|
217
|
+
self._xRotationAxisPixelPositionGroup = qt.QGroupBox("NXtomo metadata ")
|
218
|
+
self._xRotationAxisPixelPositionGroup.setLayout(qt.QGridLayout())
|
219
|
+
self.layout().addWidget(self._xRotationAxisPixelPositionGroup)
|
220
|
+
self._xRotationAxisPixelPositionLabel = qt.QLabel(
|
221
|
+
"x_rotation_axis_pixel_position"
|
222
|
+
)
|
223
|
+
self._xRotationAxisPixelPositionGroup.layout().addWidget(
|
224
|
+
self._xRotationAxisPixelPositionLabel, 0, 0, 2, 1
|
225
|
+
)
|
226
|
+
self._xRotationAxisPixelPositionDSB = QDoubleSpinBoxIgnoreWheel(self)
|
227
|
+
self._xRotationAxisPixelPositionDSB.setDecimals(2)
|
228
|
+
self._xRotationAxisPixelPositionDSB.setRange(-float("inf"), float("inf"))
|
229
|
+
self._xRotationAxisPixelPositionDSB.setSuffix(" px")
|
230
|
+
self._xRotationAxisPixelPositionDSB.setEnabled(False)
|
231
|
+
self._xRotationAxisPixelPositionGroup.layout().addWidget(
|
232
|
+
self._xRotationAxisPixelPositionDSB, 0, 1, 2, 1
|
233
|
+
)
|
234
|
+
self._xRotationAxisPixelPositionKeepUpdatedCB = qt.QCheckBox(
|
235
|
+
"Update with\n new scan"
|
236
|
+
)
|
237
|
+
self._xRotationAxisPixelPositionKeepUpdatedCB.setFont(FONT_SMALL)
|
238
|
+
self._xRotationAxisPixelPositionKeepUpdatedCB.setToolTip(
|
239
|
+
"Updates the value when a new scan arrives.\n"
|
240
|
+
"Once updated this will change the numerical value of the estimated cor (from estimated relative cor)"
|
241
|
+
)
|
242
|
+
self._xRotationAxisPixelPositionGroup.layout().addWidget(
|
243
|
+
self._xRotationAxisPixelPositionKeepUpdatedCB, 0, 2, 1, 1
|
244
|
+
)
|
245
|
+
self._yAxisInvertedCB = qt.QCheckBox("Y axis inverted")
|
246
|
+
self._yAxisInvertedCB.setFont(FONT_SMALL)
|
247
|
+
self._yAxisInvertedCB.setToolTip(
|
248
|
+
"Sometime the y axis can be inverted (like on ID11).\nIn this case the estimation of the CoR has a flip that must be handled downstream"
|
249
|
+
)
|
250
|
+
self._xRotationAxisPixelPositionGroup.layout().addWidget(
|
251
|
+
self._yAxisInvertedCB, 1, 2, 1, 1
|
252
|
+
)
|
253
|
+
|
254
|
+
# offset group
|
255
|
+
self._offsetGroup = qt.QGroupBox("Custom Offset")
|
256
|
+
self._offsetGroup.setLayout(qt.QFormLayout())
|
257
|
+
self.layout().addWidget(self._offsetGroup)
|
258
|
+
|
259
|
+
# offset
|
260
|
+
self._offsetSB = QDoubleSpinBoxIgnoreWheel(self)
|
261
|
+
self._offsetSB.setSuffix(" px")
|
262
|
+
self._offsetSB.setDecimals(2)
|
263
|
+
self._offsetSB.setRange(-float("inf"), float("inf"))
|
264
|
+
self._offsetSB.setEnabled(False)
|
265
|
+
self._offsetGroup.layout().addRow("Offset", self._offsetSB)
|
266
|
+
|
267
|
+
# motor offset
|
268
|
+
self._motorOffsetSB = QDoubleSpinBoxIgnoreWheel(self)
|
269
|
+
self._motorOffsetSB.setSuffix(" mm")
|
270
|
+
self._motorOffsetSB.setDecimals(4)
|
271
|
+
self._motorOffsetSB.setRange(-float("inf"), float("inf"))
|
272
|
+
self._offsetGroup.layout().addRow("Y Motor Offset", self._motorOffsetSB)
|
273
|
+
|
274
|
+
# pixel size
|
275
|
+
self._pixelSizeSB = QDoubleSpinBoxIgnoreWheel(self)
|
276
|
+
self._pixelSizeSB.setSuffix(" µm")
|
277
|
+
self._pixelSizeSB.setDecimals(3)
|
278
|
+
self._pixelSizeSB.setRange(0, float("inf"))
|
279
|
+
|
280
|
+
# Add a horizontal layout for the pixel size and the padlock
|
281
|
+
pixelSizeLayout = qt.QHBoxLayout()
|
282
|
+
|
283
|
+
# Add the spin box to the layout
|
284
|
+
pixelSizeLayout.addWidget(self._pixelSizeSB)
|
285
|
+
|
286
|
+
# Add a padlock button
|
287
|
+
self._pixelSizePadlock = PadlockButton(self)
|
288
|
+
self._pixelSizePadlock.setChecked(True) # Default to locked
|
289
|
+
self._pixelSizePadlock.setFixedSize(24, 24)
|
290
|
+
self._pixelSizePadlock.setToolTip("Lock/Unlock pixel size")
|
291
|
+
pixelSizeLayout.addWidget(self._pixelSizePadlock)
|
292
|
+
|
293
|
+
# Add the layout to the offset group
|
294
|
+
self._offsetGroup.layout().addRow("Pixel Size", pixelSizeLayout)
|
295
|
+
|
296
|
+
# Initialize the spin box as disabled (locked by default)
|
297
|
+
self._pixelSizeSB.setEnabled(False)
|
298
|
+
|
299
|
+
# Connect signal to handle locking behavior
|
300
|
+
self._pixelSizePadlock.toggled.connect(self._togglePixelSizeLock)
|
301
|
+
|
302
|
+
# set up
|
303
|
+
self._xRotationAxisPixelPositionKeepUpdatedCB.setChecked(True)
|
304
|
+
self._yAxisInvertedCB.setChecked(False)
|
305
|
+
|
306
|
+
# connect signal / slot
|
307
|
+
self._offsetSB.editingFinished.connect(self._offsetEdited)
|
308
|
+
self._xRotationAxisPixelPositionKeepUpdatedCB.toggled.connect(
|
309
|
+
self.sigUpdateXRotAxisPixelPosOnNewScan
|
310
|
+
)
|
311
|
+
self._motorOffsetSB.valueChanged.connect(self._motorOffsetChanged)
|
312
|
+
self._pixelSizeSB.valueChanged.connect(self._pixelSizeChanged)
|
313
|
+
self._yAxisInvertedCB.toggled.connect(self._yAxisInverted)
|
314
|
+
|
315
|
+
def getXRotationAxisPixelPosition(self) -> float:
|
316
|
+
return self._xRotationAxisPixelPositionDSB.value()
|
317
|
+
|
318
|
+
def updateXRotationAxisPixelPositionOnNewScan(self) -> bool:
|
319
|
+
return self._xRotationAxisPixelPositionKeepUpdatedCB.isChecked()
|
320
|
+
|
321
|
+
def setUpdateXRotationAxisPixelPositionOnNewScan(self, update):
|
322
|
+
self._xRotationAxisPixelPositionKeepUpdatedCB.setChecked(update)
|
323
|
+
|
324
|
+
def setXRotationAxisPixelPosition(
|
325
|
+
self, value: float, apply_flip: bool = True
|
326
|
+
) -> float:
|
327
|
+
"""Set the 'x_rotation_axis_pixel_position' and flip it if necessary"""
|
328
|
+
if apply_flip and self.isYAxisInverted():
|
329
|
+
value = -1.0 * value
|
330
|
+
self._xRotationAxisPixelPositionDSB.setValue(value)
|
331
|
+
return value
|
332
|
+
|
333
|
+
def getOffset(self) -> float:
|
334
|
+
return self._offsetSB.value()
|
335
|
+
|
336
|
+
def setOffset(self, value: float) -> None:
|
337
|
+
self._offsetSB.setValue(value)
|
338
|
+
self._offsetEdited()
|
339
|
+
|
340
|
+
def _offsetEdited(self):
|
341
|
+
with block_signals(self):
|
342
|
+
self._axis_params.x_rotation_axis_pos_px_offset = self.getOffset()
|
343
|
+
self.sigOffsetChanged.emit()
|
344
|
+
|
345
|
+
def _motorOffsetChanged(self):
|
346
|
+
"""Recalculate the offset when the motor offset changes."""
|
347
|
+
if self._pixelSizeSB.value() and self._motorOffsetSB.value():
|
348
|
+
pixel_size_m = self._pixelSizeSB.value() * MetricSystem.MICROMETER.value
|
349
|
+
motor_offset_m = self._motorOffsetSB.value() * MetricSystem.MILLIMETER.value
|
350
|
+
with block_signals(self):
|
351
|
+
self.setOffset(motor_offset_m / pixel_size_m)
|
352
|
+
self.sigOffsetChanged.emit()
|
353
|
+
|
354
|
+
def _pixelSizeChanged(self):
|
355
|
+
"""Recalculate the offset when the pixel size changes."""
|
356
|
+
if self._pixelSizeSB.value() and self._motorOffsetSB.value():
|
357
|
+
pixel_size_m = self._pixelSizeSB.value() * MetricSystem.MICROMETER.value
|
358
|
+
motor_offset_m = self._motorOffsetSB.value() * MetricSystem.MILLIMETER.value
|
359
|
+
with block_signals(self):
|
360
|
+
self.setOffset(motor_offset_m / pixel_size_m)
|
361
|
+
self.sigOffsetChanged.emit()
|
362
|
+
|
363
|
+
def _xRotationAxisPixelPositionEdited(self):
|
364
|
+
with block_signals(self._axis_params):
|
365
|
+
self._axis_params.x_rotation_axis_pixel_position = (
|
366
|
+
self.getXRotationAxisPixelPosition()
|
367
|
+
)
|
368
|
+
self.sigXRotationAxisPixelPositionChanged.emit()
|
369
|
+
|
370
|
+
def _yAxisInverted(self):
|
371
|
+
self.setXRotationAxisPixelPosition(
|
372
|
+
value=-1.0 * self.getXRotationAxisPixelPosition(),
|
373
|
+
apply_flip=False,
|
374
|
+
)
|
375
|
+
self._xRotationAxisPixelPositionEdited()
|
376
|
+
|
377
|
+
def isYAxisInverted(self) -> bool:
|
378
|
+
return self._yAxisInvertedCB.isChecked()
|
379
|
+
|
380
|
+
def setYAxisInverted(self, checked: bool):
|
381
|
+
self._yAxisInvertedCB.setChecked(checked)
|
382
|
+
|
383
|
+
def getEstimatedCor(self) -> float:
|
384
|
+
return self.getXRotationAxisPixelPosition() + self.getOffset()
|
385
|
+
|
386
|
+
def setPixelSize(self, pixel_size_m: float | None):
|
387
|
+
if pixel_size_m is None:
|
388
|
+
self._pixelSizeSB.clear()
|
389
|
+
else:
|
390
|
+
self._pixelSizeSB.setValue(pixel_size_m / MetricSystem.MICROMETER.value)
|
391
|
+
|
392
|
+
def _togglePixelSizeLock(self, locked: bool):
|
393
|
+
"""Lock or unlock the pixel size spin box."""
|
394
|
+
self._pixelSizeSB.setEnabled(not locked)
|
@@ -0,0 +1,118 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
from silx.gui import qt
|
4
|
+
from tomwer.core.process.reconstruction.axis.side import Side
|
5
|
+
from tomwer.gui.utils.scrollarea import QComboBoxIgnoreWheel
|
6
|
+
from tomwer.gui.utils.qt_utils import block_signals
|
7
|
+
|
8
|
+
|
9
|
+
class _EstimatedCorValidator(qt.QDoubleValidator):
|
10
|
+
def __init__(self, parent=None):
|
11
|
+
super().__init__(parent)
|
12
|
+
self.setDecimals(3)
|
13
|
+
|
14
|
+
def validate(self, a0: str, a1: int):
|
15
|
+
"""validate float or string that could be part of the side values..."""
|
16
|
+
for value in Side.values():
|
17
|
+
if value.startswith(a0):
|
18
|
+
return (qt.QDoubleValidator.Acceptable, a0, a1)
|
19
|
+
return super().validate(a0, a1)
|
20
|
+
|
21
|
+
|
22
|
+
class EstimatedCorComboBox(QComboBoxIgnoreWheel):
|
23
|
+
"""
|
24
|
+
Combobox that display the sides available according to the cor algorithm (left, right, center, all)
|
25
|
+
and a dedicated item for cor given manually.
|
26
|
+
|
27
|
+
This combobox is also editable and and make sure the 'estimated cor' item is up to date according to the QCombobox current value
|
28
|
+
"""
|
29
|
+
|
30
|
+
ESTIMATED_COR_ITEM_DATA = "estimated_cor"
|
31
|
+
|
32
|
+
sigEstimatedCorChanged = qt.Signal(object)
|
33
|
+
"""Emit when the estimated cor changed. Value can be a float (cor value) or a Side"""
|
34
|
+
|
35
|
+
def __init__(self, parent=None):
|
36
|
+
super().__init__(parent)
|
37
|
+
self.addItem("0.0", self.ESTIMATED_COR_ITEM_DATA)
|
38
|
+
for side in Side:
|
39
|
+
self.addItem(side.value, side)
|
40
|
+
|
41
|
+
self.setValidator(_EstimatedCorValidator())
|
42
|
+
self.setEditable(True)
|
43
|
+
|
44
|
+
self.setToolTip(
|
45
|
+
"""Estimated position of the center of rotation (COR) to be given to the cor algorithms. \n
|
46
|
+
If you don't have a fair estimate you can provide only a side
|
47
|
+
"""
|
48
|
+
)
|
49
|
+
# connect signal / slot
|
50
|
+
self.lineEdit().editingFinished.connect(self._corHasBeenEdited)
|
51
|
+
self.currentIndexChanged.connect(self._corChanged)
|
52
|
+
|
53
|
+
def _corHasBeenEdited(self):
|
54
|
+
current_cor = self.getCurrentCorValue()
|
55
|
+
if isinstance(current_cor, float):
|
56
|
+
# keep the item up to date
|
57
|
+
self._setCorItemValue(current_cor)
|
58
|
+
self.sigEstimatedCorChanged.emit(current_cor)
|
59
|
+
|
60
|
+
def _corChanged(self):
|
61
|
+
self.sigEstimatedCorChanged.emit(self.getCurrentCorValue())
|
62
|
+
|
63
|
+
def getCurrentCorValue(self) -> Side | float:
|
64
|
+
try:
|
65
|
+
return float(self.currentText())
|
66
|
+
except ValueError:
|
67
|
+
return Side(self.currentText())
|
68
|
+
|
69
|
+
def setCurrentCorValue(self, value: float | Side):
|
70
|
+
if isinstance(value, float):
|
71
|
+
self._setCorItemValue(value)
|
72
|
+
else:
|
73
|
+
side = Side(value)
|
74
|
+
item = self.findData(side)
|
75
|
+
self.setCurrentIndex(item)
|
76
|
+
|
77
|
+
def _setCorItemValue(self, value: float):
|
78
|
+
item_index = self.findData(self.ESTIMATED_COR_ITEM_DATA)
|
79
|
+
view = self.view()
|
80
|
+
hidden = view.isRowHidden(item_index)
|
81
|
+
with block_signals(self):
|
82
|
+
self.setItemText(item_index, f"{value:.2f}")
|
83
|
+
view.setRowHidden(item_index, hidden)
|
84
|
+
|
85
|
+
def get_hidden_sides(self) -> tuple[Side]:
|
86
|
+
"""Return all sides currently hidden"""
|
87
|
+
view = self.view()
|
88
|
+
return tuple(
|
89
|
+
filter(
|
90
|
+
lambda side: view.isRowHidden(self.findData(side)),
|
91
|
+
[Side(side) for side in Side],
|
92
|
+
)
|
93
|
+
)
|
94
|
+
|
95
|
+
def setSidesVisible(self, sides: tuple[Side]):
|
96
|
+
"""Set side to be visible to the user"""
|
97
|
+
sides_visibles = tuple([Side(side) for side in sides])
|
98
|
+
view = self.view()
|
99
|
+
for side in Side:
|
100
|
+
item_idx = self.findData(side)
|
101
|
+
view.setRowHidden(item_idx, side not in sides_visibles)
|
102
|
+
|
103
|
+
need_new_current_value = self.getCurrentCorValue() in self.get_hidden_sides()
|
104
|
+
if need_new_current_value:
|
105
|
+
# if the current side used cannot be used fall back to the estimated cor from motor
|
106
|
+
self.setCurrentIndex(self.findData(self.ESTIMATED_COR_ITEM_DATA))
|
107
|
+
|
108
|
+
def setFirstGuessAvailable(self, available: bool):
|
109
|
+
"""
|
110
|
+
For some method (only growing window at the moment) a first guess (estimated cor as a float) cannot be given
|
111
|
+
"""
|
112
|
+
view = self.view()
|
113
|
+
item_index = self.findData(self.ESTIMATED_COR_ITEM_DATA)
|
114
|
+
view.setRowHidden(item_index, not available)
|
115
|
+
|
116
|
+
def selectFirstGuess(self):
|
117
|
+
item_index = self.findData(self.ESTIMATED_COR_ITEM_DATA)
|
118
|
+
self.setCurrentIndex(item_index)
|