From 933ef4be652f3d0a311d7ac03aa5fbe14fe1c3cd Mon Sep 17 00:00:00 2001 From: mrava87 Date: Thu, 3 Sep 2026 21:33:58 +0100 Subject: [PATCH 1/3] feat: added optional tau/mu to Segment --- pyproximal/optimization/segmentation.py | 36 ++++++++++++++++++------- 1 file changed, 27 insertions(+), 9 deletions(-) diff --git a/pyproximal/optimization/segmentation.py b/pyproximal/optimization/segmentation.py index 5f7b7f2..4eb61b5 100644 --- a/pyproximal/optimization/segmentation.py +++ b/pyproximal/optimization/segmentation.py @@ -3,6 +3,7 @@ import numpy as np from pylops import BlockDiag, Gradient +from pylops.utils.backend import get_array_module from pylops.utils.typing import NDArray from pyproximal import L21, Simplex, VStack @@ -14,6 +15,8 @@ def Segment( cl: NDArray, sigma: float, alpha: float, + tau: float | None = 1.0, + mu: float | None = None, clsigmas: NDArray | None = None, z: NDArray | None = None, niter: int = 10, @@ -37,6 +40,14 @@ def Segment( Positive scalar weight of the misfit term alpha : :obj:`float` Positive scalar weight of the regularization term + tau : :obj:`float`, optional + Stepsize of subgradient of segmentation term. + If ``None``, it is set from ``mu`` and the + Lipschitz constant of the gradient operator. + mu : :obj:`float`, optional + Stepsize of subgradient of regularization term. + If ``None``, it is set from ``tau`` and the + Lipschitz constant of the gradient operator. clsigmas : :obj:`numpy.ndarray`, optional Classes standard deviations z : :obj:`numpy.ndarray`, optional @@ -64,6 +75,11 @@ def Segment( Estimated classes. This is a vector of the same size of the input data ``y`` with the selected classes at each pixel. + Raises + ------ + ValueError + If both ``tau`` and ``mu`` are ``None`` + Notes ----- This solver performs image segmentation over :math:`N_{cl}` classes solving @@ -90,6 +106,11 @@ def Segment( Imaging and Vision, 40, 8pp. 120–145. 2011. """ + if tau is None and mu is None: + msg = "Either tau or mu must be provided." + raise ValueError(msg) + + ncp = get_array_module(y) kwargs_simplex = {} if kwargs_simplex is None else kwargs_simplex dims = y.shape @@ -104,10 +125,7 @@ def Segment( g = g.ravel() # Gradient operator - sampling = 1.0 - Gop = Gradient( - dims=dims, sampling=sampling, edge=False, kind="forward", dtype="float64" - ) + Gop = Gradient(dims=dims, sampling=1.0, edge=False, kind="forward", dtype="float64") Gop = BlockDiag([Gop] * ncl) # Simplex and L21 proximal operators @@ -119,9 +137,9 @@ def Segment( ) # Steps - L = 8.0 / sampling**2 - tau = 1.0 - mu = 1.0 / (tau * L) + L = 4 * ndims + tau = 1.0 / (mu * L) if tau is None else tau + mu = 1.0 / (tau * L) if mu is None else mu # Inversion x: NDArray = PrimalDual( @@ -132,14 +150,14 @@ def Segment( mu=mu, z=g if z is None else g + z, theta=1.0, - x0=np.zeros_like(g) if x0 is None else x0, + x0=ncp.zeros_like(g) if x0 is None else x0, niter=niter, callback=callback, show=show, returny=False, ) x = x.reshape(ncl, dimsprod).T - cl = np.argmax(x, axis=1) + cl = ncp.argmax(x, axis=1) cl = cl.reshape(dims) return x, cl From f62c90e92fd26e69535270e7a84eef8e829f6ca9 Mon Sep 17 00:00:00 2001 From: mrava87 Date: Fri, 4 Sep 2026 11:34:29 +0100 Subject: [PATCH 2/3] minor: fix mypy --- pyproximal/optimization/segmentation.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/pyproximal/optimization/segmentation.py b/pyproximal/optimization/segmentation.py index 4eb61b5..3398651 100644 --- a/pyproximal/optimization/segmentation.py +++ b/pyproximal/optimization/segmentation.py @@ -106,10 +106,6 @@ def Segment( Imaging and Vision, 40, 8pp. 120–145. 2011. """ - if tau is None and mu is None: - msg = "Either tau or mu must be provided." - raise ValueError(msg) - ncp = get_array_module(y) kwargs_simplex = {} if kwargs_simplex is None else kwargs_simplex @@ -118,6 +114,16 @@ def Segment( dimsprod = np.prod(np.array(dims)) ncl = len(cl) + # Set step sizes + L = 4 * ndims + if tau is None: + if mu is None: + msg = "Either tau or mu must be provided." + raise ValueError(msg) + tau = 1.0 / (mu * L) + elif mu is None: + mu = 1.0 / (tau * L) + # Data (difference between image and center of classes) g = sigma / 2.0 * (y.reshape(1, dimsprod) - cl[:, np.newaxis]) ** 2 if clsigmas is not None: @@ -136,11 +142,6 @@ def Segment( [L21(ndim=ndims, sigma=0.5 * alpha)] * ncl, nn=[ndims * dimsprod] * ncl ) - # Steps - L = 4 * ndims - tau = 1.0 / (mu * L) if tau is None else tau - mu = 1.0 / (tau * L) if mu is None else mu - # Inversion x: NDArray = PrimalDual( simp, From 876597e377ab710d33d40f5411a8012be8cfc4e7 Mon Sep 17 00:00:00 2001 From: mrava87 Date: Fri, 4 Sep 2026 11:34:48 +0100 Subject: [PATCH 3/3] test: added test for Segment raise --- pytests/test_solver.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/pytests/test_solver.py b/pytests/test_solver.py index 7200a24..9292b2e 100644 --- a/pytests/test_solver.py +++ b/pytests/test_solver.py @@ -21,6 +21,7 @@ TwIST, ) from pyproximal.optimization.primaldual import AdaptivePrimalDual, PrimalDual +from pyproximal.optimization.segmentation import Segment from pyproximal.proximal import L1, L2, Box, Quadratic par1 = {"n": 10, "m": 10, "dtype": "float64"} # square, float64 @@ -186,6 +187,19 @@ def test_GPG_epsg(par): ) +@pytest.mark.parametrize("par", [(par1), (par2), (par3)]) +def test_Segment_taumu(par): + """Check Segment raises error if neither tau or mu are passed""" + with pytest.raises(ValueError, match="tau or mu must be"): + np.random.seed(0) + + # Model and classes + y = np.zeros((par["n"], par["m"])) + cl = np.array([-1, 0, 1]) + + _ = Segment(y, cl, 1.0, 1.0, tau=None, mu=None) + + @pytest.mark.parametrize("par", [(par1), (par2), (par3)]) def test_ProximalPoint(par): """Check solution of ProximalPoint for quadratic function equals the solution of the