import warnings
import numpy as np
from matplotlib.figure import Figure
from qtpy import QtWidgets, QtCore
from qtpy import QT_VERSION
from .warningtimer import WarningTimer, WarningTimerModel
qt_ver = int(QT_VERSION[0])
if qt_ver == 4:
from matplotlib.backends.backend_qt4agg import FigureCanvasQTAgg
elif qt_ver == 5:
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg
[docs]class HistogramModel(object):
"""Model for a Histogram which can apply cut levels to an image
Any View that utilizes this model must define the following methods:
``set_data``, ``change_cut_low``, ``change_cut_high``, ``change_cuts``,
``warn``, and ``change_bins``. The ``warn`` method must return a boolean
and if more than one view utilizes this model, you should consider only
one actually creating a warning box and return ``True`` while the others
just return ``False``.
Parameters
----------
image_view : :class:`ImageViewCanvas`
The image view canvas
cut_low ::obj:`float`
The lower cut level
cut_high : :obj:`float`
The higher cut level
bins : :obj:`int`
The number of bins the histogram uses
"""
def __init__(self, image_view, cut_low=None, cut_high=None, bins=100):
self._image_view = image_view
self._views = set()
self._cut_low = cut_low
self._cut_high = cut_high
self._bins = bins
@property
def image_view(self):
""":class:`ImageViewCanvas` The image view canvas
Setting the image view will reset the data
"""
return self._image_view
@image_view.setter
def image_view(self, image_view):
self._image_view = image_view
self.set_data()
@property
def cut_low(self):
""":obj:`float` The lower cut level
Setting the low cut value will adjust the cut values in the image view
and notify the views that the low cut value changed
"""
if self._cut_low is None:
self._cut_low = self.view_cuts[0]
return self._cut_low
@cut_low.setter
def cut_low(self, cut_low):
self._cut_low = cut_low
self._set_view_cuts()
self._change_cut_low()
@property
def cut_high(self):
""":obj:`float` The higher cut level
Setting the high cut value will adjust the cut values in the image view
and notify the views that the high cut value changed."""
if self._cut_high is None:
self._cut_high = self.view_cuts[1]
return self._cut_high
@cut_high.setter
def cut_high(self, cut_high):
self._cut_high = cut_high
self._set_view_cuts()
self._change_cut_high()
@property
def bins(self):
""":obj:`int` The number of bins the histogram uses
Setting the bins will notify the views that the bins have changed
"""
return self._bins
@bins.setter
def bins(self, bins):
if bins == self._bins:
pass
else:
self._bins = bins
self._change_bins()
@property
def cuts(self):
""":obj:`tuple` The lower and higher cut levels.
Setting the cuts will adjust the cut levels in the image viewer and
notify the views that the cuts have changed. The low cut must be
less than the high cut, otherwise they will be switched to satisfy
that condition.
"""
return self.cut_low, self.cut_high
@cuts.setter
def cuts(self, cuts):
cut_low, cut_high = cuts
if cut_low > cut_high:
message = (
"The low cut cannot be bigger than the high cut. " +
"Switching cuts.")
self.warn("Cut Warning", message)
cut_low, cut_high = cut_high, cut_low
diff_cut_low = cut_low != self.cut_low
diff_cut_high = cut_high != self.cut_high
if diff_cut_low and diff_cut_high:
self._cut_low, self._cut_high = cut_low, cut_high
self._set_view_cuts()
self._change_cuts()
elif diff_cut_low:
self.cut_low = cut_low
elif diff_cut_high:
self.cut_high = cut_high
@property
def view_cuts(self):
""":obj:`tuple` The image_view cut levels"""
cut_low, cut_high = self.image_view.get_cut_levels()
if cut_low > cut_high:
cut_low, cut_high = cut_high, cut_low
return cut_low, cut_high
@property
def data(self):
""":class:`ndarray` The current image data"""
return self.image_view.get_image().get_data()
[docs] def register(self, view):
"""Register a view with the model
Parameters
----------
view : :class:`QtWidgets.QWidget <PySide.QtGui.QWidget>`
A view that utilizes this model
"""
self._views.add(view)
[docs] def unregister(self, view):
"""Unregister a view with the model
Parameters
----------
view : :class:`QtWidgets.QWidget <PySide.QtGui.QWidget>`
A view that utilizes this model
"""
self._views.remove(view)
[docs] def set_data(self):
"""Set the data the histogram is to display"""
for view in self._views:
view.set_data()
[docs] def restore(self):
"""Restore the cut levels"""
cut_low, cut_high = self.view_cuts
self.cuts = cut_low, cut_high
[docs] def warn(self, title, message):
"""Display a warning box
Each view must define a ``warn`` method that returns a boolean value:
True when a warning box is displayed and False when a warning
box not displayed. Only one display box will be displayed. This is
because multiple views should not have different handling for the same
errors.
"""
warnings.warn(message)
for view in self._views:
warned = view.warn(title, message)
if warned:
break
def _set_view_cuts(self):
"""Set the image view cut levels"""
self.image_view.cut_levels(self.cut_low, self.cut_high)
def _change_cut_low(self):
"""Notfiy the views to that the low cut level was changed"""
for view in self._views:
view.change_cut_low()
def _change_cut_high(self):
"""Notify the views the high cut level was changed"""
for view in self._views:
view.change_cut_high()
def _change_cuts(self):
"""Notify the views the cut levels were changed"""
for view in self._views:
view.change_cuts()
def _change_bins(self):
"""Notify the views the number of bins were changed"""
for view in self._views:
view.change_bins()
[docs]class HistogramController(object):
"""Controller for histogram views
Parameters
----------
model : :class:`HistogramModel`
histogram model
view : :class:`object`
View with :class:`HistogramModel` as its model
Attributes
----------
model : :class:`HistogramModel`
histogram model
view : :class:`object`
View with :class:`HistogramModel` as its model
"""
def __init__(self, model, view):
self.model = model
self.view = view
[docs] def set_cut_low(self, cut_low):
"""Set the low cut level to a new value
Parameters
----------
cut_low : :obj:`float`
New low cut value
"""
self.model.cut_low = cut_low
[docs] def set_cut_high(self, cut_high):
"""Set the high cut level to a new value
Parameters
----------
cut_high : :obj:`float`
New high cut value
"""
self.model.cut_high = cut_high
[docs] def set_cuts(self, cut_low, cut_high):
"""Set both the low and high cut levels
Parameters
----------
cut_low : :obj:`float`
New low cut value
cut_high : :obj:`float`
New high cut value
"""
self.model.cuts = cut_low, cut_high
[docs] def set_bins(self, bins):
"""Change the number of bins the histogram uses
Parameters
----------
bins : :obj:`int`
The number number of bins for the histogram
"""
self.model.bins = bins
[docs] def restore(self):
"""Restore the histogram"""
self.model.restore()
[docs]class Histogram(FigureCanvasQTAgg):
"""The Histogram View
Parameters
----------
model : :class:`HistogramModel`
The view's model
Attributes
----------
model : :class:`HistogramModel`
The view's model
controller : :class:`HistogramController`
The view's controller
"""
def __init__(self, model):
fig = Figure(figsize=(2, 2), dpi=100)
fig.subplots_adjust(
left=0.0, right=1.0, top=1.0, bottom=0.0, wspace=0.0,
hspace=0.0)
super(Histogram, self).__init__(fig)
self.model = model
self.model.register(self)
self.controller = HistogramController(self.model, self)
self._figure = fig
policy = self.sizePolicy()
policy.setHeightForWidth(True)
self.setSizePolicy(policy)
self.setMinimumSize(self.size())
self._ax = fig.add_subplot(111)
self._ax.set_facecolor('black')
self._left_vline = None
self._right_vline = None
[docs] def change_cut_low(self, draw=True):
"""Change the position of the left line to the low cut level"""
if self._left_vline is None:
return
self._left_vline.set_xdata([self.model.cut_low, self.model.cut_low])
if draw:
self.draw()
[docs] def change_cut_high(self, draw=True):
"""Change the position of the right line to the high cut level"""
if self._right_vline is None:
return
self._right_vline.set_xdata([self.model.cut_high, self.model.cut_high])
if draw:
self.draw()
[docs] def change_cuts(self):
"""Change the position of the left & right lines to respective cuts"""
self.change_cut_low(draw=False)
self.change_cut_high(draw=False)
self.draw()
[docs] def change_bins(self):
"""Adjust the number of bins without adjusting the lines"""
self.set_data(False)
[docs] def set_data(self, reset_vlines=True):
"""Set the histogram's data
Parameters
----------
reset_vlines : :obj:`bool`
Reset the vertical lines to the default cut levels if True,
otherwise False. True by default
"""
self._ax.cla()
self._left_vline = None
self._right_vline = None
self._ax.hist(
self.model.data.flatten(), self.model.bins, color='white')
self._set_vlines(reset_vlines)
self.draw()
def _move_line(self, event):
# The left mouse button must be down to adjust the cut levels
if not event.inaxes or event.button != 1:
return
x = event.xdata
cut_low, cut_high = self.model.cuts
# Adjust the line that is closer to the point
if np.abs(x - self.model.cut_low) < np.abs(x - self.model.cut_high):
self.controller.set_cut_low(x)
else:
self.controller.set_cut_high(x)
def _set_vlines(self, reset=True):
if reset:
self.model.restore()
cut_low, cut_high = self.model.cuts
self._left_vline = self._ax.axvline(
cut_low, color='r', linewidth=2)
self._right_vline = self._ax.axvline(
cut_high, color='r', linewidth=2)
self._figure.canvas.mpl_connect('motion_notify_event', self._move_line)
self._figure.canvas.mpl_connect('button_press_event', self._move_line)
def warn(self, title, message):
return False