deduplication pass

This commit is contained in:
2026-04-03 18:19:08 -07:00
parent f6b47e6d79
commit c8d766677b
42 changed files with 484 additions and 689 deletions

View File

@@ -309,10 +309,34 @@ def _render_markup_image(image, shapes):
# Mask helpers (from mask.py — used by multiple mask nodes)
# ---------------------------------------------------------------------------
def mask_to_bool(mask: np.ndarray) -> np.ndarray:
"""Convert a uint8 mask (0/255) to a boolean array."""
return np.asarray(mask) > 127
def bool_to_mask(binary: np.ndarray) -> np.ndarray:
"""Convert a boolean array to a uint8 mask (0/255)."""
return np.asarray(binary, dtype=np.uint8) * 255
def normalize_mask(
mask: np.ndarray | None, shape: tuple[int, int],
) -> np.ndarray | None:
"""Validate mask shape and convert from uint8 to boolean."""
if mask is None:
return None
mask_array = np.asarray(mask)
if mask_array.shape[:2] != shape:
raise ValueError(
f"Mask shape {mask_array.shape} does not match field shape {shape}."
)
return mask_to_bool(mask_array)
def _mask_overlay(field, mask):
from backend.data_types import datafield_to_uint8
grey = datafield_to_uint8(field, "gray")
mask_bool = mask > 127
mask_bool = mask_to_bool(mask)
if not np.any(mask_bool):
return grey
@@ -728,6 +752,62 @@ def _square_unit(unit: str) -> str:
return f"{unit}^2"
def apply_masking(data: np.ndarray, mask: np.ndarray | None, masking: str) -> np.ndarray:
"""Return a boolean validity array from a mask and masking mode.
Returns a bool array the same shape as *data* indicating which pixels
should be included in calculations.
"""
if mask is None or masking == "ignore":
return np.ones(data.shape, dtype=bool)
if masking == "include":
return np.asarray(mask, dtype=bool)
if masking == "exclude":
return ~np.asarray(mask, dtype=bool)
raise ValueError(f"Unknown masking mode: {masking}")
def masked_values(data: np.ndarray, mask: np.ndarray | None, masking: str) -> np.ndarray:
"""Return the 1-D subset of *data* selected by the masking mode."""
if mask is None or masking == "ignore":
return data
if masking == "include":
return data[mask]
if masking == "exclude":
return data[~mask]
raise ValueError(f"Unknown masking mode: {masking}")
def emit_mask_preview(field, mask_uint8: np.ndarray) -> None:
"""Emit a standard mask-on-field preview if *field* is not None."""
if field is None:
return
from backend.execution_context import emit_preview
from backend.data_types import encode_preview
emit_preview(encode_preview(_mask_overlay(field, mask_uint8)))
def histogram_with_centers(data: np.ndarray, bins: int = 256):
"""Compute histogram and return (counts_float64, bin_centers)."""
raw_counts, bin_edges = np.histogram(data.ravel(), bins=int(bins))
bin_centers = 0.5 * (bin_edges[:-1] + bin_edges[1:])
counts = raw_counts.astype(np.float64)
return counts, bin_centers
def frac_to_index(axis: np.ndarray, frac: float) -> int:
"""Map a fractional position [0, 1] to the nearest index in *axis*."""
n = len(axis)
if n <= 1:
return 0
lo = float(axis[0])
hi = float(axis[-1])
if hi == lo:
return 0
target = lo + frac * (hi - lo)
return int(np.argmin(np.abs(axis - target)))
def _apply_scalar_unit(base_unit: str, operation: str) -> str:
unit = str(base_unit or "").strip()
if operation == "count":