low pri features
This commit is contained in:
81
backend/nodes/distribution_coercion.py
Normal file
81
backend/nodes/distribution_coercion.py
Normal file
@@ -0,0 +1,81 @@
|
||||
"""Distribution coercion — transform data to match a target distribution."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import numpy as np
|
||||
from math import ceil
|
||||
from scipy.stats import norm
|
||||
|
||||
from backend.node_registry import register_node
|
||||
from backend.data_types import DataField
|
||||
|
||||
|
||||
def _coerce_block(data: np.ndarray, distribution: str, n_levels: int) -> np.ndarray:
|
||||
"""Coerce a flat or 2-D block to the target distribution, returning same shape."""
|
||||
shape = data.shape
|
||||
flat = data.ravel().astype(np.float64)
|
||||
n_pixels = flat.size
|
||||
if n_pixels == 0:
|
||||
return data.copy()
|
||||
|
||||
indices = np.argsort(flat, kind="mergesort")
|
||||
|
||||
if distribution == "uniform":
|
||||
target = np.linspace(float(flat.min()), float(flat.max()), n_pixels)
|
||||
elif distribution == "gaussian":
|
||||
eps = 0.5 / n_pixels
|
||||
quantiles = np.linspace(eps, 1.0 - eps, n_pixels)
|
||||
target = norm.ppf(quantiles) * float(flat.std()) + float(flat.mean())
|
||||
elif distribution == "levels":
|
||||
n_levels = max(2, int(n_levels))
|
||||
level_values = np.linspace(float(flat.min()), float(flat.max()), n_levels)
|
||||
target = np.repeat(level_values, ceil(n_pixels / n_levels))[:n_pixels]
|
||||
else:
|
||||
raise ValueError(f"Unknown distribution: {distribution}")
|
||||
|
||||
result = np.empty_like(flat)
|
||||
result[indices] = target
|
||||
return result.reshape(shape)
|
||||
|
||||
|
||||
@register_node(display_name="Distribution Coercion")
|
||||
class DistributionCoercion:
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"field": ("DATA_FIELD",),
|
||||
"distribution": (["uniform", "gaussian", "levels"], {"default": "uniform"}),
|
||||
"n_levels": ("INT", {
|
||||
"default": 4,
|
||||
"min": 2,
|
||||
"max": 1000,
|
||||
"show_when_widget_value": {"distribution": ["levels"]},
|
||||
}),
|
||||
"processing": (["field", "rows"], {"default": "field"}),
|
||||
}
|
||||
}
|
||||
|
||||
OUTPUTS = (
|
||||
('DATA_FIELD', 'result'),
|
||||
)
|
||||
FUNCTION = "process"
|
||||
|
||||
DESCRIPTION = (
|
||||
"Transform pixel values so their distribution matches a target shape "
|
||||
"(uniform, Gaussian, or discrete levels) using rank-based reassignment. "
|
||||
"Equivalent to Gwyddion's coerce.c module."
|
||||
)
|
||||
|
||||
def process(self, field: DataField, distribution: str, n_levels: int,
|
||||
processing: str) -> tuple:
|
||||
data = np.asarray(field.data, dtype=np.float64)
|
||||
|
||||
if processing == "rows":
|
||||
result = np.empty_like(data)
|
||||
for i in range(data.shape[0]):
|
||||
result[i] = _coerce_block(data[i], distribution, n_levels)
|
||||
else:
|
||||
result = _coerce_block(data, distribution, n_levels)
|
||||
|
||||
return (field.replace(data=result),)
|
||||
Reference in New Issue
Block a user