~ubuntu-branches/ubuntu/oneiric/guiqwt/oneiric

« back to all changes in this revision

Viewing changes to guiqwt/histogram.py

  • Committer: Bazaar Package Importer
  • Author(s): Picca Frédéric-Emmanuel
  • Date: 2010-11-13 11:26:05 UTC
  • Revision ID: james.westby@ubuntu.com-20101113112605-k2ffx4p80rict966
Tags: upstream-2.0.8.1
Import upstream version 2.0.8.1

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# -*- coding: utf-8 -*-
 
2
#
 
3
# Copyright © 2009-2010 CEA
 
4
# Pierre Raybaut
 
5
# Licensed under the terms of the CECILL License
 
6
# (see guiqwt/__init__.py for details)
 
7
 
 
8
"""
 
9
guiqwt.histogram
 
10
----------------
 
11
 
 
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 
 
15
      adjustment panel`
 
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
 
19
 
 
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`.
 
23
 
 
24
Example
 
25
~~~~~~~
 
26
 
 
27
Simple histogram plotting example:
 
28
 
 
29
.. literalinclude:: ../guiqwt/tests/histogram.py
 
30
 
 
31
Reference
 
32
~~~~~~~~~
 
33
 
 
34
.. autoclass:: HistogramItem
 
35
   :members:
 
36
   :inherited-members:
 
37
.. autoclass:: ContrastAdjustment
 
38
   :members:
 
39
   :inherited-members:
 
40
.. autoclass:: LevelsHistogram
 
41
   :members:
 
42
   :inherited-members:
 
43
"""
 
44
import numpy as np
 
45
from PyQt4.QtCore import Qt
 
46
from PyQt4.QtGui import QHBoxLayout, QVBoxLayout, QToolBar
 
47
from PyQt4.Qwt5 import QwtPlotCurve
 
48
 
 
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
 
54
 
 
55
# Local imports
 
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,
 
65
                          AntiAliasingTool)
 
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
 
69
 
 
70
 
 
71
class HistDataSource(object):
 
72
    """
 
73
    An objects that provides an Histogram data source interface
 
74
    to a simple numpy array of data
 
75
    """
 
76
    __implements__ = (IHistDataSource,)
 
77
    def __init__(self, data):
 
78
        self.data = data
 
79
 
 
80
    def get_histogram(self, nbins):
 
81
        """Returns the histogram computed for nbins bins"""
 
82
        return np.histogram(self.data, nbins)
 
83
 
 
84
assert_interfaces_valid(HistDataSource)
 
85
 
 
86
 
 
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]
 
94
 
 
95
 
 
96
class HistogramItem(CurveItem):
 
97
    """A Qwt item representing histogram data"""
 
98
    __implements__ = (IBasePlotItem,)
 
99
    
 
100
    def __init__(self, curveparam=None, histparam=None):
 
101
        self.hist_count = None
 
102
        self.hist_bins = None
 
103
        self.bins = None
 
104
        self.old_bins = None
 
105
        self.source = None
 
106
        self.logscale = 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')
 
115
        else:
 
116
            self.histparam = histparam
 
117
        CurveItem.__init__(self, curveparam)
 
118
        self.setCurveAttribute(QwtPlotCurve.Inverted)
 
119
            
 
120
    def set_hist_source(self, src):
 
121
        """
 
122
        Set histogram source
 
123
        (source: object with method 'get_histogram',
 
124
         e.g. objects derived from guiqwt.image.ImageItem)
 
125
        """
 
126
        self.source = src
 
127
        self.update_histogram()
 
128
        
 
129
    def set_hist_data(self, data):
 
130
        """Set histogram data"""
 
131
        self.set_hist_source(HistDataSource(data))        
 
132
 
 
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()
 
138
        
 
139
    def get_logscale(self):
 
140
        """Returns the status of the scale"""
 
141
        return self.logscale
 
142
        
 
143
    def set_remove_first_bin(self, state):
 
144
        self.remove_first_bin = state
 
145
        self.update_histogram()
 
146
        
 
147
    def get_remove_first_bin(self):
 
148
        return self.remove_first_bin
 
149
 
 
150
    def set_bins(self, n_bins):
 
151
        self.bins = n_bins
 
152
        self.update_histogram()
 
153
        
 
154
    def get_bins(self):
 
155
        return self.bins
 
156
        
 
157
    def compute_histogram(self):
 
158
        return self.source.get_histogram(self.bins)
 
159
        
 
160
    def update_histogram(self):
 
161
        if self.source is None:
 
162
            return
 
163
        hist, bin_edges = self.compute_histogram()
 
164
        hist = np.concatenate((hist, [0]))
 
165
        if self.remove_first_bin:
 
166
            hist[0] = 0
 
167
        if self.logscale:
 
168
            hist = np.log(hist+1)
 
169
 
 
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:
 
173
            if self.plot():
 
174
                self.plot().do_autoscale()
 
175
        self.old_bins = self.bins
 
176
        self.old_logscale = self.logscale
 
177
        
 
178
        plot = self.plot()
 
179
        if plot is not None:
 
180
            plot.do_autoscale(replot=True)
 
181
 
 
182
    def update_params(self):
 
183
        self.histparam.update_hist(self)
 
184
        super(HistogramItem, self).update_params()
 
185
 
 
186
    def get_item_parameters(self, itemparams):
 
187
        super(HistogramItem, self).get_item_parameters(itemparams)
 
188
        itemparams.add("HistogramParam", self, self.histparam)
 
189
    
 
190
    def set_item_parameters(self, itemparams):
 
191
        update_dataset(self.histparam, itemparams.get("HistogramParam"),
 
192
                       visible_only=True)
 
193
        self.histparam.update_hist(self)
 
194
        super(HistogramItem, self).set_item_parameters(itemparams)
 
195
 
 
196
assert_interfaces_valid(HistogramItem)
 
197
 
 
198
 
 
199
class LevelsHistogram(CurvePlot):
 
200
    """Image levels histogram widget"""
 
201
    def __init__(self, parent=None):
 
202
        super(LevelsHistogram, self).__init__(parent=parent, title="",
 
203
                                              section="histogram")
 
204
        self.antialiased = False
 
205
 
 
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")
 
210
        
 
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
 
215
 
 
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")
 
220
        
 
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)
 
224
 
 
225
        self.setMinimumHeight(80)
 
226
        self.setAxisMaxMajor(self.yLeft, 5)
 
227
        self.setAxisMaxMinor(self.yLeft, 0)
 
228
 
 
229
        if parent is None:
 
230
            self.set_axis_title('bottom', 'Levels')
 
231
 
 
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)
 
236
            return
 
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)
 
240
        
 
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()
 
248
 
 
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
 
253
 
 
254
    def __del_known_items(self, known_items, items):
 
255
        del_curves = []
 
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)
 
261
 
 
262
    def selection_changed(self, plot):
 
263
        items = plot.get_selected_items(IVoiImageItemType)
 
264
        known_items = self._tracked_items.setdefault(plot, {})
 
265
 
 
266
        if items:
 
267
            self.__del_known_items(known_items, items)
 
268
            if len(items) == 1:
 
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, [])
 
275
        else:
 
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, [])
 
281
                    break
 
282
                
 
283
        for item in 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
 
289
 
 
290
        nb_selected = len(list(self.tracked_items_gen()))
 
291
        if not nb_selected:
 
292
            self.replot()
 
293
            return
 
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)
 
298
 
 
299
        self.active_item_changed(plot)
 
300
 
 
301
    def active_item_changed(self, plot):
 
302
        items = plot.get_selected_items(IVoiImageItemType)
 
303
        if not items:
 
304
            #XXX: workaround
 
305
            return
 
306
            
 
307
        active = plot.get_last_active_item(IVoiImageItemType)
 
308
        if active:
 
309
            active_range = active.get_lut_range()
 
310
        else:
 
311
            active_range = None
 
312
        
 
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)
 
321
        self.replot()
 
322
    
 
323
    def set_range_style(self, multiple_ranges):
 
324
        if multiple_ranges:
 
325
            self.range.shapeparam.sel_line.color = self.range_multi_color
 
326
        else:
 
327
            self.range.shapeparam.sel_line.color = self.range_mono_color
 
328
        self.range.shapeparam.update_range(self.range)
 
329
 
 
330
    def set_range(self, _min, _max):
 
331
        if _min < _max:
 
332
            self.set_range_style(False)
 
333
            self.range.set_range(_min, _max)
 
334
            self.replot()
 
335
            return True
 
336
        else:
 
337
            # Range was not changed
 
338
            return False
 
339
 
 
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)
 
344
        
 
345
    def set_full_range(self):
 
346
        """Set range bounds to image min/max levels"""
 
347
        _min = _max = None
 
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:
 
351
                _min = imin
 
352
            if _max is None or _max<imax:
 
353
                _max = imax
 
354
        if _min is not None:
 
355
            self.set_range(_min, _max)
 
356
 
 
357
    def apply_min_func(self, item, curve, min):
 
358
        _min, _max = item.get_lut_range()
 
359
        return min, _max
 
360
 
 
361
    def apply_max_func(self, item, curve, max):
 
362
        _min, _max = item.get_lut_range()
 
363
        return _min, max
 
364
 
 
365
    def reduce_range_func(self, item, curve, percent):
 
366
        return lut_range_threshold(item, curve.bins, percent)
 
367
        
 
368
    def apply_range_function(self, func, *args, **kwargs):
 
369
        item = None
 
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)
 
374
        if item is not None:
 
375
            self.active_item_changed(item.plot())
 
376
        
 
377
    def eliminate_outliers(self, percent):
 
378
        """
 
379
        Eliminate outliers:
 
380
        eliminate percent/2*N counts on each side of the histogram
 
381
        (where N is the total count number)
 
382
        """
 
383
        self.apply_range_function(self.reduce_range_func, percent)
 
384
        
 
385
    def set_min(self, _min):
 
386
        self.apply_range_function(self.apply_min_func, _min)
 
387
    
 
388
    def set_max(self, _max):
 
389
        self.apply_range_function(self.apply_max_func, _max)
 
390
        
 
391
 
 
392
class EliminateOutliersParam(DataSet):
 
393
    percent = FloatItem(_("Eliminate outliers")+" (%)",
 
394
                        default=2., min=0., max=100.-1e-6)
 
395
 
 
396
 
 
397
class ContrastAdjustment(PanelWidget):
 
398
    """Contrast adjustment tool"""
 
399
    __implements__ = (IPanel,)
 
400
    PANEL_ID = ID_CONTRAST
 
401
 
 
402
    def __init__(self, parent=None):
 
403
        super(ContrastAdjustment, self).__init__(parent)
 
404
        widget_title = _("Contrast adjustment tool")
 
405
        widget_icon = "contrast.png"
 
406
        
 
407
        self.local_manager = None # local manager for the histogram plot
 
408
        self.manager = None # manager for the associated image plot
 
409
        
 
410
        # Storing min/max markers for each active image
 
411
        self.min_markers = {}
 
412
        self.max_markers = {}
 
413
        
 
414
        # Select point tools
 
415
        self.min_select_tool = None
 
416
        self.max_select_tool = None
 
417
        
 
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)
 
432
        
 
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)
 
440
        
 
441
        self.outliers_param = EliminateOutliersParam(widget_title)
 
442
        
 
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)
 
449
        self.setup_actions()
 
450
        for plot in manager.get_plots():
 
451
            self.histogram.connect_plot(plot)
 
452
 
 
453
    def get_plot(self):
 
454
        return self.manager.get_active_plot()
 
455
 
 
456
    def closeEvent(self, event):
 
457
        self.hide()
 
458
        event.ignore()
 
459
        
 
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)        
 
485
    
 
486
    def eliminate_outliers(self):
 
487
        def apply(param):
 
488
            self.histogram.eliminate_outliers(param.percent)
 
489
        if self.outliers_param.edit(self, apply=apply):
 
490
            apply(self.outliers_param)
 
491
 
 
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)
 
497
 
 
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)
 
503
        
 
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())
 
509
 
 
510
assert_interfaces_valid(ContrastAdjustment)