Add ColorBarItem for simplified image level adjustment (#1596)
* Initial implementation of ColorBarItem * initial commit * fixed missing indent * docstring extension and corrections * Converted example to match others / run as part of tests * load local color maps instead of importing from colorcet * clean up window creation code * horizontal color bar and clean-up * switched to mkQApp initialization * cleaned up some comments
This commit is contained in:
parent
929f3bda0d
commit
758c038411
100
examples/ColorBarItem.py
Normal file
100
examples/ColorBarItem.py
Normal file
@ -0,0 +1,100 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
This example demonstrates the use of ColorBarItem, which displays a simple interactive color bar.
|
||||
"""
|
||||
## Add path to library (just for examples; you do not need this)
|
||||
import initExample
|
||||
|
||||
import numpy as np
|
||||
from pyqtgraph.Qt import QtWidgets, mkQApp
|
||||
import pyqtgraph as pg
|
||||
|
||||
class MainWindow(QtWidgets.QMainWindow):
|
||||
""" example application main window """
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(MainWindow, self).__init__(*args, **kwargs)
|
||||
gr_wid = pg.GraphicsLayoutWidget(show=True)
|
||||
self.setCentralWidget(gr_wid)
|
||||
self.setWindowTitle('pyqtgraph example: Interactive color bar')
|
||||
self.resize(800,700)
|
||||
self.show()
|
||||
|
||||
## Create image items
|
||||
data = np.fromfunction(lambda i, j: (1+0.3*np.sin(i)) * (i)**2 + (j)**2, (100, 100))
|
||||
noisy_data = data * (1 + 0.2 * np.random.random(data.shape) )
|
||||
noisy_transposed = noisy_data.transpose()
|
||||
|
||||
#--- add non-interactive image with integrated color -----------------
|
||||
i1 = pg.ImageItem(image=data)
|
||||
p1 = gr_wid.addPlot(title="non-interactive")
|
||||
p1.addItem( i1 )
|
||||
p1.setMouseEnabled( x=False, y=False)
|
||||
p1.disableAutoRange()
|
||||
p1.hideButtons()
|
||||
p1.setRange(xRange=(0,100), yRange=(0,100), padding=0)
|
||||
for key in ['left','right','top','bottom']:
|
||||
p1.showAxis(key)
|
||||
axis = p1.getAxis(key)
|
||||
axis.setZValue(1)
|
||||
if key in ['top', 'right']:
|
||||
p1.getAxis(key).setStyle( showValues=False )
|
||||
|
||||
cmap = pg.colormap.get('CET-L9')
|
||||
bar = pg.ColorBarItem(
|
||||
interactive=False, values= (0, 30_000), cmap=cmap,
|
||||
label='vertical fixed color bar'
|
||||
)
|
||||
bar.setImageItem( i1, insert_in=p1 )
|
||||
|
||||
#--- add interactive image with integrated horizontal color bar --------------
|
||||
i2 = pg.ImageItem(image=noisy_data)
|
||||
p2 = gr_wid.addPlot(1,0, 1,1, title="interactive")
|
||||
p2.addItem( i2, title='' )
|
||||
# inserted color bar also works with labels on the right.
|
||||
p2.showAxis('right')
|
||||
p2.getAxis('left').setStyle( showValues=False )
|
||||
p2.getAxis('bottom').setLabel('bottom axis label')
|
||||
p2.getAxis('right').setLabel('right axis label')
|
||||
|
||||
cmap = pg.colormap.get('CET-L4')
|
||||
bar = pg.ColorBarItem(
|
||||
values = (0, 30_000),
|
||||
cmap=cmap,
|
||||
label='horizontal color bar',
|
||||
limits = (0, None),
|
||||
rounding=1000,
|
||||
orientation = 'horizontal',
|
||||
pen='#8888FF', hoverPen='#EEEEFF', hoverBrush='#EEEEFF80'
|
||||
)
|
||||
bar.setImageItem( i2, insert_in=p2 )
|
||||
|
||||
#--- multiple images adjusted by a separate color bar ------------------------
|
||||
i3 = pg.ImageItem(image=noisy_data)
|
||||
p3 = gr_wid.addPlot(0,1, 1,1, title="shared 1")
|
||||
p3.addItem( i3 )
|
||||
|
||||
i4 = pg.ImageItem(image=noisy_transposed)
|
||||
p4 = gr_wid.addPlot(1,1, 1,1, title="shared 2")
|
||||
p4.addItem( i4 )
|
||||
|
||||
cmap = pg.colormap.get('CET-L8')
|
||||
bar = pg.ColorBarItem(
|
||||
# values = (-15_000, 15_000),
|
||||
limits = (-30_000, 30_000), # start with full range...
|
||||
rounding=1000,
|
||||
width = 10,
|
||||
cmap=cmap )
|
||||
bar.setImageItem( [i3, i4] )
|
||||
bar.setLevels( low=-5_000, high=15_000) # ... then adjust to retro sunset.
|
||||
|
||||
# manually adjust reserved space at top and bottom to align with plot
|
||||
bar.getAxis('bottom').setHeight(21)
|
||||
bar.getAxis('top').setHeight(31)
|
||||
gr_wid.addItem(bar, 0,2, 2,1) # large bar spanning both rows
|
||||
|
||||
mkQApp("ColorBarItem Example")
|
||||
main_window = MainWindow()
|
||||
|
||||
## Start Qt event loop
|
||||
if __name__ == '__main__':
|
||||
mkQApp().exec_()
|
@ -1,14 +1,8 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
This example demonstrates the use of ImageView, which is a high-level widget for
|
||||
displaying and analyzing 2D and 3D data. ImageView provides:
|
||||
|
||||
1. A zoomable region (ViewBox) for displaying the image
|
||||
2. A combination histogram and gradient editor (HistogramLUTItem) for
|
||||
controlling the visual appearance of the image
|
||||
3. A timeline for selecting the currently displayed frame (for 3D data only).
|
||||
4. Tools for very basic analysis of image data (see ROI and Norm buttons)
|
||||
|
||||
This example demonstrates generating ColorMap objects from external data.
|
||||
It displays the full list of color maps available as local files or by import
|
||||
from Matplotlib or ColorCET.
|
||||
"""
|
||||
## Add path to library (just for examples; you do not need this)
|
||||
import initExample
|
||||
|
@ -38,6 +38,7 @@ examples = OrderedDict([
|
||||
('FillBetweenItem', 'FillBetweenItem.py'),
|
||||
('ImageItem - video', 'ImageItem.py'),
|
||||
('ImageItem - draw', 'Draw.py'),
|
||||
('ColorBarItem','ColorBarItem.py'),
|
||||
('Non-uniform Image', 'NonUniformImage.py'),
|
||||
('Region-of-Interest', 'ROIExamples.py'),
|
||||
('Bar Graph', 'BarGraphItem.py'),
|
||||
|
@ -223,6 +223,7 @@ from .graphicsItems.GraphicsWidgetAnchor import *
|
||||
from .graphicsItems.PlotCurveItem import *
|
||||
from .graphicsItems.ButtonItem import *
|
||||
from .graphicsItems.GradientEditorItem import *
|
||||
from .graphicsItems.ColorBarItem import *
|
||||
from .graphicsItems.MultiPlotItem import *
|
||||
from .graphicsItems.ErrorBarItem import *
|
||||
from .graphicsItems.IsocurveItem import *
|
||||
|
258
pyqtgraph/graphicsItems/ColorBarItem.py
Normal file
258
pyqtgraph/graphicsItems/ColorBarItem.py
Normal file
@ -0,0 +1,258 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from ..Qt import QtCore
|
||||
from .. import functions as fn
|
||||
from .PlotItem import PlotItem
|
||||
from .ImageItem import ImageItem
|
||||
from .LinearRegionItem import LinearRegionItem
|
||||
|
||||
import weakref
|
||||
import math
|
||||
import numpy as np
|
||||
|
||||
__all__ = ['ColorBarItem']
|
||||
|
||||
class ColorBarItem(PlotItem):
|
||||
"""
|
||||
**Bases:** :class:`PlotItem <pyqtgraph.PlotItem>`
|
||||
|
||||
ColorBarItem is a simpler, compact alternative to HistogramLUTItem, without histogram
|
||||
or the option to adjust the look-up table.
|
||||
|
||||
A labeled axis is displayed directly next to the gradient to help identify values.
|
||||
Handles included in the color bar allow for interactive adjustment.
|
||||
|
||||
A ColorBarItem can be assigned one or more ImageItems that will be displayed according
|
||||
to the selected color map and levels. The ColorBarItem can be used as a separate
|
||||
element in a GraphicsLayout or added to the layout of a PlotItem used to display image
|
||||
data with coordinate axes.
|
||||
|
||||
============================= =============================================
|
||||
**Signals:**
|
||||
sigLevelsChanged(self) Emitted when the range sliders are moved
|
||||
sigLevelsChangeFinished(self) Emitted when the range sliders are released
|
||||
============================= =============================================
|
||||
"""
|
||||
sigLevelsChanged = QtCore.Signal(object)
|
||||
sigLevelsChangeFinished = QtCore.Signal(object)
|
||||
|
||||
def __init__(self, values=(0,1), width=25, cmap=None, label=None,
|
||||
interactive=True, limits=None, rounding=1,
|
||||
orientation='vertical', pen='w', hoverPen='r', hoverBrush='#FF000080' ):
|
||||
"""
|
||||
Create a new ColorBarItem.
|
||||
|
||||
============== ===========================================================================
|
||||
**Arguments:**
|
||||
values The range of values as tuple (min, max)
|
||||
width (default=25) The width of the displayed color bar
|
||||
cmap ColorMap object, look-up table is also applied to assigned ImageItem(s)
|
||||
label (default=None) Label applied to color bar axis
|
||||
interactive (default=True) Handles are displayed to interactively adjust level range
|
||||
limits Limits to adjustment range as (low, high) tuple, None disables limit
|
||||
rounding (default=1) Adjusted range values are rounded to multiples of this values
|
||||
orientation (default='vertical') 'horizontal' gives a horizontal color bar
|
||||
pen color of adjustement handles in interactive mode
|
||||
hoverPen color of adjustement handles when hovered over
|
||||
hoverBrush color of movable center region when hovered over
|
||||
============== ===========================================================================
|
||||
"""
|
||||
super().__init__()
|
||||
self.img_list = [] # list of controlled ImageItems
|
||||
self.values = values
|
||||
self.cmap = cmap
|
||||
self.rounding = rounding
|
||||
self.horizontal = bool(orientation == 'horizontal')
|
||||
|
||||
self.lo_prv, self.hi_prv = self.values # remember previous values while adjusting range
|
||||
if limits is None:
|
||||
self.lo_lim = None
|
||||
self.hi_lim = None
|
||||
else:
|
||||
self.lo_lim, self.hi_lim = limits
|
||||
|
||||
self.disableAutoRange()
|
||||
self.hideButtons()
|
||||
self.setMouseEnabled(x=False, y=False)
|
||||
self.setMenuEnabled( False)
|
||||
|
||||
if self.horizontal:
|
||||
self.setRange( xRange=(0,256), yRange=(0,1), padding=0 )
|
||||
self.layout.setRowFixedHeight(2, width)
|
||||
else:
|
||||
self.setRange( xRange=(0,1), yRange=(0,256), padding=0 )
|
||||
self.layout.setColumnFixedWidth(1, width) # width of color bar
|
||||
|
||||
for key in ['left','right','top','bottom']:
|
||||
self.showAxis(key)
|
||||
axis = self.getAxis(key)
|
||||
axis.setZValue(1)
|
||||
# select main axis:
|
||||
if self.horizontal and key == 'bottom':
|
||||
self.axis = axis
|
||||
elif not self.horizontal and key == 'right':
|
||||
self.axis = axis
|
||||
self.axis.setWidth(45)
|
||||
else: # show other axes to create frame
|
||||
axis.setStyle( showValues=False, tickLength=0 )
|
||||
self.axis.setStyle( showValues=True )
|
||||
self.axis.unlinkFromView()
|
||||
self.axis.setRange( self.values[0], self.values[1] )
|
||||
|
||||
self.bar = ImageItem()
|
||||
if self.horizontal:
|
||||
self.bar.setImage( np.linspace(0, 1, 256).reshape( (-1,1) ) )
|
||||
if label is not None: self.getAxis('bottom').setLabel(label)
|
||||
else:
|
||||
self.bar.setImage( np.linspace(0, 1, 256).reshape( (1,-1) ) )
|
||||
if label is not None: self.getAxis('left').setLabel(label)
|
||||
self.addItem(self.bar)
|
||||
if cmap is not None: self.setCmap(cmap)
|
||||
|
||||
if interactive:
|
||||
if self.horizontal:
|
||||
align = 'vertical'
|
||||
else:
|
||||
align = 'horizontal'
|
||||
self.region = LinearRegionItem(
|
||||
[63, 191], align, swapMode='block',
|
||||
# span=(0.15, 0.85), # limited span looks better, but disables grabbing the region
|
||||
pen=pen, brush=fn.mkBrush(None), hoverPen=hoverPen, hoverBrush=hoverBrush )
|
||||
self.region.setZValue(1000)
|
||||
self.region.lines[0].addMarker('<|>', size=6)
|
||||
self.region.lines[1].addMarker('<|>', size=6)
|
||||
self.region.sigRegionChanged.connect(self._regionChanging)
|
||||
self.region.sigRegionChangeFinished.connect(self._regionChanged)
|
||||
self.addItem(self.region)
|
||||
self.region_changed_enable = True
|
||||
self.region.setRegion( (63, 191) ) # place handles at 25% and 75% locations
|
||||
else:
|
||||
self.region = None
|
||||
self.region_changed_enable = False
|
||||
|
||||
def setImageItem(self, img, insert_in=None):
|
||||
"""
|
||||
assign ImageItem or list of ImageItems to be controlled
|
||||
|
||||
============== ==========================================================================
|
||||
**Arguments:**
|
||||
image ImageItem or list of [ImageItem, ImageItem, ...] that will be set to the
|
||||
color map of the ColorBarItem. In interactive mode, the levels of all
|
||||
assigned ImageItems will be controlled simultaneously.
|
||||
insert_in If a PlotItem is given, the color bar is inserted on the right or bottom
|
||||
of the plot
|
||||
============== ==========================================================================
|
||||
"""
|
||||
try:
|
||||
self.img_list = [ weakref.ref(item) for item in img ]
|
||||
except TypeError: # failed to iterate, make a single-item list
|
||||
self.img_list = [ weakref.ref( img ) ]
|
||||
if insert_in is not None:
|
||||
if self.horizontal:
|
||||
insert_in.layout.addItem( self, 5, 1 ) # insert in layout below bottom axis
|
||||
insert_in.layout.setRowFixedHeight(4, 10) # enforce some space to axis above
|
||||
else:
|
||||
insert_in.layout.addItem( self, 2, 5 ) # insert in layout after right-hand axis
|
||||
insert_in.layout.setColumnFixedWidth(4, 5) # enforce some space to axis on the left
|
||||
self._update_items( update_cmap = True )
|
||||
|
||||
def setCmap(self, cmap):
|
||||
"""
|
||||
sets a ColorMap object to determine the ColorBarItem's look-up table. The same
|
||||
look-up table is applied to any assigned ImageItem.
|
||||
"""
|
||||
self.cmap = cmap
|
||||
self._update_items( update_cmap = True )
|
||||
|
||||
def setLevels(self, values=None, low=None, high=None ):
|
||||
"""
|
||||
Sets the displayed range of levels as specified.
|
||||
|
||||
============== ===========================================================================
|
||||
**Arguments:**
|
||||
values Specify levels by tuple (low, high). Either value can be None to leave
|
||||
to previous value unchanged. Takes precedence over low and high parameters.
|
||||
low new low level to be applied to color bar and assigned images
|
||||
high new high level to be applied to color bar and assigned images
|
||||
============== ===========================================================================
|
||||
"""
|
||||
if values is not None: # values setting takes precendence
|
||||
low, high = values
|
||||
lo_new, hi_new = low, high
|
||||
lo_cur, hi_cur = self.values
|
||||
# allow None values to preserve original values:
|
||||
if lo_new is None: lo_new = lo_cur
|
||||
if hi_new is None: hi_new = hi_cur
|
||||
if lo_new > hi_new: # prevent reversal
|
||||
lo_new = hi_new = (lo_new + hi_new) / 2
|
||||
# clip to limits if set:
|
||||
if self.lo_lim is not None and lo_new < self.lo_lim: lo_new = self.lo_lim
|
||||
if self.hi_lim is not None and hi_new > self.hi_lim: hi_new = self.hi_lim
|
||||
self.values = self.lo_prv, self.hi_prv = (lo_new, hi_new)
|
||||
self._update_items()
|
||||
|
||||
def levels(self):
|
||||
""" returns the currently set levels as the tuple (low, high). """
|
||||
return self.values
|
||||
|
||||
def _update_items(self, update_cmap=False):
|
||||
""" internal: update color maps for bar and assigned ImageItems """
|
||||
# update color bar:
|
||||
self.axis.setRange( self.values[0], self.values[1] )
|
||||
if update_cmap and self.cmap is not None:
|
||||
self.bar.setLookupTable( self.cmap.getLookupTable(nPts=256) )
|
||||
# update assigned ImageItems, too:
|
||||
for img_weakref in self.img_list:
|
||||
img = img_weakref()
|
||||
if img is None: continue # dereference weakref
|
||||
img.setLevels( self.values ) # (min,max) tuple
|
||||
if update_cmap and self.cmap is not None:
|
||||
img.setLookupTable( self.cmap.getLookupTable(nPts=256) )
|
||||
|
||||
def _regionChanged(self):
|
||||
""" internal: snap adjusters back to default positions on release """
|
||||
self.lo_prv, self.hi_prv = self.values
|
||||
self.region_changed_enable = False # otherwise this affects the region again
|
||||
self.region.setRegion( (63, 191) )
|
||||
self.region_changed_enable = True
|
||||
self.sigLevelsChangeFinished.emit(self)
|
||||
|
||||
def _regionChanging(self):
|
||||
""" internal: recalculate levels based on new position of adjusters """
|
||||
if not self.region_changed_enable: return
|
||||
bot, top = self.region.getRegion()
|
||||
bot = ( (bot - 63) / 64 ) # -1 to +1 over half-bar range
|
||||
top = ( (top - 191) / 64 ) # -1 to +1 over half-bar range
|
||||
bot = math.copysign( bot**2, bot ) # quadratic behaviour for sensitivity to small changes
|
||||
top = math.copysign( top**2, top )
|
||||
# These are the new values if adjuster is released now, rate of change depends on original separation
|
||||
span_prv = self.hi_prv - self.lo_prv # previous span of values
|
||||
hi_new = self.hi_prv + (span_prv + 2*self.rounding) * top # make sure that we can always
|
||||
lo_new = self.lo_prv + (span_prv + 2*self.rounding) * bot # reach 2x the minimal step
|
||||
# Alternative model with speed depending on level magnitude:
|
||||
# mean_val = abs(self.lo_prv) + abs(self.hi_prv) / 2
|
||||
# hi_new = self.hi_prv + (mean_val + 2*self.rounding) * top # make sure that we can always
|
||||
# lo_new = self.lo_prv + (mean_val + 2*self.rounding) * bot # reach 2x the minimal step
|
||||
|
||||
if self.hi_lim is not None and hi_new > self.hi_lim: # limit maximum value
|
||||
# print('lim +')
|
||||
hi_new = self.hi_lim
|
||||
if lo_new > hi_new - span_prv: # avoid collapsing the span against top or bottom limits
|
||||
lo_new = hi_new - span_prv
|
||||
if self.lo_lim is not None and lo_new < self.lo_lim: # limit minimum value
|
||||
# print('lim -')
|
||||
lo_new = self.lo_lim
|
||||
if hi_new < lo_new + span_prv: # avoid collapsing the span against top or bottom limits
|
||||
hi_new = lo_new + span_prv
|
||||
if lo_new + self.rounding > hi_new: # do not allow less than one "rounding" unit of span
|
||||
# print('lim X')
|
||||
if bot == 0: hi_new = lo_new + self.rounding
|
||||
elif top == 0: lo_new = hi_new - self.rounding
|
||||
else:
|
||||
lo_new = (lo_new + hi_new - self.rounding) / 2
|
||||
hi_new = lo_new + self.rounding
|
||||
lo_new = self.rounding * round( lo_new/self.rounding )
|
||||
hi_new = self.rounding * round( hi_new/self.rounding )
|
||||
# if hi_new == lo_new: hi_new = lo_new + self.rounding # hack solution if axis span still collapses
|
||||
self.values = (lo_new, hi_new)
|
||||
self._update_items()
|
||||
self.sigLevelsChanged.emit(self)
|
@ -43,8 +43,9 @@ class LinearRegionItem(GraphicsObject):
|
||||
**Arguments:**
|
||||
values A list of the positions of the lines in the region. These are not
|
||||
limits; limits can be set by specifying bounds.
|
||||
orientation Options are 'vertical' or 'horizontal', indicating the
|
||||
The default is 'vertical', indicating that the
|
||||
orientation Options are 'vertical' or 'horizontal'
|
||||
The default is 'vertical', indicating that the region is bounded
|
||||
by vertical lines.
|
||||
brush Defines the brush that fills the region. Can be any arguments that
|
||||
are valid for :func:`mkBrush <pyqtgraph.mkBrush>`. Default is
|
||||
transparent blue.
|
||||
|
Loading…
Reference in New Issue
Block a user