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

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 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.

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.

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: