Source code for upxo.pxtal.twinned_simple_3d.subsetting_2d

"""
subsetting_2d.py
=================
Rectangular sub-domain extraction from a 2D EBSD grain-label field:
moving-rectangle tile generation and cropping. 2D sibling of
``subsetting.py`` (which does the same for 3D cuboids) -- reuses that
module's axis-interval math rather than duplicating it.
"""

import numpy as np

from upxo.pxtal.twinned_simple_3d.subsetting import _axis_intervals

_AXES = ('x', 'y')
# rg.lfi_ebsd carries the EBSD reader's native (ny, nx) axis order --
# row = y, column = x.
_AXIS_TO_SHAPE_IDX = {'x': 1, 'y': 0}


def _auto_tile_count(start_pct, stride_pct):
    """How many tiles to generate along one axis so the moving-rectangle
    sweep covers the full [start_pct, 100] extent, given only a start/
    stride percentage (the Moving Rectangle config has no explicit tile-
    count field). Deliberately generous -- any extra tile that would
    start at/past 100% is silently dropped by ``_axis_intervals`` itself,
    so overestimating here is harmless."""
    if stride_pct <= 0 or start_pct >= 100.0:
        return 1
    return max(1, int(np.ceil((100.0 - start_pct) / stride_pct)) + 1)


[docs] def generate_subset_tiles_2d(lgi_shape, start_pct, length_pct, stride_pct, overflow_policy='clip'): """ Build the full set of rectangular subset tiles for a 2D EBSD grain- label field, per a moving-rectangle sweep. Parameters ---------- lgi_shape : tuple (ny, nx) start_pct, length_pct, stride_pct : dict {'x': float, 'y': float} Percentages of the ORIGINAL domain extent along each axis. overflow_policy : 'clip' or 'skip' Returns ------- list of dict, each: {'index': (i, j), 'pct': {'x': (s, e), 'y': (s, e)}, 'px': {'x': (x0, x1), 'y': (y0, y1)}} 'px' bounds are array-index, end-exclusive, ready for direct slicing of an (ny, nx)-shaped array. """ axis_size = {a: lgi_shape[_AXIS_TO_SHAPE_IDX[a]] for a in _AXES} n_cuboids = {a: _auto_tile_count(start_pct[a], stride_pct[a]) for a in _AXES} per_axis = { a: _axis_intervals(n_cuboids[a], start_pct[a], length_pct[a], stride_pct[a], overflow_policy) for a in _AXES } tiles = [] for i, sx, ex in per_axis['x']: for j, sy, ey in per_axis['y']: pct = {'x': (sx, ex), 'y': (sy, ey)} px = {} ok = True for a in _AXES: s, e = pct[a] axsz = axis_size[a] v0 = int(round(s / 100.0 * axsz)) v1 = int(round(e / 100.0 * axsz)) v1 = max(v1, v0 + 1) v1 = min(v1, axsz) v0 = min(v0, axsz - 1) if v1 <= v0: ok = False break px[a] = (v0, v1) if not ok: continue tiles.append({'index': (i, j), 'pct': pct, 'px': px}) return tiles
[docs] def crop_lgi_2d(lgi, tile): """Crop a 2D grain-label field to `tile`'s pixel bounds.""" y0, y1 = tile['px']['y'] x0, x1 = tile['px']['x'] return lgi[y0:y1, x0:x1].copy()
[docs] def subset_centroid_um(tile, lgi_shape, step_um): """Physical (x, y) centroid of a tile, in the same units as step_um (typically microns), rounded to 2 decimal places -- the centroid of the ORIGINAL (un-cropped) domain's coordinate system, not the crop's own local coordinates, so tiles from different subsets remain directly comparable by position. """ axis_size = {a: lgi_shape[_AXIS_TO_SHAPE_IDX[a]] for a in _AXES} centroid = {} for a in _AXES: v0, v1 = tile['px'][a] centroid[a] = round((v0 + v1) / 2.0 * step_um, 2) return centroid['x'], centroid['y']