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,386 @@
|
|
|
1
|
+
"""Live indexing: a buffer of camera frames indexed and refined where it already is.
|
|
2
|
+
|
|
3
|
+
Built for a camera streaming thousands of patterns a second into GPU memory. A
|
|
4
|
+
buffer of frames is indexed in a fixed number of batched steps -- nothing loops
|
|
5
|
+
over patterns or phases -- and only each pattern's phase, orientation and score
|
|
6
|
+
leave the device.
|
|
7
|
+
|
|
8
|
+
1. **Prepare** (:meth:`LiveIndexer.prepare`): each frame averaged over the whole
|
|
9
|
+
sensor down to the refine shape, divided by the scan's static background (the
|
|
10
|
+
running mean of its frames, :meth:`LiveIndexer.observe`, started from a search's
|
|
11
|
+
and blurred for a single crystal, :meth:`LiveIndexer.restart_static`), the
|
|
12
|
+
per-pattern dynamic background removed, then averaged again to the search shape.
|
|
13
|
+
2. **Search** (:meth:`LiveIndexer.search`): one matrix product against every
|
|
14
|
+
phase's dictionary at once. The dictionaries are the phases' dynamical master
|
|
15
|
+
patterns projected over their fundamental zones, stacked into one matrix, so the
|
|
16
|
+
best entry gives the phase and a seed orientation together; a low symmetry's
|
|
17
|
+
large dictionary is scored a chunk of entries at a time.
|
|
18
|
+
3. **Refine** (:meth:`LiveIndexer.refine`): a local dictionary around each seed --
|
|
19
|
+
the seed and the six rotations of one step about each axis -- projected from the
|
|
20
|
+
master on the fly at the refine shape. A parabola through each axis's three
|
|
21
|
+
scores moves the orientation; the step shrinks each level. Every level is the
|
|
22
|
+
same seven projections per pattern, so its cost is fixed and known.
|
|
23
|
+
4. **Pattern center** (:meth:`LiveIndexer.refine_pattern_center`): the center
|
|
24
|
+
shared by a buffer's best patterns, refined the same way -- seven centers a
|
|
25
|
+
level, each scored with every pattern's orientation refined again there.
|
|
26
|
+
|
|
27
|
+
Orientations are rotation matrices ``g`` taking a sample-frame direction to the
|
|
28
|
+
crystal frame (``v_crystal = g @ v_sample``), as :meth:`MasterPattern.project`
|
|
29
|
+
uses them.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
from __future__ import annotations
|
|
33
|
+
|
|
34
|
+
import math
|
|
35
|
+
from dataclasses import replace
|
|
36
|
+
|
|
37
|
+
import numpy as np
|
|
38
|
+
import torch
|
|
39
|
+
|
|
40
|
+
from de_ebsd.core.device import resolve_device
|
|
41
|
+
from de_ebsd.core.lambert import sample_texture
|
|
42
|
+
from de_ebsd.core.rotation import quaternion_to_matrix, rotation_vector_to_matrix
|
|
43
|
+
from de_ebsd.index import _kernels
|
|
44
|
+
from de_ebsd.index.live import calibration, deformation
|
|
45
|
+
from de_ebsd.index.live.background import ScanBackground
|
|
46
|
+
from de_ebsd.index.live.common import BATCH, STENCIL, average_to, parabola_offset
|
|
47
|
+
from de_ebsd.index.preprocess import gaussian_blur, normalize_rows
|
|
48
|
+
|
|
49
|
+
__all__ = ["LiveIndexer"]
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
#: Refine steps, in degrees, each half the last. On a twin scan rendered from a
|
|
53
|
+
#: dynamical master (1024 patterns, 33 grains, 3° dictionary, 128² refine) these
|
|
54
|
+
#: gave a median of 0.14° from the truth, against 0.23° for (1.5, 0.5, 0.17) and
|
|
55
|
+
#: 0.19° with a fourth level of 0.06.
|
|
56
|
+
REFINE_STEPS_DEG = (2.0, 1.0, 0.5, 0.25, 0.12)
|
|
57
|
+
|
|
58
|
+
#: How much the search's copy of the masters is blurred (Gaussian sigma, degrees on
|
|
59
|
+
#: the sphere). A sharp dynamical pattern's score falls off so steeply away from
|
|
60
|
+
#: its orientation that at a 3° dictionary no entry is near enough for some
|
|
61
|
+
#: grains and a distant wrong one wins: 6-9% of a twin scan's patterns landed
|
|
62
|
+
#: 40-60° off unblurred, none at 2°.
|
|
63
|
+
SEARCH_BLUR_DEG = 2.0
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
#: Dictionary entries scored at a time by :meth:`LiveIndexer.search`, which bounds
|
|
67
|
+
#: its scores at (patterns, this): 64 MB for a batch. A cubic phase's 3° dictionary
|
|
68
|
+
#: (30,443 entries) is one chunk; a triclinic one's (729,000) would otherwise
|
|
69
|
+
#: score into 750 MB per batch.
|
|
70
|
+
SEARCH_CHUNK = 65_536
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def texture_of(masters, device, blur_deg: float = 0.0) -> torch.Tensor:
|
|
74
|
+
"""Every phase's hemispheres as channels of one texture ``(1, 2 * phases, n, n)``:
|
|
75
|
+
one sampling pass serves all phases, and each pattern keeps its own phase's."""
|
|
76
|
+
if len({m.npx for m in masters}) != 1:
|
|
77
|
+
raise ValueError("every master must have the same npx")
|
|
78
|
+
texture = torch.cat([m.data[m.energy_index(None)].to(device, torch.float32) for m in masters])
|
|
79
|
+
# Near the center of the square Lambert grid a pixel spans 90° / npx.
|
|
80
|
+
per_degree = masters[0].npx / 90.0
|
|
81
|
+
if blur_deg:
|
|
82
|
+
texture = gaussian_blur(texture, blur_deg * per_degree, 4.0)
|
|
83
|
+
return texture[None].contiguous()
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class LiveIndexer:
|
|
87
|
+
"""Index and refine EBSD patterns of several phases on the device.
|
|
88
|
+
|
|
89
|
+
Parameters
|
|
90
|
+
----------
|
|
91
|
+
masters
|
|
92
|
+
One :class:`~de_ebsd.sim.MasterPattern` per phase, all at the same ``npx``.
|
|
93
|
+
Each is used at its highest energy bin.
|
|
94
|
+
point_groups
|
|
95
|
+
Each phase's orix point group, whose fundamental zone its dictionary samples.
|
|
96
|
+
detector
|
|
97
|
+
The camera's :class:`~de_ebsd.sim.detector.EBSDDetector` (its pattern center
|
|
98
|
+
and angles); frames may come at any resolution, since they are averaged to
|
|
99
|
+
``search_shape`` and ``refine_shape``.
|
|
100
|
+
resolution
|
|
101
|
+
The dictionary's sampling, in degrees.
|
|
102
|
+
search_shape, refine_shape
|
|
103
|
+
Detector shapes the search and the refine compare patterns at.
|
|
104
|
+
refine_steps_deg
|
|
105
|
+
The refine levels' steps; empty for no refine.
|
|
106
|
+
search_blur_deg
|
|
107
|
+
How much the search's copy of the masters is blurred, see
|
|
108
|
+
:data:`SEARCH_BLUR_DEG`.
|
|
109
|
+
device
|
|
110
|
+
Defaults to the first master's.
|
|
111
|
+
dtype
|
|
112
|
+
The search dictionary's. ``None`` picks float16 on GPUs with tensor cores
|
|
113
|
+
(compute capability 7.0 and up) and float32 elsewhere: Pascal multiplies
|
|
114
|
+
float16 at 1/64 of its float32 rate, and a CPU has no float16 product. Half
|
|
115
|
+
precision halves the dictionary, which is what grows with low symmetry; the
|
|
116
|
+
search only picks seeds, so the refine sets the accuracy either way.
|
|
117
|
+
"""
|
|
118
|
+
|
|
119
|
+
def __init__(
|
|
120
|
+
self,
|
|
121
|
+
masters,
|
|
122
|
+
point_groups,
|
|
123
|
+
detector,
|
|
124
|
+
*,
|
|
125
|
+
resolution: float = 3.0,
|
|
126
|
+
search_shape: tuple[int, int] = (32, 32),
|
|
127
|
+
refine_shape: tuple[int, int] = (128, 128),
|
|
128
|
+
refine_steps_deg=REFINE_STEPS_DEG,
|
|
129
|
+
search_blur_deg: float = SEARCH_BLUR_DEG,
|
|
130
|
+
device=None,
|
|
131
|
+
dtype=None,
|
|
132
|
+
) -> None:
|
|
133
|
+
from orix.sampling import get_sample_fundamental
|
|
134
|
+
|
|
135
|
+
if len(masters) != len(point_groups):
|
|
136
|
+
raise ValueError("one point group per master")
|
|
137
|
+
self.device = resolve_device(device, masters[0].device)
|
|
138
|
+
if dtype is None:
|
|
139
|
+
tensor_cores = (
|
|
140
|
+
self.device.type == "cuda" and torch.cuda.get_device_capability(self.device)[0] >= 7
|
|
141
|
+
)
|
|
142
|
+
dtype = torch.float16 if tensor_cores else torch.float32
|
|
143
|
+
self.dtype = dtype
|
|
144
|
+
self.dictionary = None
|
|
145
|
+
self.detector = detector
|
|
146
|
+
self.search_shape = tuple(search_shape)
|
|
147
|
+
self.refine_shape = tuple(refine_shape)
|
|
148
|
+
self.refine_steps = tuple(math.radians(s) for s in refine_steps_deg)
|
|
149
|
+
self.fused = _kernels.available(self.device)
|
|
150
|
+
self.texture = texture_of(masters, self.device)
|
|
151
|
+
self.search_texture = texture_of(masters, self.device, search_blur_deg)
|
|
152
|
+
self.stencil = torch.tensor(STENCIL, device=self.device, dtype=torch.float32)
|
|
153
|
+
|
|
154
|
+
rotations, phases = [], []
|
|
155
|
+
for number, point_group in enumerate(point_groups):
|
|
156
|
+
q = np.asarray(
|
|
157
|
+
get_sample_fundamental(resolution=resolution, point_group=point_group).data
|
|
158
|
+
)
|
|
159
|
+
rotations.append(
|
|
160
|
+
quaternion_to_matrix(torch.as_tensor(q, device=self.device, dtype=torch.float32))
|
|
161
|
+
)
|
|
162
|
+
phases.append(torch.full((len(q),), number, device=self.device, dtype=torch.int64))
|
|
163
|
+
self.rotations = torch.cat(rotations)
|
|
164
|
+
self.phase_of = torch.cat(phases)
|
|
165
|
+
self.sizes = [len(r) for r in rotations]
|
|
166
|
+
self.set_pattern_center(detector.pattern_center)
|
|
167
|
+
self.background = ScanBackground(self.refine_shape, self.device)
|
|
168
|
+
|
|
169
|
+
def directions_at(self, pattern_center, shape) -> torch.Tensor:
|
|
170
|
+
detector = replace(
|
|
171
|
+
self.detector, pattern_center=tuple(float(v) for v in pattern_center), shape=shape
|
|
172
|
+
)
|
|
173
|
+
return detector.direction_cosines(device=self.device)
|
|
174
|
+
|
|
175
|
+
def set_pattern_center(self, pattern_center) -> None:
|
|
176
|
+
"""Index at pattern center *pattern_center* from now on: the dictionary is projected
|
|
177
|
+
again, which takes milliseconds with the fused kernels.
|
|
178
|
+
|
|
179
|
+
Each chunk of entries is written straight into the dictionary, and a new
|
|
180
|
+
center overwrites the old one's in place: the dictionary is the largest
|
|
181
|
+
thing on the device, and building it beside a copy of itself (or a list of
|
|
182
|
+
its chunks) took two to three times its size."""
|
|
183
|
+
self.detector = replace(
|
|
184
|
+
self.detector, pattern_center=tuple(float(v) for v in pattern_center)
|
|
185
|
+
)
|
|
186
|
+
self.search_directions = self.directions_at(pattern_center, self.search_shape)
|
|
187
|
+
self.refine_directions = self.directions_at(pattern_center, self.refine_shape)
|
|
188
|
+
shape = (len(self.search_directions), len(self.rotations))
|
|
189
|
+
if self.dictionary is None or tuple(self.dictionary.shape) != shape:
|
|
190
|
+
#: ``(pixels, entries)``, so the search's product reads it at full rate.
|
|
191
|
+
self.dictionary = torch.empty(shape, device=self.device, dtype=self.dtype)
|
|
192
|
+
for lo in range(0, len(self.rotations), BATCH * 8):
|
|
193
|
+
entries = normalize_rows(
|
|
194
|
+
self.project(
|
|
195
|
+
self.search_texture,
|
|
196
|
+
self.rotations[lo : lo + BATCH * 8],
|
|
197
|
+
self.phase_of[lo : lo + BATCH * 8],
|
|
198
|
+
self.search_directions,
|
|
199
|
+
)
|
|
200
|
+
)
|
|
201
|
+
self.dictionary[:, lo : lo + len(entries)] = entries.T
|
|
202
|
+
|
|
203
|
+
def project(
|
|
204
|
+
self,
|
|
205
|
+
texture: torch.Tensor,
|
|
206
|
+
rotations: torch.Tensor,
|
|
207
|
+
phases: torch.Tensor,
|
|
208
|
+
directions: torch.Tensor,
|
|
209
|
+
) -> torch.Tensor:
|
|
210
|
+
"""The patterns ``(M, pixels)`` of *rotations* ``(M, 3, 3)``, each from its
|
|
211
|
+
own phase's master in *texture*, over *directions* ``(pixels, 3)``."""
|
|
212
|
+
if self.fused:
|
|
213
|
+
return _kernels.project(texture, rotations, phases * 2, directions)
|
|
214
|
+
v = torch.einsum("mij,pj->mpi", rotations, directions)
|
|
215
|
+
return sample_texture(texture, v, phases[:, None])
|
|
216
|
+
|
|
217
|
+
def score_candidates(
|
|
218
|
+
self, candidates: torch.Tensor, phases: torch.Tensor, rows: torch.Tensor, directions
|
|
219
|
+
) -> torch.Tensor:
|
|
220
|
+
"""Scores ``(N, K)`` of candidates ``(N, K, 3, 3)`` against their patterns'
|
|
221
|
+
normalized *rows* ``(N, pixels)``, sampled at *directions*."""
|
|
222
|
+
n, k = candidates.shape[:2]
|
|
223
|
+
flat = candidates.reshape(-1, 3, 3)
|
|
224
|
+
bases = phases.repeat_interleave(k) * 2
|
|
225
|
+
if self.fused:
|
|
226
|
+
return _kernels.score(self.texture, flat, bases, directions, rows, k).view(n, k)
|
|
227
|
+
simulated = normalize_rows(self.project(self.texture, flat, bases // 2, directions))
|
|
228
|
+
return torch.einsum("nkp,np->nk", simulated.view(n, k, -1), rows)
|
|
229
|
+
|
|
230
|
+
# --- the scan's backgrounds ------------------------------------------------
|
|
231
|
+
|
|
232
|
+
@property
|
|
233
|
+
def static(self):
|
|
234
|
+
"""The scan's static background, ``None`` until enough frames have been seen."""
|
|
235
|
+
return self.background.static
|
|
236
|
+
|
|
237
|
+
@property
|
|
238
|
+
def ghost(self):
|
|
239
|
+
"""The scan's grains as the static background shows them, or ``None``; see
|
|
240
|
+
:attr:`ScanBackground.ghost`."""
|
|
241
|
+
return self.background.ghost
|
|
242
|
+
|
|
243
|
+
def pooled(self, frames) -> torch.Tensor:
|
|
244
|
+
"""*frames* on the device as float ``(N, H, W)`` averaged to the refine shape."""
|
|
245
|
+
t = torch.as_tensor(frames, device=self.device)
|
|
246
|
+
t = t[None] if t.ndim == 2 else t
|
|
247
|
+
return average_to(t.to(torch.float32), self.refine_shape)
|
|
248
|
+
|
|
249
|
+
def restart_static(self, frames=None, *, blur: float | None = None) -> None:
|
|
250
|
+
"""Forget the scan, for a new one: its static background and its ghost.
|
|
251
|
+
|
|
252
|
+
*frames* from all over the same field, such as a search's, start the new
|
|
253
|
+
static background, so the scan's first patterns are divided by one that
|
|
254
|
+
holds no grain's bands rather than by the mean of its first few frames.
|
|
255
|
+
|
|
256
|
+
With *blur* (a Gaussian sigma, as a share of the pattern's width) the
|
|
257
|
+
static background is the frames' mean blurred, for a single crystal: its
|
|
258
|
+
frames all carry its bands, so their mean would divide them out. It then
|
|
259
|
+
has no ghost."""
|
|
260
|
+
self.background.restart(blur)
|
|
261
|
+
if frames is not None:
|
|
262
|
+
self.observe(frames)
|
|
263
|
+
|
|
264
|
+
def observe(self, frames) -> None:
|
|
265
|
+
"""Add *frames* to the scan's static background."""
|
|
266
|
+
self.background.observe(self.pooled(frames))
|
|
267
|
+
|
|
268
|
+
def add_fits(self, phase, g, center_maps=None) -> None:
|
|
269
|
+
"""Add the simulations of fitted orientations *g* ``(N, 3, 3)`` to the
|
|
270
|
+
scan's :attr:`ghost`."""
|
|
271
|
+
if self.background.blur:
|
|
272
|
+
return
|
|
273
|
+
if center_maps is not None:
|
|
274
|
+
g = g @ center_maps
|
|
275
|
+
self.background.add_fits(self.project(self.texture, g, phase, self.refine_directions))
|
|
276
|
+
|
|
277
|
+
def corrected(self, frames) -> torch.Tensor:
|
|
278
|
+
"""*frames* ``(N, H, W)`` with the backgrounds removed as :meth:`prepare` removes
|
|
279
|
+
them, at their own resolution: what the indexer matches, to be looked at."""
|
|
280
|
+
t = torch.as_tensor(frames, device=self.device)
|
|
281
|
+
t = t[None] if t.ndim == 2 else t
|
|
282
|
+
return self.background.remove(t.to(torch.float32))
|
|
283
|
+
|
|
284
|
+
def prepare(self, frames) -> tuple[torch.Tensor, torch.Tensor]:
|
|
285
|
+
"""*frames* ``(N, H, W)`` corrected and normalized, as ``(search, refine)``
|
|
286
|
+
rows ``(N, pixels)`` at the two shapes."""
|
|
287
|
+
t = self.background.remove(self.pooled(frames))
|
|
288
|
+
search = average_to(t, self.search_shape)
|
|
289
|
+
return normalize_rows(search.flatten(1)), normalize_rows(t.flatten(1))
|
|
290
|
+
|
|
291
|
+
def search(self, search_rows: torch.Tensor):
|
|
292
|
+
"""The best dictionary entry for each pattern: ``(phase, g, score, entry)``,
|
|
293
|
+
*entry* indexing :attr:`rotations` (every phase's entries, in phase order).
|
|
294
|
+
|
|
295
|
+
The dictionary is scored :data:`SEARCH_CHUNK` entries at a time, keeping
|
|
296
|
+
each pattern's best so far, so the scores never outgrow one chunk."""
|
|
297
|
+
rows = search_rows.to(self.dtype)
|
|
298
|
+
score = torch.full((len(rows),), -math.inf, device=self.device)
|
|
299
|
+
entry = torch.zeros(len(rows), dtype=torch.int64, device=self.device)
|
|
300
|
+
for lo in range(0, self.dictionary.shape[1], SEARCH_CHUNK):
|
|
301
|
+
chunk_score, chunk_entry = (rows @ self.dictionary[:, lo : lo + SEARCH_CHUNK]).max(1)
|
|
302
|
+
better = chunk_score.float() > score
|
|
303
|
+
score = torch.where(better, chunk_score.float(), score)
|
|
304
|
+
entry = torch.where(better, chunk_entry + lo, entry)
|
|
305
|
+
return self.phase_of[entry], self.rotations[entry], score, entry
|
|
306
|
+
|
|
307
|
+
def refine(
|
|
308
|
+
self,
|
|
309
|
+
refine_rows: torch.Tensor,
|
|
310
|
+
phase: torch.Tensor,
|
|
311
|
+
g: torch.Tensor,
|
|
312
|
+
*,
|
|
313
|
+
steps=None,
|
|
314
|
+
directions=None,
|
|
315
|
+
):
|
|
316
|
+
"""Each orientation *g* ``(N, 3, 3)`` refined against its pattern:
|
|
317
|
+
``(g, score)``, the score being the last level's at the seed. *steps*
|
|
318
|
+
(radians) default to the indexer's, *directions* to its pattern center's."""
|
|
319
|
+
steps = self.refine_steps if steps is None else steps
|
|
320
|
+
directions = self.refine_directions if directions is None else directions
|
|
321
|
+
score = torch.zeros(len(g), device=self.device)
|
|
322
|
+
for step in steps:
|
|
323
|
+
candidates = g[:, None] @ rotation_vector_to_matrix(self.stencil * step)
|
|
324
|
+
scores = self.score_candidates(candidates, phase, refine_rows, directions)
|
|
325
|
+
g = g @ rotation_vector_to_matrix(parabola_offset(scores) * step)
|
|
326
|
+
score = scores[:, 0]
|
|
327
|
+
return g, score
|
|
328
|
+
|
|
329
|
+
# --- the pattern center and the strain --------------------------------------
|
|
330
|
+
|
|
331
|
+
def refine_pattern_center(self, frames, **kwargs) -> tuple[float, float, float]:
|
|
332
|
+
"""The pattern center that best fits the best-scored patterns of *frames*
|
|
333
|
+
``(N, H, W)``, which the indexer then uses; see
|
|
334
|
+
:func:`~de_ebsd.index.live.calibration.refine_pattern_center`."""
|
|
335
|
+
return calibration.refine_pattern_center(self, frames, **kwargs)
|
|
336
|
+
|
|
337
|
+
def center_maps(self, pattern_centers) -> torch.Tensor:
|
|
338
|
+
"""One ``(3, 3)`` map per pattern center; see
|
|
339
|
+
:func:`~de_ebsd.index.live.deformation.center_maps`."""
|
|
340
|
+
detector = replace(self.detector, shape=self.refine_shape)
|
|
341
|
+
return deformation.center_maps(detector, pattern_centers, self.device)
|
|
342
|
+
|
|
343
|
+
def refine_deformation(self, refine_rows, phase, g, **kwargs):
|
|
344
|
+
"""Each orientation refined together with its pattern's strain; see
|
|
345
|
+
:func:`~de_ebsd.index.live.deformation.refine_deformation`."""
|
|
346
|
+
return deformation.refine_deformation(self, refine_rows, phase, g, **kwargs)
|
|
347
|
+
|
|
348
|
+
def strain_from_reference(self, frames, reference, **kwargs):
|
|
349
|
+
"""The strain of *frames* relative to one real *reference* frame; see
|
|
350
|
+
:func:`~de_ebsd.index.live.deformation.strain_from_reference`."""
|
|
351
|
+
return deformation.strain_from_reference(self, frames, reference, **kwargs)
|
|
352
|
+
|
|
353
|
+
def index_with_strain(self, frames, pattern_centers=None):
|
|
354
|
+
""":meth:`index`, then each orientation refined together with its pattern's
|
|
355
|
+
strain (:meth:`refine_deformation`): ``(phase, g, strain, score, entry)``.
|
|
356
|
+
*pattern_centers* ``(N, 3)``, one per frame, default to the indexer's.
|
|
357
|
+
|
|
358
|
+
Each buffer's orientations join the scan's :attr:`ghost` before its
|
|
359
|
+
strains are fitted, as its frames joined the static background."""
|
|
360
|
+
maps = None if pattern_centers is None else self.center_maps(pattern_centers)
|
|
361
|
+
out = []
|
|
362
|
+
for lo in range(0, len(frames), BATCH):
|
|
363
|
+
search_rows, refine_rows = self.prepare(frames[lo : lo + BATCH])
|
|
364
|
+
phase, g, _, entry = self.search(search_rows)
|
|
365
|
+
g, _ = self.refine(refine_rows, phase, g)
|
|
366
|
+
batch_maps = None if maps is None else maps[lo : lo + BATCH]
|
|
367
|
+
self.add_fits(phase, g, batch_maps)
|
|
368
|
+
g, strain, score = self.refine_deformation(
|
|
369
|
+
refine_rows, phase, g, center_maps=batch_maps, ghost=self.ghost
|
|
370
|
+
)
|
|
371
|
+
out.append((phase, g, strain, score, entry))
|
|
372
|
+
return tuple(torch.cat(column) for column in zip(*out, strict=True))
|
|
373
|
+
|
|
374
|
+
def index(self, frames):
|
|
375
|
+
"""*frames* ``(N, H, W)`` on the device, indexed: ``(phase, g, score, entry)``.
|
|
376
|
+
*entry* is the dictionary entry the search seeded the refine from (see
|
|
377
|
+
:meth:`search`): anything worked out once per entry, such as a color, stands
|
|
378
|
+
for the refined orientation to within the dictionary's resolution."""
|
|
379
|
+
out = []
|
|
380
|
+
for lo in range(0, len(frames), BATCH):
|
|
381
|
+
search_rows, refine_rows = self.prepare(frames[lo : lo + BATCH])
|
|
382
|
+
phase, g, score, entry = self.search(search_rows)
|
|
383
|
+
if self.refine_steps:
|
|
384
|
+
g, score = self.refine(refine_rows, phase, g)
|
|
385
|
+
out.append((phase, g, score, entry))
|
|
386
|
+
return tuple(torch.cat(column) for column in zip(*out, strict=True))
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
"""Background removal for experimental patterns.
|
|
2
|
+
|
|
3
|
+
Static (mean over the scan) and dynamic (Gaussian-blurred per pattern)
|
|
4
|
+
background correction, batched on the GPU.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import torch
|
|
10
|
+
import torch.nn.functional as F
|
|
11
|
+
|
|
12
|
+
from de_ebsd.core.constants import DIVISION_FLOOR, SINGLE_PRECISION_FLOOR
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"remove_static_background",
|
|
16
|
+
"remove_dynamic_background",
|
|
17
|
+
"remove_background",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _as_stack(patterns, device, dtype):
|
|
22
|
+
x = torch.as_tensor(patterns, device=device, dtype=dtype)
|
|
23
|
+
if x.ndim == 2:
|
|
24
|
+
lead = ()
|
|
25
|
+
elif x.ndim == 3:
|
|
26
|
+
lead = (x.shape[0],)
|
|
27
|
+
elif x.ndim == 4:
|
|
28
|
+
lead = (x.shape[0], x.shape[1])
|
|
29
|
+
else:
|
|
30
|
+
raise ValueError(
|
|
31
|
+
f"patterns must be (H,W), (N,H,W) or (rows,cols,H,W); got shape {tuple(x.shape)}"
|
|
32
|
+
)
|
|
33
|
+
h, w = x.shape[-2], x.shape[-1]
|
|
34
|
+
stack = x.reshape(-1, h, w)
|
|
35
|
+
|
|
36
|
+
def restore(processed):
|
|
37
|
+
return processed.reshape(*lead, h, w)
|
|
38
|
+
|
|
39
|
+
return stack, restore
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _rescale01(p):
|
|
43
|
+
lo = p.amin(dim=(-2, -1), keepdim=True)
|
|
44
|
+
hi = p.amax(dim=(-2, -1), keepdim=True)
|
|
45
|
+
return (p - lo) / (hi - lo).clamp_min(DIVISION_FLOOR)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _apply(pattern, background, operation):
|
|
49
|
+
if operation == "subtract":
|
|
50
|
+
return pattern - background
|
|
51
|
+
if operation == "divide":
|
|
52
|
+
return pattern / background.clamp_min(DIVISION_FLOOR)
|
|
53
|
+
raise ValueError(f"operation must be 'subtract' or 'divide', got {operation!r}")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def normalize_rows(x: torch.Tensor) -> torch.Tensor:
|
|
57
|
+
"""Each row of *x* shifted to zero mean and scaled to unit length, so a dot product
|
|
58
|
+
of two rows is their normalized cross-correlation."""
|
|
59
|
+
x = x - x.mean(-1, keepdim=True)
|
|
60
|
+
return x / x.norm(dim=-1, keepdim=True).clamp_min(SINGLE_PRECISION_FLOOR)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _gaussian_kernel_1d(sigma, truncate, device, dtype):
|
|
64
|
+
radius = int(truncate * float(sigma) + 0.5)
|
|
65
|
+
x = torch.arange(-radius, radius + 1, device=device, dtype=dtype)
|
|
66
|
+
k = torch.exp(-0.5 * (x / float(sigma)) ** 2)
|
|
67
|
+
return k / k.sum(), radius
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def gaussian_blur(stack, sigma, truncate):
|
|
71
|
+
k, r = _gaussian_kernel_1d(sigma, truncate, stack.device, stack.dtype)
|
|
72
|
+
x = stack[:, None] # (N, 1, H, W)
|
|
73
|
+
x = F.conv2d(F.pad(x, (r, r, 0, 0), mode="reflect"), k.view(1, 1, 1, -1))
|
|
74
|
+
x = F.conv2d(F.pad(x, (0, 0, r, r), mode="reflect"), k.view(1, 1, -1, 1))
|
|
75
|
+
return x[:, 0]
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def remove_static_background(
|
|
79
|
+
patterns, static_bg, operation="subtract", device=None, dtype=torch.float32
|
|
80
|
+
):
|
|
81
|
+
"""Remove the fixed detector/beam background from a stack of patterns.
|
|
82
|
+
|
|
83
|
+
Parameters
|
|
84
|
+
----------
|
|
85
|
+
patterns
|
|
86
|
+
Detector images, ``(H, W)`` / ``(N, H, W)`` / ``(rows, cols, H, W)``, numpy or
|
|
87
|
+
torch. ``H``, ``W`` are the detector shape.
|
|
88
|
+
static_bg
|
|
89
|
+
The static background image ``(H, W)`` (e.g. a kikuchipy signal's
|
|
90
|
+
``static_background``, or the scan mean), numpy or torch.
|
|
91
|
+
operation
|
|
92
|
+
``"subtract"`` (default) or ``"divide"``.
|
|
93
|
+
device
|
|
94
|
+
Torch device to run on. ``None`` (default) keeps the patterns' device
|
|
95
|
+
(``"cuda"`` recommended for a whole scan).
|
|
96
|
+
dtype
|
|
97
|
+
Working floating dtype (default ``float32``).
|
|
98
|
+
|
|
99
|
+
Returns
|
|
100
|
+
-------
|
|
101
|
+
torch.Tensor
|
|
102
|
+
The cleaned patterns, same leading shape as the input, each rescaled to
|
|
103
|
+
``[0, 1]`` (indexing is invariant to the per-pattern affine scale).
|
|
104
|
+
"""
|
|
105
|
+
if device is None:
|
|
106
|
+
device = torch.as_tensor(patterns).device if torch.is_tensor(patterns) else "cpu"
|
|
107
|
+
stack, restore = _as_stack(patterns, device, dtype)
|
|
108
|
+
bg = torch.as_tensor(static_bg, device=stack.device, dtype=dtype)
|
|
109
|
+
if bg.shape != stack.shape[-2:]:
|
|
110
|
+
raise ValueError(f"static_bg shape {tuple(bg.shape)} != detector {tuple(stack.shape[-2:])}")
|
|
111
|
+
out = _rescale01(_apply(stack, bg[None], operation))
|
|
112
|
+
return restore(out)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def remove_dynamic_background(
|
|
116
|
+
patterns, sigma=None, operation="subtract", truncate=4.0, device=None, dtype=torch.float32
|
|
117
|
+
):
|
|
118
|
+
"""Remove the per-pattern dynamic background (a Gaussian blur of each pattern).
|
|
119
|
+
|
|
120
|
+
Parameters
|
|
121
|
+
----------
|
|
122
|
+
patterns
|
|
123
|
+
Detector images, ``(H, W)`` / ``(N, H, W)`` / ``(rows, cols, H, W)``, numpy or
|
|
124
|
+
torch.
|
|
125
|
+
sigma
|
|
126
|
+
Standard deviation of the Gaussian window. ``None`` (default) uses
|
|
127
|
+
``width / 8`` (kikuchipy's default), the width being the number of columns.
|
|
128
|
+
operation
|
|
129
|
+
``"subtract"`` (default) or ``"divide"``.
|
|
130
|
+
truncate
|
|
131
|
+
Truncate the Gaussian window at this many standard deviations (default ``4.0``,
|
|
132
|
+
matching scipy/kikuchipy).
|
|
133
|
+
device, dtype
|
|
134
|
+
As in :func:`remove_static_background`.
|
|
135
|
+
|
|
136
|
+
Returns
|
|
137
|
+
-------
|
|
138
|
+
torch.Tensor
|
|
139
|
+
The cleaned patterns, same leading shape as the input, each rescaled to
|
|
140
|
+
``[0, 1]``.
|
|
141
|
+
"""
|
|
142
|
+
if device is None:
|
|
143
|
+
device = torch.as_tensor(patterns).device if torch.is_tensor(patterns) else "cpu"
|
|
144
|
+
stack, restore = _as_stack(patterns, device, dtype)
|
|
145
|
+
if sigma is None:
|
|
146
|
+
sigma = stack.shape[-1] / 8.0
|
|
147
|
+
bg = gaussian_blur(stack, sigma, truncate)
|
|
148
|
+
out = _rescale01(_apply(stack, bg, operation))
|
|
149
|
+
return restore(out)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def remove_background(
|
|
153
|
+
patterns,
|
|
154
|
+
static_bg=None,
|
|
155
|
+
dynamic=True,
|
|
156
|
+
operation="subtract",
|
|
157
|
+
sigma=None,
|
|
158
|
+
truncate=4.0,
|
|
159
|
+
device=None,
|
|
160
|
+
dtype=torch.float32,
|
|
161
|
+
):
|
|
162
|
+
"""Full EBSD preprocessing: static then dynamic background removal, on the GPU.
|
|
163
|
+
|
|
164
|
+
Convenience wrapper that runs :func:`remove_static_background` (when ``static_bg``
|
|
165
|
+
is given) followed by :func:`remove_dynamic_background` (when ``dynamic``), the
|
|
166
|
+
usual pre-indexing pipeline, all on ``device`` in one pass over the scan.
|
|
167
|
+
|
|
168
|
+
Parameters
|
|
169
|
+
----------
|
|
170
|
+
patterns
|
|
171
|
+
The raw scan, ``(H, W)`` / ``(N, H, W)`` / ``(rows, cols, H, W)``, numpy or torch.
|
|
172
|
+
static_bg
|
|
173
|
+
The static background image ``(H, W)``; if ``None``, the static step is skipped.
|
|
174
|
+
dynamic
|
|
175
|
+
Whether to also remove the dynamic (per-pattern Gaussian) background
|
|
176
|
+
(default ``True``).
|
|
177
|
+
operation, sigma, truncate, device, dtype
|
|
178
|
+
Passed through to the two steps (see those functions).
|
|
179
|
+
|
|
180
|
+
Returns
|
|
181
|
+
-------
|
|
182
|
+
torch.Tensor
|
|
183
|
+
The cleaned patterns, same leading shape as the input, rescaled to ``[0, 1]``,
|
|
184
|
+
ready to hand to :meth:`~de_ebsd.index.dictionary.DictionaryIndexer.index`.
|
|
185
|
+
"""
|
|
186
|
+
out = patterns
|
|
187
|
+
if static_bg is not None:
|
|
188
|
+
out = remove_static_background(
|
|
189
|
+
out, static_bg, operation=operation, device=device, dtype=dtype
|
|
190
|
+
)
|
|
191
|
+
if dynamic:
|
|
192
|
+
out = remove_dynamic_background(
|
|
193
|
+
out, sigma=sigma, operation=operation, truncate=truncate, device=device, dtype=dtype
|
|
194
|
+
)
|
|
195
|
+
return out
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
"""Gradient refinement of orientation and pattern center."""
|
|
2
|
+
|
|
3
|
+
from de_ebsd.index.refine.highres import refine_hr
|
|
4
|
+
from de_ebsd.index.refine.orientation import refine_orientation
|
|
5
|
+
from de_ebsd.index.refine.pattern_center import determine_pattern_center
|
|
6
|
+
|
|
7
|
+
__all__ = ["determine_pattern_center", "refine_hr", "refine_orientation"]
|