upxo.gbops.mcgb2dops module

Module: mcgb2dops

Grain-boundary operations for 2D labelled pixel (Monte-Carlo) structures.

Provides routines for detecting, segmenting, and characterising cell (grain) boundary pixels in a 2D labelled feature image (LFI), along with utilities for locating and sorting junction points (triple/quadruple points) and circularly ordering boundary coordinates.

Functions

PL_cell_boundaries

Pipeline: detect → segment → characterise cell boundary pixels.

PL_cellb_junction_points

Pipeline: detect → sort → stat-summarise junction points.

detect_cell_boundaries

Detect grain-boundary pixels using skimage.segmentation.find_boundaries.

pad_lfi

Pad a labelled feature image with a constant value.

find_common_interface_boundaries

Detect subpixel common interfaces using the 'subpixel' boundary mode.

segment_cell_boundaries

Assign each boundary pixel to the grain pair it separates.

see_bsegs_gids

Plot the boundary-segment pixel map for a single grain.

characterise_boundary_segments

Count segments and measure segment lengths per grain.

detect_junction_points

Find pixels shared by two or more boundary segments (junction points).

find_basic_JPO_stats

Compute min/max/median junction-point order (JPO) tessellation-wide.

sort_junction_points

Group junction points by order (T-junction, Q-junction, …).

mask_featIDImg_at_coords

Paint segment IDs onto a copy of the boundary image.

pad_with

Custom pad function compatible with numpy.pad.

segment_existing_boundaries

Walk boundary pixels and assign them to grain-pair interface lists.

circular_sort

Sort boundary coordinates in circular (CW / CCW) order. (stub)

circular_sort_method1

Clockwise/anti-clockwise sort of junction-point coordinates per grain.

method1

Four-corner sort (TL → TR → BR → BL) adapted from a public reference.

method2

Angle-based CCW sort centred on the leftmost point.

method3_2d

Centroid-angle sort with configurable direction and origin.

calculate_junction_order

Count unique grains in the 3×3 neighbourhood of a junction pixel.

Usage

from upxo.gbops import mcgb2dops as mcgbOps2d

@author: Dr. Sunil Anandatheertha

upxo.gbops.mcgb2dops.PL_cell_boundaries(lfi=None, nfeatures=None, neigh_fid=None, connectivity=1, mode='thick', background=0, local_seg_id_nDecPlaces=4, segIDMask_dtype=numpy.int32)[source]

Run the full cell-boundary detection–segmentation–characterisation pipeline.

Calls detect_cell_boundaries(), segment_cell_boundaries(), and characterise_boundary_segments() in sequence and returns their combined outputs.

Parameters:
  • lfi (numpy.ndarray of int, shape (R, C)) – Labelled feature image (grain ID at every pixel).

  • nfeatures (int) – Total number of grains in lfi. Used to choose the pad value nfeatures + 1 so the padded border is a unique sentinel grain.

  • neigh_fid (dict[int, set[int]]) – Mapping {grain_id: {neighbour_ids}}.

  • connectivity (int, optional) – Pixel connectivity for boundary detection (1 = 4-connected, 2 = 8-connected). Default is 1.

  • mode (str, optional) – Boundary mode passed to skimage.segmentation.find_boundaries. Default is 'thick'.

  • background (int, optional) – Background label value. Default is 0.

  • local_seg_id_nDecPlaces (int, optional) – Decimal places for local segment sub-IDs. Default is 4.

  • segIDMask_dtype (numpy dtype, optional) – dtype of segment ID arrays. Default is np.int32.

Returns:

  • lfi_boundaries (numpy.ndarray of int, shape (R, C)) – Pixel image where non-zero values are grain IDs of boundary pixels.

  • segInfo (dict) – Segmentation result with keys:

    • 'bsegCoords'dict[int, dict[int, ndarray]]: pixel coords per grain pair.

    • 'local_seg_ids' — sub-IDs within each grain.

    • 'global_seg_ids' — globally unique segment IDs per grain.

    • 'segidList_lcl' — flat list of all local IDs.

    • 'segidList_gbl' — flat list of all global IDs.

    • 'segid_gbl_pfid_map' — maps global segment ID to parent grain ID.

  • bseg_props (dict) – Boundary-segment properties with keys 'nsegments' and 'segment_lengths'.

Examples

import upxo.gbops.mcgb2dops as gbops2d
lfi_boundaries, segInfo, bseg_props = gbops2d.PL_cell_boundaries(
    lfi=pxt.gs[3].lgi,
    nfeatures=pxt.gs[3].n,
    neigh_fid=pxt.gs[3].neigh_gid,
    connectivity=1,
    mode='thick',
    background=0,
    local_seg_id_nDecPlaces=4,
    segIDMask_dtype=np.int32,
)
upxo.gbops.mcgb2dops.PL_cellb_junction_points(bsegCoords, segidList_gbl)[source]

Run the full junction-point detection–sort–stat pipeline.

Calls detect_junction_points(), sort_junction_points(), and find_basic_JPO_stats() in sequence.

Parameters:
Returns:

  • junctionPoints (dict[int, numpy.ndarray]) – Grain ID → array of shape (N_junctions, 3) with columns [row, col, junction_order].

  • JPSorted (dict[str, dict[int, numpy.ndarray]]) – Junction points grouped by order label ('jp1', 'jp2', 'tjp', 'qjp', …), each sub-dict keyed by grain ID.

  • JPOStats (dict) – Summary statistics; see find_basic_JPO_stats() for the full key list.

Examples

import upxo.gbops.mcgb2dops as gbops2d
junctionPoints, JPSorted, JPOStats = gbops2d.PL_cellb_junction_points(
    segInfo['bsegCoords'],
    segInfo['segidList_gbl'],
)
upxo.gbops.mcgb2dops.detect_cell_boundaries(lfi=None, nfeatures=None, connectivity=1, mode='thick', background=0)[source]

Detect grain-boundary pixels in a labelled feature image.

Pads lfi with the sentinel value nfeatures + 1 to ensure the RVE outer shell is also detected as a boundary, then calls skimage.segmentation.find_boundaries and multiplies the boolean result by the padded image so every boundary pixel retains its original grain ID. The padding is stripped before returning.

Parameters:
  • lfi (numpy.ndarray of int, shape (R, C)) – Labelled feature image.

  • nfeatures (int) – Number of grains; the pad sentinel is nfeatures + 1.

  • connectivity (int, optional) – Connectivity for find_boundaries (1 = 4-connected, 2 = 8-connected). Default is 1.

  • mode (str, optional) – Boundary detection mode ('thick', 'inner', 'outer', 'subpixel'). Default is 'thick'.

  • background (int, optional) – Background label. Default is 0.

Returns:

lfi_boundaries – Image where non-zero values are the grain IDs of boundary pixels and zero values are interior pixels.

Return type:

numpy.ndarray of int, shape (R, C)

Examples

import upxo.gbops.mcgb2dops as gbops2d
lfi_boundaries = gbops2d.detect_cell_boundaries(
    lfi=pxt.gs[3].lgi,
    nfeatures=pxt.gs[3].n,
    connectivity=1,
    mode='thick',
    background=0,
)
upxo.gbops.mcgb2dops.pad_lfi(lfi=None, pad_value=-100, _pad_width=1)[source]

Pad a labelled feature image with a constant sentinel value.

A thin wrapper around numpy.pad using a custom pad function (pad_with()) that fills the border ring with pad_value.

Parameters:
  • lfi (numpy.ndarray of int, shape (R, C)) – Labelled feature image to pad.

  • pad_value (int or float, optional) – Constant value written into the padded border. Choose a value that does not appear as a grain ID in lfi (e.g. nfeatures + 1 or -100). Default is -100.

  • _pad_width (int, optional) – Number of pixels to add on each side. Default is 1; there is normally no reason to change this.

Returns:

lfi_padded – Padded copy of lfi.

Return type:

numpy.ndarray of int, shape (R + 2*_pad_width, C + 2*_pad_width)

Notes

_pad_width is intentionally fixed at 1 for all internal callers. Changing it to values greater than 1 is supported but untested.

Examples

import upxo.gbops.mcgb2dops as gbops2d
lfi_padded = gbops2d.pad_lfi(lfi=pxt.gs[3].lgi, pad_value=0)
upxo.gbops.mcgb2dops.find_common_interface_boundaries(lfi=None, nfeatures=None, connectivity=1)[source]

Detect subpixel common interfaces between grains.

Uses the 'subpixel' boundary mode of skimage.segmentation.find_boundaries to locate the precise common interface lines between adjacent grains. Returns a boolean or int mask at twice the input resolution (as per the skimage subpixel convention).

Parameters:
  • lfi (numpy.ndarray of int, shape (R, C)) – Labelled feature image.

  • nfeatures (int) – Number of grains; used to compute the pad sentinel nfeatures + 1.

  • connectivity (int, optional) – Connectivity for boundary detection. Default is 1.

Returns:

common_interface – Subpixel boundary image (boolean or uint8) produced by find_boundaries in 'subpixel' mode.

Return type:

numpy.ndarray, shape (2R + 1, 2C + 1)

Examples

import upxo.gbops.mcgb2dops as gbops2d
common_interface = gbops2d.find_common_interface_boundaries(
    lfi=pxt.gs[3].lgi,
    nfeatures=pxt.gs[3].n,
    connectivity=1,
)
upxo.gbops.mcgb2dops.segment_cell_boundaries(lfi=None, lfi_boundaries=None, neigh_fid=None, connectivity=1, local_seg_id_nDecPlaces=4)[source]

Assign each boundary pixel to the grain pair it separates.

Calls segment_existing_boundaries() to walk every boundary pixel and record which two grains it lies between, then restructures the result to match the neigh_fid neighbour hierarchy. Assigns both local sub-IDs (decimal fractions of the parent grain ID) and globally unique integer segment IDs.

Parameters:
Returns:

segInfo – Dictionary with the following keys:

  • 'bsegCoords' : dict[int, dict[int, numpy.ndarray]] Pixel coordinates of boundary segments, indexed by {grain_id: {neighbour_id: coords_array}}.

  • 'local_seg_ids' : dict[int, numpy.ndarray] Local decimal sub-IDs for each segment within its parent grain.

  • 'global_seg_ids' : dict[int, list[int]] Globally unique integer segment IDs grouped by parent grain.

  • 'segidList_lcl' : list Flat list of all local segment IDs.

  • 'segidList_gbl' : list Flat list of all global segment IDs.

  • 'segid_gbl_pfid_map' : dict[int, int] Maps every global segment ID back to its parent grain ID.

Return type:

dict

Examples

import upxo.gbops.mcgb2dops as gbops2d

# First detect the cell boundaries
lfi_boundaries = gbops2d.detect_cell_boundaries(
    lfi=pxt.gs[3].lgi,
    nfeatures=pxt.gs[3].n,
    connectivity=1,
    mode='thick',
    background=0,
)
# Then segment those boundaries
segInfo = gbops2d.segment_cell_boundaries(
    lfi=pxt.gs[3].lgi,
    lfi_boundaries=lfi_boundaries,
    neigh_fid=pxt.gs[3].neigh_gid,
    connectivity=1,
    local_seg_id_nDecPlaces=4,
)
bsegCoords = segInfo['bsegCoords']
global_seg_ids = segInfo['global_seg_ids']
upxo.gbops.mcgb2dops.see_bsegs_gids(localSegIDMasked_lfi, gid)[source]

Plot the boundary-segment pixel map for a single grain.

Extracts and displays the slice of the locally-masked boundary image that corresponds to grain gid, masking out all pixels outside the [gid, gid+1) sub-ID range. Pixels below gid are set to NaN so they render as transparent in the colormap.

Parameters:
  • localSegIDMasked_lfi (numpy.ndarray of float, shape (R, C)) – Boundary image where each non-zero pixel carries the local decimal sub-ID of its boundary segment.

  • gid (int) – Grain ID whose boundary segments should be visualised.

Returns:

Displays a matplotlib figure; no value is returned.

Return type:

None

Examples

import upxo.gbops.mcgb2dops as gbops2d
# Select the largest grain
gid = int(np.argmax(pxt.gs[3].prop.npixels.to_numpy())) + 1
gbops2d.see_bsegs_gids(localSegIDMasked_lfi, gid)
upxo.gbops.mcgb2dops.characterise_boundary_segments(bsegCoords, neigh_fid)[source]

Compute per-grain segment counts and segment pixel lengths.

Parameters:
  • bsegCoords (dict[int, dict[int, numpy.ndarray]]) – Boundary-segment pixel coordinates indexed by {grain_id: {neighbour_id: coords_array}}.

  • neigh_fid (dict[int, set[int]]) – Mapping {grain_id: {neighbour_ids}}.

Returns:

bseg_props – Dictionary with two keys:

  • 'nsegments' : dict[int, int] Number of boundary segments per grain.

  • 'segment_lengths' : dict[int, list[int]] List of pixel counts for each segment, per grain.

Return type:

dict

Examples

import upxo.gbops.mcgb2dops as gbops2d
bseg_props = gbops2d.characterise_boundary_segments(
    segInfo['bsegCoords'], pxt.gs[3].neigh_gid
)
print(bseg_props['nsegments'])
upxo.gbops.mcgb2dops.detect_junction_points(segments)[source]

Find pixels shared by two or more boundary segments of the same grain.

For each grain, stacks all boundary-segment coordinate arrays and looks for pixel positions that appear in more than one segment. These are junction points where three or more grains meet. The returned order value is count + 1 (e.g. a pixel shared by 2 segments has order 3, corresponding to a triple junction).

Parameters:

segments (dict[int, dict[int, numpy.ndarray]]) – Boundary-segment coordinates indexed by {grain_id: {neighbour_id: coords_array}}.

Returns:

all_junctions – Grain ID → array of shape (N_junctions, 3). Columns are [row, col, junction_order]. Only grains that have at least one junction point appear as keys.

Return type:

dict[int, numpy.ndarray]

Examples

import upxo.gbops.mcgb2dops as gbops2d
junctionPoints = gbops2d.detect_junction_points(segInfo['bsegCoords'])
# Inspect junctions for grain 5
print(junctionPoints.get(5))
upxo.gbops.mcgb2dops.find_basic_JPO_stats(junctionPoints, segidList_gbl)[source]

Compute min, max, and median junction-point order (JPO) statistics.

Calculates per-grain and tessellation-wide summary statistics for the junction-point order (JPO), which is the number of grains meeting at a junction pixel.

Parameters:
  • junctionPoints (dict[int, numpy.ndarray]) – Output of detect_junction_points(). Each array has shape (N_junctions, 3) with the third column holding the junction order.

  • segidList_gbl (list of int) – Flat list of global segment IDs (segInfo['segidList_gbl']). Used to size the tessellation-wide median accumulator.

Returns:

JPOStats – Dictionary with the following keys:

  • 'min_tess' : int — global minimum JPO.

  • 'max_tess' : int — global maximum JPO.

  • 'median_tess' : float — global median JPO across all segments.

  • 'min_feat' : dict[int, int] — per-grain minimum JPO.

  • 'max_feat' : dict[int, int] — per-grain maximum JPO.

  • 'median_feat' : dict[int, float] — per-grain median JPO.

Return type:

dict

Examples

import upxo.gbops.mcgb2dops as gbops2d
JPOStats = gbops2d.find_basic_JPO_stats(
    junctionPoints, segInfo['segidList_gbl']
)
print("Global max JPO:", JPOStats['max_tess'])
upxo.gbops.mcgb2dops.sort_junction_points(junctionPoints)[source]

Group junction points by their junction-point order (JPO).

Organises the flat junctionPoints dict into a nested dict keyed by human-readable JPO labels ('jp1''jp6', 'tjp' for order 3, 'qjp' for order 4), with each entry further keyed by grain ID.

Parameters:

junctionPoints (dict[int, numpy.ndarray]) – Output of detect_junction_points().

Returns:

JPSorted – Nested dictionary {jpo_label: {grain_id: coords_array}}. coords_array has shape (N, 2) (row, col only; order column dropped). Keys present are only those JPO values actually found in the data.

Return type:

dict[str, dict[int, numpy.ndarray]]

Notes

The label mapping is: 1 → 'jp1', 2 → 'jp2', 3 → 'tjp' (triple junction), 4 → 'qjp' (quadruple junction), 5 → 'jp5', 6 → 'jp6'.

Examples

import upxo.gbops.mcgb2dops as gbops2d
JPSorted = gbops2d.sort_junction_points(junctionPoints)
# Retrieve all triple-junction pixels for grain 7
tjp_grain7 = JPSorted['tjp'].get(7)
upxo.gbops.mcgb2dops.mask_featIDImg_at_coords(featIDImg, coordData, featName='fbseg', maskDType=numpy.int32)[source]

Paint segment IDs onto a copy of the boundary image at given coordinates.

Creates a deep copy of featIDImg and overwrites each pixel listed in coordData with its corresponding segment sub-ID.

Parameters:
  • featIDImg (numpy.ndarray of int, shape (R, C)) – Source boundary image (e.g. lfi_boundaries) to be copied and annotated.

  • coordData (dict[int, dict[int, numpy.ndarray]]) – Boundary-segment pixel coordinates indexed by {grain_id: {neighbour_id: coords_array}}.

  • featName (str, optional) – Feature type selector. Currently only 'fbseg', 'gbseg', and 'feature_boudnary_segments' are handled. Default is 'fbseg'.

  • maskDType (numpy dtype, optional) – dtype of the output array. Default is np.int32.

Returns:

featIDImg_new – Copy of featIDImg with segment IDs written at boundary-pixel positions.

Return type:

numpy.ndarray of int, shape (R, C)

Examples

import upxo.gbops.mcgb2dops as gbops2d
masked = gbops2d.mask_featIDImg_at_coords(
    lfi_boundaries,
    segInfo['bsegCoords'],
    featName='fbseg',
    maskDType=np.int32,
)
upxo.gbops.mcgb2dops.pad_with(vector, pad_width, iaxis, kwargs)[source]

Custom padding function compatible with numpy.pad.

Fills the leading and trailing pad regions of vector with the value supplied via kwargs['padder']. This function is passed directly to numpy.pad as the mode argument (callable form).

Parameters:
  • vector (numpy.ndarray, 1-D) – The padded edge of one axis, as provided by numpy.pad.

  • pad_width (tuple of int) – (before, after) number of padded values on each end.

  • iaxis (int) – The axis currently being padded (not used; required by the interface).

  • kwargs (dict) – Must contain key 'padder' whose value is the constant fill value. Defaults to 10 if the key is absent.

Returns:

vector – The modified vector with pad regions filled.

Return type:

numpy.ndarray, 1-D

Notes

Implementation taken verbatim from the NumPy documentation example: https://numpy.org/doc/stable/reference/generated/numpy.pad.html

upxo.gbops.mcgb2dops.segment_existing_boundaries(labels, gb_mask, connectivity=8)[source]

Walk boundary pixels and assign them to grain-pair interface lists.

Iterates over every non-zero pixel in gb_mask and, for each adjacent pixel (within the requested connectivity), records the pixel as belonging to the interface between the two grain IDs. Duplicate pixel positions are removed with numpy.unique.

Parameters:
  • labels (numpy.ndarray of int, shape (R, C)) – Labelled feature image (grain ID at every pixel).

  • gb_mask (numpy.ndarray of int or bool, shape (R, C)) – Boundary pixel mask (non-zero where boundary pixels exist).

  • connectivity (int, optional) – Neighbourhood size: 4 (face neighbours only) or 8 (face + diagonal). Default is 8.

Returns:

interfaces – Maps each (grain_A, grain_B) pair to a unique array of (row, col) pixel positions on their shared boundary.

Return type:

dict[tuple[int, int], numpy.ndarray]

Notes

Called internally by segment_cell_boundaries() with connectivity = 4 * user_connectivity.

Usage

import upxo.gbops.mcgb2dops as gbops2d

Examples

import upxo.gbops.mcgb2dops as gbops2d
interfaces = gbops2d.segment_existing_boundaries(
    labels=lfi,
    gb_mask=lfi_boundaries,
    connectivity=4,
)
coords_1_2 = interfaces.get((1, 2))
upxo.gbops.mcgb2dops.circular_sort(coords=None, order='ccw', origin_spec='tl', origin_point=(0, 0))[source]

Sort boundary-point coordinates in circular (CW or CCW) order.

Wrapper that dispatches to the chosen sorting method. Currently only method1 (four-corner sort) is called; methods 2 and 3 are stubs pending implementation.

Parameters:
  • coords (numpy.ndarray, shape (N, 2), optional) – Array of (x, y) boundary-point coordinates. Default is None.

  • order (str, optional) – Circular sorting direction. Accepted values: 'cw', 'clockwise', 'ccw', 'acw', 'counterclockwise', 'anticlockwise'. Default is 'ccw'.

  • origin_spec (str, optional) –

    Specifies which point becomes the first in the sorted output. Options:

    • 'tl' — top-left (default)

    • 'tr' — top-right

    • 'br' — bottom-right

    • 'bl' — bottom-left

    • 'l' — leftmost

    • 't' — topmost

    • 'r' — rightmost

    • 'b' — bottommost

    • 'closest' — point nearest to origin_point

    • 'ignore' — use origin_point directly as the start

  • origin_point (tuple or list of float, optional) – Reference point used when origin_spec is 'closest' or 'ignore'. Default is (0, 0).

Returns:

This function is currently incomplete. It calls circular_sort_method1() but does not propagate or return its result. Methods 2 and 3 are not yet implemented.

Return type:

None

See also

circular_sort_method1

Full implementation of method 1.

method2

Angle-based CCW sort.

method3_2d

Centroid-angle sort.

upxo.gbops.mcgb2dops.circular_sort_method1(jp_grainwise, sort_order, method='method1', debug_mode=True)[source]

Sort grain junction-point coordinates clockwise or anti-clockwise.

Iterates over each grain’s junction-point coordinate set and sorts them using the selected geometric method. Builds a structured output dict that records the sort method, the effective sort order, and the sorted coordinates with their original-index permutation for each grain.

Parameters:
  • jp_grainwise (dict[int, numpy.ndarray]) – Grain ID → array of shape (2, N) where row 0 is x-coordinates and row 1 is y-coordinates of the junction points.

  • sort_order (str) – Sorting direction; used only when method == 'method2'. For 'method1' the sort is always clockwise ('cw').

  • method (str, optional) –

    Sorting algorithm to use. Options:

    • 'method1' — four-corner ordering (TL → TR → BR → BL) via method1(). Works best for convex, roughly rectangular sets.

    • 'method2' — angle-based CCW sort via method2().

    Default is 'method1'.

  • debug_mode (bool, optional) – When True, prints intermediate coordinate arrays to stdout. Default is True.

Returns:

jp_sorted – Dictionary with keys:

  • 'method' : str — the method used.

  • 'sortorder' : str — effective sort order ('cw' for method1, sort_order for method2, or the default method name otherwise).

  • 'sorted' : dict[int, dict] — per-grain result where each entry has sub-keys 'coords' (sorted coordinate array) and 'indices' (index permutation into the original array).

Return type:

dict

Notes

When a grain has only a single junction point, no sorting is performed and the original coordinate is stored directly. When exactly two points exist under method1, they are returned as-is with indices [0, 1].

Examples

Prerequisite setup:

from upxo.pxtal.mcgs import monte_carlo_grain_structure as mcgs
PXGS = mcgs()
PXGS.simulate()
PXGS.detect_grains()

Sort the junction points:

from upxo.gbops.mcgb2dops import circular_sort_method1
jp_sorted = circular_sort_method1(PXGS.gs[8].jp_grainwise, sort_order='cw')
upxo.gbops.mcgb2dops.method1(coords)[source]

Sort four points in TL → TR → BR → BL order.

Separates the input points into the two leftmost and two rightmost by x-coordinate, then orders each pair by y-coordinate to identify top-left, bottom-left, top-right, and bottom-right corners.

Parameters:

coords (numpy.ndarray, shape (N, 2)) – Point coordinates as rows of (x, y). Intended for exactly four points; behaviour for N ≠ 4 is undefined.

Returns:

  • sorted_coords (numpy.ndarray, shape (4, 2)) – Points ordered TL → TR → BR → BL.

  • sorted_indices (None) – Index permutation is not computed by this method; always returns None.

Notes

Adapted from: https://gist.github.com/flashlib/e8261539915426866ae910d55a3f9959

upxo.gbops.mcgb2dops.method2(coords)[source]

Sort points in CCW order around the leftmost point.

Centres the point cloud on its leftmost (then topmost) point and sorts by the angle each point subtends relative to that origin.

Parameters:

coords (numpy.ndarray, shape (N, 2)) – Point coordinates as rows of (x, y).

Returns:

  • sorted_coords (numpy.ndarray, shape (N, 2)) – Points in ascending angle order (counter-clockwise from the leftmost point).

  • sorted_indices (numpy.ndarray of int, shape (N,)) – Index permutation mapping the sorted positions back to the original coords rows.

upxo.gbops.mcgb2dops.method3_2d(coords=None, order='ccw', origin_spec='tl', origin_point=(0, 0), method=3)[source]

Sort boundary points in circular order using centroid-angle decomposition.

Computes the centroid of coords, then sorts points by the angle they subtend with respect to that centroid. Supports both clockwise and counter-clockwise ordering.

Parameters:
  • coords (numpy.ndarray, shape (N, 2), optional) – (x, y) boundary-point coordinates. Default is None.

  • order (str, optional) – Circular sorting direction. Accepted values: 'cw', 'clockwise', 'ccw', 'acw', 'counterclockwise', 'anticlockwise'. Default is 'ccw'.

  • origin_spec (str, optional) – Specifies the starting point of the sorted output. Options: 'tl', 'tr', 'br', 'bl', 'l', 't', 'r', 'b', 'closest', 'ignore'. Default is 'tl'.

  • origin_point (tuple or list of float, optional) – Reference point used when origin_spec is 'closest' or 'ignore'. Default is (0, 0).

  • method (int, optional) – Sorting method selector (1, 2, or 3). This parameter is accepted for API compatibility but is unused by the centroid-angle logic here. Default is 3.

Returns:

  • clockwise_sorted_points (numpy.ndarray, shape (N, 2)) – Points sorted in clockwise order.

  • clockwise_sorted_indices (list of int) – Index permutation for the clockwise-sorted output.

  • counterclockwise_sorted_points (numpy.ndarray, shape (N, 2)) – Points sorted in counter-clockwise order.

  • counterclockwise_sorted_indices (list of int) – Index permutation for the CCW-sorted output.

Notes

numpy.arctan2 returns angles in [-π, π]. Sorting in ascending order yields a counter-clockwise sequence; inverting the angle before sorting yields clockwise.

Examples

import numpy as np
import upxo.gbops.mcgb2dops as gbops2d
pts = np.array([[1, 2], [3, 4], [2, 0], [0, 1]], dtype=float)
cw, cw_idx, ccw, ccw_idx = gbops2d.method3_2d(coords=pts, order='cw')
upxo.gbops.mcgb2dops.calculate_junction_order(matrix, junction_point)[source]

Count the number of distinct grains in the 3×3 neighbourhood of a junction pixel.

Extracts a 3×3 window centred on junction_point and counts the unique non-zero grain IDs within it. This count is the junction-point order (JPO): 3 for a triple junction, 4 for a quadruple junction, etc.

Parameters:
  • matrix (numpy.ndarray of int, shape (R, C)) – Labelled feature image (grain ID at every pixel).

  • junction_point (tuple of int) – (row, col) pixel position of the junction point to evaluate.

Returns:

order – Number of distinct non-zero grain IDs in the 3×3 neighbourhood, equal to the junction-point order.

Return type:

int

Notes

The neighbourhood is clipped to the image boundary at the edges, so junction points on the border of the image may return a lower order than expected.

Examples

import upxo.gbops.mcgb2dops as gbops2d
order = gbops2d.calculate_junction_order(lfi, junction_point=(45, 32))
print(f"Junction order: {order}")
upxo.gbops.mcgb2dops.build_grain_pairs(neigh_gid)[source]

Build unique grain pairs from a neighbour dictionary.

Parameters:

neigh_gid (dict[int, list[int]]) – Mapping of grain ID → list of neighbour grain IDs.

Returns:

grain_pairs – Unique pairs (g1, g2) with g2 > g1.

Return type:

list[tuple[int, int]]

upxo.gbops.mcgb2dops.identify_grain_boundary_pixels(lgi, grain_pairs)[source]

Return pixel coordinates of each grain-pair interface.

Scans horizontal and vertical pixel adjacencies to find pixels where two specified grain IDs are immediate neighbours.

Parameters:
  • lgi (numpy.ndarray of int, shape (R, C)) – Labelled grain image.

  • grain_pairs (list of tuple) – Each entry is (grain_id1, grain_id2); order within the tuple does not matter.

Returns:

gb_dict – Sorted pair (g1, g2) → array of shape (N, 2) with [row, col] coordinates of boundary pixels.

Return type:

dict[tuple[int, int], numpy.ndarray]

upxo.gbops.mcgb2dops.find_gb_junction_point_map(lgi)[source]

Return a binary map of grain-boundary junction pixels.

A pixel is a junction if three or more distinct non-zero grain IDs appear in its 3×3 neighbourhood. Only boundary pixels (detected by skimage.segmentation.find_boundaries) are tested, which avoids evaluating interior pixels and reduces cost for large images.

Parameters:

lgi (numpy.ndarray of int, shape (R, C)) – Labelled grain image.

Returns:

jmap – 1 at junction pixels, 0 elsewhere.

Return type:

numpy.ndarray of int, shape (R, C)

Notes

Delegates per-pixel neighbourhood counting to calculate_junction_order(), the single-pixel companion of this function.

upxo.gbops.mcgb2dops.compute_grain_boundary_locs(grain_mask)[source]

Return pixel indices of a single grain’s boundary via binary erosion.

Boundary pixels are those inside the grain but absent from the eroded grain: boundary = mask AND NOT erode(mask).

Parameters:

grain_mask (numpy.ndarray of bool or uint8, shape (R, C)) – Binary mask where non-zero marks the grain’s pixels.

Returns:

gbloc – Row/column indices of boundary pixels.

Return type:

numpy.ndarray of int, shape (N, 2)