Color map linearization (#1797)

* extended color map functions

* assertion string, documentation pass, comment purge

* fix some documentation links

* simplify assert statement

* removed comments and redundancy, renamed modulated bar

* include modulatedBarData in documentation

* test running matplotlib with dummy backend

* skip color maps example on OSX/PySide6.1.1

* removed skipping of tests

* reverted some accidental whitespace, removed unneeded numpy import

* removed unneded alpha parameter
This commit is contained in:
Nils Nemitz 2021-06-09 12:41:46 +09:00 committed by GitHub
parent f002d70adc
commit 7d41e8a878
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 465 additions and 74 deletions

View File

@ -73,6 +73,9 @@ API Reference
.. autofunction:: pyqtgraph.colormap.getFromColorcet
.. autofunction:: pyqtgraph.colormap.modulatedBarData
.. autoclass:: pyqtgraph.ColorMap

View File

@ -30,12 +30,18 @@ Qt uses the classes QColor, QPen, and QBrush to determine how to draw lines and
.. autofunction:: pyqtgraph.intColor
.. autofunction:: pyqtgraph.CIELabColor
.. autofunction:: pyqtgraph.colorCIELab
.. autofunction:: pyqtgraph.colorTuple
.. autofunction:: pyqtgraph.colorStr
.. autofunction:: pyqtgraph.glColor
.. autofunction:: pyqtgraph.colorDistance
Data Slicing
------------

View File

@ -7,13 +7,11 @@ from Matplotlib or ColorCET.
## Add path to library (just for examples; you do not need this)
import initExample
import numpy as np
from pyqtgraph.Qt import QtCore, QtGui
import pyqtgraph as pg
app = pg.mkQApp()
## Create window with ImageView widget
win = QtGui.QMainWindow()
win.resize(1000,800)
@ -25,70 +23,51 @@ scr = QtGui.QScrollArea()
scr.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
scr.setWidget(lw)
win.setCentralWidget(scr)
win.show()
win.setWindowTitle('pyqtgraph example: Color maps')
win.show()
## Create color map test image
width = 3*256
height = 32
img = np.zeros( (width, height) )
gradient = np.linspace(0.05, 0.95, width)
modulation = np.zeros(width)
for idx in range(width):
modulation[idx] = -0.05 * np.sin( 200 * np.pi * idx/width )
for idx in range(height):
img[:,idx] = gradient + (idx/(height-1)) * modulation
bar_width = 32
bar_data = pg.colormap.modulatedBarData(width=bar_width)
num_bars = 0
lw.addLabel('=== local color maps ===')
num_bars += 1
lw.nextRow()
def add_heading(lw, name):
global num_bars
lw.addLabel('=== '+name+' ===')
num_bars += 1
lw.nextRow()
def add_bar(lw, name, cm):
global num_bars
lw.addLabel(name)
imi = pg.ImageItem( bar_data )
imi.setLookupTable( cm.getLookupTable(alpha=True) )
vb = lw.addViewBox(lockAspect=True, enableMouse=False)
vb.addItem( imi )
num_bars += 1
lw.nextRow()
add_heading(lw, 'local color maps')
list_of_maps = pg.colormap.listMaps()
for map_name in list_of_maps:
num_bars += 1
lw.addLabel(map_name)
cmap = pg.colormap.get(map_name)
imi = pg.ImageItem()
imi.setImage(img)
imi.setLookupTable( cmap.getLookupTable(alpha=True) )
vb = lw.addViewBox(lockAspect=True, enableMouse=False)
vb.addItem(imi)
lw.nextRow()
cm = pg.colormap.get(map_name)
add_bar(lw, map_name, cm)
lw.addLabel('=== Matplotlib import ===')
num_bars += 1
lw.nextRow()
add_heading(lw, 'Matplotlib import')
list_of_maps = pg.colormap.listMaps('matplotlib')
for map_name in list_of_maps:
num_bars += 1
lw.addLabel(map_name)
cmap = pg.colormap.get(map_name, source='matplotlib', skipCache=True)
if cmap is not None:
imi = pg.ImageItem()
imi.setImage(img)
imi.setLookupTable( cmap.getLookupTable(alpha=True) )
vb = lw.addViewBox(lockAspect=True, enableMouse=False)
vb.addItem(imi)
lw.nextRow()
cm = pg.colormap.get(map_name, source='matplotlib', skipCache=True)
if cm is not None:
add_bar(lw, map_name, cm)
lw.addLabel('=== ColorCET import ===')
num_bars += 1
lw.nextRow()
add_heading(lw, 'ColorCET import')
list_of_maps = pg.colormap.listMaps('colorcet')
for map_name in list_of_maps:
num_bars += 1
lw.addLabel(map_name)
cmap = pg.colormap.get(map_name, source='colorcet', skipCache=True)
if cmap is not None:
imi = pg.ImageItem()
imi.setImage(img)
imi.setLookupTable( cmap.getLookupTable(alpha=True) )
vb = lw.addViewBox(lockAspect=True, enableMouse=False)
vb.addItem(imi)
lw.nextRow()
lw.setFixedHeight(num_bars * (height+5) )
cm = pg.colormap.get(map_name, source='colorcet', skipCache=True)
if cm is not None:
add_bar(lw, map_name, cm)
lw.setFixedHeight(num_bars * (bar_width+5) )
if __name__ == '__main__':
pg.exec()

View File

@ -0,0 +1,120 @@
# -*- coding: utf-8 -*-
"""
This example demonstrates linearized ColorMap objects using colormap.makeMonochrome()
or using the `ColorMap`'s `linearize()` method.
"""
# Add path to library (just for examples; you do not need this)
import initExample
import numpy as np
from pyqtgraph.Qt import QtCore, QtGui
import pyqtgraph as pg
name_list = (
'warm','neutral','cool',
'green','amber','blue','red','pink','lavender',
(0.5, 0.2, 0.1, 0.8)
)
ramp_list = [
pg.colormap.makeMonochrome(name)
for name in name_list
]
cm_list = []
# Create a gray ramp for demonstrating the idea:
cm = pg.ColorMap( None, [
QtGui.QColor( 0, 0, 0),
QtGui.QColor( 10, 10, 10),
QtGui.QColor(127, 127, 127),
QtGui.QColor(240, 240, 240),
QtGui.QColor(255, 255, 255)
])
cm_list.append(('Distorted gray ramp',cm))
# Create a rainbow scale in HSL color space:
length = 41
col_list = []
for idx in range(length):
frac = idx/(length-1)
qcol = QtGui.QColor()
qcol.setHslF( (2*frac-0.15)%1.0, 0.8, 0.5-0.5*np.cos(np.pi*frac) )
col_list.append(qcol)
cm = pg.ColorMap( None, col_list )
cm_list.append( ('Distorted HSL spiral', cm) )
# Create some random examples:
for example_idx in range(3):
previous = None
col_list = []
for idx in range(8):
values = np.random.random((3))
if previous is not None:
intermediate = (values + previous) / 2
qcol = QtGui.QColor()
qcol.setRgbF( *intermediate )
col_list.append( qcol)
qcol1 = QtGui.QColor()
qcol1.setRgbF( *values )
col_list.append( qcol1)
previous = values
cm = pg.ColorMap( None, col_list )
cm_list.append( (f'random {example_idx+1}', cm) )
app = pg.mkQApp()
win = QtGui.QMainWindow()
win.resize(1000,800)
lw = pg.GraphicsLayoutWidget()
lw.setFixedWidth(1000)
lw.setSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Expanding)
scr = QtGui.QScrollArea()
scr.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
scr.setWidget(lw)
win.setCentralWidget(scr)
win.setWindowTitle('pyqtgraph example: Linearized color maps')
win.show()
bar_width = 32
bar_data = pg.colormap.modulatedBarData(width=bar_width)
num_bars = 0
def add_heading(lw, name):
global num_bars
lw.addLabel('=== '+name+' ===')
num_bars += 1
lw.nextRow()
def add_bar(lw, name, cm):
global num_bars
lw.addLabel(name)
imi = pg.ImageItem( bar_data )
imi.setLookupTable( cm.getLookupTable(alpha=True) )
vb = lw.addViewBox(lockAspect=True, enableMouse=False)
vb.addItem( imi )
num_bars += 1
lw.nextRow()
add_heading(lw, 'ramp generator')
for cm in ramp_list:
add_bar(lw, cm.name, cm)
add_heading(lw, 'linearizer demonstration')
for (name, cm) in cm_list:
add_bar(lw, name, cm)
cm.linearize()
add_bar(lw, '> linearized', cm)
add_heading(lw, 'consistency with included maps')
for name in ('CET-C3', 'CET-L17', 'CET-L2'):
# lw.addLabel(str(name))
cm = pg.colormap.get(name)
add_bar(lw, name, cm)
cm.linearize()
add_bar(lw, '> linearized', cm)
lw.setFixedHeight(num_bars * (bar_width+5) )
if __name__ == '__main__':
pg.exec()

View File

@ -13,8 +13,6 @@ examples = OrderedDict([
('Timestamps on x axis', 'DateAxisItem.py'),
('Image Analysis', 'imageAnalysis.py'),
('Matrix Display', 'MatrixDisplayExample.py'),
('Color Maps', 'colorMaps.py'),
('Color Gradient Plots', 'ColorGradientPlots.py'),
('ViewBox Features', Namespace(filename='ViewBoxFeatures.py', recommended=True)),
('Dock widgets', 'dockarea.py'),
('Console', 'ConsoleWidget.py'),
@ -31,6 +29,11 @@ examples = OrderedDict([
('Verlet chain', 'verlet_chain_demo.py'),
('Koch Fractal', 'fractal.py'),
])),
('Colors', OrderedDict([
('Color Maps', 'colorMaps.py'),
('Color Map Linearization', 'colorMapsLinearized.py'),
('Color Gradient Plots', 'ColorGradientPlots.py')
])),
('GraphicsItems', OrderedDict([
('Scatter Plot', 'ScatterPlot.py'),
#('PlotItem', 'PlotItem.py'),

View File

@ -1,6 +1,6 @@
import numpy as np
from .Qt import QtGui, QtCore
from .functions import mkColor, eq
from .functions import mkColor, eq, colorDistance
from os import path, listdir
from collections.abc import Callable, Sequence
import warnings
@ -129,7 +129,9 @@ def _getFromFile(name):
cm = ColorMap(
pos=np.linspace(0.0, 1.0, len(color_list)),
color=color_list) #, names=color_names)
_mapCache[name] = cm
if cm is not None:
cm.name = name
_mapCache[name] = cm
return cm
def getFromMatplotlib(name):
@ -173,6 +175,7 @@ def getFromMatplotlib(name):
col_data = np.array(col_map.colors)
cm = ColorMap(pos=np.linspace(0.0, 1.0, col_data.shape[0]), color=255*col_data[:,:3]+0.5 )
if cm is not None:
cm.name = name
_mapCache[name] = cm
return cm
@ -195,9 +198,91 @@ def getFromColorcet(name):
cm = ColorMap(
pos=np.linspace(0.0, 1.0, len(color_list)),
color=color_list) #, names=color_names)
_mapCache[name] = cm
if cm is not None:
cm.name = name
_mapCache[name] = cm
return cm
def makeMonochrome(color='green'):
"""
Returns a ColorMap object with a dark to bright ramp and adjustable tint.
In addition to neutral, warm or cold grays, imitations of monochrome computer monitors are also
available. The following predefined color ramps are available:
`neutral`, `warm`, `cool`, `green`, `amber`, `blue`, `red`, `pink`, `lavender`.
The ramp can also be specified by a tuple of float values in the range of 0 to 1.
In this case `(h, s, l0, l1)` describe hue, saturation, minimum lightness and maximum lightness
within the HSL color space. The values `l0` and `l1` can be omitted. They default to
`l0=0.0` and `l1=1.0` in this case.
Parameters
----------
color: str or tuple of floats
Color description. Can be one of the predefined identifiers, or a tuple
`(h, s, l0, l1)`, `(h, s)` or (`h`).
'green', 'amber', 'blue', 'red', 'lavender', 'pink'
or a tuple of relative ``(R,G,B)`` contributions in range 0.0 to 1.0
"""
name=f'Monochrome {color}'
defaults = {
'neutral': (0.00, 0.00, 0.00, 1.00),
'warm' : (0.10, 0.08, 0.00, 0.95),
'cool' : (0.60, 0.08, 0.00, 0.95),
'green' : (0.35, 0.55, 0.02, 0.90),
'amber' : (0.09, 0.80, 0.02, 0.80),
'blue' : (0.58, 0.85, 0.02, 0.95),
'red' : (0.01, 0.60, 0.02, 0.90),
'pink' : (0.93, 0.65, 0.02, 0.95),
'lavender': (0.75, 0.50, 0.02, 0.90)
}
if isinstance(color, str):
if color in defaults:
h_val, s_val, l_min, l_max = defaults[color]
else:
valid = ','.join(defaults.keys())
raise ValueError(f"Undefined color descriptor '{color}', known values are:\n{valid}")
else:
s_val = 0.70 # set up default values
l_min = 0.00
l_max = 1.00
if not hasattr(color,'__len__'):
h_val = float(color)
elif len(color) == 1:
h_val = color[0]
elif len(color) == 2:
h_val, s_val = color
elif len(color) == 4:
h_val, s_val, l_min, l_max = color
else:
raise ValueError(f"Invalid color descriptor '{color}'")
l_vals = np.linspace(l_min, l_max, num=10)
color_list = []
for l_val in l_vals:
qcol = QtGui.QColor()
qcol.setHslF( h_val, s_val, l_val )
color_list.append( qcol )
return ColorMap( None, color_list, name=name, linearize=True )
def modulatedBarData(length=768, width=32):
"""
Returns an NumPy array that represents a modulated color bar ranging from 0 to 1.
This is used to judge the perceived variation of the color gradient.
Parameters
----------
length: int
Length of the data set. Values will vary from 0 to 1 over this axis.
width: int
Width of the data set. The modulation will vary from 0% to 4% over this axis.
"""
gradient = np.linspace(0.00, 1.00, length)
modulation = -0.04 * np.sin( (np.pi/4) * np.arange(length) )
data = np.zeros( (length, width) )
for idx in range(width):
data[:,idx] = gradient + (idx/(width-1)) * modulation
np.clip(data, 0.0, 1.0)
return data
class ColorMap(object):
"""
@ -216,7 +301,6 @@ class ColorMap(object):
A ColorMap object provides access to the interpolated colors by indexing with a float value:
``cm[0.5]`` returns a QColor corresponding to the center of ColorMap `cm`.
"""
## mapping modes
CLIP = 1
REPEAT = 2
@ -238,14 +322,14 @@ class ColorMap(object):
'qcolor': QCOLOR,
}
def __init__(self, pos, color, mapping=CLIP, mode=None): #, names=None):
def __init__(self, pos, color, mapping=CLIP, mode=None, linearize=False, name=''):
"""
__init__(pos, color, mapping=ColorMap.CLIP)
Parameters
----------
pos: array_like of float in range 0 to 1
Assigned positions of specified colors
pos: array_like of float in range 0 to 1, or None
Assigned positions of specified colors. `None` sets equal spacing.
color: array_like of colors
List of colors, interpreted via :func:`mkColor() <pyqtgraph.mkColor>`.
mapping: str or int, optional
@ -255,24 +339,35 @@ class ColorMap(object):
The default of `ColorMap.CLIP` continues to show
the colors assigned to 0 and 1 for all values below or above this range, respectively.
"""
self.name = name # storing a name helps identify ColorMaps sampled by Palette
if mode is not None:
warnings.warn(
"'mode' argument is deprecated and does nothing.",
DeprecationWarning, stacklevel=2
)
self.pos = np.array(pos)
order = np.argsort(self.pos)
self.pos = self.pos[order]
self.color = np.apply_along_axis(
func1d = lambda x: np.uint8( mkColor(x).getRgb() ), # cast RGB integer values to uint8
axis = -1,
arr = color,
)[order]
if pos is None:
order = range(len(color))
self.pos = np.linspace(0.0, 1.0, num=len(color))
else:
self.pos = np.array(pos)
order = np.argsort(self.pos)
self.pos = self.pos[order]
self.color = np.zeros( (len(color), 4) ) # stores float rgba values
for cnt, idx in enumerate(order):
self.color[cnt] = mkColor(color[idx]).getRgbF()
# alternative code may be more efficient, but fails to handle lists of QColor.
# self.color = np.apply_along_axis(
# func1d = lambda x: np.uint8( mkColor(x).getRgb() ), # cast RGB integer values to uint8
# axis = -1,
# arr = color,
# )[order]
self.mapping_mode = self.CLIP # default to CLIP mode
if mapping is not None:
self.setMappingMode( mapping )
self.stopsCache = {}
if linearize: self.linearize()
def setMappingMode(self, mapping):
"""
@ -294,6 +389,12 @@ class ColorMap(object):
self.mapping_mode = mapping # only allow defined values
else:
raise ValueError("Undefined mapping type '{:s}'".format(str(mapping)) )
def __str__(self):
""" provide human-readable identifier """
if self.name is None:
return 'unnamed ColorMap({:d})'.format(len(self.pos))
return "ColorMap({:d}):'{:s}'".format(len(self.pos),self.name)
def __getitem__(self, key):
""" Convenient shorthand access to palette colors """
@ -306,6 +407,17 @@ class ColorMap(object):
except ValueError: pass
return None
def linearize(self):
"""
Adjusts the positions assigned to color stops to approximately equalize the perceived color difference
for a fixed step.
"""
colors = self.getColors(mode=self.QCOLOR)
distances = colorDistance(colors)
positions = np.insert( np.cumsum(distances), 0, 0.0 )
self.pos = positions / positions[-1] # normalize last value to 1.0
self.stopsCache = {}
def reverse(self):
"""
Reverses the color map, so that the color assigned to a value of 1 now appears at 0 and vice versa.

View File

@ -214,7 +214,7 @@ def mkColor(*args):
types. Accepted arguments are:
================ ================================================
'c' one of: r, g, b, c, m, y, k, w
'c' one of: r, g, b, c, m, y, k, w
R, G, B, [A] integers 0-255
(R, G, B, [A]) tuple of integers 0-255
float greyscale, 0.0-1.0
@ -372,13 +372,170 @@ def hsvColor(hue, sat=1.0, val=1.0, alpha=1.0):
c = QtGui.QColor()
c.setHsvF(hue, sat, val, alpha)
return c
# Matrices and math taken from "CIELab Color Space" by Gernot Hoffmann
# http://docs-hoffmann.de/cielab03022003.pdf
MATRIX_XYZ_FROM_RGB = np.array( (
( 0.4124, 0.3576, 0.1805),
( 0.2126, 0.7152, 0.0722),
( 0.0193, 0.1192, 0.9505) ) )
MATRIX_RGB_FROM_XYZ = np.array( (
( 3.2410,-1.5374,-0.4985),
(-0.9692, 1.8760, 0.0416),
( 0.0556,-0.2040, 1.0570) ) )
VECTOR_XYZn = np.array( ( 0.9505, 1.0000, 1.0891) ) # white reference at illuminant D65
def CIELabColor(L, a, b, alpha=1.0):
"""
Generates as QColor from CIE L*a*b* values.
Parameters
----------
L: float
Lightness value ranging from 0 to 100
a, b: float
(green/red) and (blue/yellow) coordinates, typically -127 to +127.
alpha: float, optional
Opacity, ranging from 0 to 1
Notes
-----
The CIE L*a*b* color space parametrizes color in terms of a luminance `L`
and the `a` and `b` coordinates that locate the hue in terms of
a "green to red" and a "blue to yellow" axis.
These coordinates seek to parametrize human color preception in such a way
that the Euclidean distance between the coordinates of two colors represents
the visual difference between these colors. In particular, the difference
ΔE = sqrt( (L1-L2)² + (a1-a2)² + (b1-b2)² ) = 2.3
is considered the smallest "just noticeable difference" between colors.
This simple equation represents the CIE76 standard. Later standards CIE94
and CIE2000 refine the difference calculation ΔE, while maintaining the
L*a*b* coordinates.
Alternative (and arguably more accurate) methods exist to quantify color
difference, but the CIELab color space remains a convenient approximation.
Under a known illumination, assumed to be white standard illuminant D65
here, a CIELab color induces a response in the human eye
that is described by the tristimulus value XYZ. Once this is known, an
sRGB color can be calculated to induce the same response.
More information and underlying mathematics can be found in e.g.
"CIELab Color Space" by Gernot Hoffmann, available at
http://docs-hoffmann.de/cielab03022003.pdf .
Also see :func:`colorDistance() <pyqtgraph.colorDistance>`.
"""
# convert to tristimulus XYZ values
vec_XYZ = np.full(3, ( L +16)/116 ) # Y1 = (L+16)/116
vec_XYZ[0] += a / 500 # X1 = (L+16)/116 + a/500
vec_XYZ[2] -= b / 200 # Z1 = (L+16)/116 - b/200
for idx, val in enumerate(vec_XYZ):
if val > 0.20689:
vec_XYZ[idx] = vec_XYZ[idx]**3
else:
vec_XYZ[idx] = (vec_XYZ[idx] - 16/116) / 7.787
vec_XYZ = VECTOR_XYZn * vec_XYZ # apply white reference
# print(f'XYZ: {vec_XYZ}')
# convert XYZ to linear RGB
vec_RGB = MATRIX_RGB_FROM_XYZ @ vec_XYZ
# gamma-encode linear RGB
arr_sRGB = np.zeros(3)
for idx, val in enumerate( vec_RGB[:3] ):
if val > 0.0031308: # (t) RGB value for linear/exponential transition
arr_sRGB[idx] = 1.055 * val**(1/2.4) - 0.055
else:
arr_sRGB[idx] = 12.92 * val # (s)
arr_sRGB = clip_array( arr_sRGB, 0.0, 1.0 ) # avoid QColor errors
qcol = QtGui.QColor()
qcol.setRgbF( *arr_sRGB )
if alpha < 1.0: qcol.setAlpha(alpha)
return qcol
def colorCIELab(qcol):
"""
Describes a QColor by an array of CIE L*a*b* values.
Also see :func:`CIELabColor() <pyqtgraph.CIELabColor>` .
Parameters
----------
qcol: QColor
QColor to be converted
Returns
-------
NumPy array
Color coordinates `[L, a, b]`.
"""
srgb = qcol.getRgbF()[:3] # get sRGB values from QColor
# convert gamma-encoded sRGB to linear:
vec_RGB = np.zeros(3)
for idx, val in enumerate( srgb ):
if val > (12.92 * 0.0031308): # coefficients (s) * (t)
vec_RGB[idx] = ((val+0.055)/1.055)**2.4
else:
vec_RGB[idx] = val / 12.92 # (s) coefficient
# converted linear RGB to tristimulus XYZ:
vec_XYZ = MATRIX_XYZ_FROM_RGB @ vec_RGB
# normalize with white reference and convert to L*a*b* values
vec_XYZ1 = vec_XYZ / VECTOR_XYZn
for idx, val in enumerate(vec_XYZ1):
if val > 0.008856:
vec_XYZ1[idx] = vec_XYZ1[idx]**(1/3)
else:
vec_XYZ1[idx] = 7.787*vec_XYZ1[idx] + 16/116
vec_Lab = np.array([
116 * vec_XYZ1[1] - 16, # Y1
500 * (vec_XYZ1[0] - vec_XYZ1[1]), # X1 - Y1
200 * (vec_XYZ1[1] - vec_XYZ1[2])] ) # Y1 - Z1
return vec_Lab
def colorDistance(colors, metric='CIE76'):
"""
Returns the perceptual distances between a sequence of QColors.
See :func:`CIELabColor() <pyqtgraph.CIELabColor>` for more information.
Parameters
----------
colors: list of QColor
Two or more colors to calculate the distances between.
metric: string, optional
Metric used to determined the difference. Only 'CIE76' is supported at this time,
where a distance of 2.3 is considered a "just noticeable difference".
The default may change as more metrics become available.
Returns
-------
List
The `N-1` sequential distances between `N` colors.
"""
metric = metric.upper()
if len(colors) < 1: return np.array([], dtype=np.float)
if metric == 'CIE76':
dist = []
lab1 = None
for col in colors:
lab2 = colorCIELab(col)
if lab1 is None: #initialize on first element
lab1 = lab2
continue
dE = math.sqrt( np.sum( (lab1-lab2)**2 ) )
dist.append(dE)
lab1 = lab2
return np.array(dist)
raise ValueError(f'Metric {metric} is not available.')
def colorTuple(c):
"""Return a tuple (R,G,B,A) from a QColor"""
return (c.red(), c.green(), c.blue(), c.alpha())
def colorStr(c):
"""Generate a hex string code from a QColor"""
return ('%02x'*4) % colorTuple(c)

View File

@ -248,6 +248,18 @@ def test_siParse(s, suffix, expected):
with pytest.raises(expected):
pg.siParse(s, suffix=suffix)
def test_CIELab_reconversion():
color_list = [ pg.Qt.QtGui.QColor('#100235') ] # known problematic values
for _ in range(20):
qcol = pg.Qt.QtGui.QColor()
qcol.setRgbF( *np.random.random((3)) )
color_list.append(qcol)
for qcol1 in color_list:
vec_Lab = pg.functions.colorCIELab( qcol1 )
qcol2 = pg.functions.CIELabColor(*vec_Lab)
for val1, val2 in zip( qcol1.getRgb(), qcol2.getRgb() ):
assert abs(val1-val2)<=1, f'Excess CIELab reconversion error ({qcol1.name() } > {vec_Lab } > {qcol2.name()})'
MoveToElement = pg.QtGui.QPainterPath.ElementType.MoveToElement
LineToElement = pg.QtGui.QPainterPath.ElementType.LineToElement
@ -321,4 +333,3 @@ def test_arrayToQPath(xs, ys, connect, expected):
continue
element = path.elementAt(i)
assert eq(expected[i], (element.type, element.x, element.y))