From 8c0ff5d94d30abb00935dec71b6f6c4cd87677b3 Mon Sep 17 00:00:00 2001 From: Clemens Schmid Date: Mon, 5 Aug 2024 14:05:32 +0200 Subject: [PATCH 1/5] Add "percentile" mode for autoscaling It autoscales from 1st to 99th (nan)percentiles of data. --- src/silx/gui/colors.py | 5 ++++- src/silx/gui/dialog/ColormapDialog.py | 1 + src/silx/math/colormap.py | 3 +++ 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/silx/gui/colors.py b/src/silx/gui/colors.py index 471725276b..e98214e477 100755 --- a/src/silx/gui/colors.py +++ b/src/silx/gui/colors.py @@ -331,7 +331,10 @@ class Colormap(qt.QObject): """constant for autoscale using mean +/- 3*std(data) with a clamp on min/max of the data""" - AUTOSCALE_MODES = (MINMAX, STDDEV3) + PERCENTILE = "percentile" + """constant for autoscale using 1st and 99th percentile of data""" + + AUTOSCALE_MODES = (MINMAX, STDDEV3, PERCENTILE) """Tuple of managed auto scale algorithms""" sigChanged = qt.Signal() diff --git a/src/silx/gui/dialog/ColormapDialog.py b/src/silx/gui/dialog/ColormapDialog.py index 7dadfd05b4..b270c65698 100644 --- a/src/silx/gui/dialog/ColormapDialog.py +++ b/src/silx/gui/dialog/ColormapDialog.py @@ -234,6 +234,7 @@ class _AutoscaleModeComboBox(qt.QComboBox): DATA = { Colormap.MINMAX: ("Min/max", "Use the data min/max"), Colormap.STDDEV3: ("Mean±3std", "Use the data mean ± 3 × standard deviation"), + Colormap.PERCENTILE: ("Percentile", "Use 1st to 99th percentile of data"), } def __init__(self, parent: qt.QWidget): diff --git a/src/silx/math/colormap.py b/src/silx/math/colormap.py index 8dd12f3281..943e0b3bc6 100644 --- a/src/silx/math/colormap.py +++ b/src/silx/math/colormap.py @@ -265,6 +265,9 @@ def autoscale(self, data, mode): vmax = dmax else: vmax = min(dmax, stdmax) + elif mode == "percentile": + vmin = numpy.nanpercentile(data, 1) + vmax = numpy.nanpercentile(data, 99) else: raise ValueError("Unsupported mode: %s" % mode) From a278a7446ae050c012bcfe1c25ebff1ea9460cb1 Mon Sep 17 00:00:00 2001 From: Clemens Schmid Date: Tue, 13 Aug 2024 10:22:35 +0200 Subject: [PATCH 2/5] Tweak percentile mode variable names --- src/silx/gui/colors.py | 4 ++-- src/silx/gui/dialog/ColormapDialog.py | 2 +- src/silx/math/colormap.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/silx/gui/colors.py b/src/silx/gui/colors.py index e98214e477..9704cddc04 100755 --- a/src/silx/gui/colors.py +++ b/src/silx/gui/colors.py @@ -331,10 +331,10 @@ class Colormap(qt.QObject): """constant for autoscale using mean +/- 3*std(data) with a clamp on min/max of the data""" - PERCENTILE = "percentile" + PERCENTILE_1_99 = "percentile_1_99" """constant for autoscale using 1st and 99th percentile of data""" - AUTOSCALE_MODES = (MINMAX, STDDEV3, PERCENTILE) + AUTOSCALE_MODES = (MINMAX, STDDEV3, PERCENTILE_1_99) """Tuple of managed auto scale algorithms""" sigChanged = qt.Signal() diff --git a/src/silx/gui/dialog/ColormapDialog.py b/src/silx/gui/dialog/ColormapDialog.py index b270c65698..5c26b948f2 100644 --- a/src/silx/gui/dialog/ColormapDialog.py +++ b/src/silx/gui/dialog/ColormapDialog.py @@ -234,7 +234,7 @@ class _AutoscaleModeComboBox(qt.QComboBox): DATA = { Colormap.MINMAX: ("Min/max", "Use the data min/max"), Colormap.STDDEV3: ("Mean±3std", "Use the data mean ± 3 × standard deviation"), - Colormap.PERCENTILE: ("Percentile", "Use 1st to 99th percentile of data"), + Colormap.PERCENTILE_1_99: ("Percentile 1-99", "Use 1st to 99th percentile of data"), } def __init__(self, parent: qt.QWidget): diff --git a/src/silx/math/colormap.py b/src/silx/math/colormap.py index 943e0b3bc6..9f7fc00371 100644 --- a/src/silx/math/colormap.py +++ b/src/silx/math/colormap.py @@ -265,7 +265,7 @@ def autoscale(self, data, mode): vmax = dmax else: vmax = min(dmax, stdmax) - elif mode == "percentile": + elif mode == "percentile_1_99": vmin = numpy.nanpercentile(data, 1) vmax = numpy.nanpercentile(data, 99) From a1530889d79336a534fe630ef6d8ea4054c33593 Mon Sep 17 00:00:00 2001 From: Clemens Schmid Date: Thu, 15 Aug 2024 15:08:48 +0200 Subject: [PATCH 3/5] Compute 1st and 99th percentiles simultaneously --- src/silx/math/colormap.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/silx/math/colormap.py b/src/silx/math/colormap.py index 9f7fc00371..7288673727 100644 --- a/src/silx/math/colormap.py +++ b/src/silx/math/colormap.py @@ -266,8 +266,7 @@ def autoscale(self, data, mode): else: vmax = min(dmax, stdmax) elif mode == "percentile_1_99": - vmin = numpy.nanpercentile(data, 1) - vmax = numpy.nanpercentile(data, 99) + vmin, vmax = numpy.nanpercentile(data, (1, 99)) else: raise ValueError("Unsupported mode: %s" % mode) From 1c95a8652935e4fe5f09c321ca4ac2fa0b946ac8 Mon Sep 17 00:00:00 2001 From: Thomas VINCENT Date: Mon, 7 Oct 2024 13:55:14 +0200 Subject: [PATCH 4/5] Add tests for percentile 1-99 autoscale --- src/silx/gui/test/test_colors.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/silx/gui/test/test_colors.py b/src/silx/gui/test/test_colors.py index 8c252a70ea..a9b0321185 100755 --- a/src/silx/gui/test/test_colors.py +++ b/src/silx/gui/test/test_colors.py @@ -430,6 +430,8 @@ def testAutoscaleMode(self): self.assertEqual(colormap.getAutoscaleMode(), Colormap.STDDEV3) colormap.setAutoscaleMode(Colormap.MINMAX) self.assertEqual(colormap.getAutoscaleMode(), Colormap.MINMAX) + colormap.setAutoscaleMode(Colormap.PERCENTILE_1_99) + self.assertEqual(colormap.getAutoscaleMode(), Colormap.PERCENTILE_1_99) def testStoreRestore(self): colormaps = [Colormap(name="viridis"), Colormap(normalization=Colormap.SQRT)] @@ -592,6 +594,8 @@ def testAutoscaleRange(self): ), (Colormap.LINEAR, Colormap.STDDEV3, numpy.array([10, 100]), (10, 100)), (Colormap.LOGARITHM, Colormap.STDDEV3, numpy.array([10, 100]), (10, 100)), + (Colormap.LINEAR, Colormap.PERCENTILE_1_99, numpy.array([10, 100]), (10.9, 99.1)), + (Colormap.LOGARITHM, Colormap.PERCENTILE_1_99, numpy.array([10, 100]), (10.9, 99.1)), # With nan ( Colormap.LINEAR, @@ -617,6 +621,18 @@ def testAutoscaleRange(self): data_std_inside_nan, (1, 1.6733506885453602), ), + ( + Colormap.LINEAR, + Colormap.PERCENTILE_1_99, + numpy.array([10, 20, 50, nan]), + (10.2, 49.4), + ), + ( + Colormap.LOGARITHM, + Colormap.PERCENTILE_1_99, + numpy.array([10, 50, 100, nan]), + (10.8, 99.), + ), # With negative ( Colormap.LOGARITHM, @@ -630,6 +646,12 @@ def testAutoscaleRange(self): numpy.array([10, 100, -10]), (10, 100), ), + ( + Colormap.LOGARITHM, + Colormap.PERCENTILE_1_99, + numpy.array([10, 50, 100, -50]), + (10.8, 99.), + ), ] for norm, mode, array, expectedRange in data: with self.subTest(norm=norm, mode=mode, array=array): From e9edc4f51b0224004849e76acd2c08b6bc68d243 Mon Sep 17 00:00:00 2001 From: Thomas VINCENT Date: Mon, 7 Oct 2024 13:59:03 +0200 Subject: [PATCH 5/5] sanitize data before calling np.percentile --- src/silx/gui/test/test_colors.py | 7 +++++++ src/silx/math/colormap.py | 11 ++++++++++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src/silx/gui/test/test_colors.py b/src/silx/gui/test/test_colors.py index a9b0321185..ce86095208 100755 --- a/src/silx/gui/test/test_colors.py +++ b/src/silx/gui/test/test_colors.py @@ -652,6 +652,13 @@ def testAutoscaleRange(self): numpy.array([10, 50, 100, -50]), (10.8, 99.), ), + # With inf + ( + Colormap.LOGARITHM, + Colormap.PERCENTILE_1_99, + numpy.array([10, 50, 100, float("inf")]), + (10.8, 99.), + ), ] for norm, mode, array, expectedRange in data: with self.subTest(norm=norm, mode=mode, array=array): diff --git a/src/silx/math/colormap.py b/src/silx/math/colormap.py index 7288673727..ede4fb92a1 100644 --- a/src/silx/math/colormap.py +++ b/src/silx/math/colormap.py @@ -266,7 +266,7 @@ def autoscale(self, data, mode): else: vmax = min(dmax, stdmax) elif mode == "percentile_1_99": - vmin, vmax = numpy.nanpercentile(data, (1, 99)) + vmin, vmax = self.autoscale_percentile_1_99(data) else: raise ValueError("Unsupported mode: %s" % mode) @@ -321,6 +321,15 @@ def autoscale_mean3std(self, data): mean + 3 * std, 0.0, 1.0 ) + def autoscale_percentile_1_99(self, data): + """Autoscale using [1st, 99th] percentiles""" + data = data[self.is_valid(data)] + if data.dtype.kind == "f": # Strip +/-inf + data = data[numpy.isfinite(data)] + if data.size == 0: + return None, None + return numpy.nanpercentile(data, (1, 99)) + class _LinearNormalizationMixIn(_NormalizationMixIn): """Colormap normalization mix-in class specific to autoscale taken from initial range"""