adding more nodes
This commit is contained in:
99
backend/nodes/grain_distributions.py
Normal file
99
backend/nodes/grain_distributions.py
Normal file
@@ -0,0 +1,99 @@
|
||||
"""Grain property distributions — compute histograms of grain properties."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import numpy as np
|
||||
from scipy.ndimage import label
|
||||
|
||||
from backend.node_registry import register_node
|
||||
from backend.data_types import DataField, LineData
|
||||
from backend.nodes.helpers import mask_to_bool
|
||||
|
||||
|
||||
@register_node(display_name="Grain Distributions")
|
||||
class GrainDistributions:
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"field": ("DATA_FIELD",),
|
||||
"mask": ("IMAGE",),
|
||||
"property": (["area", "equiv_diameter", "mean_height", "max_height",
|
||||
"volume", "boundary_length"], {"default": "area"}),
|
||||
"n_bins": ("INT", {"default": 30, "min": 5, "max": 200, "step": 1}),
|
||||
"min_size": ("INT", {"default": 10, "min": 1, "max": 100000, "step": 1}),
|
||||
}
|
||||
}
|
||||
|
||||
OUTPUTS = (
|
||||
('LINE_DATA', 'distribution'),
|
||||
)
|
||||
FUNCTION = "process"
|
||||
|
||||
DESCRIPTION = (
|
||||
"Compute a histogram of a grain property from a labeled mask. "
|
||||
"Supported properties: area, equivalent diameter, mean height, "
|
||||
"max height, volume, and boundary length. "
|
||||
)
|
||||
|
||||
def process(self, field: DataField, mask: np.ndarray, property: str,
|
||||
n_bins: int, min_size: int) -> tuple:
|
||||
data = np.asarray(field.data, dtype=np.float64)
|
||||
grain_mask = mask_to_bool(mask)
|
||||
labeled, n_grains = label(grain_mask.astype(np.int32))
|
||||
|
||||
pixel_area = field.dx * field.dy
|
||||
xy_unit = field.si_unit_xy or "m"
|
||||
z_unit = field.si_unit_z or "m"
|
||||
|
||||
values = []
|
||||
for gid in range(1, n_grains + 1):
|
||||
gpx = labeled == gid
|
||||
n_px = int(gpx.sum())
|
||||
if n_px < min_size:
|
||||
continue
|
||||
|
||||
if property == "area":
|
||||
values.append(n_px * pixel_area)
|
||||
elif property == "equiv_diameter":
|
||||
area = n_px * pixel_area
|
||||
values.append(2.0 * np.sqrt(area / np.pi))
|
||||
elif property == "mean_height":
|
||||
values.append(float(data[gpx].mean()))
|
||||
elif property == "max_height":
|
||||
values.append(float(data[gpx].max()))
|
||||
elif property == "volume":
|
||||
base = float(data[~grain_mask].mean()) if (~grain_mask).any() else 0.0
|
||||
values.append(float(np.sum(data[gpx] - base) * pixel_area))
|
||||
elif property == "boundary_length":
|
||||
# Count boundary pixels (pixels with at least one non-grain neighbour)
|
||||
padded = np.pad(gpx, 1, mode='constant', constant_values=False)
|
||||
boundary = gpx & ~(
|
||||
padded[:-2, 1:-1] & padded[2:, 1:-1] &
|
||||
padded[1:-1, :-2] & padded[1:-1, 2:]
|
||||
)
|
||||
values.append(int(boundary.sum()) * max(field.dx, field.dy))
|
||||
|
||||
if len(values) == 0:
|
||||
values = [0.0]
|
||||
|
||||
# Unit labels
|
||||
unit_map = {
|
||||
"area": f"{xy_unit}²",
|
||||
"equiv_diameter": xy_unit,
|
||||
"mean_height": z_unit,
|
||||
"max_height": z_unit,
|
||||
"volume": f"{xy_unit}²·{z_unit}",
|
||||
"boundary_length": xy_unit,
|
||||
}
|
||||
|
||||
arr = np.array(values)
|
||||
counts, edges = np.histogram(arr, bins=n_bins)
|
||||
centers = 0.5 * (edges[:-1] + edges[1:])
|
||||
|
||||
return (LineData(
|
||||
data=counts.astype(np.float64),
|
||||
x_axis=centers,
|
||||
x_unit=unit_map.get(property, ""),
|
||||
y_unit="count",
|
||||
),)
|
||||
Reference in New Issue
Block a user