# pyfli/data_cc/norm.py
"""
Normalize FLI arrays with zero-one, min-max, reference-scale, peak, and PDF transforms.
This module belongs to :mod:`pyfli.data_cc` and is part of PyFLI array preprocessing
helpers for normalization, masking, ROI extraction, and IRF alignment. Public API
includes classes :class:`Normalization`.
"""
from typing import Any
import numpy as np
[docs]
class Normalization:
"""
Run the normalization routine.
and exposes zero-one scaling, min-max scaling, reference scaling, global peak
normalization, and probability-density conversion.
Parameters
----------
data : np.ndarray
Array of values to normalize, mask, or summarize.
"""
def __init__(self, data: np.ndarray) -> None:
if isinstance(data, (list, tuple)):
self.data = [np.asarray(d) for d in data]
else:
self.data = [np.asarray(data)]
def _compute_min_max(self, arr: np.ndarray) -> tuple[Any, ...]:
"""
Compute min max.
Parameters
----------
arr : np.ndarray
Array processed by the routine.
Returns
-------
tuple[Any, ...]
Tuple containing finite minimum and maximum values.
"""
if arr.ndim == 1:
return np.min(arr), np.max(arr)
elif arr.ndim == 3:
min_val = np.min(arr, axis=-1, keepdims=True)
max_val = np.max(arr, axis=-1, keepdims=True)
return min_val, max_val
else:
raise ValueError("Only 1D or 3D data supported")
def _threshold_mask(self, arr: np.ndarray, threshold: float) -> Any:
"""
Run the threshold mask routine.
Parameters
----------
arr : np.ndarray
Array processed by the routine.
threshold : float
Threshold used to mask, classify, or validate data.
Returns
-------
Any
Object produced by threshold mask.
"""
if arr.ndim == 1:
return np.sum(arr) > threshold
elif arr.ndim == 3:
return np.sum(arr, axis=2, keepdims=True) > threshold
[docs]
def zerone(self, threshold: int = 0) -> Any:
"""
Run the zerone routine.
Parameters
----------
threshold : int
Threshold used to mask, classify, or validate data.
Returns
-------
Any
Object produced by zerone.
"""
normalized = []
for arr in self.data:
mask = self._threshold_mask(arr, threshold)
if arr.ndim == 1:
if not mask:
normalized.append(arr)
continue
min_val, max_val = self._compute_min_max(arr)
denom = (max_val - min_val) + 1e-12
norm = (arr - min_val) / denom
elif arr.ndim == 3:
min_val, max_val = self._compute_min_max(arr)
denom = (max_val - min_val) + 1e-12
norm = (arr - min_val) / denom
# apply mask (broadcasted)
norm = np.where(mask, norm, arr)
normalized.append(norm)
return normalized if len(normalized) > 1 else normalized[0]
[docs]
def minmax(self, threshold: int = 0) -> Any:
"""
Run the minmax routine.
Parameters
----------
threshold : int
Threshold used to mask, classify, or validate data.
Returns
-------
Any
Object produced by minmax.
"""
normalized = []
for arr in self.data:
mask = self._threshold_mask(arr, threshold)
if arr.ndim == 1:
if not mask:
normalized.append(arr)
continue
max_val = np.max(arr)
norm = arr / (max_val + 1e-12)
elif arr.ndim == 3:
max_val = np.max(arr, axis=-1, keepdims=True)
norm = arr / (max_val + 1e-12)
norm = np.where(mask, norm, arr)
normalized.append(norm)
return normalized if len(normalized) > 1 else normalized[0]
[docs]
def norm_scale(self, ref_data: np.ndarray, threshold: int = 0) -> Any:
"""
Run the norm scale routine.
Parameters
----------
ref_data : np.ndarray
Reference data used for normalization.
threshold : int
Threshold used to mask, classify, or validate data.
Returns
-------
Any
Object produced by norm scale.
"""
ref_data = np.asarray(ref_data)
if ref_data.ndim == 1:
ref_max = np.max(ref_data)
elif ref_data.ndim == 3:
ref_max = np.max(ref_data, axis=-1, keepdims=True)
else:
raise ValueError("Reference must be 1D or 3D")
scaled = []
zero_one_data = self.zerone(threshold=threshold)
if not isinstance(zero_one_data, list):
zero_one_data = [zero_one_data]
for arr in zero_one_data:
if arr.ndim == 1:
if np.sum(arr) <= threshold:
scaled.append(arr)
else:
scaled.append(arr * ref_max)
elif arr.ndim == 3:
mask = np.sum(arr, axis=2, keepdims=True) > threshold
scaled_arr = arr * ref_max
scaled_arr = np.where(mask, scaled_arr, arr)
scaled.append(scaled_arr)
return scaled if len(scaled) > 1 else scaled[0]
[docs]
def global_peak_norm_3d(self, threshold: int = 0) -> Any:
"""
Run the global peak norm 3d routine.
Parameters
----------
threshold : int
Threshold used to mask, classify, or validate data.
Returns
-------
Any
Object produced by global peak norm 3d.
"""
normalized = []
for arr in self.data:
if arr.ndim != 3:
raise ValueError("global_peak_norm_3d only supports 3D data")
mask = self._threshold_mask(arr, threshold)
pixel_max = np.max(arr, axis=2)
global_max = np.max(pixel_max)
norm = arr / (global_max + 1e-12)
norm = np.where(mask, norm, arr)
normalized.append(norm)
return normalized if len(normalized) > 1 else normalized[0]
[docs]
def to_pdf(self, threshold: int = 0) -> Any:
"""
Run the to PDF routine.
Parameters
----------
threshold : int
Threshold used to mask, classify, or validate data.
Returns
-------
Any
Object produced by to PDF.
"""
pdf_data = []
for arr in self.data:
mask = self._threshold_mask(arr, threshold)
if arr.ndim == 1:
if not mask:
pdf_data.append(arr)
continue
total = np.sum(arr)
pdf = arr / (total + 1e-12)
elif arr.ndim == 3:
total = np.sum(arr, axis=2, keepdims=True)
pdf = arr / (total + 1e-12)
pdf = np.where(mask, pdf, arr)
else:
raise ValueError("Only 1D or 3D data supported")
pdf_data.append(pdf)
return pdf_data if len(pdf_data) > 1 else pdf_data[0]