Spaces:
Sleeping
Sleeping
File size: 3,474 Bytes
3ecb175 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 | """Polygon-based morphometric traits for a single segmented tuber.
The width and height used by downstream stages are the extents of the
polygon bounding box. The aspect_ratio field is bbox max/min and is not
used as the published axis-ratio; a PCA-based ratio is derived from
eccentricity in the modeling stage.
"""
from __future__ import annotations
import numpy as np
def close_polygon(x: np.ndarray, y: np.ndarray) -> tuple[np.ndarray, np.ndarray]:
if x.size == 0 or y.size == 0:
return x, y
if x[0] != x[-1] or y[0] != y[-1]:
x = np.concatenate([x, x[:1]])
y = np.concatenate([y, y[:1]])
return x, y
def polygon_area_px(x: np.ndarray, y: np.ndarray) -> float:
"""Shoelace formula. Eq. (2) in the paper."""
x, y = close_polygon(x, y)
if x.size < 3:
return float("nan")
area = 0.0
for i in range(len(x) - 1):
area += x[i] * y[i + 1] - x[i + 1] * y[i]
return abs(area) / 2.0
def polygon_perimeter_px(x: np.ndarray, y: np.ndarray) -> float:
"""Sum of Euclidean distances between consecutive vertices. Eq. (1) in the paper."""
x, y = close_polygon(x, y)
if x.size < 2:
return float("nan")
dx = np.diff(x)
dy = np.diff(y)
return float(np.sqrt(dx**2 + dy**2).sum())
def polygon_circularity(x: np.ndarray, y: np.ndarray) -> float:
"""4路蟺路A / P虏."""
A = polygon_area_px(x, y)
P = polygon_perimeter_px(x, y)
if not np.isfinite(A) or not np.isfinite(P) or P <= 0:
return float("nan")
return float(4.0 * np.pi * A / (P**2))
def polygon_eccentricity(x: np.ndarray, y: np.ndarray) -> float:
"""sqrt(1 - 位_min / 位_max) of the polygon-vertex covariance matrix."""
if len(x) < 3:
return float("nan")
coords = np.column_stack([x, y])
coords_centered = coords - coords.mean(axis=0, keepdims=True)
cov_mat = np.cov(coords_centered, rowvar=False)
eigvals = np.linalg.eigvalsh(cov_mat)
eigvals = np.sort(eigvals)[::-1]
if eigvals.size < 2 or eigvals[0] <= 0:
return float("nan")
return float(np.sqrt(1.0 - eigvals[1] / eigvals[0]))
def aspect_ratio_bbox(width: float, height: float) -> float:
if width <= 0 or height <= 0:
return float("nan")
return float(max(width, height) / min(width, height))
def roundness_bbox(width: float, height: float) -> float:
if width <= 0 or height <= 0:
return float("nan")
area_bbox = width * height
max_dim = max(width, height)
return float(4.0 * area_bbox / (np.pi * max_dim**2))
def polygon_shape_metrics(x: np.ndarray, y: np.ndarray) -> dict[str, float]:
"""All per-tuber pixel-space metrics in a single call.
Returns: area_poly_px, perimeter_px, circularity, eccentricity,
width_px, height_px, aspect_ratio, roundness_bbox.
"""
area_px = polygon_area_px(x, y)
perim_px = polygon_perimeter_px(x, y)
circ = polygon_circularity(x, y)
ecc = polygon_eccentricity(x, y)
width_px = float(x.max() - x.min()) if x.size > 0 else float("nan")
height_px = float(y.max() - y.min()) if y.size > 0 else float("nan")
ar = aspect_ratio_bbox(width_px, height_px)
rb = roundness_bbox(width_px, height_px)
return {
"area_poly_px": area_px,
"perimeter_px": perim_px,
"circularity": circ,
"eccentricity": ecc,
"width_px": width_px,
"height_px": height_px,
"aspect_ratio": ar,
"roundness_bbox": rb,
}
|