1
# -*- coding: utf-8 -*-
3
# Copyright © 2009-2010 CEA
5
# Licensed under the terms of the CECILL License
6
# (see guiqwt/__init__.py for details)
12
The `histogram` module provides histogram related objects:
13
* :py:class:`guiqwt.histogram.HistogramItem`: an histogram plot item
14
* :py:class:`guiqwt.histogram.ContrastAdjustment`: the `contrast
16
* :py:class:`guiqwt.histogram.LevelsHistogram`: a curve plotting widget
17
used by the `contrast adjustment panel` to compute, manipulate and
18
display the image levels histogram
20
``HistogramItem`` objects are plot items (derived from QwtPlotItem) that may
21
be displayed on a 2D plotting widget like :py:class:`guiqwt.curve.CurvePlot`
22
or :py:class:`guiqwt.image.ImagePlot`.
27
Simple histogram plotting example:
29
.. literalinclude:: ../guiqwt/tests/histogram.py
34
.. autoclass:: HistogramItem
37
.. autoclass:: ContrastAdjustment
40
.. autoclass:: LevelsHistogram
45
from PyQt4.QtCore import Qt
46
from PyQt4.QtGui import QHBoxLayout, QVBoxLayout, QToolBar
47
from PyQt4.Qwt5 import QwtPlotCurve
49
from guidata.dataset.datatypes import DataSet
50
from guidata.dataset.dataitems import FloatItem
51
from guidata.utils import assert_interfaces_valid, update_dataset
52
from guidata.configtools import get_icon, get_image_layout
53
from guidata.qthelpers import add_actions, create_action
56
from guiqwt.config import CONF, _
57
from guiqwt.interfaces import (IBasePlotItem, IHistDataSource,
58
IVoiImageItemType, IPanel)
59
from guiqwt.panels import PanelWidget, ID_CONTRAST
60
from guiqwt.curve import CurveItem, CurvePlot
61
from guiqwt.image import ImagePlot
62
from guiqwt.styles import HistogramParam, CurveParam
63
from guiqwt.shapes import XRangeSelection
64
from guiqwt.tools import (SelectTool, BasePlotMenuTool, SelectPointTool,
66
from guiqwt.signals import (SIG_RANGE_CHANGED, SIG_VOI_CHANGED,
67
SIG_ITEM_SELECTION_CHANGED, SIG_ACTIVE_ITEM_CHANGED)
68
from guiqwt.plot import PlotManager
71
class HistDataSource(object):
73
An objects that provides an Histogram data source interface
74
to a simple numpy array of data
76
__implements__ = (IHistDataSource,)
77
def __init__(self, data):
80
def get_histogram(self, nbins):
81
"""Returns the histogram computed for nbins bins"""
82
return np.histogram(self.data, nbins)
84
assert_interfaces_valid(HistDataSource)
87
def lut_range_threshold(item, bins, percent):
88
hist, bin_edges = item.get_histogram(bins)
89
hist = np.concatenate((hist, [0]))
90
threshold = .5*percent/100*hist.sum()
91
i_bin_min = np.cumsum(hist).searchsorted(threshold)
92
i_bin_max = -1-np.cumsum(np.flipud(hist)).searchsorted(threshold)
93
return bin_edges[i_bin_min], bin_edges[i_bin_max]
96
class HistogramItem(CurveItem):
97
"""A Qwt item representing histogram data"""
98
__implements__ = (IBasePlotItem,)
100
def __init__(self, curveparam=None, histparam=None):
101
self.hist_count = None
102
self.hist_bins = None
107
self.remove_first_bin = None
108
self.old_logscale = None
109
if curveparam is None:
110
curveparam = CurveParam(_("Curve"), icon='curve.png')
111
curveparam.curvestyle = "Steps"
112
if histparam is None:
113
self.histparam = HistogramParam(title=_("Histogram"),
114
icon='histogram.png')
116
self.histparam = histparam
117
CurveItem.__init__(self, curveparam)
118
self.setCurveAttribute(QwtPlotCurve.Inverted)
120
def set_hist_source(self, src):
123
(source: object with method 'get_histogram',
124
e.g. objects derived from guiqwt.image.ImageItem)
127
self.update_histogram()
129
def set_hist_data(self, data):
130
"""Set histogram data"""
131
self.set_hist_source(HistDataSource(data))
133
def set_logscale(self, state):
134
"""Sets whether we use a logarithm or linear scale
135
for the histogram counts"""
136
self.logscale = state
137
self.update_histogram()
139
def get_logscale(self):
140
"""Returns the status of the scale"""
143
def set_remove_first_bin(self, state):
144
self.remove_first_bin = state
145
self.update_histogram()
147
def get_remove_first_bin(self):
148
return self.remove_first_bin
150
def set_bins(self, n_bins):
152
self.update_histogram()
157
def compute_histogram(self):
158
return self.source.get_histogram(self.bins)
160
def update_histogram(self):
161
if self.source is None:
163
hist, bin_edges = self.compute_histogram()
164
hist = np.concatenate((hist, [0]))
165
if self.remove_first_bin:
168
hist = np.log(hist+1)
170
self.set_data(bin_edges, hist)
171
# Autoscale only if logscale/bins have changed
172
if self.bins != self.old_bins or self.logscale != self.old_logscale:
174
self.plot().do_autoscale()
175
self.old_bins = self.bins
176
self.old_logscale = self.logscale
180
plot.do_autoscale(replot=True)
182
def update_params(self):
183
self.histparam.update_hist(self)
184
super(HistogramItem, self).update_params()
186
def get_item_parameters(self, itemparams):
187
super(HistogramItem, self).get_item_parameters(itemparams)
188
itemparams.add("HistogramParam", self, self.histparam)
190
def set_item_parameters(self, itemparams):
191
update_dataset(self.histparam, itemparams.get("HistogramParam"),
193
self.histparam.update_hist(self)
194
super(HistogramItem, self).set_item_parameters(itemparams)
196
assert_interfaces_valid(HistogramItem)
199
class LevelsHistogram(CurvePlot):
200
"""Image levels histogram widget"""
201
def __init__(self, parent=None):
202
super(LevelsHistogram, self).__init__(parent=parent, title="",
204
self.antialiased = False
206
# a dict of dict : plot -> selected items -> HistogramItem
207
self._tracked_items = {}
208
self.curveparam = CurveParam(_("Curve"), icon="curve.png")
209
self.curveparam.read_config(CONF, "histogram", "curve")
211
self.histparam = HistogramParam(_("Histogram"), icon="histogram.png")
212
self.histparam.logscale = False
213
self.histparam.remove_first_bin = True
214
self.histparam.n_bins = 256
216
self.range = XRangeSelection(0, 1)
217
self.range_mono_color = self.range.shapeparam.sel_line.color
218
self.range_multi_color = CONF.get("histogram",
219
"range/multi/color", "red")
221
self.add_item(self.range, z=5)
222
self.connect(self, SIG_RANGE_CHANGED, self.range_changed)
223
self.set_active_item(self.range)
225
self.setMinimumHeight(80)
226
self.setAxisMaxMajor(self.yLeft, 5)
227
self.setAxisMaxMinor(self.yLeft, 0)
230
self.set_axis_title('bottom', 'Levels')
232
def connect_plot(self, plot):
233
if not isinstance(plot, ImagePlot):
234
# Connecting only to image plot widgets (allow mixing image and
235
# curve widgets for the same plot manager -- e.g. in pyplot)
237
self.connect(self, SIG_VOI_CHANGED, plot.notify_colormap_changed)
238
self.connect(plot, SIG_ITEM_SELECTION_CHANGED, self.selection_changed)
239
self.connect(plot, SIG_ACTIVE_ITEM_CHANGED, self.active_item_changed)
241
def standard_tools(self, manager):
242
manager.add_tool(SelectTool)
243
manager.add_tool(BasePlotMenuTool, "item")
244
manager.add_tool(BasePlotMenuTool, "axes")
245
manager.add_tool(BasePlotMenuTool, "grid")
246
manager.add_tool(AntiAliasingTool)
247
manager.get_default_tool().activate()
249
def tracked_items_gen(self):
250
for plot, items in self._tracked_items.items():
251
for item in items.items():
252
yield item # tuple item,curve
254
def __del_known_items(self, known_items, items):
256
for item in known_items.keys():
257
if item not in items:
258
curve = known_items.pop(item)
259
del_curves.append(curve)
260
self.del_items(del_curves)
262
def selection_changed(self, plot):
263
items = plot.get_selected_items(IVoiImageItemType)
264
known_items = self._tracked_items.setdefault(plot, {})
267
self.__del_known_items(known_items, items)
269
# Removing any cached item for other plots
270
for other_plot, _items in self._tracked_items.items():
271
if other_plot is not plot:
272
if not other_plot.get_selected_items(IVoiImageItemType):
273
other_known_items = self._tracked_items[other_plot]
274
self.__del_known_items(other_known_items, [])
276
# if all items are deselected we keep the last known
277
# selection (for one plot only)
278
for other_plot, _items in self._tracked_items.items():
279
if other_plot.get_selected_items(IVoiImageItemType):
280
self.__del_known_items(known_items, [])
284
if item not in known_items:
285
curve = HistogramItem(self.curveparam, self.histparam)
286
curve.set_hist_source(item)
287
self.add_item(curve, z=0)
288
known_items[item] = curve
290
nb_selected = len(list(self.tracked_items_gen()))
294
self.curveparam.shade = 1.0/nb_selected
295
for item, curve in self.tracked_items_gen():
296
self.curveparam.update_curve(curve)
297
self.histparam.update_hist(curve)
299
self.active_item_changed(plot)
301
def active_item_changed(self, plot):
302
items = plot.get_selected_items(IVoiImageItemType)
307
active = plot.get_last_active_item(IVoiImageItemType)
309
active_range = active.get_lut_range()
313
multiple_ranges = False
314
for item, curve in self.tracked_items_gen():
315
if active_range != item.get_lut_range():
316
multiple_ranges = True
317
if active_range is not None:
318
_m, _M = active_range
319
self.set_range_style(multiple_ranges)
320
self.range.set_range(_m, _M, dosignal=False)
323
def set_range_style(self, multiple_ranges):
325
self.range.shapeparam.sel_line.color = self.range_multi_color
327
self.range.shapeparam.sel_line.color = self.range_mono_color
328
self.range.shapeparam.update_range(self.range)
330
def set_range(self, _min, _max):
332
self.set_range_style(False)
333
self.range.set_range(_min, _max)
337
# Range was not changed
340
def range_changed(self, _rangesel, _min, _max):
341
for item, curve in self.tracked_items_gen():
342
item.set_lut_range([_min, _max])
343
self.emit(SIG_VOI_CHANGED)
345
def set_full_range(self):
346
"""Set range bounds to image min/max levels"""
348
for item, curve in self.tracked_items_gen():
349
imin, imax = item.get_lut_range_full()
350
if _min is None or _min>imin:
352
if _max is None or _max<imax:
355
self.set_range(_min, _max)
357
def apply_min_func(self, item, curve, min):
358
_min, _max = item.get_lut_range()
361
def apply_max_func(self, item, curve, max):
362
_min, _max = item.get_lut_range()
365
def reduce_range_func(self, item, curve, percent):
366
return lut_range_threshold(item, curve.bins, percent)
368
def apply_range_function(self, func, *args, **kwargs):
370
for item, curve in self.tracked_items_gen():
371
_min, _max = func(item, curve, *args, **kwargs)
372
item.set_lut_range([_min, _max])
373
self.emit(SIG_VOI_CHANGED)
375
self.active_item_changed(item.plot())
377
def eliminate_outliers(self, percent):
380
eliminate percent/2*N counts on each side of the histogram
381
(where N is the total count number)
383
self.apply_range_function(self.reduce_range_func, percent)
385
def set_min(self, _min):
386
self.apply_range_function(self.apply_min_func, _min)
388
def set_max(self, _max):
389
self.apply_range_function(self.apply_max_func, _max)
392
class EliminateOutliersParam(DataSet):
393
percent = FloatItem(_("Eliminate outliers")+" (%)",
394
default=2., min=0., max=100.-1e-6)
397
class ContrastAdjustment(PanelWidget):
398
"""Contrast adjustment tool"""
399
__implements__ = (IPanel,)
400
PANEL_ID = ID_CONTRAST
402
def __init__(self, parent=None):
403
super(ContrastAdjustment, self).__init__(parent)
404
widget_title = _("Contrast adjustment tool")
405
widget_icon = "contrast.png"
407
self.local_manager = None # local manager for the histogram plot
408
self.manager = None # manager for the associated image plot
410
# Storing min/max markers for each active image
411
self.min_markers = {}
412
self.max_markers = {}
415
self.min_select_tool = None
416
self.max_select_tool = None
418
style = "<span style=\'color: #444444\'><b>%s</b></span>"
419
layout, _label = get_image_layout(widget_icon,
420
style % widget_title,
421
alignment=Qt.AlignCenter)
422
layout.setAlignment(Qt.AlignCenter)
423
vlayout = QVBoxLayout()
424
vlayout.addLayout(layout)
425
self.local_manager = PlotManager(self)
426
self.histogram = LevelsHistogram(parent)
427
vlayout.addWidget(self.histogram)
428
self.local_manager.add_plot(self.histogram)
429
hlayout = QHBoxLayout()
430
self.setLayout(hlayout)
431
hlayout.addLayout(vlayout)
433
self.toolbar = toolbar = QToolBar(self)
434
toolbar.setOrientation(Qt.Vertical)
435
# toolbar.setToolButtonStyle(Qt.ToolButtonTextBesideIcon)
436
hlayout.addWidget(toolbar)
437
self.histogram.standard_tools(self.local_manager)
438
self.setWindowIcon(get_icon(widget_icon))
439
self.setWindowTitle(widget_title)
441
self.outliers_param = EliminateOutliersParam(widget_title)
443
def register_panel(self, manager):
444
"""Register panel to plot manager"""
445
self.manager = manager
446
default_toolbar = self.manager.get_default_toolbar()
447
self.manager.add_toolbar(self.toolbar, "contrast")
448
self.manager.set_default_toolbar(default_toolbar)
450
for plot in manager.get_plots():
451
self.histogram.connect_plot(plot)
454
return self.manager.get_active_plot()
456
def closeEvent(self, event):
460
def setup_actions(self):
461
fullrange_ac = create_action(self, _("Full range"),
462
icon=get_icon("full_range.png"),
463
triggered=self.histogram.set_full_range,
464
tip=_("Scale the image's display range "
465
"according to data range") )
466
autorange_ac = create_action(self, _("Eliminate outliers"),
467
icon=get_icon("eliminate_outliers.png"),
468
triggered=self.eliminate_outliers,
469
tip=_("Eliminate levels histogram "
470
"outliers and scale the image's "
471
"display range accordingly") )
472
add_actions(self.toolbar,[fullrange_ac, autorange_ac])
473
self.min_select_tool = self.manager.add_tool(SelectPointTool,
474
title=_("Minimum level"),
475
on_active_item=True,mode="create",
476
tip=_("Select minimum level on image"),
477
toolbar_id="contrast",
478
end_callback=self.apply_min_selection)
479
self.max_select_tool = self.manager.add_tool(SelectPointTool,
480
title=_("Maximum level"),
481
on_active_item=True,mode="create",
482
tip=_("Select maximum level on image"),
483
toolbar_id="contrast",
484
end_callback=self.apply_max_selection)
486
def eliminate_outliers(self):
488
self.histogram.eliminate_outliers(param.percent)
489
if self.outliers_param.edit(self, apply=apply):
490
apply(self.outliers_param)
492
def apply_min_selection(self, tool):
493
item = self.get_plot().get_last_active_item(IVoiImageItemType)
494
point = self.min_select_tool.get_coordinates()
495
z = item.get_data(*point)
496
self.histogram.set_min(z)
498
def apply_max_selection(self, tool):
499
item = self.get_plot().get_last_active_item(IVoiImageItemType)
500
point = self.max_select_tool.get_coordinates()
501
z = item.get_data(*point)
502
self.histogram.set_max(z)
504
def set_range(self, _min, _max):
505
"""Set contrast panel's histogram range"""
506
self.histogram.set_range(_min, _max)
507
# Update the levels histogram in case active item data has changed:
508
self.histogram.selection_changed(self.get_plot())
510
assert_interfaces_valid(ContrastAdjustment)