Commit 1b5aee93 authored by Damien Naudet's avatar Damien Naudet
Browse files

Added temporary ImageRois.py file from silx dev branch until it is released.

parent 813b9c01
# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2004-2016 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
# ###########################################################################*/
"""Roi items."""
from collections import OrderedDict, namedtuple
import numpy as np
from silx.gui import qt, icons
__author__ = ["D. Naudet"]
__license__ = "MIT"
__date__ = "01/09/2016"
_registeredRoiItems = OrderedDict()
def RoiItemClassDef(roiName, shape,
actionToolTip=None,
actionIcon=None,
actionText=None):
def inner(cls):
cls.roiName = roiName
cls.shape = shape
cls.actionToolTip = actionToolTip
cls.actionIcon = actionIcon
cls.actionText = actionText
registerRoiItem(cls)
return cls
return inner
_RoiData = namedtuple('RoiData', ['x', 'y', 'shape'])
""" Named tuple used to return a ROI's x and y data """
def registerRoiItem(klass):
global _registeredRoiItems
roiName = klass.roiName
if roiName is None:
raise AttributeError('Failed to register Roi class {0} roiName '
'attribute is None.'.format(klass.__name__))
# TODO : some kind of checks
if roiName in _registeredRoiItems:
raise ValueError('Cannot register roi class "{0}" :'
' a ROI with the same roiName already exists.'
''.format(roiName))
# TODO : some kind of checks on the klass
_registeredRoiItems[roiName] = klass
class RoiItemBase(qt.QObject):
sigRoiDrawingStarted = qt.Signal(str)
sigRoiDrawingFinished = qt.Signal(object)
sigRoiDrawingCanceled = qt.Signal(str)
sigRoiMoved = qt.Signal(object)
roiName = None
shape = None
actionIcon = None
actionText = None
actionToolTip = None
plot = property(lambda self: self._plot)
name = property(lambda self: self._name)
def __init__(self, plot, parent, name=None):
super(RoiItemBase, self).__init__(parent=parent)
self._manager = parent
self._plot = plot
self._handles = []
self._items = []
self._points = {}
self._kwargs = []
self._finished = False
self._startNotified = False
self._connected = False
self._editing = False
self._visible = True
self._xData = []
self._yData = []
if not name:
uuid = str(id(self))
name = '{0}_{1}'.format(self.__class__.__name__, uuid)
self._name = name
def setVisible(self, visible):
changed = self._visible == visible
self._visible = visible
if not visible:
self._disconnect()
self._remove()
else:
self._connect()
self._draw(drawHandles=self._editing)
if changed:
self._visibilityChanged(visible)
def _remove(self, handles=True, shape=True):
if handles:
{self._plot.removeMarker(item) for item in self._handles}
if shape:
self._plot.removeItem(self._name)
{self._plot.removeItem(item) for item in self._items}
def _interactiveModeChanged(self, source):
if source is not self or source is not self.parent():
self.stop()
def _plotSignal(self, event):
evType = event['event']
if (evType == 'drawingFinished' and
event['parameters']['label'] == self._name):
self._finish(event)
elif (evType == 'drawingProgress' and
event['parameters']['label'] == self._name):
if not self._startNotified:
self._drawStarted()
# TODO : this is a temporary workaround
# until we can get a mouse click event
self.sigRoiDrawingStarted.emit(self.name)
self._startNotified = True
self._drawEvent(event)
self._emitDataEvent(self.sigRoiMoved)
elif evType == 'markerMoving':
label = event['label']
try:
idx = self._handles.index(label)
except ValueError:
idx = None
else:
x = event['x']
y = event['y']
self._setHandleData(label, (x, y))
self._handleMoved(label, x, y, idx)
self._emitDataEvent(self.sigRoiMoved)
self._draw()
def _registerHandle(self, handle, point, idx=-1):
if handle in self._handles:
return
if idx is not None and idx >= 0 and idx < len(self._handles):
self._handles.insert(handle, idx)
else:
self._handles.append(handle)
idx = len(self._handles)
self._points[handle] = point
return idx
def _unregisterHandle(self, label):
try:
self._handles.remove(label)
except ValueError:
pass
def _registerItem(self, legend):
if legend in self._items:
raise ValueError('Item {0} is already registered.'
''.format(legend))
self._items.append(legend)
def _unregisterItem(self, legend):
try:
self._items.remove(legend)
except ValueError:
pass
def _connect(self):
if self._connected:
return
self._plot.sigPlotSignal.connect(self._plotSignal)
self._plot.sigInteractiveModeChanged.connect(
self._interactiveModeChanged)
self._connected = True
def _disconnect(self):
if not self._connected:
return
self._plot.sigPlotSignal.disconnect(self._plotSignal)
self._plot.sigInteractiveModeChanged.disconnect(
self._interactiveModeChanged)
self._connected = False
def _draw(self, drawHandles=True, excludes=()):
if drawHandles:
if excludes is not None and len(excludes) > 0:
draw_legends = set(self._handles) - set(excludes)
else:
draw_legends = self._handles
self._drawHandles(draw_legends)
self._drawShape()
def _drawHandles(self, handles):
for i_handle, handle in enumerate(handles):
item = self._plot.addMarker(self._points[handle][0],
self._points[handle][1],
legend=handle,
draggable=True,
symbol='x')
assert item == handle
def _drawShape(self):
item = self._plot.addItem(self.xData,
self.yData,
shape=self.shape,
legend=self._name,
overlay=True)
assert item == self._name
def _setHandleData(self, name, point):
self._points[name] = point
def start(self):
self.edit(False)
self._finished = False
self._startNotified = False
self._visible = True
self._plot.setInteractiveMode('draw',
shape=self.shape,
source=self,
label=self._name)
self._connect()
def edit(self, enable):
if not self._finished:
return
if self._editing == enable:
return
if enable:
self._connect()
self._editStarted()
self._draw()
else:
self._disconnect()
self._remove(shape=False)
self._editStopped()
self._draw(drawHandles=False)
self._editing = enable
def _finish(self, event):
self._drawFinished(event)
self._draw(drawHandles=False)
self._finished = True
self._emitDataEvent(self.sigRoiDrawingFinished)
self._disconnect()
def _emitDataEvent(self, signal):
signal.emit({'name': self._name,
'shape': self.shape,
'xdata': self._xData,
'ydata': self._yData})
def stop(self):
"""
Stops whatever state the ROI is in.
draw state : cancel the drawning, emit sigRoiDrawningCanceled
edit state : ends the editing, emit sigRoiEditingFinished
"""
if not self._finished:
self._disconnect()
self._drawCanceled()
if self._startNotified:
self.sigRoiDrawingCanceled.emit(self.name)
return
if self._editing:
self.edit(False)
xData = property(lambda self: self._xData[:])
yData = property(lambda self: self._yData[:])
def _drawEvent(self, event):
"""
This method updates the _xData and _yData members with data found
in the event object. The default implementation just uses the
xdata and ydata fields from the event dictionary.
This method should be overridden if necessary.
"""
self._xData = event['xdata'].reshape(-1)
self._yData = event['ydata'].reshape(-1)
def _handleMoved(self, label, x, y, idx):
"""
Called when one of the registered handle has moved.
To be overridden if necessary
"""
pass
def _drawStarted(self):
"""
To be overridden if necessary
"""
pass
def _drawFinished(self, event):
"""
To be overridden if necessary
"""
pass
def _drawCanceled(self):
"""
To be overridden if necessary
"""
pass
def _editStarted(self):
"""
To be overridden if necessary
"""
pass
def _editStopped(self):
"""
To be overridden if necessary
"""
pass
def _visibilityChanged(self, visible):
"""
To be overridden if necessary
"""
pass
class ImageRoiManager(qt.QObject):
"""
Developpers doc : to add a new ROI simply append the necessary values to
these three members
"""
sigRoiDrawingStarted = qt.Signal(str)
sigRoiDrawingFinished = qt.Signal(object)
sigRoiDrawingCanceled = qt.Signal(str)
sigRoiMoved = qt.Signal(object)
sigRoiRemoved = qt.Signal(str)
def __init__(self, plot, parent=None):
super(ImageRoiManager, self).__init__(parent=parent)
self._plot = plot
self._klassInfos = _registeredRoiItems
self._multipleSelection = False
self._roiVisible = True
self._roiInProgress = None
self._roiActions = None
self._optionActions = None
self._roiActionGroup = None
self._currentKlass = None
self._rois = {}
self._plot.sigInteractiveModeChanged.connect(
self._interactiveModeChanged, qt.Qt.QueuedConnection)
def _createRoiActions(self):
if self._roiActions:
return self._roiActions
# roi shapes
self._roiActionGroup = roiActionGroup = qt.QActionGroup(self)
self._roiActions = roiActions = OrderedDict()
for name, klass in self._klassInfos.items():
try:
qIcon = icons.getQIcon(klass.actionIcon)
except:
qIcon = qt.QIcon()
text = klass.actionText
if text is None:
text = klass.roiName
action = qt.QAction(qIcon, text, None)
action.setCheckable(True)
toolTip = klass.actionToolTip
if toolTip is not None:
action.setToolTip(toolTip)
roiActions[name] = action
roiActionGroup.addAction(action)
if klass.roiName == self._currentKlass:
action.setChecked(True)
else:
action.setChecked(False)
roiActionGroup.triggered.connect(self._roiActionTriggered,
qt.Qt.QueuedConnection)
return roiActions
def _createOptionActions(self):
if self._optionActions:
return self._optionActions
# options
self._optionActions = optionActions = OrderedDict()
# temporary Unicode icons until I have time to draw some icons.
action = qt.QAction(u'\u2200', None)
action.setCheckable(False)
action.setToolTip('Select all [WIP]')
action.triggered.connect(self._selectAll)
action.setEnabled(False)
optionActions['selectAll'] = action
# temporary Unicode icons until I have time to draw some icons.
action = qt.QAction(u'\u2717', None)
action.setCheckable(False)
action.setToolTip('Clear all ROIs')
action.triggered.connect(self._clearRois)
optionActions['clearAll'] = action
action = qt.QAction(u'\u2685', None)
action.setCheckable(True)
action.setChecked(self._multipleSelection)
action.setToolTip('Single/Multiple ROI selection')
action.setText(u'\u2682' if self._multipleSelection else u'\u2680')
action.triggered.connect(self.allowMultipleSelections)
optionActions['multiple'] = action
action = qt.QAction(u'\U0001F440', None)
action.setCheckable(True)
action.setChecked(self._roiVisible)
action.setToolTip('Show/Hide ROI(s)')
action.triggered.connect(self.showRois)
optionActions['show'] = action
return optionActions
def _selectAll(self, checked):
print self._plot.getGraphXLimits(), self._plot.getGraphYLimits()
def _clearRois(self, checked):
self.clear()
def clear(self, name=None):
if name is None:
for roi in self._rois.values():
roi.stop()
roi.setVisible(False)
try:
roi.sigRoiMoved.disconnect(self._roiMoved)
except:
pass
self.sigRoiRemoved.emit(roi.name)
self._rois = {}
else:
try:
roi = self._rois.pop(name)
roi.stop()
roi.setVisible(False)
try:
roi.sigRoiMoved.disconnect(self._roiMoved)
except:
pass
self.sigRoiRemoved.emit(roi.name)
except KeyError:
# TODO : to raise or not to raise?
pass
rois = property(lambda self: self._rois.keys())
def showRois(self, show):
# TODO : name param to tell that we only want to toggle
# one specific ROI
# TODO : exclusive param to tell that we want to show only
# one given roi (w/ name param)
self._roiVisible = show
if self._optionActions:
action = self._optionActions['show']
action.setText(u'\U0001F440' if show else u'\u2012')
if self.sender() != action:
action.setChecked(show)
{roi.setVisible(show) for roi in self._rois.values()}
def allowMultipleSelections(self, allow):
self._multipleSelection = allow
if self._optionActions:
action = self._optionActions['multiple']
action.setText(u'\u2682' if allow else u'\u2680')
if self.sender() != action:
action.setChecked(allow)
def _roiActionTriggered(self, action):
if not action.isChecked():
return
name = [k for k, v in self._roiActions.items() if v == action][0]
self._currentKlass = name
self._startRoi()
def _editRois(self):
# TODO : should we call stop first?
{item.edit(True) for item in self._rois.values()}
def _startRoi(self):
"""
Initialize a new roi, ready to be drawn.
"""
if self._currentKlass not in self._klassInfos.keys():
return
self._stopRoi()
{item.edit(False) for item in self._rois.values()}
self.showRois(True)
klass = self._klassInfos[self._currentKlass]
item = klass(self._plot, self)
self._roiInProgress = item
item.sigRoiDrawingFinished.connect(self._roiDrawingFinished,
qt.Qt.QueuedConnection)
item.sigRoiDrawingStarted.connect(self._roiDrawingStarted,
qt.Qt.QueuedConnection)
item.sigRoiDrawingCanceled.connect(self._roiDrawingCanceled,
qt.Qt.QueuedConnection)
item.sigRoiMoved.connect(self.sigRoiMoved,
qt.Qt.QueuedConnection)
item.start()
def _stopRoi(self):
"""
Stops the roi that was ready to be drawn, if any.
"""
if self._roiInProgress is None:
return
self._roiInProgress.stop()
self._roiInProgress = None
def _roiDrawingStarted(self, name):
if not self._multipleSelection:
self.clear()
self.sigRoiDrawingStarted.emit(name)
def _roiDrawingFinished(self, event):
# TODO : check if the sender is the same as the roiInProgress
item = self._roiInProgress
assert item.name == event['name']
self._roiInProgress = None
item.sigRoiDrawingFinished.disconnect(self._roiDrawingFinished)
item.sigRoiDrawingStarted.disconnect(self._roiDrawingStarted)
item.sigRoiDrawingCanceled.disconnect(self._roiDrawingCanceled)
self._rois[item.name] = item
self._startRoi()
self.sigRoiDrawingFinished.emit(event)
def _roiDrawingCanceled(self, name):
self.sigRoiDrawingCanceled.emit(name)
def _interactiveModeChanged(self, source):
"""Handle plot interactive mode changed:
If changed from elsewhere, disable tool.
"""