upxo.interfaces.defdap.ebsd_reader module

upxo.interfaces.defdap.ebsd_reader

Thin, UPXO-native wrapper around DefDAP for loading 2D EBSD maps from Oxford Instruments files (.ctf / .crc) and exposing the data as plain NumPy arrays that the rest of UPXO (repgen2d, gsan2d, etc.) can consume directly.

Supported file formats

.ctf Oxford Instruments text -> data_type=’OxfordText’ .crc Oxford Instruments binary -> data_type=’OxfordBinary’

Extracted arrays (all stored as plain NumPy — no DefDAP objects leak out)

lfi_ebsdnp.ndarray, int, shape (ny, nx)

Grain label field. Values >= 1 are grain IDs. -1 = remnant grain-boundary pixel (not assigned to any grain). -2 = pixel belonging to a grain smaller than min_grain_size. 0 = non-indexed point.

euler_ebsdnp.ndarray, float, shape (ny, nx, 3)

Bunge Euler angles (phi1, Phi, phi2) in radians. Transposed from DefDAP’s native (3, ny, nx) to image-convention (ny, nx, 3) so that euler_ebsd[row, col] gives a 3-vector.

quat_ebsdnp.ndarray, float, shape (ny, nx, 4)

Unit quaternion per pixel (q0, q1, q2, q3) extracted from DefDAP’s object array. Positive-hemisphere convention (q0 >= 0).

step_sizefloat

Pixel step size in microns, read directly from the file metadata.

Usage

from upxo.interfaces.defdap.ebsd_reader import EBSDReader

rdr = EBSDReader.from_file(‘scan.ctf’, min_grain_size=10) # or rdr = EBSDReader.from_file(‘scan.crc’, min_grain_size=10)

rdr.lfi_ebsd # (ny, nx) int array rdr.euler_ebsd # (ny, nx, 3) float array, radians rdr.quat_ebsd # (ny, nx, 4) float array rdr.step_size # float, microns

Loading and grain detection as separate steps (e.g. to re-tune detection parameters interactively without re-parsing the file from disk each time):

rdr = EBSDReader.load(‘scan.ctf’) # lfi_ebsd is None here rdr.detect_grains(min_grain_size=10, misori_tol=10) rdr.detect_grains(min_grain_size=25, misori_tol=5) # re-tune, no reload

class upxo.interfaces.defdap.ebsd_reader.EBSDReader[source]

Bases: object

UPXO-native EBSD file reader backed by DefDAP.

Parameters are not passed directly; use the class-methods as constructors.

lfi_ebsd

Grain label field (see module docstring for value conventions).

Type:

np.ndarray, int, shape (ny, nx)

euler_ebsd

Bunge Euler angles phi1, Phi, phi2 in radians.

Type:

np.ndarray, float, shape (ny, nx, 3)

quat_ebsd

Unit quaternion coefficients q0, q1, q2, q3 per pixel.

Type:

np.ndarray, float, shape (ny, nx, 4)

step_size

Step size in microns.

Type:

float

file_path

Absolute path to the source EBSD file.

Type:

str

shape

Map shape (ny, nx).

Type:

tuple(int, int)

classmethod load(file_path, data_type=None)[source]

Load an EBSD map from a .ctf or .crc file WITHOUT detecting grains yet – builds the quaternion array and extracts euler_ebsd / quat_ebsd / step_size / shape / file_path, but leaves lfi_ebsd as None until detect_grains() is called.

Split out from from_file() so grain detection can be re-run with different parameters (min_grain_size, misori_tol) without re-parsing the file from disk each time – the underlying DefDAP map object is retained internally (self._defdap_map) for detect_grains() to reuse.

Parameters:
  • file_path (str or pathlib.Path) – Path to the EBSD file. Extension must be .ctf or .crc unless data_type is supplied explicitly.

  • data_type (str or None, optional) – Override the DefDAP data_type string. When None (default) the format is inferred from the file extension: ‘.ctf’ -> ‘OxfordText’ ‘.crc’ -> ‘OxfordBinary’

Returns:

Instance with euler_ebsd, quat_ebsd, step_size, file_path, shape populated; lfi_ebsd is None until detect_grains() is called.

Return type:

EBSDReader

Raises:
detect_grains(min_grain_size=10, misori_tol=10)[source]

(Re-)detect grain boundaries and grains on this already-loaded map, reusing the DefDAP map object retained by load() – does NOT re-parse the file from disk, so this can be called repeatedly with different parameters to tune detection interactively.

Requires this instance to have been constructed via load() (not from_file(), which already calls this internally, or crop(), whose cropped copies do not retain a DefDAP map object).

Parameters:
  • min_grain_size (int, optional) – Minimum grain size in pixels passed to DefDAP’s find_grains(). Grains smaller than this are labelled -2 in lfi_ebsd. Default 10.

  • misori_tol (float, optional) – Misorientation tolerance in degrees for grain boundary detection inside DefDAP. Default 10.

Returns:

self, for chaining. Populates/overwrites lfi_ebsd.

Return type:

EBSDReader

Raises:

RuntimeError – If this instance has no retained DefDAP map object (i.e. it was not built via load()).

classmethod from_file(file_path, min_grain_size=10, misori_tol=10, data_type=None)[source]

Load an EBSD map from a .ctf or .crc file and extract all UPXO-relevant arrays – a convenience wrapper combining load() and detect_grains() for callers who don’t need to re-tune detection separately.

Parameters:
  • file_path (str or pathlib.Path) – Path to the EBSD file. Extension must be .ctf or .crc unless data_type is supplied explicitly.

  • min_grain_size (int, optional) – Minimum grain size in pixels passed to DefDAP’s find_grains(). Grains smaller than this are labelled -2 in lfi_ebsd. Default 10.

  • misori_tol (float, optional) – Misorientation tolerance in degrees for grain boundary detection inside DefDAP. Default 10.

  • data_type (str or None, optional) – Override the DefDAP data_type string. When None (default) the format is inferred from the file extension: ‘.ctf’ -> ‘OxfordText’ ‘.crc’ -> ‘OxfordBinary’

Returns:

Populated instance with lfi_ebsd, euler_ebsd, quat_ebsd, step_size, file_path, shape.

Return type:

EBSDReader

Raises:
classmethod from_file_subsampled(src_path, dst_path, stride_x: int = 2, stride_y: int = 2, min_grain_size: int = 10, misori_tol: float = 10, data_type=None)[source]

Sub-sample a CTF file, write the result to dst_path, then load it via the normal from_file pipeline.

This is Method C sub-sampling: the file is reduced before DefDAP ever reads it, so grain detection runs on the smaller map and memory usage is proportional to the sub-sampled size.

Parameters:
  • src_path (str or pathlib.Path) – Source .ctf file.

  • dst_path (str or pathlib.Path) – Destination for the written sub-sampled CTF file. The parent directory must already exist.

  • stride_x (int) – Keep every stride_x-th pixel along X. Default 2.

  • stride_y (int) – Keep every stride_y-th pixel along Y. Default 2.

  • min_grain_size (int) – Forwarded to from_file(). Default 10.

  • misori_tol (float) – Forwarded to from_file(). Default 10.

  • data_type (str or None) – Forwarded to from_file(). Default None (auto-detected).

Returns:

Instance loaded from the written sub-sampled CTF file.

Return type:

EBSDReader

property ny

Number of rows (y pixels).

property nx

Number of columns (x pixels).

property n_grains

Number of grains detected (label values >= 1).

crop(region, inplace=False)[source]

Crop the EBSD map to a rectangular sub-region specified as percentage extents of the full map.

Parameters:
  • region (sequence of 4 numbers) –

    [xstart%, ystart%, xend%, yend%] — all values in the range [0, 100].

    • xstart% / xend% are measured along the column (x) axis (i.e. nx dimension).

    • ystart% / yend% are measured along the row (y) axis (i.e. ny dimension).

    Example: [10, 10, 80, 80] keeps the central 70 % of the map in both directions, discarding a 10 % border on each side.

  • inplace (bool, optional) – If True, modify self and return None. If False (default), return a new EBSDReader with the cropped arrays; self is left unchanged.

Returns:

Cropped reader (when inplace=False) or None (when inplace=True).

Return type:

EBSDReader or None

Raises:

ValueError – If region does not have exactly 4 elements, any value is outside [0, 100], or start >= end in either axis.

Notes

DefDAP (0.93.x) has no built-in crop, so the crop is performed entirely on the extracted NumPy arrays. step_size is unchanged (cropping does not affect pixel pitch). Grain IDs in lfi_ebsd are preserved as-is — they are not re-numbered after cropping.

characterise(connectivity=4, min_grain_size=0)[source]

Run the complete post-load characterisation pipeline on self and return a result dict ready for consumption by repgen2d.rechar() or directly by user code.

Steps

  1. rechar_lfi(connectivity) — fill non-positive pixels, update euler_ebsd / quat_ebsd in-place.

  2. _char_lfi(lfi_ebsd, step_size, min_grain_size) — compute per-grain morphological properties; grains smaller than min_grain_size pixels are excluded.

  3. find_neighs2d(lfi_ebsd, conn) — build first-order neighbour dict.

param connectivity:

cc3d connectivity for rechar_lfi and find_neighs2d. Valid 2D values: 4 or 8. Default 4.

type connectivity:

int

param min_grain_size:

Minimum grain size in pixels. Grains with fewer pixels are excluded from the returned 'prop' dict. Default 0 (all grains included).

type min_grain_size:

int, optional

returns:

'lfi' np.ndarray int32 (ny, nx) cleaned label field 'euler' np.ndarray float64 (ny, nx, 3) Bunge angles, radians 'quat' np.ndarray float64 (ny, nx, 4) unit quaternions 'neigh_gid' dict grain_id -> list[int] first-order neighbours 'prop' dict grain_id -> property dict (see _char_lfi) 'step_size' float pixel step size in microns

rtype:

dict with keys

rechar_lfi(connectivity=4)[source]

Re-characterise lfi_ebsd by filling every non-positive pixel (values <= 0, including DefDAP’s -1 remnant-boundary and -2 sub-minimum-size codes, and non-indexed 0) with the label of the spatially largest grain that borders that connected region. euler_ebsd and quat_ebsd for the filled pixels are then updated with the grain-average orientation of the assigned grain.

Algorithm

  1. Identify all non-positive pixels as the unknown mask.

  2. Use cc3d to label connected components of the unknown mask (connectivity 4 or 8, matching the supplied parameter).

  3. Assign each connected component a temporary unique label n_grains + cc_id in a working copy of lfi_ebsd.

  4. Call find_neighs2d on the augmented label field to obtain the adjacency list for every label (standard + temporary).

  5. For each temporary CC label, find the neighbouring valid grain with the greatest pixel count and assign that grain’s ID to all pixels of the CC.

  6. Compute per-grain mean Euler angles and mean (normalised) quaternion from the original valid pixels of each grain.

  7. Write those averages into euler_ebsd and quat_ebsd at the newly filled pixel positions.

  8. Store the updated arrays back to self.

param connectivity:

cc3d connectivity for labelling the unknown region. Valid 2D values: 4 (edge-only) or 8 (edge+corner). Default 4.

type connectivity:

int

raises ValueError:

If connectivity is not 4 or 8.

Notes

After calling this method lfi_ebsd should contain only positive grain labels (>= 1). euler_ebsd and quat_ebsd are updated in-place on self. The grain-average orientation used for filling is the arithmetic mean of the original valid pixels — adequate for intra-grain spread; it is not a proper quaternion mean but sufficient for neighbourhood-assignment.

split_disconnected_grains(connectivity=4)[source]

Ensure every grain id in lfi_ebsd is one spatially connected pixel region.

A grain is one connected region by construction; the same id spread across more than one spatially disjoint pixel cluster is a labelling artifact, not a real grain – most commonly caused by crop() slicing a rectangular window through a single grain’s irregular shape in more than one place, exposing two or more disconnected pieces under the same id. Left uncorrected, this breaks pixel-exact geometrification code that assumes one polygon per grain id (polygonised_grain_structure.pix_to_geom).

For every grain id with more than one connected component, the largest component keeps the original id; every additional component is relabelled with a new, previously-unused id appended after the current maximum. euler_ebsd / quat_ebsd are left untouched – the per-pixel orientation data is already correct regardless of which id a pixel ends up under, so a split-off fragment still averages correctly from its own pixels via grain_average_euler_deg().

Parameters:

connectivity (int) – cc3d connectivity for identifying disconnected components within a single grain id. Valid 2D values: 4 or 8. Default 4.

Returns:

Mapping new_id -> original_id for every id introduced by a split. Ids that were not split are unchanged and not included.

Return type:

dict

Raises:

ValueError – If connectivity is not 4 or 8.

grain_average_euler_deg()[source]

Compute per-grain Bunge Euler angles (phi1, Phi, phi2), in degrees, averaged over every pixel belonging to each grain.

Averaging is done on quat_ebsd (mean quaternion, renormalised, positive-hemisphere convention), then converted to Bunge Euler – the same approach already used internally by rechar_lfi() to fill unindexed pixels, exposed here as a standalone per-grain result. This is an arithmetic quaternion mean, not a symmetry-aware SO(3) mean: adequate for the low intra-grain misorientation spread typical of a single EBSD grain, not for averaging across misorientation-related variants.

Requires lfi_ebsd to contain only positive grain labels (call rechar_lfi() or characterise() first if the map still has unindexed / sub-minimum-size / remnant-boundary pixels).

Returns:

Mapping grain_id (int) -> (phi1, Phi, phi2) tuple of floats, in degrees.

Return type:

dict

Raises:

ValueError – If lfi_ebsd contains non-positive pixels.

plot_grain_map(**kwargs)[source]

Plot the grain label field (lfi_ebsd) with optional boundary overlay.

All keyword arguments are forwarded to upxo.viz.ebsdviz.plot_grain_labels.

Return type:

fig, ax

plot_euler_maps(**kwargs)[source]

Plot phi1, Phi, phi2 Euler angle maps side-by-side.

All keyword arguments are forwarded to upxo.viz.ebsdviz.plot_euler_maps.

Return type:

fig, axes

plot_grain_size_histogram(**kwargs)[source]

Plot a grain area histogram in physical units (µm²).

All keyword arguments are forwarded to upxo.viz.ebsdviz.plot_grain_size_histogram.

Return type:

fig, ax

plot_grain_structure_with_boundaries(**kwargs)[source]

Plot grain label map and Euler channel side-by-side with boundaries.

All keyword arguments are forwarded to upxo.viz.ebsdviz.plot_grain_structure_with_boundaries.

Return type:

fig, axes

see_distr(prop='area', nbins=40, vis='hist', show_kde=True, show_stats=True, color='steelblue', figsize=(7, 4), log_scale=False)[source]

Visualise the distribution of a grain morphological property.

Requires characterise() to have been called first (it populates self.prop with per-grain property dicts).

Parameters:
  • prop (str) – Grain property to plot. Valid names (all scaled to physical units via step_size): 'area', 'perimeter', 'eq_diameter', 'aspect_ratio', 'major_axis_length', 'minor_axis_length', 'eccentricity', 'solidity', 'npixels'.

  • nbins (int) – Number of histogram bins. Default 40.

  • vis (str) – Plot style: 'hist' — histogram with optional KDE overlay; 'kde' — pure kernel density estimate; 'hist_kde' — density-normalised histogram + KDE. Default 'hist'.

  • show_kde (bool) – Overlay a KDE curve on the histogram (vis='hist' only). Default True.

  • show_stats (bool) – Annotate mean and median lines. Default True.

  • color (str) – Histogram / KDE fill colour. Default 'steelblue'.

  • figsize (tuple) – Figure size (width, height) in inches. Default (7, 4).

  • log_scale (bool) – Apply log scale to the x-axis. Default False.

Returns:

fig, ax

Return type:

matplotlib Figure and Axes

Raises:
  • RuntimeError – If self.prop is not populated (characterise not yet called).

  • KeyError – If prop is not present in the property dicts.

  • ValueError – If vis is not one of the supported values.

Examples

>>> fig, ax = rdr.see_distr(prop='area', nbins=40)
>>> plt.show()
>>> fig, ax = rdr.see_distr(prop='aspect_ratio', vis='hist_kde')
>>> plt.show()
lfi_ebsd
euler_ebsd
quat_ebsd
step_size
file_path
shape
prop
upxo.interfaces.defdap.ebsd_reader.write_subsampled_ctf(src_path, dst_path, stride_x: int = 2, stride_y: int = 2) → Path[source]

Read a CTF file, sub-sample every stride_x-th column and stride_y-th row, update the header metadata, and write a new valid CTF file to dst_path.

The X / Y physical-coordinate values in the kept data rows are left unchanged — they already carry the correct micron positions. Only the XCells, YCells, XStep, and YStep header fields are updated to reflect the new pixel pitch and map dimensions.

Parameters:
  • src_path (str or pathlib.Path) – Source .ctf file. A ValueError is raised for other extensions.

  • dst_path (str or pathlib.Path) – Destination path for the sub-sampled CTF file. The parent directory must already exist.

  • stride_x (int) – Keep every stride_x-th pixel along the X (column) axis. Default 2.

  • stride_y (int) – Keep every stride_y-th pixel along the Y (row) axis. Default 2.

Returns:

Absolute path to the written file (dst_path), so the caller can chain directly into EBSDReader.from_file().

Return type:

pathlib.Path

Raises: