Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

silx.gui.colors: Added Colormap.get|setNaNColor to change color used for NaN #3143

Merged
merged 9 commits into from
Jul 1, 2020
70 changes: 58 additions & 12 deletions silx/gui/colors.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
import logging
import collections
from silx.gui import qt
from silx.gui.utils import blockSignals
from silx.math.combo import min_max
from silx.math import colormap as _colormap
from silx.utils.exceptions import NotEditableError
Expand Down Expand Up @@ -542,10 +543,14 @@ class Colormap(qt.QObject):
sigChanged = qt.Signal()
"""Signal emitted when the colormap has changed."""

_DEFAULT_NAN_COLOR = 255, 255, 255, 0

def __init__(self, name=None, colors=None, normalization=LINEAR, vmin=None, vmax=None, autoscaleMode=MINMAX):
qt.QObject.__init__(self)
self._editable = True
self.__gamma = 2.0
# Default NaN color: fully transparent white
self.__nanColor = numpy.array(self._DEFAULT_NAN_COLOR, dtype=numpy.uint8)

assert normalization in Colormap.NORMALIZATIONS
assert autoscaleMode in Colormap.AUTOSCALE_MODES
Expand Down Expand Up @@ -593,15 +598,19 @@ def setFromColormap(self, other):
raise NotEditableError('Colormap is not editable')
if self == other:
return
old = self.blockSignals(True)
name = other.getName()
if name is not None:
self.setName(name)
else:
self.setColormapLUT(other.getColormapLUT())
self.setNormalization(other.getNormalization())
self.setVRange(other.getVMin(), other.getVMax())
self.blockSignals(old)
with blockSignals(self):
name = other.getName()
if name is not None:
self.setName(name)
else:
self.setColormapLUT(other.getColormapLUT())
self.setNaNColor(other.getNaNColor())
self.setNormalization(other.getNormalization())
self.setGammaNormalizationParameter(
other.getGammaNormalizationParameter())
self.setAutoscaleMode(other.getAutoscaleMode())
self.setVRange(*other.getVRange())
self.setEditable(other.isEditable())
self.sigChanged.emit()

def getNColors(self, nbColors=None):
Expand Down Expand Up @@ -689,6 +698,24 @@ def setColormapLUT(self, colors):
self._name = None
self.sigChanged.emit()

def getNaNColor(self):
"""Returns the color to use for Not-A-Number floating point value.

:rtype: QColor
"""
return qt.QColor(*self.__nanColor)

def setNaNColor(self, color):
"""Set the color to use for Not-A-Number floating point value.

:param color: RGB(A) color to use for NaN values
:type color: QColor, str, tuple of uint8 or float in [0., 1.]
"""
color = (numpy.array(rgba(color)) * 255).astype(numpy.uint8)
if not numpy.array_equal(self.__nanColor, color):
self.__nanColor = color
self.sigChanged.emit()

def getNormalization(self):
"""Return the normalization of the colormap.

Expand Down Expand Up @@ -1021,8 +1048,10 @@ def copy(self):
vmax=self._vmax,
normalization=self.getNormalization(),
autoscaleMode=self.getAutoscaleMode())
colormap.setNaNColor(self.getNaNColor())
colormap.setGammaNormalizationParameter(
self.getGammaNormalizationParameter())
colormap.setEditable(self.isEditable())
return colormap

def applyToData(self, data, reference=None):
Expand All @@ -1041,7 +1070,12 @@ def applyToData(self, data, reference=None):
data = data.getColormappedData()

return _colormap.cmap(
data, self._colors, vmin, vmax, self._getNormalizer())
data,
self._colors,
vmin,
vmax,
self._getNormalizer(),
self.__nanColor)

@staticmethod
def getSupportedColormaps():
Expand Down Expand Up @@ -1086,7 +1120,7 @@ def __eq__(self, other):
numpy.array_equal(self.getColormapLUT(), other.getColormapLUT())
)

_SERIAL_VERSION = 2
_SERIAL_VERSION = 3

def restoreState(self, byteArray):
"""
Expand All @@ -1106,7 +1140,7 @@ def restoreState(self, byteArray):
return False

version = stream.readUInt32()
if version not in (1, self._SERIAL_VERSION):
if version not in numpy.arange(1, self._SERIAL_VERSION+1):
_logger.warning("Serial version mismatch. Found %d." % version)
return False

Expand All @@ -1133,6 +1167,11 @@ def restoreState(self, byteArray):
else:
autoscaleMode = stream.readQString()

if version <= 2:
nanColor = self._DEFAULT_NAN_COLOR
else:
nanColor = stream.readInt32(), stream.readInt32(), stream.readInt32(), stream.readInt32()

# emit change event only once
old = self.blockSignals(True)
try:
Expand All @@ -1142,6 +1181,7 @@ def restoreState(self, byteArray):
self.setVRange(vmin, vmax)
if gamma is not None:
self.setGammaNormalizationParameter(gamma)
self.setNaNColor(nanColor)
finally:
self.blockSignals(old)
self.sigChanged.emit()
Expand Down Expand Up @@ -1169,6 +1209,12 @@ def saveState(self):
if self.getNormalization() == Colormap.GAMMA:
stream.writeFloat(self.getGammaNormalizationParameter())
stream.writeQString(self.getAutoscaleMode())
nanColor = self.getNaNColor()
stream.writeInt32(nanColor.red())
stream.writeInt32(nanColor.green())
stream.writeInt32(nanColor.blue())
stream.writeInt32(nanColor.alpha())

return data


Expand Down
4 changes: 3 additions & 1 deletion silx/gui/plot/backends/BackendOpenGL.py
Original file line number Diff line number Diff line change
Expand Up @@ -866,6 +866,7 @@ def addImage(self, data,
cmapRange = colormap.getColormapRange(data=data)
colormapLut = colormap.getNColors(nbColors=256)
gamma = colormap.getGammaNormalizationParameter()
nanColor = colors.rgba(colormap.getNaNColor())

image = GLPlotColormap(data,
origin,
Expand All @@ -874,7 +875,8 @@ def addImage(self, data,
normalization,
gamma,
cmapRange,
alpha)
alpha,
nanColor)

else: # Fallback applying colormap on CPU
rgba = colormap.applyToData(data)
Expand Down
22 changes: 19 additions & 3 deletions silx/gui/plot/backends/glutils/GLPlotImage.py
Original file line number Diff line number Diff line change
Expand Up @@ -160,13 +160,19 @@ class GLPlotColormap(_GLPlotData2D):
'fragment': """
#version 120

/* isnan declaration for compatibility with GLSL 1.20 */
bool isnan(float value) {
return (value != value);
}

uniform sampler2D data;
uniform sampler2D cmap_texture;
uniform int cmap_normalization;
uniform float cmap_parameter;
uniform float cmap_min;
uniform float cmap_oneOverRange;
uniform float alpha;
uniform vec4 nancolor;

varying vec2 coords;

Expand All @@ -175,7 +181,8 @@ class GLPlotColormap(_GLPlotData2D):
const float oneOverLog10 = 0.43429448190325176;

void main(void) {
float value = texture2D(data, textureCoords()).r;
float data = texture2D(data, textureCoords()).r;
float value = data;
if (cmap_normalization == 1) { /*Logarithm mapping*/
if (value > 0.) {
value = clamp(cmap_oneOverRange *
Expand All @@ -202,7 +209,11 @@ class GLPlotColormap(_GLPlotData2D):
value = clamp(cmap_oneOverRange * (value - cmap_min), 0., 1.);
}

gl_FragColor = texture2D(cmap_texture, vec2(value, 0.5));
if (isnan(data)) {
gl_FragColor = nancolor;
} else {
gl_FragColor = texture2D(cmap_texture, vec2(value, 0.5));
}
gl_FragColor.a *= alpha;
}
"""
Expand Down Expand Up @@ -232,7 +243,7 @@ class GLPlotColormap(_GLPlotData2D):

def __init__(self, data, origin, scale,
colormap, normalization='linear', gamma=0., cmapRange=None,
alpha=1.0):
alpha=1.0, nancolor=(1., 1., 1., 0.)):
"""Create a 2D colormap

:param data: The 2D scalar data array to display
Expand All @@ -252,6 +263,8 @@ def __init__(self, data, origin, scale,
TODO: check consistency with matplotlib
:type cmapRange: (float, float) or None
:param float alpha: Opacity from 0 (transparent) to 1 (opaque)
:param nancolor: RGBA color for Not-A-Number values
:type nancolor: 4-tuple of float in [0., 1.]
"""
assert data.dtype in self._INTERNAL_FORMATS
assert normalization in self.SUPPORTED_NORMALIZATIONS
Expand All @@ -263,6 +276,7 @@ def __init__(self, data, origin, scale,
self._cmapRange = (1., 10.) # Colormap range
self.cmapRange = cmapRange # Update _cmapRange
self._alpha = numpy.clip(alpha, 0., 1.)
self._nancolor = numpy.clip(nancolor, 0., 1.)

self._cmap_texture = None
self._texture = None
Expand Down Expand Up @@ -376,6 +390,8 @@ def _setCMap(self, prog):
oneOverRange = 0. # Fall-back
gl.glUniform1f(prog.uniforms['cmap_oneOverRange'], oneOverRange)

gl.glUniform4f(prog.uniforms['nancolor'], *self._nancolor)

self._cmap_texture.bind()

def _renderLinear(self, matrix):
Expand Down
17 changes: 17 additions & 0 deletions silx/gui/plot/test/testPlotWidget.py
Original file line number Diff line number Diff line change
Expand Up @@ -326,6 +326,23 @@ def testPlotColormapCustom(self):
resetzoom=False)
self.plot.resetZoom()

def testPlotColormapNaNColor(self):
self.plot.setKeepDataAspectRatio(False)
self.plot.setGraphTitle('Colormap with NaN color')

colormap = Colormap()
colormap.setNaNColor('red')
self.assertEqual(colormap.getNaNColor(), qt.QColor(255, 0, 0))
data = DATA_2D.astype(numpy.float32)
data[len(data)//2:] = numpy.nan
self.plot.addImage(data, legend="image 1", colormap=colormap,
resetzoom=False)
self.plot.resetZoom()

colormap.setNaNColor((0., 1., 0., 1.))
self.assertEqual(colormap.getNaNColor(), qt.QColor(0, 255, 0))
self.qapp.processEvents()

def testImageOriginScale(self):
"""Test of image with different origin and scale"""
self.plot.setGraphTitle('origin and scale')
Expand Down
1 change: 1 addition & 0 deletions silx/gui/plot3d/items/mixins.py
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ def _syncSceneColormap(self):
self.__sceneColormap.norm = colormap.getNormalization()
self.__sceneColormap.gamma = colormap.getGammaNormalizationParameter()
self.__sceneColormap.range_ = colormap.getColormapRange(self)
self.__sceneColormap.nancolor = rgba(colormap.getNaNColor())


class ComplexMixIn(_ComplexMixIn):
Expand Down
26 changes: 25 additions & 1 deletion silx/gui/plot3d/scene/function.py
Original file line number Diff line number Diff line change
Expand Up @@ -389,10 +389,13 @@ class Colormap(event.Notifier, ProgramFunction):
uniform float cmap_parameter;
uniform float cmap_min;
uniform float cmap_oneOverRange;
uniform vec4 nancolor;

const float oneOverLog10 = 0.43429448190325176;

vec4 colormap(float value) {
float data = value; /* Keep original input value for isnan test */

if (cmap_normalization == 1) { /* Log10 mapping */
if (value > 0.0) {
value = clamp(cmap_oneOverRange *
Expand Down Expand Up @@ -421,7 +424,12 @@ class Colormap(event.Notifier, ProgramFunction):

$discard

vec4 color = texture2D(cmap_texture, vec2(value, 0.5));
vec4 color;
if (data != data) { /* isnan alternative for compatibility with GLSL 1.20 */
color = nancolor;
} else {
color = texture2D(cmap_texture, vec2(value, 0.5));
}
return color;
}
""")
Expand Down Expand Up @@ -458,6 +466,7 @@ def __init__(self, colormap=None, norm='linear', gamma=0., range_=(1., 10.)):
self._gamma = -1.
self._range = 1., 10.
self._displayValuesBelowMin = True
self._nancolor = numpy.array((1., 1., 1., 0.), dtype=numpy.float32)

self._texture = None
self._update_texture = True
Expand Down Expand Up @@ -494,6 +503,20 @@ def colormap(self, colormap):
self._update_texture = True
self.notify()

@property
def nancolor(self):
"""RGBA color to use for Not-A-Number values as 4 float in [0., 1.]"""
return self._nancolor

@nancolor.setter
def nancolor(self, color):
color = numpy.clip(numpy.array(color, dtype=numpy.float32), 0., 1.)
assert color.ndim == 1
assert len(color) == 4
if not numpy.array_equal(self._nancolor, color):
self._nancolor = color
self.notify()

@property
def norm(self):
"""Normalization to use for colormap mapping.
Expand Down Expand Up @@ -607,6 +630,7 @@ def setupProgram(self, context, program):
gl.glUniform1f(program.uniforms['cmap_min'], min_)
gl.glUniform1f(program.uniforms['cmap_oneOverRange'],
(1. / (max_ - min_)) if max_ != min_ else 0.)
gl.glUniform4f(program.uniforms['nancolor'], *self._nancolor)

def prepareGL2(self, context):
if self._texture is None or self._update_texture:
Expand Down
Loading