nabu 2024.1.10__py3-none-any.whl → 2024.2.0__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.
- nabu/__init__.py +1 -1
- nabu/app/bootstrap.py +2 -3
- nabu/app/cast_volume.py +4 -2
- nabu/app/cli_configs.py +5 -0
- nabu/app/composite_cor.py +1 -1
- nabu/app/create_distortion_map_from_poly.py +5 -6
- nabu/app/diag_to_pix.py +7 -19
- nabu/app/diag_to_rot.py +14 -29
- nabu/app/double_flatfield.py +32 -44
- nabu/app/parse_reconstruction_log.py +3 -0
- nabu/app/reconstruct.py +53 -15
- nabu/app/reconstruct_helical.py +2 -2
- nabu/app/stitching.py +27 -13
- nabu/app/tests/__init__.py +0 -0
- nabu/app/tests/test_reduce_dark_flat.py +4 -1
- nabu/cuda/kernel.py +11 -2
- nabu/cuda/processing.py +2 -2
- nabu/cuda/src/cone.cu +77 -0
- nabu/cuda/src/hierarchical_backproj.cu +271 -0
- nabu/cuda/utils.py +0 -6
- nabu/estimation/alignment.py +5 -19
- nabu/estimation/cor.py +173 -599
- nabu/estimation/cor_sino.py +356 -26
- nabu/estimation/focus.py +63 -11
- nabu/estimation/tests/test_cor.py +124 -58
- nabu/estimation/tests/test_focus.py +6 -6
- nabu/estimation/tilt.py +2 -1
- nabu/estimation/utils.py +5 -33
- nabu/io/__init__.py +1 -1
- nabu/io/cast_volume.py +1 -1
- nabu/io/reader.py +416 -21
- nabu/io/tests/test_readers.py +422 -0
- nabu/io/tests/test_writers.py +1 -102
- nabu/io/writer.py +4 -433
- nabu/opencl/kernel.py +14 -3
- nabu/opencl/processing.py +8 -0
- nabu/pipeline/config_validators.py +5 -2
- nabu/pipeline/datadump.py +12 -5
- nabu/pipeline/estimators.py +162 -188
- nabu/pipeline/fullfield/chunked.py +168 -92
- nabu/pipeline/fullfield/chunked_cuda.py +7 -3
- nabu/pipeline/fullfield/computations.py +2 -7
- nabu/pipeline/fullfield/dataset_validator.py +0 -4
- nabu/pipeline/fullfield/nabu_config.py +37 -13
- nabu/pipeline/fullfield/processconfig.py +22 -13
- nabu/pipeline/fullfield/reconstruction.py +13 -9
- nabu/pipeline/helical/helical_chunked_regridded.py +1 -1
- nabu/pipeline/helical/helical_chunked_regridded_cuda.py +1 -0
- nabu/pipeline/helical/helical_reconstruction.py +1 -1
- nabu/pipeline/params.py +21 -1
- nabu/pipeline/processconfig.py +1 -12
- nabu/pipeline/reader.py +146 -0
- nabu/pipeline/tests/test_estimators.py +44 -72
- nabu/pipeline/utils.py +4 -2
- nabu/pipeline/writer.py +10 -2
- nabu/preproc/ccd_cuda.py +1 -1
- nabu/preproc/ctf.py +14 -7
- nabu/preproc/ctf_cuda.py +2 -3
- nabu/preproc/double_flatfield.py +5 -12
- nabu/preproc/double_flatfield_cuda.py +2 -2
- nabu/preproc/flatfield.py +5 -1
- nabu/preproc/flatfield_cuda.py +5 -1
- nabu/preproc/phase.py +24 -73
- nabu/preproc/phase_cuda.py +5 -8
- nabu/preproc/tests/test_ctf.py +11 -7
- nabu/preproc/tests/test_flatfield.py +67 -122
- nabu/preproc/tests/test_paganin.py +54 -30
- nabu/processing/azim.py +206 -0
- nabu/processing/convolution_cuda.py +1 -1
- nabu/processing/fft_cuda.py +15 -17
- nabu/processing/histogram.py +2 -0
- nabu/processing/histogram_cuda.py +2 -1
- nabu/processing/kernel_base.py +3 -0
- nabu/processing/muladd_cuda.py +1 -0
- nabu/processing/padding_opencl.py +1 -1
- nabu/processing/roll_opencl.py +1 -0
- nabu/processing/rotation_cuda.py +2 -2
- nabu/processing/tests/test_fft.py +17 -10
- nabu/processing/unsharp_cuda.py +1 -1
- nabu/reconstruction/cone.py +104 -40
- nabu/reconstruction/fbp.py +3 -0
- nabu/reconstruction/fbp_base.py +7 -2
- nabu/reconstruction/filtering.py +20 -7
- nabu/reconstruction/filtering_cuda.py +7 -1
- nabu/reconstruction/hbp.py +424 -0
- nabu/reconstruction/mlem.py +99 -0
- nabu/reconstruction/reconstructor.py +2 -0
- nabu/reconstruction/rings_cuda.py +19 -19
- nabu/reconstruction/sinogram_cuda.py +1 -0
- nabu/reconstruction/sinogram_opencl.py +3 -1
- nabu/reconstruction/tests/test_cone.py +10 -5
- nabu/reconstruction/tests/test_deringer.py +7 -6
- nabu/reconstruction/tests/test_fbp.py +124 -10
- nabu/reconstruction/tests/test_filtering.py +13 -11
- nabu/reconstruction/tests/test_halftomo.py +30 -4
- nabu/reconstruction/tests/test_mlem.py +91 -0
- nabu/reconstruction/tests/test_reconstructor.py +8 -3
- nabu/resources/dataset_analyzer.py +142 -92
- nabu/resources/gpu.py +1 -0
- nabu/resources/nxflatfield.py +134 -125
- nabu/resources/templates/id16a_fluo.conf +42 -0
- nabu/resources/tests/test_extract.py +10 -0
- nabu/resources/tests/test_nxflatfield.py +2 -2
- nabu/stitching/alignment.py +80 -24
- nabu/stitching/config.py +105 -68
- nabu/stitching/definitions.py +1 -0
- nabu/stitching/frame_composition.py +68 -60
- nabu/stitching/overlap.py +91 -51
- nabu/stitching/single_axis_stitching.py +32 -0
- nabu/stitching/slurm_utils.py +6 -6
- nabu/stitching/stitcher/__init__.py +0 -0
- nabu/stitching/stitcher/base.py +124 -0
- nabu/stitching/stitcher/dumper/__init__.py +3 -0
- nabu/stitching/stitcher/dumper/base.py +94 -0
- nabu/stitching/stitcher/dumper/postprocessing.py +356 -0
- nabu/stitching/stitcher/dumper/preprocessing.py +60 -0
- nabu/stitching/stitcher/post_processing.py +555 -0
- nabu/stitching/stitcher/pre_processing.py +1068 -0
- nabu/stitching/stitcher/single_axis.py +484 -0
- nabu/stitching/stitcher/stitcher.py +0 -0
- nabu/stitching/stitcher/y_stitcher.py +13 -0
- nabu/stitching/stitcher/z_stitcher.py +45 -0
- nabu/stitching/stitcher_2D.py +278 -0
- nabu/stitching/tests/test_config.py +12 -37
- nabu/stitching/tests/test_frame_composition.py +33 -59
- nabu/stitching/tests/test_overlap.py +149 -7
- nabu/stitching/tests/test_utils.py +1 -1
- nabu/stitching/tests/test_y_preprocessing_stitching.py +132 -0
- nabu/stitching/tests/{test_z_stitching.py → test_z_postprocessing_stitching.py} +167 -561
- nabu/stitching/tests/test_z_preprocessing_stitching.py +431 -0
- nabu/stitching/utils/__init__.py +1 -0
- nabu/stitching/utils/post_processing.py +281 -0
- nabu/stitching/utils/tests/test_post-processing.py +21 -0
- nabu/stitching/{utils.py → utils/utils.py} +79 -52
- nabu/stitching/y_stitching.py +27 -0
- nabu/stitching/z_stitching.py +32 -2281
- nabu/testutils.py +1 -152
- nabu/thirdparty/tomocupy_remove_stripe.py +43 -9
- nabu/utils.py +158 -61
- {nabu-2024.1.10.dist-info → nabu-2024.2.0.dist-info}/METADATA +24 -17
- {nabu-2024.1.10.dist-info → nabu-2024.2.0.dist-info}/RECORD +145 -121
- {nabu-2024.1.10.dist-info → nabu-2024.2.0.dist-info}/WHEEL +1 -1
- nabu/io/tiffwriter_zmm.py +0 -99
- nabu/pipeline/fallback_utils.py +0 -149
- nabu/pipeline/helical/tests/test_accumulator.py +0 -158
- nabu/pipeline/helical/tests/test_pipeline_elements_full.py +0 -355
- nabu/pipeline/helical/tests/test_strategy.py +0 -61
- nabu/pipeline/helical/utils.py +0 -51
- nabu/pipeline/tests/test_chunk_reader.py +0 -74
- {nabu-2024.1.10.dist-info → nabu-2024.2.0.dist-info}/LICENSE +0 -0
- {nabu-2024.1.10.dist-info → nabu-2024.2.0.dist-info}/entry_points.txt +0 -0
- {nabu-2024.1.10.dist-info → nabu-2024.2.0.dist-info}/top_level.txt +0 -0
nabu/estimation/cor_sino.py
CHANGED
@@ -1,30 +1,16 @@
|
|
1
|
-
"""
|
2
|
-
This module provides global definitions and methods to compute COR in extrem
|
3
|
-
Half Acquisition mode
|
4
|
-
"""
|
5
|
-
|
6
|
-
__authors__ = ["C. Nemoz", "H.Payno"]
|
7
|
-
__license__ = "MIT"
|
8
|
-
__date__ = "13/04/2021"
|
9
|
-
|
10
1
|
import numpy as np
|
11
2
|
from scipy.signal import convolve2d
|
3
|
+
from scipy.fft import rfft
|
4
|
+
|
5
|
+
from ..utils import deprecation_warning, is_scalar
|
12
6
|
from ..resources.logger import LoggerOrPrint
|
13
7
|
|
8
|
+
try:
|
9
|
+
from algotom.prep.calculation import find_center_vo, find_center_360
|
14
10
|
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
if val < 0:
|
19
|
-
s = -1.0
|
20
|
-
val = s * val
|
21
|
-
ker[1, 1] = 1 - val
|
22
|
-
if s > 0:
|
23
|
-
ker[1, 2] = val
|
24
|
-
else:
|
25
|
-
ker[1, 0] = val
|
26
|
-
mat = convolve2d(mat, ker, mode="same")
|
27
|
-
return mat
|
11
|
+
__have_algotom__ = True
|
12
|
+
except ImportError:
|
13
|
+
__have_algotom__ = False
|
28
14
|
|
29
15
|
|
30
16
|
class SinoCor:
|
@@ -57,6 +43,21 @@ class SinoCor:
|
|
57
43
|
|
58
44
|
self.window_width = round(self.sx / 5)
|
59
45
|
|
46
|
+
@staticmethod
|
47
|
+
def schift(mat, val):
|
48
|
+
ker = np.zeros((3, 3))
|
49
|
+
s = 1.0
|
50
|
+
if val < 0:
|
51
|
+
s = -1.0
|
52
|
+
val = s * val
|
53
|
+
ker[1, 1] = 1 - val
|
54
|
+
if s > 0:
|
55
|
+
ker[1, 2] = val
|
56
|
+
else:
|
57
|
+
ker[1, 0] = val
|
58
|
+
mat = convolve2d(mat, ker, mode="same")
|
59
|
+
return mat
|
60
|
+
|
60
61
|
def overlap(self, side="right", window_width=None):
|
61
62
|
"""
|
62
63
|
Compute COR by minimizing difference of circulating ROI
|
@@ -90,7 +91,7 @@ class SinoCor:
|
|
90
91
|
imax = i
|
91
92
|
self.cor_abs = self.sx - (imax + window_width + 1.0) / 2.0
|
92
93
|
self.cor_rel = self.sx / 2 - (imax + window_width + 1.0) / 2.0
|
93
|
-
|
94
|
+
elif side == "left":
|
94
95
|
for i in nr:
|
95
96
|
imout = self.data1[:, i : i + window_width] - self.data2[:, self.sx - window_width : self.sx]
|
96
97
|
diff = imout.max() - imout.min()
|
@@ -99,6 +100,8 @@ class SinoCor:
|
|
99
100
|
imax = i
|
100
101
|
self.cor_abs = (imax + window_width - 1.0) / 2
|
101
102
|
self.cor_rel = self.cor_abs - self.sx / 2.0 - 1
|
103
|
+
else:
|
104
|
+
raise ValueError(f"Invalid side given ({side}). should be 'left' or 'right'")
|
102
105
|
if imax < 1:
|
103
106
|
self.logger.warning("sliding width %d seems too large!" % window_width)
|
104
107
|
self.rcor_abs = round(self.cor_abs)
|
@@ -151,7 +154,7 @@ class SinoCor:
|
|
151
154
|
x0 = xc1 + pix
|
152
155
|
for isf in isfr:
|
153
156
|
if isf != 0:
|
154
|
-
ims = schift(self.data1[:, x0 : x0 + xwin].copy(), -p_sign * isf)
|
157
|
+
ims = self.schift(self.data1[:, x0 : x0 + xwin].copy(), -p_sign * isf)
|
155
158
|
else:
|
156
159
|
ims = self.data1[:, x0 : x0 + xwin]
|
157
160
|
|
@@ -175,9 +178,336 @@ class SinoCorInterface:
|
|
175
178
|
def __init__(self, logger=None, **kwargs):
|
176
179
|
self._logger = logger
|
177
180
|
|
178
|
-
def find_shift(
|
181
|
+
def find_shift(
|
182
|
+
self,
|
183
|
+
img_1,
|
184
|
+
img_2,
|
185
|
+
side="right",
|
186
|
+
window_width=None,
|
187
|
+
neighborhood=7,
|
188
|
+
shift_value=0.1,
|
189
|
+
return_relative_to_middle=None,
|
190
|
+
**kwargs,
|
191
|
+
):
|
192
|
+
|
193
|
+
# COMPAT.
|
194
|
+
if return_relative_to_middle is None:
|
195
|
+
deprecation_warning(
|
196
|
+
"The current default behavior is to return the shift relative the the middle of the image. In a future release, this function will return the shift relative to the left-most pixel. To keep the current behavior, please use 'return_relative_to_middle=True'.",
|
197
|
+
do_print=True,
|
198
|
+
func_name="CenterOfRotationCoarseToFine.find_shift",
|
199
|
+
)
|
200
|
+
return_relative_to_middle = True # the kwarg above will be False by default in a future release
|
201
|
+
# ---
|
202
|
+
|
179
203
|
cor_finder = SinoCor(img_1, img_2, logger=self._logger)
|
180
204
|
cor_finder.estimate_cor_coarse(side=side, window_width=window_width)
|
181
205
|
cor = cor_finder.estimate_cor_fine(neighborhood=neighborhood, shift_value=shift_value)
|
182
206
|
# offset will be added later - keep compatibility with result from AlignmentBase.find_shift()
|
183
|
-
|
207
|
+
if return_relative_to_middle:
|
208
|
+
return cor - (img_1.shape[1] - 1) / 2
|
209
|
+
else:
|
210
|
+
return cor
|
211
|
+
|
212
|
+
|
213
|
+
class CenterOfRotationFourierAngles:
|
214
|
+
"""This CoR estimation algo is proposed by V. Valls (BCU). It is based on the Fourier
|
215
|
+
transform of the columns on the sinogram.
|
216
|
+
It requires an initial guesss of the CoR wich is retrieved from
|
217
|
+
dataset_info.dataset_scanner.x_rotation_axis_pixel_position. It is assumed in mm and pixel size in um.
|
218
|
+
Options are (for the moment) hard-coded in the SinoCORFinder.cor_finder.extra_options dict.
|
219
|
+
"""
|
220
|
+
|
221
|
+
def __init__(self, *args, **kwargs):
|
222
|
+
pass
|
223
|
+
|
224
|
+
def _convert_from_fft_2_fftpack_format(self, f_signal, o_signal_length):
|
225
|
+
"""
|
226
|
+
Converts a scipy.fft.rfft into the (legacy) scipy.fftpack.rfft format.
|
227
|
+
The fftpack.rfft returns a (roughly) twice as long array as fft.rfft as the latter returns an array
|
228
|
+
of complex numbers wheras the former returns an array with real and imag parts in consecutive
|
229
|
+
spots in the array.
|
230
|
+
|
231
|
+
Parameters
|
232
|
+
----------
|
233
|
+
f_signal : array_like
|
234
|
+
The output of scipy.fft.rfft(signal)
|
235
|
+
o_signal_length : int
|
236
|
+
Size of the original signal (before FT).
|
237
|
+
|
238
|
+
Returns
|
239
|
+
-------
|
240
|
+
out
|
241
|
+
The rfft converted to the fftpack.rfft format (roughly twice as long).
|
242
|
+
"""
|
243
|
+
out = np.zeros(o_signal_length, dtype=np.float32)
|
244
|
+
if o_signal_length % 2 == 0:
|
245
|
+
out[0] = f_signal[0].real
|
246
|
+
out[1::2] = f_signal[1:].real
|
247
|
+
out[2::2] = f_signal[1:-1].imag
|
248
|
+
else:
|
249
|
+
out[0] = f_signal[0].real
|
250
|
+
out[1::2] = f_signal[1:].real
|
251
|
+
out[2::2] = f_signal[1:].imag
|
252
|
+
return out
|
253
|
+
|
254
|
+
def _freq_radio(self, sinos, ifrom, ito):
|
255
|
+
size = (sinos.shape[0] + sinos.shape[0] % 2) // 2
|
256
|
+
fs = np.empty((size, sinos.shape[1]))
|
257
|
+
for i in range(ifrom, ito):
|
258
|
+
line = sinos[:, i]
|
259
|
+
f_signal = rfft(line)
|
260
|
+
f_signal = self._convert_from_fft_2_fftpack_format(f_signal, line.shape[0])
|
261
|
+
f = np.abs(f_signal[: (f_signal.size - 1) // 2 + 1])
|
262
|
+
f2 = np.abs(f_signal[(f_signal.size - 1) // 2 + 1 :][::-1])
|
263
|
+
if len(f) > len(f2):
|
264
|
+
f[1:] += f2
|
265
|
+
else:
|
266
|
+
f[0:] += f2
|
267
|
+
fs[:, i] = f
|
268
|
+
with np.errstate(divide="ignore", invalid="ignore", under="ignore"):
|
269
|
+
fs = np.log(fs)
|
270
|
+
return fs
|
271
|
+
|
272
|
+
def gaussian(self, p, x):
|
273
|
+
return p[3] + p[2] * np.exp(-((x - p[0]) ** 2) / (2 * p[1] ** 2))
|
274
|
+
|
275
|
+
def tukey(self, p, x):
|
276
|
+
pos, std, alpha, height, background = p
|
277
|
+
alpha = np.clip(alpha, 0, 1)
|
278
|
+
pi = np.pi
|
279
|
+
inv_alpha = 1 - alpha
|
280
|
+
width = std / (1 - alpha * 0.5)
|
281
|
+
xx = (np.abs(x - pos) - (width * 0.5 * inv_alpha)) / (width * 0.5 * alpha)
|
282
|
+
xx = np.clip(xx, 0, 1)
|
283
|
+
return (0.5 + np.cos(pi * xx) * 0.5) * height + background
|
284
|
+
|
285
|
+
def sinlet(self, p, x):
|
286
|
+
std = p[1] * 2.5
|
287
|
+
lin = np.maximum(0, std - np.abs(p[0] - x)) * 0.5 * np.pi / std
|
288
|
+
return p[3] + p[2] * np.sin(lin)
|
289
|
+
|
290
|
+
def _px(self, detector_width, abs_pos, near_width, near_std, crop_around_cor, near_step):
|
291
|
+
sym_range = None
|
292
|
+
if abs_pos is not None:
|
293
|
+
if crop_around_cor:
|
294
|
+
sym_range = int(abs_pos - near_std * 2), int(abs_pos + near_std * 2)
|
295
|
+
|
296
|
+
window = near_width
|
297
|
+
if sym_range is not None:
|
298
|
+
xx_from = max(window, sym_range[0])
|
299
|
+
xx_to = max(xx_from, min(detector_width - window, sym_range[1]))
|
300
|
+
if xx_from == xx_to:
|
301
|
+
sym_range = None
|
302
|
+
if sym_range is None:
|
303
|
+
xx_from = window
|
304
|
+
xx_to = detector_width - window
|
305
|
+
|
306
|
+
xx = np.arange(xx_from, xx_to, near_step)
|
307
|
+
|
308
|
+
return xx
|
309
|
+
|
310
|
+
def _symmetry_correlation(self, px, array, angles, window, shift_sino):
|
311
|
+
if shift_sino:
|
312
|
+
shift_index = np.argmin(np.abs(angles - np.pi)) - np.argmin(np.abs(angles - 0))
|
313
|
+
else:
|
314
|
+
shift_index = None
|
315
|
+
px_from = int(px[0])
|
316
|
+
px_to = int(np.ceil(px[-1]))
|
317
|
+
f_coef = np.empty(len(px))
|
318
|
+
f_array = self._freq_radio(array, px_from - window, px_to + window)
|
319
|
+
if shift_index is not None:
|
320
|
+
shift_array = np.empty(array.shape, dtype=array.dtype)
|
321
|
+
shift_array[0 : len(shift_array) - shift_index, :] = array[shift_index:, :]
|
322
|
+
shift_array[len(shift_array) - shift_index :, :] = array[:shift_index, :]
|
323
|
+
f_shift_array = self._freq_radio(shift_array, px_from - window, px_to + window)
|
324
|
+
else:
|
325
|
+
f_shift_array = f_array
|
326
|
+
|
327
|
+
for j, x in enumerate(px):
|
328
|
+
i = int(np.floor(x))
|
329
|
+
if x - i > 0.4: # TO DO : Specific to near_step = 0.5?
|
330
|
+
f_left = f_array[:, i - window : i]
|
331
|
+
f_right = f_shift_array[:, i + 1 : i + window + 1][:, ::-1]
|
332
|
+
else:
|
333
|
+
f_left = f_array[:, i - window : i]
|
334
|
+
f_right = f_shift_array[:, i : i + window][:, ::-1]
|
335
|
+
with np.errstate(divide="ignore", invalid="ignore"):
|
336
|
+
f_coef[j] = np.sum(np.abs(f_left - f_right))
|
337
|
+
return f_coef
|
338
|
+
|
339
|
+
def _cor_correlation(self, px, abs_pos, near_std, signal, near_weight, near_alpha):
|
340
|
+
if abs_pos is not None:
|
341
|
+
if signal == "sinlet":
|
342
|
+
coef = self.sinlet((abs_pos, near_std, -near_weight, 1), px)
|
343
|
+
elif signal == "gaussian":
|
344
|
+
coef = self.gaussian((abs_pos, near_std, -near_weight, 1), px)
|
345
|
+
elif signal == "tukey":
|
346
|
+
coef = self.tukey((abs_pos, near_std * 2, near_alpha, -near_weight, 1), px)
|
347
|
+
else:
|
348
|
+
raise ValueError("Shape unsupported")
|
349
|
+
else:
|
350
|
+
coef = np.ones_like(px)
|
351
|
+
return coef
|
352
|
+
|
353
|
+
def find_shift(
|
354
|
+
self,
|
355
|
+
sino,
|
356
|
+
angles=None,
|
357
|
+
side="center",
|
358
|
+
near_std=100,
|
359
|
+
near_width=20,
|
360
|
+
shift_sino=True,
|
361
|
+
crop_around_cor=False,
|
362
|
+
signal="tukey",
|
363
|
+
near_weight=0.1,
|
364
|
+
near_alpha=0.5,
|
365
|
+
near_step=0.5,
|
366
|
+
return_relative_to_middle=None,
|
367
|
+
):
|
368
|
+
detector_width = sino.shape[1]
|
369
|
+
|
370
|
+
# COMPAT.
|
371
|
+
if return_relative_to_middle is None:
|
372
|
+
deprecation_warning(
|
373
|
+
"The current default behavior is to return the shift relative the the middle of the image. In a future release, this function will return the shift relative to the left-most pixel. To keep the current behavior, please use 'return_relative_to_middle=True'.",
|
374
|
+
do_print=True,
|
375
|
+
func_name="CenterOfRotationFourierAngles.find_shift",
|
376
|
+
)
|
377
|
+
return_relative_to_middle = True # the kwarg above will be False by default in a future release
|
378
|
+
# ---
|
379
|
+
|
380
|
+
if angles is None:
|
381
|
+
angles = np.linspace(0, 2 * np.pi, sino.shape[0], endpoint=True)
|
382
|
+
increment = np.abs(angles[0] - angles[1])
|
383
|
+
if np.abs(angles[0] - angles[-1]) < (360 - 0.5) * np.pi / 180 - increment:
|
384
|
+
raise ValueError("Not enough angles, estimator skipped")
|
385
|
+
|
386
|
+
if is_scalar(side):
|
387
|
+
abs_pos = side
|
388
|
+
# COMPAT.
|
389
|
+
elif side == "near":
|
390
|
+
deprecation_warning(
|
391
|
+
"side='near' is deprecated, please use side=<a scalar>", do_print=True, func_name="fourier_angles_near"
|
392
|
+
)
|
393
|
+
abs_pos = detector_width // 2
|
394
|
+
##.
|
395
|
+
elif side == "center":
|
396
|
+
abs_pos = detector_width // 2
|
397
|
+
elif side == "left":
|
398
|
+
abs_pos = detector_width // 4
|
399
|
+
elif side == "right":
|
400
|
+
abs_pos = detector_width * 3 // 4
|
401
|
+
else:
|
402
|
+
raise ValueError(f"side '{side}' is not handled")
|
403
|
+
|
404
|
+
px = self._px(detector_width, abs_pos, near_width, near_std, crop_around_cor, near_step)
|
405
|
+
|
406
|
+
coef_f = self._symmetry_correlation(
|
407
|
+
px,
|
408
|
+
sino,
|
409
|
+
angles,
|
410
|
+
near_width,
|
411
|
+
shift_sino,
|
412
|
+
)
|
413
|
+
coef_p = self._cor_correlation(px, abs_pos, near_std, signal, near_weight, near_alpha)
|
414
|
+
coef = coef_f * coef_p
|
415
|
+
|
416
|
+
if len(px) > 0:
|
417
|
+
cor = px[np.argmin(coef)] - (detector_width - 1) / 2
|
418
|
+
else:
|
419
|
+
# raise ValueError ?
|
420
|
+
cor = None
|
421
|
+
if not (return_relative_to_middle):
|
422
|
+
cor += (detector_width - 1) / 2
|
423
|
+
return cor
|
424
|
+
|
425
|
+
__call__ = find_shift
|
426
|
+
|
427
|
+
|
428
|
+
class CenterOfRotationVo:
|
429
|
+
"""
|
430
|
+
A wrapper around algotom 'find_center_vo' and 'find_center_360'.
|
431
|
+
|
432
|
+
Nghia T. Vo, Michael Drakopoulos, Robert C. Atwood, and Christina Reinhard,
|
433
|
+
"Reliable method for calculating the center of rotation in parallel-beam tomography,"
|
434
|
+
Opt. Express 22, 19078-19086 (2014)
|
435
|
+
"""
|
436
|
+
|
437
|
+
default_extra_options = {}
|
438
|
+
|
439
|
+
def __init__(self, logger=None, verbose=False, extra_options=None):
|
440
|
+
if not (__have_algotom__):
|
441
|
+
raise ImportError("Need the 'algotom' package")
|
442
|
+
self.extra_options = self.default_extra_options.copy()
|
443
|
+
self.extra_options.update(extra_options or {})
|
444
|
+
|
445
|
+
def find_shift(
|
446
|
+
self,
|
447
|
+
sino,
|
448
|
+
halftomo=False,
|
449
|
+
is_360=False,
|
450
|
+
win_width=100,
|
451
|
+
side="center",
|
452
|
+
search_width_fraction=0.1,
|
453
|
+
step=0.25,
|
454
|
+
radius=4,
|
455
|
+
ratio=0.5,
|
456
|
+
dsp=True,
|
457
|
+
ncore=None,
|
458
|
+
hor_drop=None,
|
459
|
+
ver_drop=None,
|
460
|
+
denoise=True,
|
461
|
+
norm=True,
|
462
|
+
use_overlap=False,
|
463
|
+
return_relative_to_middle=None,
|
464
|
+
):
|
465
|
+
# COMPAT.
|
466
|
+
if return_relative_to_middle is None:
|
467
|
+
deprecation_warning(
|
468
|
+
"The current default behavior is to return the shift relative the the middle of the image. In a future release, this function will return the shift relative to the left-most pixel. To keep the current behavior, please use 'return_relative_to_middle=True'.",
|
469
|
+
do_print=True,
|
470
|
+
func_name="CenterOfRotationVo.find_shift",
|
471
|
+
)
|
472
|
+
return_relative_to_middle = True # the kwarg above will be False by default in a future release
|
473
|
+
# ---
|
474
|
+
|
475
|
+
if halftomo:
|
476
|
+
side_algotom = {"left": 0, "right": 1}.get(side, None)
|
477
|
+
cor, _, _, _ = find_center_360(
|
478
|
+
sino, win_width, side=side_algotom, denoise=denoise, norm=norm, use_overlap=use_overlap, ncore=ncore
|
479
|
+
)
|
480
|
+
else:
|
481
|
+
if is_360 and not (halftomo):
|
482
|
+
# Take only one part of the sinogram and use "find_center_vo" - this works better in this case
|
483
|
+
sino = sino[: sino.shape[0] // 2]
|
484
|
+
|
485
|
+
sino_width = sino.shape[-1]
|
486
|
+
search_width = int(search_width_fraction * sino_width)
|
487
|
+
|
488
|
+
if side == "left":
|
489
|
+
start, stop = 0, search_width
|
490
|
+
elif side == "center":
|
491
|
+
start, stop = sino_width // 2 - search_width, sino_width // 2 + search_width
|
492
|
+
elif side == "right":
|
493
|
+
start, stop = sino_width - search_width, sino_width
|
494
|
+
elif is_scalar(side):
|
495
|
+
# side is passed as an offset from the middle of detector
|
496
|
+
side = side + (sino.shape[-1] - 1) / 2.0
|
497
|
+
start, stop = max(0, side - search_width), min(sino_width, side + search_width)
|
498
|
+
else:
|
499
|
+
raise ValueError("Expected 'side' to be 'left', 'center', 'right' or a scalar")
|
500
|
+
|
501
|
+
cor = find_center_vo(
|
502
|
+
sino,
|
503
|
+
start=start,
|
504
|
+
stop=stop,
|
505
|
+
step=step,
|
506
|
+
radius=radius,
|
507
|
+
ratio=ratio,
|
508
|
+
dsp=dsp,
|
509
|
+
ncore=ncore,
|
510
|
+
hor_drop=hor_drop,
|
511
|
+
ver_drop=ver_drop,
|
512
|
+
)
|
513
|
+
return cor if not (return_relative_to_middle) else cor - (sino.shape[1] - 1) / 2
|
nabu/estimation/focus.py
CHANGED
@@ -1,10 +1,54 @@
|
|
1
1
|
import numpy as np
|
2
2
|
|
3
|
+
from scipy.fft import fftn
|
4
|
+
|
5
|
+
from ..processing.azim import azimuthal_integration_skimage_stack, azimuthal_integration_imagej_stack, __have_skimage__
|
3
6
|
from .alignment import plt
|
4
7
|
from .cor import CenterOfRotation
|
5
8
|
|
6
9
|
|
7
10
|
class CameraFocus(CenterOfRotation):
|
11
|
+
|
12
|
+
def _check_position_jitter(self, img_pos):
|
13
|
+
pos_diff = np.diff(img_pos)
|
14
|
+
if np.any(pos_diff <= 0):
|
15
|
+
self.logger.warning(
|
16
|
+
"Image position regressed throughout scan! (negative movement for some image positions)"
|
17
|
+
)
|
18
|
+
|
19
|
+
@staticmethod
|
20
|
+
def _gradient(x, axes):
|
21
|
+
d = [None] * len(axes)
|
22
|
+
for ii in range(len(axes)):
|
23
|
+
ind = -(ii + 1)
|
24
|
+
padding = [(0, 0)] * len(x.shape)
|
25
|
+
padding[ind] = (0, 1)
|
26
|
+
temp_x = np.pad(x, padding, mode="constant")
|
27
|
+
d[ind] = np.diff(temp_x, n=1, axis=ind)
|
28
|
+
return np.stack(d, axis=0)
|
29
|
+
|
30
|
+
@staticmethod
|
31
|
+
def _compute_metric_value(data, metric, axes=(-2, -1)):
|
32
|
+
if metric.lower() == "std":
|
33
|
+
return np.std(data, axis=axes) / np.mean(data, axis=axes)
|
34
|
+
elif metric.lower() == "grad":
|
35
|
+
grad_data = CameraFocus._gradient(data, axes=axes)
|
36
|
+
grad_mag = np.sqrt(np.sum(grad_data**2, axis=0))
|
37
|
+
return np.sum(grad_mag, axis=axes)
|
38
|
+
elif metric.lower() == "psd":
|
39
|
+
f_data = fftn(data, axes=axes, workers=4)
|
40
|
+
f_data = np.fft.fftshift(f_data, axes=(-2, -1))
|
41
|
+
# octave-fasttomo3 uses |.|^2, probably with scaled FFT (norm="forward" in python),
|
42
|
+
# but tests show that it's less accurate.
|
43
|
+
f_data = np.abs(f_data)
|
44
|
+
ai_func = azimuthal_integration_skimage_stack if __have_skimage__ else azimuthal_integration_imagej_stack
|
45
|
+
az_data = ai_func(f_data, n_threads=4)
|
46
|
+
max_vals = np.max(az_data, axis=0)
|
47
|
+
az_data /= max_vals[None, :]
|
48
|
+
return np.mean(az_data, axis=-1)
|
49
|
+
else:
|
50
|
+
raise ValueError("Unknown metric function %s" % metric)
|
51
|
+
|
8
52
|
def find_distance(
|
9
53
|
self,
|
10
54
|
img_stack: np.ndarray,
|
@@ -76,6 +120,7 @@ class CameraFocus(CenterOfRotation):
|
|
76
120
|
is the associated image position (starting from 1).
|
77
121
|
"""
|
78
122
|
self._check_img_stack_size(img_stack, img_pos)
|
123
|
+
self._check_position_jitter(img_pos)
|
79
124
|
|
80
125
|
if peak_fit_radius < 1:
|
81
126
|
self.logger.warning("Parameter peak_fit_radius should be at least 1, given: %d instead." % peak_fit_radius)
|
@@ -93,19 +138,25 @@ class CameraFocus(CenterOfRotation):
|
|
93
138
|
high_pass=high_pass,
|
94
139
|
)
|
95
140
|
|
96
|
-
|
141
|
+
img_resp = self._compute_metric_value(img_stack, metric=metric, axes=(-2, -1))
|
97
142
|
|
98
|
-
# assuming images are equispaced
|
143
|
+
# assuming images are equispaced!
|
144
|
+
# focus_step = np.mean(np.abs(np.diff(img_pos)))
|
99
145
|
focus_step = (img_pos[-1] - img_pos[0]) / (num_imgs - 1)
|
100
146
|
|
101
147
|
img_inds = np.arange(num_imgs)
|
102
|
-
(f_vals, f_pos) = self.extract_peak_regions_1d(
|
103
|
-
focus_ind,
|
148
|
+
(f_vals, f_pos) = self.extract_peak_regions_1d(img_resp, peak_radius=peak_fit_radius, cc_coords=img_inds)
|
149
|
+
focus_ind, img_resp_max = self.refine_max_position_1d(f_vals, return_vertex_val=True, return_all_coeffs=True)
|
104
150
|
focus_ind += f_pos[1, :]
|
105
151
|
|
106
152
|
focus_pos = img_pos[0] + focus_step * focus_ind
|
107
153
|
focus_ind += 1
|
108
154
|
|
155
|
+
if focus_pos.size == 1:
|
156
|
+
focus_pos = focus_pos[0]
|
157
|
+
if focus_ind.size == 1:
|
158
|
+
focus_ind = focus_ind[0]
|
159
|
+
|
109
160
|
if self.verbose:
|
110
161
|
self.logger.info(
|
111
162
|
"Fitted focus motor position:",
|
@@ -115,9 +166,9 @@ class CameraFocus(CenterOfRotation):
|
|
115
166
|
)
|
116
167
|
f, ax = plt.subplots(1, 1)
|
117
168
|
self._add_plot_window(f, ax=ax)
|
118
|
-
ax.stem(img_pos,
|
119
|
-
ax.stem(focus_pos,
|
120
|
-
ax.set_title("Images
|
169
|
+
ax.stem(img_pos, img_resp)
|
170
|
+
ax.stem(focus_pos, img_resp_max, linefmt="C1-", markerfmt="C1o")
|
171
|
+
ax.set_title("Images response (metric: %s)" % metric)
|
121
172
|
plt.show(block=False)
|
122
173
|
|
123
174
|
return focus_pos, focus_ind
|
@@ -272,6 +323,7 @@ class CameraFocus(CenterOfRotation):
|
|
272
323
|
... img_stack, img_pos, roi_yxhw=img_roi, regions_number=regions_number)
|
273
324
|
"""
|
274
325
|
self._check_img_stack_size(img_stack, img_pos)
|
326
|
+
self._check_position_jitter(img_pos)
|
275
327
|
|
276
328
|
if peak_fit_radius < 1:
|
277
329
|
self.logger.warning("Parameter peak_fit_radius should be at least 1, given: %d instead." % peak_fit_radius)
|
@@ -306,15 +358,15 @@ class CameraFocus(CenterOfRotation):
|
|
306
358
|
)
|
307
359
|
img_stack = np.reshape(img_stack, block_stack_size)
|
308
360
|
|
309
|
-
|
310
|
-
|
361
|
+
img_resp = self._compute_metric_value(img_stack, metric=metric, axes=(-3, -1))
|
362
|
+
img_resp = np.reshape(img_resp, [num_imgs, -1]).transpose()
|
311
363
|
|
312
364
|
# assuming images are equispaced
|
313
365
|
focus_step = (img_pos[-1] - img_pos[0]) / (num_imgs - 1)
|
314
366
|
|
315
367
|
img_inds = np.arange(num_imgs)
|
316
|
-
(f_vals, f_pos) = self.extract_peak_regions_1d(
|
317
|
-
focus_inds = self.refine_max_position_1d(f_vals)
|
368
|
+
(f_vals, f_pos) = self.extract_peak_regions_1d(img_resp, peak_radius=peak_fit_radius, cc_coords=img_inds)
|
369
|
+
focus_inds = self.refine_max_position_1d(f_vals, return_all_coeffs=True)
|
318
370
|
focus_inds += f_pos[1, :]
|
319
371
|
|
320
372
|
focus_poss = img_pos[0] + focus_step * focus_inds
|