de-ebsd 0.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.
- de_ebsd/__init__.py +76 -0
- de_ebsd/core/__init__.py +23 -0
- de_ebsd/core/constants.py +81 -0
- de_ebsd/core/device.py +12 -0
- de_ebsd/core/geometry.py +67 -0
- de_ebsd/core/lambert.py +148 -0
- de_ebsd/core/rotation.py +139 -0
- de_ebsd/crystal/__init__.py +22 -0
- de_ebsd/crystal/data/__init__.py +0 -0
- de_ebsd/crystal/data/element_properties.py +29 -0
- de_ebsd/crystal/data/scattering_tables.py +414 -0
- de_ebsd/crystal/lattice.py +517 -0
- de_ebsd/crystal/scattering.py +339 -0
- de_ebsd/crystal/scattering_torch.py +233 -0
- de_ebsd/crystal/structure_factors.py +291 -0
- de_ebsd/diffraction/__init__.py +45 -0
- de_ebsd/diffraction/backends.py +318 -0
- de_ebsd/diffraction/bethe.py +28 -0
- de_ebsd/diffraction/kinematic.py +389 -0
- de_ebsd/diffraction/kvectors.py +120 -0
- de_ebsd/diffraction/prism.py +204 -0
- de_ebsd/diffraction/reflections.py +234 -0
- de_ebsd/index/__init__.py +61 -0
- de_ebsd/index/_kernels.py +222 -0
- de_ebsd/index/backproject.py +171 -0
- de_ebsd/index/crosscorr.py +654 -0
- de_ebsd/index/dictionary.py +362 -0
- de_ebsd/index/io.py +324 -0
- de_ebsd/index/live/__init__.py +5 -0
- de_ebsd/index/live/background.py +103 -0
- de_ebsd/index/live/calibration.py +70 -0
- de_ebsd/index/live/common.py +44 -0
- de_ebsd/index/live/deformation.py +289 -0
- de_ebsd/index/live/indexer.py +386 -0
- de_ebsd/index/preprocess.py +195 -0
- de_ebsd/index/refine/__init__.py +7 -0
- de_ebsd/index/refine/highres.py +479 -0
- de_ebsd/index/refine/levenberg.py +33 -0
- de_ebsd/index/refine/orientation.py +367 -0
- de_ebsd/index/refine/pattern_center.py +356 -0
- de_ebsd/index/spherical/__init__.py +41 -0
- de_ebsd/index/spherical/correlation.py +106 -0
- de_ebsd/index/spherical/harmonics.py +311 -0
- de_ebsd/index/spherical/indexer.py +345 -0
- de_ebsd/index/spherical/wigner.py +190 -0
- de_ebsd/sim/__init__.py +20 -0
- de_ebsd/sim/detector.py +145 -0
- de_ebsd/sim/masterpattern.py +231 -0
- de_ebsd/sim/microscope.py +93 -0
- de_ebsd/sim/montecarlo/__init__.py +19 -0
- de_ebsd/sim/montecarlo/depth.py +167 -0
- de_ebsd/sim/montecarlo/transport.py +532 -0
- de_ebsd/sim/simulator.py +291 -0
- de_ebsd/viz/__init__.py +53 -0
- de_ebsd/viz/camera.py +56 -0
- de_ebsd/viz/depth_widget.js +76 -0
- de_ebsd/viz/explorer.py +647 -0
- de_ebsd/viz/figures.py +67 -0
- de_ebsd/viz/maps.py +277 -0
- de_ebsd/viz/patterns.py +164 -0
- de_ebsd/viz/setup_widget.js +171 -0
- de_ebsd/viz/views.py +125 -0
- de_ebsd/viz/widgets.py +191 -0
- de_ebsd-0.2.0.dist-info/METADATA +251 -0
- de_ebsd-0.2.0.dist-info/RECORD +69 -0
- de_ebsd-0.2.0.dist-info/WHEEL +5 -0
- de_ebsd-0.2.0.dist-info/licenses/LICENSE +674 -0
- de_ebsd-0.2.0.dist-info/licenses/NOTICE +44 -0
- de_ebsd-0.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
"""Gradient refinement of orientation and (optionally) pattern center.
|
|
2
|
+
|
|
3
|
+
Maximizes a masked spherical NCC between the back-projected pattern and the
|
|
4
|
+
master pattern sampled at trial-rotated directions -- both differentiable, so a
|
|
5
|
+
coarse seed sharpens to sub-grid-cell accuracy. ``method="adam"`` is first-order
|
|
6
|
+
and supports pattern-center refinement; ``method="lm"`` is a batched
|
|
7
|
+
Gauss-Newton with an analytic Jacobian, ~2x faster but orientation-only.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import numpy as np
|
|
13
|
+
import torch
|
|
14
|
+
|
|
15
|
+
from de_ebsd.core.constants import DIVISION_FLOOR
|
|
16
|
+
from de_ebsd.core.device import resolve_device
|
|
17
|
+
from de_ebsd.core.rotation import quaternion_to_matrix, rotation_vector_to_matrix, skew_matrix
|
|
18
|
+
from de_ebsd.index.backproject import pattern_to_sphere
|
|
19
|
+
from de_ebsd.index.refine.levenberg import adapt_damping
|
|
20
|
+
|
|
21
|
+
__all__ = ["refine_orientation"]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _masked_ncc(a: torch.Tensor, b: torch.Tensor, mask: torch.Tensor) -> torch.Tensor:
|
|
25
|
+
denom = mask.sum(-1, keepdim=True).clamp_min(DIVISION_FLOOR)
|
|
26
|
+
ma = (a * mask).sum(-1, keepdim=True) / denom
|
|
27
|
+
mb = (b * mask).sum(-1, keepdim=True) / denom
|
|
28
|
+
a0 = (a - ma) * mask
|
|
29
|
+
b0 = (b - mb) * mask
|
|
30
|
+
num = (a0 * b0).sum(-1)
|
|
31
|
+
na = (a0 * a0).sum(-1).clamp_min(DIVISION_FLOOR).sqrt()
|
|
32
|
+
nb = (b0 * b0).sum(-1).clamp_min(DIVISION_FLOOR).sqrt()
|
|
33
|
+
return num / (na * nb)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _fov_directions(detector, device, dtype, target: int = 1024) -> torch.Tensor:
|
|
37
|
+
pix = detector.direction_cosines(device=device, dtype=dtype).reshape(-1, 3)
|
|
38
|
+
step = max(1, pix.shape[0] // int(target))
|
|
39
|
+
return pix[::step][: int(target)].contiguous()
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _backproject(patt, detector, dirs, pattern_center, per_pattern):
|
|
43
|
+
if not per_pattern:
|
|
44
|
+
e, mask = pattern_to_sphere(patt, detector, dirs, pattern_center=pattern_center)
|
|
45
|
+
return e, mask
|
|
46
|
+
es, masks = [], []
|
|
47
|
+
for i in range(patt.shape[0]):
|
|
48
|
+
e_i, m_i = pattern_to_sphere(
|
|
49
|
+
patt[i : i + 1], detector, dirs, pattern_center=pattern_center[i]
|
|
50
|
+
)
|
|
51
|
+
es.append(e_i)
|
|
52
|
+
masks.append(m_i)
|
|
53
|
+
return torch.cat(es, dim=0), torch.stack(masks, dim=0)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _standardize(x: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]:
|
|
57
|
+
c = x - x.mean(-1, keepdim=True)
|
|
58
|
+
return c, c.norm(dim=-1, keepdim=True).clamp_min(DIVISION_FLOOR)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _lm_residual_jacobian(master, dirs, r_mat, e_hat, device, dtype):
|
|
62
|
+
n = dirs # (M, 3), all in the FOV
|
|
63
|
+
# grid_sample has no forward-mode AD, and being pointwise its Jacobian is
|
|
64
|
+
# diagonal in the points -- so one reverse-mode pass gives every dM/dv
|
|
65
|
+
with torch.enable_grad():
|
|
66
|
+
v = (n @ r_mat.transpose(-1, -2)).detach().requires_grad_(True) # (Np, M, 3)
|
|
67
|
+
m_raw = master.sample(v) # (Np, M)
|
|
68
|
+
(g_m,) = torch.autograd.grad(m_raw.sum(), v) # (Np, M, 3) = dM_i/dv_i
|
|
69
|
+
m = m_raw.detach().to(device=device, dtype=dtype)
|
|
70
|
+
g_m = g_m.to(device=device, dtype=dtype)
|
|
71
|
+
# tangent of v = R n wrt delta_k: R @ (-skew(n)), columns k -> (Np, M, 3, 3)
|
|
72
|
+
tang = r_mat[:, None] @ (-skew_matrix(n))[None]
|
|
73
|
+
g = torch.einsum("pma,pmak->pmk", g_m, tang) # raw dM/ddelta (Np, M, 3)
|
|
74
|
+
c, s = _standardize(m)
|
|
75
|
+
m_hat = c / s
|
|
76
|
+
mu_g = g.mean(1) # mean_k(g) over the directions (Np, 3)
|
|
77
|
+
nu = torch.einsum("pm,pmk->pk", c, g) / s # (Np, 3)
|
|
78
|
+
dm_hat = ((g - mu_g[:, None]) - m_hat[..., None] * nu[:, None]) / s[..., None]
|
|
79
|
+
return e_hat - m_hat, -dm_hat, m
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _lm_refine(
|
|
83
|
+
master, dirs, r0_mat, e, mask, n_steps, damping, keep_best, return_history, device, dtype
|
|
84
|
+
):
|
|
85
|
+
e_c, e_s = _standardize(e)
|
|
86
|
+
e_hat = e_c / e_s # (Np, M) standardized experimental field
|
|
87
|
+
r_mat = r0_mat.clone()
|
|
88
|
+
n_p = r_mat.shape[0]
|
|
89
|
+
damping = torch.full((n_p,), float(damping), device=device, dtype=dtype)
|
|
90
|
+
|
|
91
|
+
with torch.no_grad():
|
|
92
|
+
m_seed = master.sample(dirs @ r0_mat.transpose(-1, -2)).to(device=device, dtype=dtype)
|
|
93
|
+
seed_score = _masked_ncc(e, m_seed, mask)
|
|
94
|
+
cur_score = seed_score
|
|
95
|
+
history = [] if return_history else None
|
|
96
|
+
|
|
97
|
+
for _ in range(n_steps):
|
|
98
|
+
if return_history:
|
|
99
|
+
history.append(cur_score)
|
|
100
|
+
res, j_r, _ = _lm_residual_jacobian(master, dirs, r_mat, e_hat, device, dtype)
|
|
101
|
+
with torch.no_grad():
|
|
102
|
+
jtr = torch.einsum("pm,pmk->pk", res, j_r) # (Np, 3)
|
|
103
|
+
jtj = torch.einsum("pmk,pml->pkl", j_r, j_r) # (Np, 3, 3)
|
|
104
|
+
# LM damping: add damping * diag(JtJ) to the Gauss-Newton normal matrix.
|
|
105
|
+
diag = torch.diagonal(jtj, dim1=-2, dim2=-1).clamp_min(1e-9)
|
|
106
|
+
a = jtj + damping[:, None, None] * torch.diag_embed(diag)
|
|
107
|
+
delta = -torch.linalg.solve(a, jtr[..., None])[..., 0] # (Np, 3)
|
|
108
|
+
r_new = r_mat @ rotation_vector_to_matrix(delta)
|
|
109
|
+
m_new = master.sample(dirs @ r_new.transpose(-1, -2)).to(device=device, dtype=dtype)
|
|
110
|
+
new_score = _masked_ncc(e, m_new, mask)
|
|
111
|
+
# per-pattern accept/reject: robust to the bilinear grid_sample kinks,
|
|
112
|
+
# and monotone by construction
|
|
113
|
+
better = new_score > cur_score
|
|
114
|
+
r_mat = torch.where(better[:, None, None], r_new, r_mat)
|
|
115
|
+
damping = adapt_damping(damping, better)
|
|
116
|
+
cur_score = torch.where(better, new_score, cur_score)
|
|
117
|
+
|
|
118
|
+
refined_score = cur_score
|
|
119
|
+
if keep_best:
|
|
120
|
+
# already guaranteed by the accept/reject; kept symmetric with the Adam path
|
|
121
|
+
keep = (refined_score >= seed_score).reshape(-1, 1, 1)
|
|
122
|
+
r_final = torch.where(keep, r_mat, r0_mat)
|
|
123
|
+
score = torch.maximum(refined_score, seed_score)
|
|
124
|
+
else:
|
|
125
|
+
r_final = r_mat
|
|
126
|
+
score = refined_score
|
|
127
|
+
if return_history:
|
|
128
|
+
hist = torch.stack(history, dim=0) if history else e.new_zeros((0, n_p))
|
|
129
|
+
else:
|
|
130
|
+
hist = None
|
|
131
|
+
return r_final, score, hist
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def refine_orientation(
|
|
135
|
+
patterns,
|
|
136
|
+
master,
|
|
137
|
+
detector,
|
|
138
|
+
rotations,
|
|
139
|
+
pattern_center=None,
|
|
140
|
+
refine_pattern_center=False,
|
|
141
|
+
method="adam",
|
|
142
|
+
n_iterations=None,
|
|
143
|
+
learning_rate=0.02,
|
|
144
|
+
pattern_center_learning_rate=None,
|
|
145
|
+
damping=1e-3,
|
|
146
|
+
directions=None,
|
|
147
|
+
device=None,
|
|
148
|
+
keep_best=True,
|
|
149
|
+
return_history=False,
|
|
150
|
+
):
|
|
151
|
+
"""Refine ``rotations`` (and optionally the pattern center) by autograd.
|
|
152
|
+
|
|
153
|
+
Sharpens a coarse seed to sub-grid-cell accuracy by maximizing a spherical
|
|
154
|
+
masked NCC between the back-projected pattern ``E(n)`` and the master sampled at
|
|
155
|
+
the trial-rotated directions ``M(R n)``, with ``R = R0 . dR(w)`` for a
|
|
156
|
+
differentiable rotation-vector correction ``w``. The whole stack is refined in
|
|
157
|
+
one parallel optimization on the master's device.
|
|
158
|
+
|
|
159
|
+
Parameters
|
|
160
|
+
----------
|
|
161
|
+
patterns
|
|
162
|
+
A detector image ``(H, W)`` or stack ``(Np, H, W)``, numpy or torch.
|
|
163
|
+
master, detector
|
|
164
|
+
The continuous :class:`de_ebsd.sim.MasterPattern` and the
|
|
165
|
+
:class:`~de_ebsd.sim.detector.EBSDDetector` whose pixel -> direction map is
|
|
166
|
+
inverted (differentiably in the pattern center) to build ``E(n)``.
|
|
167
|
+
rotations
|
|
168
|
+
Seed :class:`orix.quaternion.Rotation`, typically the coarse index output.
|
|
169
|
+
pattern_center
|
|
170
|
+
``None`` uses ``detector.pattern_center``. A ``(3,)`` value shares one center across the
|
|
171
|
+
stack -- the common, fully-vectorized path; ``(Np, 3)`` gives a per-pattern
|
|
172
|
+
center and loops the back-projection.
|
|
173
|
+
refine_pattern_center
|
|
174
|
+
Refine the pattern center jointly with the orientation. Forces
|
|
175
|
+
``method="adam"``, since the LM path is orientation-only.
|
|
176
|
+
method
|
|
177
|
+
``"adam"`` (default) is first-order. ``"lm"`` is Levenberg-Marquardt with an
|
|
178
|
+
analytic Jacobian, reaching the same sub-degree accuracy in ~10 steps
|
|
179
|
+
instead of ~60 (~2.3x faster at equal accuracy on real data). LM is
|
|
180
|
+
orientation-only and assumes the default in-FOV ``directions``.
|
|
181
|
+
n_iterations
|
|
182
|
+
Step budget. ``None`` resolves per method: 60 for Adam, 12 for LM.
|
|
183
|
+
learning_rate
|
|
184
|
+
Adam step size for the rotation vector, in radians. The key stability knob:
|
|
185
|
+
too large a step overshoots the shallow, noisy basin of real patterns and
|
|
186
|
+
diverges. ``0.02`` is stable on real data; use ~0.01 for noisy scans.
|
|
187
|
+
pattern_center_learning_rate
|
|
188
|
+
Adam step size for the pattern center (default ``learning_rate / 5``, since its natural
|
|
189
|
+
scale is finer). Ignored unless ``refine_pattern_center``.
|
|
190
|
+
damping
|
|
191
|
+
Initial LM damping, adapted per pattern each step (x0.5 accepted, x3
|
|
192
|
+
rejected). LM only.
|
|
193
|
+
directions
|
|
194
|
+
Sphere directions ``(M, 3)`` the NCC is evaluated over. ``None`` uses the
|
|
195
|
+
detector's in-FOV pixel directions, where every point contributes -- ~3x
|
|
196
|
+
faster and more accurate than a whole-sphere set that is mostly masked away.
|
|
197
|
+
``method="lm"`` requires an all-in-FOV set; the default satisfies this, but a
|
|
198
|
+
custom non-FOV set is unsafe for LM (use Adam, whose masked NCC handles it).
|
|
199
|
+
device
|
|
200
|
+
Defaults to ``master.data.device``; otherwise everything is moved there.
|
|
201
|
+
keep_best
|
|
202
|
+
Never return an orientation worse than the seed, making refinement monotone.
|
|
203
|
+
This removes the stray outliers gradient refinement can introduce on
|
|
204
|
+
low-quality real patterns.
|
|
205
|
+
return_history
|
|
206
|
+
Also return the per-step NCC trajectory.
|
|
207
|
+
|
|
208
|
+
Returns
|
|
209
|
+
-------
|
|
210
|
+
rotation : orix.quaternion.Rotation
|
|
211
|
+
The refined orientation(s), in the convention ``MasterPattern.project``
|
|
212
|
+
consumes.
|
|
213
|
+
score : torch.Tensor
|
|
214
|
+
Final NCC per pattern, in ``[-1, 1]``; ``>=`` the coarse score. This is what
|
|
215
|
+
``index_scan(refine=True)`` stores as ``prop["quality"]``.
|
|
216
|
+
pattern_center : torch.Tensor
|
|
217
|
+
Only when ``refine_pattern_center``: the refined pattern center, shaped like the input.
|
|
218
|
+
history : torch.Tensor
|
|
219
|
+
Only when ``return_history``: the NCC at every step, ``(n_iterations, Np)``, with
|
|
220
|
+
``history[0]`` the seed score.
|
|
221
|
+
"""
|
|
222
|
+
# LM is orientation-only, so refine_pattern_center forces Adam
|
|
223
|
+
method = str(method).lower()
|
|
224
|
+
if method not in ("adam", "lm"):
|
|
225
|
+
raise ValueError(f"method must be 'adam' or 'lm', got {method!r}")
|
|
226
|
+
if method == "lm" and refine_pattern_center:
|
|
227
|
+
method = "adam" # joint pattern-center + orientation LM is future work
|
|
228
|
+
if n_iterations is None:
|
|
229
|
+
n_iterations = 12 if method == "lm" else 60
|
|
230
|
+
|
|
231
|
+
device = resolve_device(device, master.data.device)
|
|
232
|
+
dtype = master.data.dtype
|
|
233
|
+
# master.data.device drives sampling, so move the master to honor `device`
|
|
234
|
+
if master.data.device != device:
|
|
235
|
+
master = master.to(device)
|
|
236
|
+
|
|
237
|
+
patt = torch.as_tensor(patterns, device=device, dtype=dtype)
|
|
238
|
+
single = patt.ndim == 2
|
|
239
|
+
if single:
|
|
240
|
+
patt = patt[None]
|
|
241
|
+
if patt.ndim != 3:
|
|
242
|
+
raise ValueError(f"patterns must be (H, W) or (Np, H, W), got shape {tuple(patt.shape)}")
|
|
243
|
+
n_p = patt.shape[0]
|
|
244
|
+
|
|
245
|
+
# the detached anchor: exactly the matrix MasterPattern.project builds
|
|
246
|
+
q0 = torch.as_tensor(np.asarray(rotations.data), device=device, dtype=dtype).reshape(-1, 4)
|
|
247
|
+
if q0.shape[0] != n_p:
|
|
248
|
+
raise ValueError(f"rotations length {q0.shape[0]} != number of patterns {n_p}")
|
|
249
|
+
r0_mat = quaternion_to_matrix(q0).detach() # (Np, 3, 3)
|
|
250
|
+
|
|
251
|
+
if directions is None:
|
|
252
|
+
dirs = _fov_directions(detector, device, dtype)
|
|
253
|
+
else:
|
|
254
|
+
dirs = torch.as_tensor(directions, device=device, dtype=dtype).reshape(-1, 3)
|
|
255
|
+
|
|
256
|
+
# pattern center: shared (3,) or per-pattern (Np, 3)
|
|
257
|
+
if pattern_center is None:
|
|
258
|
+
pc_init = torch.as_tensor(detector.pattern_center, device=device, dtype=dtype)
|
|
259
|
+
else:
|
|
260
|
+
pc_init = torch.as_tensor(pattern_center, device=device, dtype=dtype)
|
|
261
|
+
if pc_init.ndim == 1 and pc_init.shape[0] == 3:
|
|
262
|
+
per_pattern = False
|
|
263
|
+
elif pc_init.ndim == 2 and pc_init.shape == (n_p, 3):
|
|
264
|
+
per_pattern = True
|
|
265
|
+
else:
|
|
266
|
+
raise ValueError(
|
|
267
|
+
f"pattern_center must be (3,) shared or (Np, 3) per-pattern (Np={n_p}), got "
|
|
268
|
+
f"{tuple(pc_init.shape)}"
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
# orientation-only, so the pattern center is fixed and E is back-projected once
|
|
272
|
+
if method == "lm":
|
|
273
|
+
with torch.no_grad():
|
|
274
|
+
e_lm, mask_lm = _backproject(patt, detector, dirs, pc_init, per_pattern)
|
|
275
|
+
r_final, score, hist = _lm_refine(
|
|
276
|
+
master,
|
|
277
|
+
dirs,
|
|
278
|
+
r0_mat,
|
|
279
|
+
e_lm,
|
|
280
|
+
mask_lm,
|
|
281
|
+
n_iterations,
|
|
282
|
+
damping,
|
|
283
|
+
keep_best,
|
|
284
|
+
return_history,
|
|
285
|
+
device,
|
|
286
|
+
dtype,
|
|
287
|
+
)
|
|
288
|
+
from orix.quaternion import Rotation
|
|
289
|
+
|
|
290
|
+
rotation = Rotation.from_matrix(r_final.cpu().numpy())
|
|
291
|
+
out = [rotation, score]
|
|
292
|
+
if return_history:
|
|
293
|
+
out.append(hist)
|
|
294
|
+
return tuple(out)
|
|
295
|
+
|
|
296
|
+
# the FOV mask is held fixed at the seed geometry so the objective stays smooth
|
|
297
|
+
# as the pattern center moves
|
|
298
|
+
with torch.no_grad():
|
|
299
|
+
_, mask = _backproject(patt, detector, dirs, pc_init, per_pattern)
|
|
300
|
+
|
|
301
|
+
w = torch.zeros(n_p, 3, device=device, dtype=dtype, requires_grad=True)
|
|
302
|
+
params = [{"params": [w], "lr": learning_rate}]
|
|
303
|
+
if refine_pattern_center:
|
|
304
|
+
pc_param = pc_init.clone().requires_grad_(True)
|
|
305
|
+
params.append(
|
|
306
|
+
{
|
|
307
|
+
"params": [pc_param],
|
|
308
|
+
"lr": pattern_center_learning_rate
|
|
309
|
+
if pattern_center_learning_rate is not None
|
|
310
|
+
else learning_rate / 5.0,
|
|
311
|
+
}
|
|
312
|
+
)
|
|
313
|
+
else:
|
|
314
|
+
pc_param = pc_init
|
|
315
|
+
# pattern center is fixed -> E does not change; back-project it once.
|
|
316
|
+
e_fixed, _ = _backproject(patt, detector, dirs, pc_param, per_pattern)
|
|
317
|
+
|
|
318
|
+
opt = torch.optim.Adam(params)
|
|
319
|
+
history = [] if return_history else None
|
|
320
|
+
|
|
321
|
+
def _score():
|
|
322
|
+
if refine_pattern_center:
|
|
323
|
+
e = _backproject(patt, detector, dirs, pc_param, per_pattern)[0]
|
|
324
|
+
else:
|
|
325
|
+
e = e_fixed
|
|
326
|
+
d_r = rotation_vector_to_matrix(w) # (Np, 3, 3)
|
|
327
|
+
r = r0_mat @ d_r # right-compose: R = R0 . dR(w)
|
|
328
|
+
v = dirs @ r.transpose(-1, -2) # (Np, M, 3) = R n, matching project
|
|
329
|
+
m_r = master.sample(v).to(device=device, dtype=dtype) # (Np, M)
|
|
330
|
+
return _masked_ncc(e, m_r, mask)
|
|
331
|
+
|
|
332
|
+
# seed NCC (w = 0), for the keep_best gate below
|
|
333
|
+
with torch.no_grad():
|
|
334
|
+
seed_score = _score().detach()
|
|
335
|
+
|
|
336
|
+
for _ in range(n_iterations):
|
|
337
|
+
opt.zero_grad()
|
|
338
|
+
s = _score()
|
|
339
|
+
if return_history:
|
|
340
|
+
history.append(s.detach())
|
|
341
|
+
(-s.sum()).backward()
|
|
342
|
+
opt.step()
|
|
343
|
+
|
|
344
|
+
with torch.no_grad():
|
|
345
|
+
refined_score = _score().detach()
|
|
346
|
+
r_ref = r0_mat @ rotation_vector_to_matrix(w) # (Np, 3, 3)
|
|
347
|
+
if keep_best:
|
|
348
|
+
# keep the coarse seed where refinement did not raise the NCC. Under
|
|
349
|
+
# refine_pattern_center the gate reverts only the orientation, so seed_score (taken
|
|
350
|
+
# at the seed pattern_center) is an approximation there -- a safe one.
|
|
351
|
+
keep = (refined_score >= seed_score).reshape(-1, 1, 1)
|
|
352
|
+
r_final = torch.where(keep, r_ref, r0_mat).cpu().numpy()
|
|
353
|
+
score = torch.maximum(refined_score, seed_score)
|
|
354
|
+
else:
|
|
355
|
+
r_final = r_ref.cpu().numpy()
|
|
356
|
+
score = refined_score
|
|
357
|
+
|
|
358
|
+
from orix.quaternion import Rotation
|
|
359
|
+
|
|
360
|
+
rotation = Rotation.from_matrix(r_final)
|
|
361
|
+
|
|
362
|
+
out = [rotation, score]
|
|
363
|
+
if refine_pattern_center:
|
|
364
|
+
out.append(pc_param.detach())
|
|
365
|
+
if return_history:
|
|
366
|
+
out.append(torch.stack(history, dim=0))
|
|
367
|
+
return tuple(out)
|