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(), andcharacterise_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 valuenfeatures + 1so 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(), andfind_basic_JPO_stats()in sequence.- Parameters:
bsegCoords (dict[int, dict[int, numpy.ndarray]]) – Boundary-segment pixel coordinates produced by
segment_cell_boundaries()(segInfo['bsegCoords']).segidList_gbl (list of int) – Flat list of global segment IDs (
segInfo['segidList_gbl']).
- 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
lfiwith the sentinel valuenfeatures + 1to ensure the RVE outer shell is also detected as a boundary, then callsskimage.segmentation.find_boundariesand 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.padusing a custom pad function (pad_with()) that fills the border ring withpad_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 + 1or-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_widthis 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 ofskimage.segmentation.find_boundariesto 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_boundariesin'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 theneigh_fidneighbour hierarchy. Assigns both local sub-IDs (decimal fractions of the parent grain ID) and globally unique integer segment IDs.- Parameters:
lfi (numpy.ndarray of int, shape (R, C)) – Labelled feature image.
lfi_boundaries (numpy.ndarray of int, shape (R, C)) – Boundary pixel image produced by
detect_cell_boundaries().neigh_fid (dict[int, set[int]]) – Mapping
{grain_id: {neighbour_ids}}.connectivity (int, optional) – Connectivity multiplier (passed as
4 * connectivitytosegment_existing_boundaries()). Default is 1.local_seg_id_nDecPlaces (int, optional) – Decimal places for local sub-IDs. Default is 4.
- 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:
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 belowgidare 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
matplotlibfigure; 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:
- 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:
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:
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:
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
junctionPointsdict 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_arrayhas 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
featIDImgand overwrites each pixel listed incoordDatawith 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
featIDImgwith 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
vectorwith the value supplied viakwargs['padder']. This function is passed directly tonumpy.padas themodeargument (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_maskand, 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 withnumpy.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()withconnectivity = 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 toorigin_point'ignore'— useorigin_pointdirectly as the start
origin_point (tuple or list of float, optional) – Reference point used when
origin_specis'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_method1Full implementation of method 1.
method2Angle-based CCW sort.
method3_2dCentroid-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) viamethod1(). Works best for convex, roughly rectangular sets.'method2'— angle-based CCW sort viamethod2().
Default is
'method1'.debug_mode (bool, optional) – When
True, prints intermediate coordinate arrays to stdout. Default isTrue.
- Returns:
jp_sorted – Dictionary with keys:
'method': str — the method used.'sortorder': str — effective sort order ('cw'for method1,sort_orderfor 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:
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
coordsrows.
- 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_specis'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.arctan2returns 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_pointand 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:
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.
- 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)