nabu 2024.1.0rc1__py3-none-any.whl → 2024.1.0rc3__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.
- doc/doc_config.py +32 -0
- nabu/__init__.py +1 -1
- nabu/app/bootstrap_stitching.py +1 -1
- nabu/app/cli_configs.py +5 -0
- nabu/app/composite_cor.py +23 -10
- nabu/app/multicor.py +0 -1
- nabu/app/reduce_dark_flat.py +5 -2
- nabu/app/stitching.py +16 -2
- nabu/estimation/cor.py +169 -73
- nabu/estimation/tests/test_cor.py +27 -18
- nabu/io/writer.py +5 -3
- nabu/pipeline/estimators.py +72 -30
- nabu/pipeline/fullfield/chunked.py +5 -1
- nabu/pipeline/fullfield/computations.py +2 -2
- nabu/pipeline/fullfield/nabu_config.py +4 -4
- nabu/pipeline/helical/gridded_accumulator.py +22 -4
- nabu/pipeline/helical/helical_chunked_regridded.py +9 -4
- nabu/pipeline/helical/tests/test_accumulator.py +1 -0
- nabu/pipeline/params.py +3 -0
- nabu/pipeline/tests/test_estimators.py +9 -11
- nabu/pipeline/writer.py +1 -0
- nabu/stitching/config.py +2 -2
- nabu/stitching/z_stitching.py +61 -38
- {nabu-2024.1.0rc1.dist-info → nabu-2024.1.0rc3.dist-info}/METADATA +2 -1
- {nabu-2024.1.0rc1.dist-info → nabu-2024.1.0rc3.dist-info}/RECORD +29 -29
- nabu/app/tests/__init__.py +0 -0
- {nabu-2024.1.0rc1.dist-info → nabu-2024.1.0rc3.dist-info}/LICENSE +0 -0
- {nabu-2024.1.0rc1.dist-info → nabu-2024.1.0rc3.dist-info}/WHEEL +0 -0
- {nabu-2024.1.0rc1.dist-info → nabu-2024.1.0rc3.dist-info}/entry_points.txt +0 -0
- {nabu-2024.1.0rc1.dist-info → nabu-2024.1.0rc3.dist-info}/top_level.txt +0 -0
doc/doc_config.py
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
#!/usr/bin/env python
|
2
|
+
|
3
|
+
from nabu.resources.nabu_config import nabu_config
|
4
|
+
|
5
|
+
|
6
|
+
def generate(file_):
|
7
|
+
def write(content):
|
8
|
+
print(content, file=file_)
|
9
|
+
for section, values in nabu_config.items():
|
10
|
+
if section == "about":
|
11
|
+
continue
|
12
|
+
write("## %s\n" % section)
|
13
|
+
for key, val in values.items():
|
14
|
+
if val["type"] == "unsupported":
|
15
|
+
continue
|
16
|
+
write(val["help"] + "\n")
|
17
|
+
write(
|
18
|
+
"```ini\n%s = %s\n```"
|
19
|
+
% (key, val["default"])
|
20
|
+
)
|
21
|
+
|
22
|
+
|
23
|
+
|
24
|
+
if __name__ == "__main__":
|
25
|
+
|
26
|
+
import sys, os
|
27
|
+
print(os.path.abspath(__file__))
|
28
|
+
exit(0)
|
29
|
+
|
30
|
+
fname = "/tmp/test.md"
|
31
|
+
with open(fname, "w") as f:
|
32
|
+
generate(f)
|
nabu/__init__.py
CHANGED
nabu/app/bootstrap_stitching.py
CHANGED
@@ -43,7 +43,7 @@ def guess_tomo_objects(my_str: str) -> tuple:
|
|
43
43
|
def bootstrap_stitching():
|
44
44
|
args = parse_params_values(
|
45
45
|
BootstrapStitchingConfig,
|
46
|
-
parser_description="Initialize a nabu configuration file",
|
46
|
+
parser_description="Initialize a 'nabu-stitching' configuration file",
|
47
47
|
)
|
48
48
|
|
49
49
|
prefilled_values = {}
|
nabu/app/cli_configs.py
CHANGED
@@ -329,6 +329,11 @@ StitchingConfig = {
|
|
329
329
|
"help": "Logging level. Can be 'debug', 'info', 'warning', 'error'. Default is 'info'.",
|
330
330
|
"default": "info",
|
331
331
|
},
|
332
|
+
"--only-create-master-file": {
|
333
|
+
"help": "Will create the master file with all sub files (volumes or scans). It expects the processing to be finished. It can happen if all slurm job have been submitted but you've been kicked out of the cluster of if you need to relaunch manually some failling job slurm for any reason",
|
334
|
+
"default": False,
|
335
|
+
"action": "store_true",
|
336
|
+
},
|
332
337
|
}
|
333
338
|
|
334
339
|
# Default configuration for "stitching-bootstrap" command
|
nabu/app/composite_cor.py
CHANGED
@@ -5,8 +5,9 @@ import numpy as np
|
|
5
5
|
import re
|
6
6
|
|
7
7
|
from nabu.resources.dataset_analyzer import HDF5DatasetAnalyzer
|
8
|
-
from nabu.pipeline.estimators import CompositeCOREstimator
|
8
|
+
from nabu.pipeline.estimators import CompositeCOREstimator, estimate_cor
|
9
9
|
from nabu.resources.nxflatfield import update_dataset_info_flats_darks
|
10
|
+
from nabu.resources.utils import extract_parameters
|
10
11
|
from nxtomo.application.nxtomo import NXtomo
|
11
12
|
from .. import version
|
12
13
|
from .cli_configs import CompositeCorConfig
|
@@ -84,15 +85,27 @@ def composite_cor_entry_point(args_dict):
|
|
84
85
|
dataset_info = HDF5DatasetAnalyzer(nexus_name, extra_options={"h5_entry": args.entry_name})
|
85
86
|
update_dataset_info_flats_darks(dataset_info, flatfield_mode=1)
|
86
87
|
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
88
|
+
### JL start ###
|
89
|
+
# Extract CoR parameters from configuration file
|
90
|
+
try:
|
91
|
+
cor_options = extract_parameters(args.cor_options, sep=";")
|
92
|
+
except Exception as exc:
|
93
|
+
msg = "Could not extract parameters from cor_options: %s" % (str(exc))
|
94
|
+
raise ValueError(msg)
|
95
|
+
### JL end ###
|
96
|
+
|
97
|
+
# JL start ###
|
98
|
+
#: next bit will be done in estimate
|
99
|
+
# if "near_pos" not in args.cor_options:
|
100
|
+
# scan = NXtomo()
|
101
|
+
# scan.load(file_path=nexus_name, data_path=args.entry_name)
|
102
|
+
# estimated_near = scan.instrument.detector.estimated_cor_from_motor
|
103
|
+
#
|
104
|
+
# cor_options = args.cor_options + f" ; near_pos = {estimated_near} "
|
105
|
+
#
|
106
|
+
# else:
|
107
|
+
# cor_options = args.cor_options
|
108
|
+
### JL end ###
|
96
109
|
|
97
110
|
cor_finder = CompositeCOREstimator(
|
98
111
|
dataset_info,
|
nabu/app/multicor.py
CHANGED
nabu/app/reduce_dark_flat.py
CHANGED
@@ -10,7 +10,7 @@ from .. import version
|
|
10
10
|
from tomoscan.framereducer.method import ReduceMethod
|
11
11
|
from tomoscan.scanbase import TomoScanBase
|
12
12
|
from tomoscan.esrf.scan.edfscan import EDFTomoScan
|
13
|
-
from tomoscan.
|
13
|
+
from tomoscan.factory import Factory
|
14
14
|
from silx.io.url import DataUrl
|
15
15
|
|
16
16
|
|
@@ -153,6 +153,9 @@ def reduce_dark_flat(
|
|
153
153
|
|
154
154
|
|
155
155
|
def main(argv=None):
|
156
|
+
"""
|
157
|
+
Compute reduce dark(s) and flat(s) of a dataset
|
158
|
+
"""
|
156
159
|
if argv is None:
|
157
160
|
argv = sys.argv[1:]
|
158
161
|
|
@@ -163,7 +166,7 @@ def main(argv=None):
|
|
163
166
|
user_args=argv,
|
164
167
|
)
|
165
168
|
|
166
|
-
scan =
|
169
|
+
scan = Factory.create_scan_object(args["dataset"], entry=args["entry"])
|
167
170
|
exit(
|
168
171
|
reduce_dark_flat(
|
169
172
|
scan=scan,
|
nabu/app/stitching.py
CHANGED
@@ -31,7 +31,21 @@ def main():
|
|
31
31
|
|
32
32
|
stitching_config = dict_to_config_obj(conf_dict)
|
33
33
|
stitching_config.settle_inputs()
|
34
|
-
if
|
34
|
+
if args["only_create_master_file"]:
|
35
|
+
# option to ease creation of the master in the following cases:
|
36
|
+
# * user has submitted all the job but has been quicked out of the cluster
|
37
|
+
# * only a few slurm job for some random version (cluster update...) and user want to retriger only those job and process the aggragation only. On those cases no need to redo it all.
|
38
|
+
tomo_objs = []
|
39
|
+
for _, sub_config in split_stitching_configuration_to_slurm_job(stitching_config, yield_configuration=True):
|
40
|
+
tomo_objs.append(sub_config.get_output_object().get_identifier().to_str())
|
41
|
+
|
42
|
+
post_processing = StitchingPostProcAggregation(
|
43
|
+
existing_objs_ids=tomo_objs,
|
44
|
+
stitching_config=stitching_config,
|
45
|
+
)
|
46
|
+
post_processing.process()
|
47
|
+
|
48
|
+
elif stitching_config.slurm_config.partition in (None, ""):
|
35
49
|
# case 1: run locally
|
36
50
|
_logger.info(f"run stitching locally with {stitching_config}")
|
37
51
|
|
@@ -60,7 +74,7 @@ def main():
|
|
60
74
|
# 2.2 wait for future to be done and concatenate the result
|
61
75
|
post_processing = StitchingPostProcAggregation(
|
62
76
|
futures=futures,
|
63
|
-
stitching_config=stitching_config
|
77
|
+
stitching_config=stitching_config,
|
64
78
|
)
|
65
79
|
post_processing.process()
|
66
80
|
|
nabu/estimation/cor.py
CHANGED
@@ -133,7 +133,9 @@ class CenterOfRotation(AlignmentBase):
|
|
133
133
|
|
134
134
|
if isinstance(self.cor_options.get("near_pos", None), (int, float)):
|
135
135
|
near_pos = self.cor_options["near_pos"]
|
136
|
-
if
|
136
|
+
if (
|
137
|
+
np.abs(near_pos - estimated_cor) / near_pos > 0.2
|
138
|
+
): # For comparison, near_pos is RELATIVE (as estimated_cor is).
|
137
139
|
validity_check_result = cor_result_validity["questionable"]
|
138
140
|
else:
|
139
141
|
validity_check_result = cor_result_validity["sound"]
|
@@ -266,8 +268,8 @@ class CenterOfRotationSlidingWindow(CenterOfRotation):
|
|
266
268
|
)
|
267
269
|
img_shape = img_2.shape
|
268
270
|
|
269
|
-
near_pos = self.cor_options.get("near_pos",
|
270
|
-
if near_pos
|
271
|
+
near_pos = self.cor_options.get("near_pos", None)
|
272
|
+
if near_pos is None:
|
271
273
|
if window_width is None:
|
272
274
|
if side.lower() == "center":
|
273
275
|
window_width = round(img_shape[-1] / 4.0 * 3.0)
|
@@ -280,7 +282,8 @@ class CenterOfRotationSlidingWindow(CenterOfRotation):
|
|
280
282
|
# number of pixels where the window will "slide".
|
281
283
|
n = img_shape[-1] - window_width
|
282
284
|
else:
|
283
|
-
|
285
|
+
abs_pos = near_pos + img_shape[-1] // 2
|
286
|
+
offset = min(img_shape[-1] - abs_pos, abs_pos) # distance to closest edge.
|
284
287
|
|
285
288
|
window_fraction = 0.8 # Hard-coded ?
|
286
289
|
window_shift = int(np.floor(offset * window_fraction))
|
@@ -288,7 +291,7 @@ class CenterOfRotationSlidingWindow(CenterOfRotation):
|
|
288
291
|
|
289
292
|
sliding_shift = int(np.floor(offset * (1 - window_fraction))) - 1
|
290
293
|
n = 2 * sliding_shift + 1
|
291
|
-
win_1_start_seed =
|
294
|
+
win_1_start_seed = abs_pos - window_shift - sliding_shift
|
292
295
|
|
293
296
|
if side.lower() == "right":
|
294
297
|
win_2_start = 0
|
@@ -904,39 +907,19 @@ class CenterOfRotationFourierAngles(CenterOfRotation):
|
|
904
907
|
"""
|
905
908
|
|
906
909
|
_default_cor_options = {
|
907
|
-
"crop_around_cor":
|
910
|
+
"crop_around_cor": False,
|
911
|
+
"side": "center",
|
908
912
|
"near_pos": None,
|
913
|
+
"near_std": 100,
|
909
914
|
"near_width": 20,
|
910
|
-
"near_weight": 0.1,
|
911
915
|
"near_shape": "tukey",
|
912
|
-
"
|
916
|
+
"near_weight": 0.1,
|
913
917
|
"near_alpha": 0.5,
|
914
918
|
"shift_sino": True,
|
915
|
-
"use_paganin": True,
|
916
919
|
"near_step": 0.5,
|
917
|
-
"side": "center",
|
918
920
|
"refine": False,
|
919
921
|
}
|
920
922
|
|
921
|
-
# def __hash__(self):
|
922
|
-
# if self.cor_options["near_shape"] == "tukey":
|
923
|
-
# cor_shape = (
|
924
|
-
# self.cor_options["near_shape"],
|
925
|
-
# self.cor_options["near_std"],
|
926
|
-
# self.cor_options["near_alpha"],
|
927
|
-
# )
|
928
|
-
# else:
|
929
|
-
# cor_shape = (self.cor_options["near_shape"], self.cor_options["near_std"])
|
930
|
-
# return hash(
|
931
|
-
# (
|
932
|
-
# self.cor_options["use_paganin"],
|
933
|
-
# self.cor_options["crop_around_cor"],
|
934
|
-
# self.cor_options["near_weight"],
|
935
|
-
# self.cor_options["near_width"],
|
936
|
-
# cor_shape,
|
937
|
-
# )
|
938
|
-
# )
|
939
|
-
|
940
923
|
def _freq_radio(self, sinos, ifrom, ito):
|
941
924
|
size = (sinos.shape[0] + sinos.shape[0] % 2) // 2
|
942
925
|
fs = np.empty((size, sinos.shape[1]))
|
@@ -972,11 +955,11 @@ class CenterOfRotationFourierAngles(CenterOfRotation):
|
|
972
955
|
lin = np.maximum(0, std - np.abs(p[0] - x)) * 0.5 * np.pi / std
|
973
956
|
return p[3] + p[2] * np.sin(lin)
|
974
957
|
|
975
|
-
def _px(self, detector_width,
|
958
|
+
def _px(self, detector_width, abs_pos, near_std):
|
976
959
|
sym_range = None
|
977
|
-
if
|
960
|
+
if abs_pos is not None:
|
978
961
|
if self.cor_options["crop_around_cor"]:
|
979
|
-
sym_range = int(
|
962
|
+
sym_range = int(abs_pos - near_std * 2), int(abs_pos + near_std * 2)
|
980
963
|
|
981
964
|
window = self.cor_options["near_width"]
|
982
965
|
if sym_range is not None:
|
@@ -1012,7 +995,7 @@ class CenterOfRotationFourierAngles(CenterOfRotation):
|
|
1012
995
|
|
1013
996
|
for j, x in enumerate(px):
|
1014
997
|
i = int(np.floor(x))
|
1015
|
-
if x - i > 0.4: # Specific to near_step = 0.5?
|
998
|
+
if x - i > 0.4: # TO DO : Specific to near_step = 0.5?
|
1016
999
|
f_left = f_array[:, i - window : i]
|
1017
1000
|
f_right = f_shift_array[:, i + 1 : i + window + 1][:, ::-1]
|
1018
1001
|
else:
|
@@ -1022,24 +1005,36 @@ class CenterOfRotationFourierAngles(CenterOfRotation):
|
|
1022
1005
|
f_coef[j] = np.sum(np.abs(f_left - f_right))
|
1023
1006
|
return f_coef
|
1024
1007
|
|
1025
|
-
def _cor_correlation(self, px,
|
1026
|
-
if
|
1008
|
+
def _cor_correlation(self, px, abs_pos, near_std):
|
1009
|
+
if abs_pos is not None:
|
1027
1010
|
signal = self.cor_options["near_shape"]
|
1028
1011
|
weight = self.cor_options["near_weight"]
|
1029
1012
|
alpha = self.cor_options["near_alpha"]
|
1030
1013
|
if signal == "sinlet":
|
1031
|
-
coef = self.sinlet((
|
1014
|
+
coef = self.sinlet((abs_pos, near_std, -weight, 1), px)
|
1032
1015
|
elif signal == "gaussian":
|
1033
|
-
coef = self.gaussian((
|
1016
|
+
coef = self.gaussian((abs_pos, near_std, -weight, 1), px)
|
1034
1017
|
elif signal == "tukey":
|
1035
|
-
coef = self.tukey((
|
1018
|
+
coef = self.tukey((abs_pos, near_std * 2, alpha, -weight, 1), px)
|
1036
1019
|
else:
|
1037
1020
|
raise ValueError("Shape unsupported")
|
1038
1021
|
else:
|
1039
1022
|
coef = np.ones_like(px)
|
1040
1023
|
return coef
|
1041
1024
|
|
1042
|
-
def find_shift(
|
1025
|
+
def find_shift(
|
1026
|
+
self,
|
1027
|
+
img_1,
|
1028
|
+
img_2,
|
1029
|
+
angles,
|
1030
|
+
side,
|
1031
|
+
roi_yxhw=None,
|
1032
|
+
median_filt_shape=None,
|
1033
|
+
padding_mode=None,
|
1034
|
+
peak_fit_radius=1,
|
1035
|
+
high_pass=None,
|
1036
|
+
low_pass=None,
|
1037
|
+
):
|
1043
1038
|
sinos = np.vstack([img_1, np.fliplr(img_2).copy()])
|
1044
1039
|
detector_width = sinos.shape[1]
|
1045
1040
|
|
@@ -1048,28 +1043,38 @@ class CenterOfRotationFourierAngles(CenterOfRotation):
|
|
1048
1043
|
self.logger.warning("Not enough angles, estimator skipped")
|
1049
1044
|
return None
|
1050
1045
|
|
1051
|
-
|
1052
|
-
# Convert near_pos (which is relative) to absolute.
|
1053
|
-
near_pos = self.cor_options.get("near_pos")
|
1046
|
+
near_pos = self.cor_options.get("near_pos", None) # A RELATIVE estimation of the COR
|
1054
1047
|
|
1055
1048
|
# Default coarse estimate to center of detector
|
1056
1049
|
# if no one is given either in NX or by user.
|
1057
|
-
if near_pos
|
1050
|
+
if near_pos is None:
|
1058
1051
|
self.logger.warning("No initial guess was found (from metadata or user) for CoR")
|
1059
1052
|
self.logger.warning("Setting initial guess to center of detector.")
|
1060
|
-
|
1053
|
+
if side == "center":
|
1054
|
+
abs_pos = detector_width // 2
|
1055
|
+
elif side == "left":
|
1056
|
+
abs_pos = detector_width // 4
|
1057
|
+
elif side == "right":
|
1058
|
+
abs_pos = detector_width * 3 // 4
|
1059
|
+
elif side == "near":
|
1060
|
+
abs_pos = detector_width // 2
|
1061
|
+
else:
|
1062
|
+
raise ValueError(f"side '{side}' is not handled")
|
1063
|
+
elif isinstance(near_pos, (int, float)): # Convert RELATIVE to ABSOLUTE position
|
1064
|
+
abs_pos = near_pos + detector_width / 2
|
1065
|
+
|
1061
1066
|
near_std = None
|
1062
|
-
if
|
1067
|
+
if abs_pos is not None:
|
1063
1068
|
near_std = self.cor_options["near_std"]
|
1064
1069
|
|
1065
|
-
px = self._px(detector_width,
|
1070
|
+
px = self._px(detector_width, abs_pos, near_std)
|
1066
1071
|
|
1067
1072
|
coef_f = self._symmetry_correlation(
|
1068
1073
|
px,
|
1069
1074
|
sinos,
|
1070
1075
|
angles,
|
1071
1076
|
)
|
1072
|
-
coef_p = self._cor_correlation(px,
|
1077
|
+
coef_p = self._cor_correlation(px, abs_pos, near_std)
|
1073
1078
|
coef = coef_f * coef_p
|
1074
1079
|
|
1075
1080
|
if len(px) > 0:
|
@@ -1090,12 +1095,21 @@ class CenterOfRotationFourierAngles(CenterOfRotation):
|
|
1090
1095
|
class CenterOfRotationOctaveAccurate(AlignmentBase):
|
1091
1096
|
"""This is a Python implementation of Octave/fastomo3/accurate COR estimator.
|
1092
1097
|
The Octave 'accurate' function is renamed `local_correlation`.
|
1093
|
-
The Nabu standard `find_shift` has the same API as other COR estimators (sliding, growing...)
|
1098
|
+
The Nabu standard `find_shift` has the same API as the other COR estimators (sliding, growing...)
|
1094
1099
|
|
1095
1100
|
The class inherits directly from AlignmentBase.
|
1096
1101
|
"""
|
1097
1102
|
|
1098
|
-
|
1103
|
+
_default_cor_options = {
|
1104
|
+
"maxsize": [5, 5],
|
1105
|
+
"refine": None,
|
1106
|
+
"pmcc": False,
|
1107
|
+
"normalize": True,
|
1108
|
+
"low_pass": 0.01,
|
1109
|
+
"limz": 0.5,
|
1110
|
+
}
|
1111
|
+
|
1112
|
+
def _cut(self, im, nrows, ncols, new_center_row=None, new_center_col=None):
|
1099
1113
|
"""Cuts a sub-matrix out of a larger matrix.
|
1100
1114
|
Cuts in the center of the original matrix, except if new center is specified
|
1101
1115
|
NO CHECKING of validity indices sub-matrix!
|
@@ -1143,7 +1157,7 @@ class CenterOfRotationOctaveAccurate(AlignmentBase):
|
|
1143
1157
|
|
1144
1158
|
return im[rb:re, cb:ce]
|
1145
1159
|
|
1146
|
-
def
|
1160
|
+
def _checkifpart(self, rapp, rapp_hist):
|
1147
1161
|
res = 0
|
1148
1162
|
for k in range(rapp_hist.shape[0]):
|
1149
1163
|
if np.allclose(rapp, rapp_hist[k, :]):
|
@@ -1151,7 +1165,7 @@ class CenterOfRotationOctaveAccurate(AlignmentBase):
|
|
1151
1165
|
return res
|
1152
1166
|
return res
|
1153
1167
|
|
1154
|
-
def
|
1168
|
+
def _interpolate(self, input, shift, mode="mean", interpolation_method="linear"):
|
1155
1169
|
"""Applies to the input a translation by a vector `shift`. Based on
|
1156
1170
|
`scipy.ndimage.affine_transform` function.
|
1157
1171
|
JL: This Octave function was initially used in the refine clause of the local_correlation (Octave find_shift).
|
@@ -1218,7 +1232,7 @@ class CenterOfRotationOctaveAccurate(AlignmentBase):
|
|
1218
1232
|
|
1219
1233
|
return affine_transform(input, matrix, mode=mode, order=order)
|
1220
1234
|
|
1221
|
-
def
|
1235
|
+
def _local_correlation(
|
1222
1236
|
self,
|
1223
1237
|
z1,
|
1224
1238
|
z2,
|
@@ -1227,7 +1241,6 @@ class CenterOfRotationOctaveAccurate(AlignmentBase):
|
|
1227
1241
|
refine=None,
|
1228
1242
|
pmcc=False,
|
1229
1243
|
normalize=True,
|
1230
|
-
verbose=False,
|
1231
1244
|
):
|
1232
1245
|
"""Returns the 2D shift in pixels between two images.
|
1233
1246
|
It looks for a local optimum around the initial shift cor_estimate
|
@@ -1261,8 +1274,6 @@ class CenterOfRotationOctaveAccurate(AlignmentBase):
|
|
1261
1274
|
normalize: Boolean (default is True)
|
1262
1275
|
Set mean of each image to 1 if True.
|
1263
1276
|
|
1264
|
-
verbose: Boolean (default is False)
|
1265
|
-
|
1266
1277
|
Returns
|
1267
1278
|
-------
|
1268
1279
|
c = [row,column] (or [NaN,NaN] if unsuccessful.)
|
@@ -1361,7 +1372,7 @@ class CenterOfRotationOctaveAccurate(AlignmentBase):
|
|
1361
1372
|
if pmcc:
|
1362
1373
|
z2p = z2[z2beg[0] - k : z2end[0] - k, z2beg[1] - l : z2end[1] - l].flatten()
|
1363
1374
|
std_z2p = z2p.std()
|
1364
|
-
cc[k, l] = -np.cov(z1p, z2p) / (std_z1p * std_z2p)
|
1375
|
+
cc[k, l] = -np.cov(z1p, z2p, rowvar=True)[1, 0] / (std_z1p * std_z2p)
|
1365
1376
|
else:
|
1366
1377
|
if normalize == 2:
|
1367
1378
|
z2p = z2[z2beg[0] - k : z2end[0] - k, z2beg[1] - l : z2end[1] - l].flatten()
|
@@ -1388,25 +1399,25 @@ class CenterOfRotationOctaveAccurate(AlignmentBase):
|
|
1388
1399
|
if not shiftfound:
|
1389
1400
|
cor_estimate = c
|
1390
1401
|
# Check that new shift estimate was not already done (avoid eternal loop)
|
1391
|
-
if self.
|
1392
|
-
if verbose:
|
1402
|
+
if self._checkifpart(cor_estimate, rapp_hist):
|
1403
|
+
if self.verbose:
|
1393
1404
|
self.logger.info(f"Stuck in loop?")
|
1394
1405
|
refine = True
|
1395
1406
|
shiftfound = True
|
1396
1407
|
c = np.array([np.nan, np.nan])
|
1397
1408
|
else:
|
1398
1409
|
rapp_hist.append(cor_estimate)
|
1399
|
-
if verbose:
|
1410
|
+
if self.verbose:
|
1400
1411
|
self.logger.info(f"Changing shift estimate: {cor_estimate}")
|
1401
1412
|
maxsize = np.minimum(maxsize, np.array(z1.shape) - np.abs(cor_estimate) - 1).astype(int)
|
1402
1413
|
if (maxsize == 0).sum():
|
1403
|
-
if verbose:
|
1414
|
+
if self.verbose:
|
1404
1415
|
self.logger.info(f"Edge of image reached")
|
1405
1416
|
refine = False
|
1406
1417
|
shiftfound = True
|
1407
1418
|
c = np.array([np.nan, np.nan])
|
1408
1419
|
elif len(rapp_hist) > 0:
|
1409
|
-
if verbose:
|
1420
|
+
if self.verbose:
|
1410
1421
|
self.logger.info("\n")
|
1411
1422
|
|
1412
1423
|
####################################
|
@@ -1450,16 +1461,86 @@ class CenterOfRotationOctaveAccurate(AlignmentBase):
|
|
1450
1461
|
roi_yxhw=None,
|
1451
1462
|
median_filt_shape=None,
|
1452
1463
|
padding_mode=None,
|
1453
|
-
high_pass=None,
|
1454
1464
|
low_pass=0.01,
|
1455
|
-
|
1465
|
+
high_pass=None,
|
1456
1466
|
):
|
1467
|
+
"""Automatically finds the Center of Rotation (CoR), given two images
|
1468
|
+
(projections/radiographs). Suitable for half-aquisition scan.
|
1469
|
+
|
1470
|
+
This method finds the half-shift between two opposite images, by
|
1471
|
+
minimizing the variance of small ROI around a global COR estimate
|
1472
|
+
(obtained by maximizing Fourier-space computed global correlations).
|
1473
|
+
|
1474
|
+
|
1475
|
+
The output of this function, allows to compute motor movements for
|
1476
|
+
aligning the sample rotation axis. Given the following values:
|
1477
|
+
|
1478
|
+
- L1: distance from source to motor
|
1479
|
+
- L2: distance from source to detector
|
1480
|
+
- ps: physical pixel size
|
1481
|
+
- v: output of this function
|
1482
|
+
|
1483
|
+
displacement of motor = (L1 / L2 * ps) * v
|
1484
|
+
|
1485
|
+
Parameters
|
1486
|
+
----------
|
1487
|
+
img_1: numpy.ndarray
|
1488
|
+
First image
|
1489
|
+
img_2: numpy.ndarray
|
1490
|
+
Second image, it needs to have been flipped already (e.g. using numpy.fliplr).
|
1491
|
+
side: string
|
1492
|
+
Expected region of the CoR. Must be 'center' in that case.
|
1493
|
+
roi_yxhw: (2, ) or (4, ) numpy.ndarray, tuple, or array, optional
|
1494
|
+
4 elements vector containing: vertical and horizontal coordinates
|
1495
|
+
of first pixel, plus height and width of the Region of Interest (RoI).
|
1496
|
+
Or a 2 elements vector containing: plus height and width of the
|
1497
|
+
centered Region of Interest (RoI).
|
1498
|
+
Default is None -> deactivated.
|
1499
|
+
The ROI will be used for the global estimate.
|
1500
|
+
median_filt_shape: (2, ) numpy.ndarray, tuple, or array, optional
|
1501
|
+
Shape of the median filter window. Default is None -> deactivated.
|
1502
|
+
padding_mode: str in numpy.pad's mode list, optional
|
1503
|
+
Padding mode, which determines the type of convolution. If None or
|
1504
|
+
'wrap' are passed, this resorts to the traditional circular convolution.
|
1505
|
+
If 'edge' or 'constant' are passed, it results in a linear convolution.
|
1506
|
+
Default is the circular convolution.
|
1507
|
+
All options are:
|
1508
|
+
None | 'constant' | 'edge' | 'linear_ramp' | 'maximum' | 'mean'
|
1509
|
+
| 'median' | 'minimum' | 'reflect' | 'symmetric' |'wrap'
|
1510
|
+
low_pass: float or sequence of two floats
|
1511
|
+
Low-pass filter properties, as described in `nabu.misc.fourier_filters`
|
1512
|
+
high_pass: float or sequence of two floats
|
1513
|
+
High-pass filter properties, as described in `nabu.misc.fourier_filters`
|
1514
|
+
|
1515
|
+
Raises
|
1516
|
+
------
|
1517
|
+
ValueError
|
1518
|
+
In case images are not 2-dimensional or have different sizes.
|
1519
|
+
|
1520
|
+
Returns
|
1521
|
+
-------
|
1522
|
+
float
|
1523
|
+
Estimated center of rotation position from the center of the RoI in pixels.
|
1524
|
+
|
1525
|
+
Examples
|
1526
|
+
--------
|
1527
|
+
The following code computes the center of rotation position for two
|
1528
|
+
given images in a tomography scan, where the second image is taken at
|
1529
|
+
180 degrees from the first.
|
1530
|
+
|
1531
|
+
>>> radio1 = data[0, :, :]
|
1532
|
+
... radio2 = np.fliplr(data[1, :, :])
|
1533
|
+
... CoR_calc = CenterOfRotationOctaveAccurate()
|
1534
|
+
... cor_position = CoR_calc.find_shift(radio1, radio2)
|
1535
|
+
|
1536
|
+
Or for noisy images:
|
1537
|
+
|
1538
|
+
>>> cor_position = CoR_calc.find_shift(radio1, radio2, median_filt_shape=(3, 3))
|
1539
|
+
"""
|
1540
|
+
|
1457
1541
|
self.logger.info(
|
1458
|
-
f"Estimation of the COR with following options: high_pass={high_pass}, low_pass={low_pass}, limz={limz}."
|
1542
|
+
f"Estimation of the COR with following options: high_pass={high_pass}, low_pass={low_pass}, limz={self.cor_options['limz']}."
|
1459
1543
|
)
|
1460
|
-
# Retrieve cor_options
|
1461
|
-
# self._set_cor_options(cor_options)
|
1462
|
-
# self.logger.info(f"cor_options are :{self.cor_options}")
|
1463
1544
|
|
1464
1545
|
self._check_img_pair_sizes(img_1, img_2)
|
1465
1546
|
|
@@ -1491,7 +1572,7 @@ class CenterOfRotationOctaveAccurate(AlignmentBase):
|
|
1491
1572
|
shift -= np.array(img_shape) // 2
|
1492
1573
|
|
1493
1574
|
# The real "accurate" starts here (i.e. the octave findshift() func).
|
1494
|
-
if np.abs(shift[0]) > 10 * limz:
|
1575
|
+
if np.abs(shift[0]) > 10 * self.cor_options["limz"]:
|
1495
1576
|
# This is suspiscious. We don't trust results of correlate.
|
1496
1577
|
self.logger.info(f"Pre-correlation yields {shift[0]} pixels vertical motion")
|
1497
1578
|
self.logger.info(f"We do not consider it.")
|
@@ -1502,20 +1583,35 @@ class CenterOfRotationOctaveAccurate(AlignmentBase):
|
|
1502
1583
|
cutsize = img_shape[1] // 2
|
1503
1584
|
oldshift = np.round(shift).astype(int)
|
1504
1585
|
if (img_shape[0] > cutsize) or (img_shape[1] > cutsize):
|
1505
|
-
im0 = self.
|
1506
|
-
im1 = self.
|
1586
|
+
im0 = self._cut(img_1, min(img_shape[0], cutsize), min(img_shape[1], cutsize))
|
1587
|
+
im1 = self._cut(
|
1507
1588
|
np.roll(img_2, oldshift, axis=(0, 1)), min(img_shape[0], cutsize), min(img_shape[1], cutsize)
|
1508
1589
|
)
|
1509
|
-
shift = oldshift + self.
|
1590
|
+
shift = oldshift + self._local_correlation(
|
1591
|
+
im0,
|
1592
|
+
im1,
|
1593
|
+
maxsize=self.cor_options["maxsize"],
|
1594
|
+
refine=self.cor_options["refine"],
|
1595
|
+
pmcc=self.cor_options["pmcc"],
|
1596
|
+
normalize=self.cor_options["normalize"],
|
1597
|
+
)
|
1510
1598
|
else:
|
1511
|
-
shift = self.
|
1599
|
+
shift = self._local_correlation(
|
1600
|
+
img_1,
|
1601
|
+
img_2,
|
1602
|
+
maxsize=self.cor_options["maxsize"],
|
1603
|
+
cor_estimate=oldshift,
|
1604
|
+
refine=self.cor_options["refine"],
|
1605
|
+
pmcc=self.cor_options["pmcc"],
|
1606
|
+
normalize=self.cor_options["normalize"],
|
1607
|
+
)
|
1512
1608
|
if ((shift - oldshift) ** 2).sum() > 4:
|
1513
1609
|
self.logger.info(f"Pre-correlation ({oldshift}) and accurate correlation ({shift}) are not consistent.")
|
1514
1610
|
self.logger.info("Please check!!!")
|
1515
1611
|
|
1516
1612
|
offset = shift[1] / 2
|
1517
1613
|
|
1518
|
-
if np.abs(shift[0]) > limz:
|
1614
|
+
if np.abs(shift[0]) > self.cor_options["limz"]:
|
1519
1615
|
self.logger.info("Verify alignment or sample motion.")
|
1520
1616
|
self.logger.info(f"Verical motion: {shift[0]} pixels.")
|
1521
1617
|
self.logger.info(f"Offset?: {offset} pixels.")
|