Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
57 commits
Select commit Hold shift + click to select a range
8aa2c86
silx.gui.plot: Generalize code to use any percentile value and not on…
payno Sep 12, 2025
d3d5f4e
from_saturation_to_percentile: add check on input and allow integers
payno Sep 12, 2025
1e62810
ColormapDialog: rework scale widget layout:
payno Sep 12, 2025
3b4a001
ColormapDialog: replace the concept of 'saturation' by a concept of '…
payno Sep 12, 2025
321a68d
Update src/silx/gui/colors.py
payno Sep 12, 2025
ea848fc
Update src/silx/gui/colors.py
payno Sep 12, 2025
3ad7889
Update src/silx/gui/colors.py
payno Sep 12, 2025
00e5c22
Update src/silx/gui/colors.py
payno Sep 12, 2025
8259a16
Update src/silx/gui/colors.py
payno Sep 12, 2025
cef5665
Update src/silx/gui/colors.py
payno Sep 12, 2025
979037f
Update src/silx/gui/colors.py
payno Sep 12, 2025
db1b0a4
Update src/silx/gui/dialog/ColormapDialog.py
payno Sep 12, 2025
04654f7
Colormap: rename '_percentiles_autoscale_values' to '_percentiles'
payno Sep 12, 2025
f2afe95
silx.gui.colors: remove duplicated code for computing min/max
payno Sep 12, 2025
df98476
Move silx.gui.widgets.SliderWithSpinBox to private
payno Sep 12, 2025
3904be1
SliderWithSpinBox: replace the QDoubleSpinBox by a SpinBox
payno Sep 12, 2025
9cb3839
Revert plotWidget modifications
payno Sep 12, 2025
856c6b2
ColormapDialog: rename 'percentile_range' to 'lateral_percentile_rang…
payno Sep 12, 2025
7146f0d
ColormapDialog: Rename '_usedPercentileWidget' to '_centralPercentile…
payno Sep 12, 2025
72b2bda
ColormapDialog: rename '_updateUsedPercentileVisibility' to '_updateC…
payno Sep 12, 2025
c8f8856
Remove: test_contrast_enhancer
payno Sep 12, 2025
fb70b7a
silx.math.colormap: fix 'normalize'
payno Sep 12, 2025
0032165
TestAutoscaleRange: fix test
payno Sep 12, 2025
56f7900
black
payno Sep 12, 2025
f478fef
silx.gui.colors: fix missing 'Literal'
payno Sep 12, 2025
cfe8926
Update src/silx/gui/dialog/ColormapDialog.py
payno Sep 16, 2025
d226326
Update src/silx/gui/dialog/ColormapDialog.py
payno Sep 16, 2025
9cbe182
Update src/silx/gui/widgets/_SliderWithSpinBox.py
payno Sep 16, 2025
f4624da
Update src/silx/math/colormap.py
payno Sep 16, 2025
ae58843
Update src/silx/math/colormap.py
payno Sep 16, 2025
92f4bbb
Update src/silx/math/colormap.py
payno Sep 16, 2025
9c14ced
Colormap.getAutoscalePercentile: update docstring
payno Sep 16, 2025
d95b14c
move and rename silx.gui;widgets._SliderWithSpinBox to silx.gui.dialo…
payno Sep 16, 2025
7010d4d
silx.gui.dialog: move ColormapDialog functions 'from_lateral_percenti…
payno Sep 16, 2025
1130cf9
silx.gui.colors: rename Colormap.setAutoscalePercentile to Colormap.s…
payno Sep 16, 2025
a7a0d51
silx.match.colormap: update 'AutoScaleModeType'
payno Sep 16, 2025
34bf6dd
ColormapPercentileWidget: rename 'fromCentralPercentileToLateralPerce…
payno Sep 16, 2025
61f8182
Colormap: update _DEFAULT_PERCENTILES to be floats
payno Sep 18, 2025
136d14e
Colormap: use '_DEFAULT_PERCENTILES' to set default values
payno Sep 18, 2025
be99993
ColormapRow: remove todo and add an issue instead (https://github.com…
payno Sep 18, 2025
b1fa723
silx.gui.colors.Colormap: use 'AutoScaleModeType' instead of redefini…
payno Sep 18, 2025
95df4fd
silx.math.colormap._NormalizationMixIn: if mode 'percentile_1_99' giv…
payno Sep 18, 2025
a6c6e3d
silx.math.colormap: rename several 'percentile' to 'percentiles' (the…
payno Sep 18, 2025
2a9bff9
ColormapPercentilesWidget: improve docstring
payno Sep 18, 2025
ca20f50
ColormapPercentilesWidget: avoid using 'blocksignals'
payno Sep 18, 2025
823e4a4
Rework ColormapPercentilesWidget:
payno Sep 18, 2025
9e420b3
ColormapDialog: clean unused code
payno Sep 18, 2025
f64e2c2
ColormapPercentilesWidget: rework access to 'setSaturationValue' in o…
payno Sep 18, 2025
9978f1b
Update src/silx/gui/colors.py
payno Sep 22, 2025
01a84da
Update src/silx/gui/dialog/_ColormapPercentileWidget.py
payno Sep 22, 2025
ac7b863
ColormapPercentilesWidget: rename 'getSaturationValue' and 'setSatura…
payno Sep 22, 2025
809daa1
ColormapPercentilesWidget: move widget fine tuning in the constructor
payno Sep 22, 2025
8958c44
ColormapDialog: removed unecessary set of percentiles
payno Sep 22, 2025
c28a550
ColormapPercentilesWidget: store last saturation value to check if ch…
payno Sep 22, 2025
578d53f
ColormapPercentileWidget: remove ununsed import
payno Sep 23, 2025
ccc09b4
ColormapDialog: remove unecessary connection.
payno Sep 23, 2025
4f5f5ab
Colormap: improve deprecation warning when using percentile_1_99 in c…
payno Sep 23, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 45 additions & 8 deletions src/silx/gui/colors.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
from silx.gui.utils import blockSignals
from silx.math import colormap as _colormap
from silx.utils.exceptions import NotEditableError
from silx.utils.deprecation import deprecated_warning


_logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -334,16 +335,21 @@ class Colormap(qt.QObject):
with a clamp on min/max of the data"""

PERCENTILE_1_99 = "percentile_1_99"
"""constant for autoscale using 1st and 99th percentile of data"""
"""constant for autoscale using 1st and 99th percentile of data. Deprecated to the benefit of 'percentile'"""

AUTOSCALE_MODES = (MINMAX, STDDEV3, PERCENTILE_1_99)
PERCENTILE = "percentile"
"""constant for autoscale using n'st and m'th percentile of data"""

AUTOSCALE_MODES = (MINMAX, STDDEV3, PERCENTILE)
"""Tuple of managed auto scale algorithms"""

sigChanged = qt.Signal()
"""Signal emitted when the colormap has changed."""

_DEFAULT_NAN_COLOR = 255, 255, 255, 0

_DEFAULT_PERCENTILES = (1.0, 99.0)

def __init__(
self,
name: str | None = None,
Expand Down Expand Up @@ -387,6 +393,7 @@ def __init__(

self._normalization = str(normalization)
self._autoscaleMode = str(autoscaleMode)
self._percentiles = self._DEFAULT_PERCENTILES
self._vmin = float(vmin) if vmin is not None else None
self._vmax = float(vmax) if vmax is not None else None
self.__warnBadVmin = True
Expand Down Expand Up @@ -556,22 +563,44 @@ def getGammaNormalizationParameter(self) -> float:
"""Returns the gamma correction parameter value."""
return self.__gamma

def getAutoscaleMode(self) -> str:
"""Return the autoscale mode of the colormap ('minmax' or 'stddev3')"""
def getAutoscaleMode(self) -> _colormap.AutoScaleModeType:
"""Return the autoscale mode of the colormap."""
return self._autoscaleMode

def setAutoscaleMode(self, mode: str):
"""Set the autoscale mode: either 'minmax' or 'stddev3'
def setAutoscaleMode(self, mode: _colormap.AutoScaleModeType):
"""Set the autoscale mode.

:param mode: the mode to set
"""
if self.isEditable() is False:
raise NotEditableError("Colormap is not editable")
if mode == self.PERCENTILE_1_99:
deprecated_warning(
type_="Mode",
name="mode",
replacement="percentile",
since_version="3.0",
)
mode = self.PERCENTILE
self.setAutoscalePercentiles((1.0, 99.0))
assert mode in self.AUTOSCALE_MODES
if mode != self._autoscaleMode:
self._autoscaleMode = mode
self.sigChanged.emit()

def setAutoscalePercentiles(self, value: tuple[float, float]):
self._percentiles = value
self.sigChanged.emit()

def getAutoscalePercentiles(self) -> tuple[float, float]:
"""
Return the (min, max) percentiles used for autoscaling in 'percentile' mode.
'min' and 'max' are between 0 and 100 included.

:return: (min, max)
"""
return self._percentiles

def isAutoscale(self) -> bool:
"""Return True if both min and max are in autoscale mode"""
return self._vmin is None and self._vmax is None
Expand Down Expand Up @@ -661,7 +690,11 @@ def _computeAutoscaleRange(self, data: numpy.ndarray):
:param data: The data for which to compute the range
:return: (vmin, vmax) range
"""
return self._getNormalizer().autoscale(data, mode=self.getAutoscaleMode())
return self._getNormalizer().autoscale(
data,
mode=self.getAutoscaleMode(),
percentiles=self.getAutoscalePercentiles(),
)

def getColormapRange(
self,
Expand Down Expand Up @@ -703,7 +736,11 @@ def getColormapRange(
fmin = normalizer.DEFAULT_RANGE[0] if min_ is None else min_
fmax = normalizer.DEFAULT_RANGE[1] if max_ is None else max_
else:
fmin, fmax = normalizer.autoscale(data, mode=self.getAutoscaleMode())
fmin, fmax = normalizer.autoscale(
data,
mode=self.getAutoscaleMode(),
percentiles=self.getAutoscalePercentiles(),
)

if vmin is None: # Set vmin respecting provided vmax
vmin2 = fmin if vmax is None else min(fmin, vmax)
Expand Down
52 changes: 42 additions & 10 deletions src/silx/gui/dialog/ColormapDialog.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@
from ..plot import PlotWidget
from ..plot.items.axis import Axis
from ..plot.items import BoundingRect
from ._ColormapPercentileWidget import ColormapPercentilesWidget
from silx.gui.widgets.FloatEdit import FloatEdit
import weakref
from silx.math.combo import min_max
Expand Down Expand Up @@ -233,9 +234,9 @@ 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_1_99: (
"Percentile 1-99",
"Use 1st to 99th percentile of data",
Colormap.PERCENTILE: (
"Percentile",
"Use n'st to (100-n)'th percentile of data",
),
}

Expand Down Expand Up @@ -958,7 +959,9 @@ def __init__(self, parent=None, title="Colormap Dialog"):
autoScaleCombo = _AutoscaleModeComboBox(self)
autoScaleCombo.currentIndexChanged.connect(self._autoscaleModeUpdated)
self._autoScaleCombo = autoScaleCombo

self._autoScaleCombo.currentTextChanged.connect(
self._updatePercentilesWidgetEnabled
)
# Min row
self._minValue = _BoundaryWidget(parent=self, value=1.0)
self._minValue.sigAutoScaleChanged.connect(self._minAutoscaleUpdated)
Expand All @@ -974,6 +977,10 @@ def __init__(self, parent=None, title="Colormap Dialog"):
self._autoButtons = _AutoScaleButton(self)
self._autoButtons.autoRangeChanged.connect(self._autoRangeButtonsUpdated)

# used percentile
self._percentilesWidget = ColormapPercentilesWidget(self)
self._percentilesWidget.percentilesChanged.connect(self._percentilesChanged)

rangeLayout = qt.QGridLayout()
miniFont = qt.QFont(self.font())
miniFont.setPixelSize(8)
Expand Down Expand Up @@ -1057,11 +1064,15 @@ def __init__(self, parent=None, title="Colormap Dialog"):
self._scaleToAreaGroup.setLayout(layout)
self._scaleToAreaGroup.setVisible(False)

layoutScale = qt.QHBoxLayout()
layoutScale = qt.QGridLayout()
layoutScale.setContentsMargins(0, 0, 0, 0)
layoutScale.addWidget(self._autoButtons)
layoutScale.addWidget(self._autoScaleCombo)
layoutScale.addStretch()
layoutScale.addWidget(self._autoButtons, 0, 0, 1, 1)
layoutScale.addWidget(self._autoScaleCombo, 0, 1, 1, 1)
layoutScale.addItem(
qt.QSpacerItem(0, 0, qt.QSizePolicy.Fixed, qt.QSizePolicy.Fixed), 0, 2, 1, 1
)

layoutScale.addWidget(self._percentilesWidget, 1, 1, 1, 1)

formLayout = FormGridLayout(self)
formLayout.setContentsMargins(10, 10, 10, 10)
Expand All @@ -1079,7 +1090,9 @@ def __init__(self, parent=None, title="Colormap Dialog"):
formLayout.addItem(
qt.QSpacerItem(1, 1, qt.QSizePolicy.Fixed, qt.QSizePolicy.Fixed)
)
formLayout.addRow("Scale:", layoutScale)
scaleLabel = qt.QLabel("Scale:")
scaleLabel.setAlignment(qt.Qt.AlignTop)
formLayout.addRow(scaleLabel, layoutScale)
formLayout.addRow("Fixed scale on:", self._scaleToAreaGroup)
formLayout.addRow(self._buttonsModal)
formLayout.addRow(self._buttonsNonModal)
Expand All @@ -1096,6 +1109,7 @@ def __init__(self, parent=None, title="Colormap Dialog"):
self.setTabOrder(self._selectedAreaButton, self._buttonsModal)
self.setTabOrder(self._buttonsModal, self._buttonsNonModal)

self._updatePercentilesWidgetEnabled()
self._applyColormap()

def getHistogramWidget(self):
Expand Down Expand Up @@ -1638,6 +1652,10 @@ def _applyColormap(self):
with utils.blockSignals(self._autoButtons):
self._autoButtons.setEnabled(colormap.isEditable())
self._autoButtons.setAutoRangeFromColormap(colormap)
with utils.blockSignals(self._percentilesWidget):
self._percentilesWidget.setPercentilesRange(
colormap.getAutoscalePercentiles()
)

vmin, vmax = colormap.getVRange()
if vmin is None or vmax is None:
Expand Down Expand Up @@ -1702,7 +1720,6 @@ def _normalizationUpdated(self, index):
if colormap is not None:
normalization = self._comboBoxNormalization.itemData(index)
self._gammaSpinBox.setEnabled(normalization == "gamma")

with self._colormapChange:
colormap.setNormalization(normalization)
self._histoWidget.updateNormalization()
Expand All @@ -1728,6 +1745,21 @@ def _autoscaleModeUpdated(self):

self._updateWidgetRange()

def _updatePercentilesWidgetEnabled(self):
enableWidget = (
self._autoScaleCombo.currentText()
== _AutoscaleModeComboBox.DATA[Colormap.PERCENTILE][0]
)
self._percentilesWidget.setEnabled(enableWidget)

def _percentilesChanged(self, percentiles):
"""Callback executed when the saturation level has been changed (will impact the 'PERCENTILE' mode)"""
colormap = self.getColormap()
if colormap is not None:
with self._colormapChange:
colormap.setAutoscalePercentiles(percentiles)
self._updateWidgetRange()

def _minAutoscaleUpdated(self, autoEnabled):
"""Callback executed when the min autoscale from
the lineedit is updated by user input"""
Expand Down
90 changes: 90 additions & 0 deletions src/silx/gui/dialog/_ColormapPercentileWidget.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
from __future__ import annotations

from silx.gui import qt
from ..colors import Colormap


class ColormapPercentilesWidget(qt.QWidget):
"""
Widget to define the percentiles to be used when computing the colormap in autoscale / percentile mode.

A scalar value (that can be seen as saturation) is defined by the user and then converted to percentiles using the 'fromSaturationToPercentiles' function.
"""

percentilesChanged = qt.Signal(tuple)

def __init__(self, parent=None):
super().__init__(parent)
self._saturationLastValue = None

self.setLayout(qt.QHBoxLayout())

self._slider = qt.QSlider(qt.Qt.Horizontal, self)
self.layout().addWidget(self._slider)

self._spinBox = qt.QSpinBox(self)
self.layout().addWidget(self._spinBox)

self._setRange(0, 100)

self.setTickPosition(qt.QSlider.TicksBelow)

self.setPercentilesRange(Colormap._DEFAULT_PERCENTILES)
self.setTracking(False)

# connect signal / slot
self._slider.valueChanged.connect(self._setSaturation)
self._spinBox.valueChanged.connect(self._setSaturation)

def _setSaturation(self, value: int):
self._slider.setValue(value)
self._spinBox.setValue(value)
if self._saturationLastValue != value:
self._saturationLastValue = value
self.percentilesChanged.emit(self.getPercentilesRange())

def _getSaturation(self) -> int:
return self._slider.value()

def _setRange(self, min: int, max: int):
"""
Set the slider / spin box range
"""
self._slider.setRange(min, max)
self._spinBox.setRange(min, max)

def setPercentilesRange(self, percentiles: tuple[float, float]):
self._setSaturation(self.fromPercentilesToSaturation(percentiles))

def getPercentilesRange(self) -> tuple[float, float]:
return self.fromSaturationToPercentiles(self._getSaturation())

# expose API
def setTickPosition(self, position):
self._slider.setTickPosition(position)

def setTracking(self, enable: bool):
self._slider.setTracking(enable)

@staticmethod
def fromPercentilesToSaturation(
percentiles: tuple[float, float],
) -> int:
"""
Example: if we want to have saturation = 90% then the percentile we will return percentiles (5th, 95th)
"""
return int(100 - (percentiles[0] + (100 - percentiles[1])))

@staticmethod
def fromSaturationToPercentiles(
saturation: float | int,
) -> tuple[float, float]:
"""
Example: if we use percentiles (1st, 99th) we use 98% of the percentiles. This is the saturation (can be seen also as the central percentile)
"""
if not isinstance(saturation, (float, int)):
raise TypeError(
f"central_percentile is expected to be float. Got {type(saturation)}"
)
ignored_percentile = 100 - saturation
return (ignored_percentile / 2.0, 100 - (ignored_percentile / 2.0))
14 changes: 11 additions & 3 deletions src/silx/gui/plot/items/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -663,9 +663,12 @@ def _setColormappedData(
# Fill-up colormap range cache if values are provided
if max_ is not None and numpy.isfinite(max_):
if min_ is not None and numpy.isfinite(min_):
self.__cacheColormapRange[Colormap.LINEAR, Colormap.MINMAX] = min_, max_
self.__cacheColormapRange[Colormap.LINEAR, Colormap.MINMAX, None] = (
min_,
max_,
)
if minPositive is not None and numpy.isfinite(minPositive):
self.__cacheColormapRange[Colormap.LOGARITHM, Colormap.MINMAX] = (
self.__cacheColormapRange[Colormap.LOGARITHM, Colormap.MINMAX, None] = (
minPositive,
max_,
)
Expand Down Expand Up @@ -703,7 +706,12 @@ def _getColormapAutoscaleRange(self, colormap=None):

normalization = colormap.getNormalization()
autoscaleMode = colormap.getAutoscaleMode()
key = normalization, autoscaleMode
if autoscaleMode == Colormap.PERCENTILE:
percentile = colormap.getAutoscalePercentiles()
else:
percentile = None

key = normalization, autoscaleMode, percentile
vRange = self.__cacheColormapRange.get(key, None)
if vRange is None:
vRange = colormap._computeAutoscaleRange(data)
Expand Down
Loading
Loading