from collections import OrderedDict
import copy
import numpy as np
import sys
from typing import Union, Iterable, List, Dict
import qt_themes
from qtpy import QtCore, QtGui, QtWidgets
from qtpy.QtCore import QObject, Slot, Signal
import pyqtgraph as pg
from pyqtgraph.graphicsItems.GradientEditorItem import Gradients
from pyqtgraph import ROI as pgROI
from pymodaq_gui.plotting.items.roi_sync import roi_format
from pymodaq_gui.plotting.utils.lineout import Lineouts
from pymodaq_gui.plotting.utils.plot_utils import ViewBox
from pymodaq_gui.qt_utils import mkQApp
from pymodaq_gui.utils.widgets.widget_with_label_title import WidgetWithLabelTitle
from pymodaq_utils import utils
from pymodaq_utils.config import GlobalConfig as Config
from pymodaq_utils.logger import set_logger, get_module_name
from pymodaq_data.data import (Axis, DataToExport, DataRaw,
DataDistribution, DataWithAxes)
from pymodaq_data.plotting.utils import PlotColors
from pymodaq_gui.managers.roi_viewer_manager import ROIViewerManager, ROIDim
from pymodaq_gui.plotting.items.roi import SimpleRectROI, RoiInfo
from pymodaq_gui.managers.action_manager import ActionManager
from pymodaq_gui.plotting.widgets import ImageWidget
from pymodaq_gui.plotting.data_viewers.viewer import ViewerBase
from pymodaq_gui.plotting.data_viewers.viewer1D import Viewer1D
from pymodaq_gui.plotting.data_viewers.viewer0D import Viewer0D
from pymodaq_gui.plotting.items.image import UniformImageItem, SpreadImageItem
from pymodaq_gui.plotting.items.axis_scaled import AXIS_POSITIONS, AxisItemScaled
from pymodaq_gui.plotting.items.crosshair import Crosshair
from pymodaq_gui.plotting.utils.filter import Filter2DFromCrosshair, Filter2DFromRois
from pymodaq_gui.plotting.utils.plot_utils import make_dashed_pens
from pymodaq_gui.utils.dock import Dock
from pymodaq_gui.plotting.utils.plot_utils import display_in_dock
logger = set_logger(get_module_name(__file__))
config = Config()
Gradients.update(OrderedDict([
('red', {'ticks': [(0.0, (0, 0, 0, 255)), (1.0, (255, 0, 0, 255))], 'mode': 'rgb'}),
('green', {'ticks': [(0.0, (0, 0, 0, 255)), (1.0, (0, 255, 0, 255))], 'mode': 'rgb'}),
('blue', {'ticks': [(0.0, (0, 0, 0, 255)), (1.0, (0, 0, 255, 255))], 'mode': 'rgb'}),
('spread', {'ticks': [(0.0, (0, 0, 0, 255)), (1.0, (255, 255, 255, 255))], 'mode': 'rgb'})]))
COLORS_DICT = dict(red=(255, 0, 0), green=(0, 255, 0), blue=(0, 0, 255), spread=(128, 128, 128))
IMAGE_TYPES = ['red', 'green', 'blue']
COLOR_LIST = PlotColors()
crosshair_pens = make_dashed_pens(color=(255, 255, 0))
[docs]
def image_item_factory(item_type='uniform', axisOrder='row-major', pen='r') -> Union[UniformImageItem, SpreadImageItem]:
if item_type == 'uniform':
image = UniformImageItem(pen=pen)
image.setOpts(axisOrder=axisOrder)
elif item_type == 'spread':
image = SpreadImageItem(pen=pen)
image.setCompositionMode(QtGui.QPainter.CompositionMode_Plus)
return image
[docs]
def histogram_factory(image_item=None, gradient='red'):
"""
Create a pyqtgraph HistogramLUTWidget widget (histogram) and link it to the corresponding image_item
Parameters
----------
image_item: (ImageItem) the image item to be linked with the histogram
gradient: (str) either 'red', 'green', 'blue', 'spread' or one of the Gradients
Returns
-------
HistogramLUTWidget instance
"""
if gradient not in Gradients:
raise KeyError(f'Possible gradient are {Gradients} not {gradient}')
histo = pg.HistogramLUTWidget()
if image_item is not None:
histo.setImageItem(image_item)
histo.gradient.loadPreset(gradient)
return histo
[docs]
class ImageDisplayer(QObject):
"""
This Object deals with the display of 2D data with a plotitem
"""
updated_item = Signal(dict)
def __init__(self, plotitem, data_distribution: DataDistribution):
super().__init__()
self._plotitem = plotitem
self._plotitem.addLegend()
self.show_legend(False)
self.display_type = data_distribution
self._image_items: dict[str, Union[UniformImageItem, SpreadImageItem]] = dict([])
self._autolevels = False
self._levels_sym = False
self._data: DataWithAxes = None
self.update_display_items()
[docs]
def show_legend(self, show=True):
self.legend.setVisible(show)
@property
def legend(self):
return self._plotitem.legend
[docs]
def get_images(self):
return self._image_items
[docs]
def get_image(self, name):
if name not in self._image_items:
raise KeyError(f'The image {name} is not defined in {self.__class__.__name__}')
else:
return self._image_items[name]
@property
def labels(self):
if self._data is None:
return []
else:
return self._data.labels
@property
def autolevels(self):
return self._autolevels
[docs]
def set_autolevels(self, isautolevel: bool):
self._autolevels = isautolevel
@property
def levels_sym(self) -> bool:
return self._levels_sym
[docs]
def set_levels_sym(self, levels_sym: bool):
self._levels_sym = levels_sym
[docs]
def update_data(self, dwa: DataWithAxes):
if dwa.labels != self.labels:
self.update_display_items(dwa.labels)
if dwa.distribution != self.display_type:
self.display_type = dwa.distribution
self._data = dwa
for ind_data, data_array in enumerate(dwa.data):
if data_array.size > 0:
if self.display_type == 'uniform':
self._image_items[IMAGE_TYPES[ind_data]].setImage(data_array, self.autolevels,
levels_sym=self.levels_sym)
else:
nav_axes = dwa.get_nav_axes()
data_array = np.stack((nav_axes[0].get_data(),
nav_axes[1].get_data(),
data_array), axis=0).T
self._image_items[IMAGE_TYPES[ind_data]].setImage(data_array, self.autolevels,
levels_sym=self.levels_sym)
[docs]
def update_display_items(self, labels: List[str] = None):
while len(self._image_items) > 0:
self._plotitem.removeItem(self._image_items.pop(next(iter(self._image_items))))
if labels is None:
labels = []
while len(labels) != len(IMAGE_TYPES):
labels.append(IMAGE_TYPES[len(labels)])
for ind, img_key in enumerate(IMAGE_TYPES):
self._image_items[img_key] = image_item_factory(self.display_type, pen=img_key[0])
self._plotitem.addItem(self._image_items[img_key])
if ind < len(labels):
self.legend.addItem(self._image_items[img_key], labels[ind])
self.updated_item.emit(self._image_items)
[docs]
def update_image_visibility(self, are_items_visible):
if len(are_items_visible) != len(self._image_items):
raise ValueError(f'The length of the argument is not equal with the number of images')
for ind, key in enumerate(IMAGE_TYPES):
self._image_items[key].setVisible(are_items_visible[ind])
[docs]
class Histogrammer(QObject):
gradient_changed = Signal()
def __init__(self, histogram_container: QtWidgets.QWidget, histogram_refs=IMAGE_TYPES):
super().__init__()
self._histograms = dict([])
self._histogram_refs = histogram_refs
self._histogram_container = histogram_container
self.setup_histograms()
self._autolevels = False
self._levels_sym = False
[docs]
def setup_histograms(self):
for histo_key in self._histogram_refs:
self._histograms[histo_key] = histogram_factory(None, gradient=histo_key)
self.add_histogram(self._histograms[histo_key])
self._histograms[histo_key].setVisible(False)
self._histograms[histo_key].item.sigLookupTableChanged.connect(lambda: self.gradient_changed.emit())
[docs]
def get_histograms(self):
return self._histograms
[docs]
def get_histogram(self, name):
if name not in self.get_histograms():
raise KeyError(f'The histogram {name} is not defined in {self.__class__.__name__}')
else:
return self._histograms[name]
@property
def autolevels(self):
return self._autolevels
[docs]
@Slot(bool)
def set_autolevels(self, isautolevels=True):
self._autolevels = isautolevels
for histo in self._histograms.values():
histo.region.setVisible(not isautolevels)
[docs]
@Slot(bool)
def activated(self, histo_action_checked):
if histo_action_checked:
for histo in self._histograms.values():
histo.regionChanged()
[docs]
def affect_histo_to_imageitems(self, image_items):
# TODO: if self._histogram_refs doesn't contains the same refs as image_items, we have an issue...
for img_key in self._histogram_refs:
self._histograms[img_key].setImageItem(image_items[img_key])
[docs]
def add_histogram(self, histogram):
if self._histogram_container.layout() is None:
self._histogram_container.setLayout(QtWidgets.QHBoxLayout())
self._histogram_container.layout().addWidget(histogram)
[docs]
def show_hide_histogram(self, checked, are_items_visible):
for ind_histo, histo_name in enumerate(self._histogram_refs):
self._histograms[histo_name].setVisible(are_items_visible[ind_histo] and checked)
[docs]
def set_gradient(self, histo='red', gradient='grey'):
"""
Change the color gradient of the specified histogram
Parameters
----------
histo: (str) either 'red', 'green', 'blue', 'spread' or 'all'
gradient: (str or Gradient)
"""
if gradient in Gradients:
if histo == 'all':
for key in self._histogram_refs:
self._histograms[key].item.gradient.loadPreset(gradient)
else:
self._histograms[histo].item.gradient.loadPreset(gradient)
[docs]
class IsoCurver(QObject):
def __init__(self, image_source, histogram_parent):
super().__init__()
self._histogram_parent = histogram_parent
self.setup_iso_curve()
self.update_image_source(image_source)
self.update_histogram_parent(histogram_parent)
self.show_hide_iso(False)
[docs]
def setup_iso_curve(self, parent_image_item='red'):
# # Isocurve drawing
self._isocurve_item = pg.IsocurveItem(level=0.8, pen='g', axisOrder='row-major')
self._isocurve_item.setZValue(5)
# # Draggable line for setting isocurve level
self._isoLine = pg.InfiniteLine(angle=0, movable=True, pen='g')
self._isoLine.setValue(0.8)
self._isoLine.setZValue(1000) # bring iso line above contrast controls
self._isoLine.sigDragged.connect(self.update_isocurve)
[docs]
def update_image_source(self, image_source):
self._isocurve_item.setParentItem(image_source)
[docs]
def update_histogram_parent(self, histogram):
if histogram != self._histogram_parent:
histogram.vb.removeItem(self._isoLine)
self._histogram_parent = histogram
histogram.vb.addItem(self._isoLine)
histogram.vb.setMouseEnabled(y=False) # makes user interaction a little easier
[docs]
def update_isocurve(self):
self._isocurve_item.setLevel(self._isoLine.value())
[docs]
def set_isocurve_data(self, data):
self._isocurve_item.setData(data)
[docs]
@Slot(bool)
def show_hide_iso(self, show=True):
if show:
self._isocurve_item.show()
self._isoLine.show()
else:
self._isocurve_item.hide()
self._isoLine.hide()
[docs]
class View2D(ActionManager, QtCore.QObject):
lineout_types = ['hor', 'ver', 'int']
def __init__(self, parent_widget=None, title='',
rois_dock: Dock = None):
QtCore.QObject.__init__(self)
ActionManager.__init__(self, toolbar=QtWidgets.QToolBar())
self.roi_vb: ViewBox = None
self._title = title
self.rois_dock = rois_dock
self.ROIselect = SimpleRectROI([0, 0], [10, 10], centered=True, sideScalers=True)
self._lineout_widgets = {widg_key: QtWidgets.QWidget() for widg_key in Lineouts.values()}
self.lineout_viewers: Dict[str, Viewer1D] = None
self.setup_actions()
self.parent_widget = parent_widget
if self.parent_widget is None:
self.parent_widget = QtWidgets.QWidget()
self.parent_widget.show()
self.image_widget = ImageWidget()
self.roi_manager = ROIViewerManager(self.image_widget.plotitem.vb, ROIDim.ROI2D)
self.roi_widget = WidgetWithLabelTitle(self.title, self.roi_manager.roiwidget)
self.roi_target: Union[pgROI, Crosshair] = None
self.setup_view_box()
self.setup_lineouts()
self.setup_widgets()
self.histogrammer = Histogrammer(self.widget_histo)
self.data_displayer: ImageDisplayer = None
self.isocurver: IsoCurver = None
self.crosshair = Crosshair(self.image_widget)
self.connect_things()
self.prepare_ui()
self.set_axis_label('bottom', label='', units='index')
self.set_axis_label('left', label='', units='index')
self.set_image_displayer(DataDistribution['uniform'])
@property
def title(self) -> str:
return self._title
@title.setter
def title(self, value: str):
self._title = value
self.roi_manager.title = value
self.roi_widget.set_title(value)
[docs]
@staticmethod
def get_theme(name: str = None) -> qt_themes.Theme:
theme = qt_themes.get_theme(name)
if theme is None:
themes_dict = qt_themes.get_themes()
theme = themes_dict[list(themes_dict.keys())[0]]
return theme
[docs]
def setup_view_box(self):
""" create and axis-sync a viewbox dedicated to ROIselect """
self.roi_vb = ViewBox()
self.plotitem.scene().addItem(self.roi_vb)
self.plotitem.getAxis('right').linkToView(self.roi_vb)
self.plotitem.getAxis('top').linkToView(self.roi_vb)
self.roi_vb.setXLink(self.plotitem.vb)
self.roi_vb.setYLink(self.plotitem.vb)
self.update_view_box()
self.plotitem.vb.sigResized.connect(self.update_view_box)
self.roi_vb.addItem(self.ROIselect)
[docs]
def update_view_box(self):
self.roi_vb.setGeometry(self.plotitem.vb.sceneBoundingRect())
self.roi_vb.linkedViewChanged(self.plotitem.vb, self.roi_vb.XAxis)
self.roi_vb.linkedViewChanged(self.plotitem.vb, self.roi_vb.YAxis)
[docs]
def setup_lineouts(self):
self.lineout_viewers: Dict[str, Viewer1D] = (
dict(hor=Viewer1D(self._lineout_widgets[Lineouts.HOR], show_toolbar=False, no_margins=True),
ver=Viewer1D(self._lineout_widgets[Lineouts.VER], show_toolbar=False, no_margins=True,
flip_axes=True),
int=Viewer0D(self._lineout_widgets[Lineouts.INT], show_toolbar=False, no_margins=True)))
self.link_lineouts()
[docs]
def link_lineouts(self, do_link=True):
hor_vb: ViewBox = self.lineout_viewers[Lineouts.HOR].view.plotitem.vb
ver_vb: ViewBox = self.lineout_viewers[Lineouts.VER].view.plotitem.vb
hor_vb.linkView(hor_vb.XAxis, self.plotitem.vb if do_link else None)
ver_vb.linkView(ver_vb.YAxis, self.plotitem.vb if do_link else None)
[docs]
def clear_plot_item(self):
for item in self.plotitem.items[:]:
if isinstance(item, (SpreadImageItem, UniformImageItem)):
self.plotitem.removeItem(item)
[docs]
def set_image_displayer(self, data_distribution: DataDistribution):
self.clear_plot_item()
self.data_displayer = ImageDisplayer(self.plotitem, data_distribution)
self.isocurver = IsoCurver(self.data_displayer.get_image('red'), self.histogrammer.get_histogram('red'))
self.connect_action('isocurve', self.isocurver.show_hide_iso)
self.data_displayer.updated_item.connect(self.histogrammer.affect_histo_to_imageitems)
self.connect_action('autolevels', self.data_displayer.set_autolevels)
self.connect_action('auto_levels_sym', self.data_displayer.set_levels_sym)
for key in IMAGE_TYPES:
self.connect_action(key, self.notify_visibility_data_displayer)
self.histogrammer.affect_histo_to_imageitems(self.data_displayer.get_images())
if data_distribution.name == 'uniform':
self.roi_target = pgROI(pos=(0, 0), size=(20, 20), movable=False, rotatable=False,
resizable=False)
self.plotitem.addItem(self.roi_target)
elif data_distribution.name == 'spread':
self.roi_target = Crosshair(self.image_widget, pen=(255, 255, 255))
self.roi_target.setVisible(False)
[docs]
def show_roi_target(self, show=True):
self.roi_target.setVisible(show)
[docs]
def move_scale_roi_target(self, pos=None, size=None):
"""
Move and scale the target ROI (used to display a particular area,
for instance the currently scanned points
during a scan
Parameters
----------
pos: (iterable) setting the central position of the ROI in the view
size: (iterable) setting the size of the ROI
"""
if isinstance(self.roi_target, pgROI):
self.move_scale_roi(self.roi_target, pos, size)
else:
self.roi_target.set_crosshair_position(*list(pos))
[docs]
def move_scale_roi(self, roi: pgROI, pos=None, size=None):
if size is not None:
x_offset, x_scaling, y_offset, y_scaling = self._get_axis_scaling_offset()
size = list(np.divide(list(size), [x_scaling, y_scaling]))
if list(roi.size()) != size:
roi.setSize(size, center=(0.5, 0.5))
if pos is not None:
pos = self.unscale_axis(*list(pos))
pos = list(pos)
if list(roi.pos()) != pos:
roi.setPos(pos)
[docs]
def setup_graphs(self, graphs_layout):
self.splitter = QtWidgets.QSplitter(QtCore.Qt.Horizontal)
graphs_layout.addWidget(self.splitter)
self.widget_histo = QtWidgets.QWidget()
graphs_layout.addWidget(self.widget_histo)
self.widget_histo.setLayout(QtWidgets.QHBoxLayout())
self.splitter_VLeft = QtWidgets.QSplitter(QtCore.Qt.Vertical)
self.splitter_VRight = QtWidgets.QSplitter(QtCore.Qt.Vertical)
self.splitter.addWidget(self.splitter_VLeft)
self.splitter.addWidget(self.splitter_VRight)
self.splitter_VLeft.addWidget(self.image_widget)
self.splitter_VLeft.addWidget(self._lineout_widgets[Lineouts.HOR])
self.splitter_VRight.addWidget(self._lineout_widgets[Lineouts.VER])
self.splitter_VRight.addWidget(self._lineout_widgets[Lineouts.INT])
self.image_widget.add_scaled_axis('left')
self.image_widget.add_scaled_axis('bottom')
self.roi_widget.setVisible(False)
[docs]
def setup_actions(self):
self.add_action('position', '(,)')
self.add_action('red', 'Red Channel', 'r_icon', tip='Show/Hide Red Channel', checkable=True)
self.add_action('green', 'Green Channel', 'g_icon', tip='Show/Hide Green Channel', checkable=True)
self.add_action('blue', 'Blue Channel', 'b_icon', tip='Show/Hide Blue Channel', checkable=True)
self.get_action('red').setChecked(True)
self.get_action('green').setChecked(True)
self.get_action('blue').setChecked(True)
self.add_action('autolevels', 'AutoLevels', 'fit_page_height',
tip='Scale Histogram to Min/Max intensity', checkable=True)
self.add_action('auto_levels_sym', 'AutoLevels Sym.', 'fit_page_height',
tip='Make the autoscale of the histograms symetric with respect to 0', checkable=True)
self.add_action('histo', 'Histogram', 'bar_chart', tip='Show/Hide Histogram', checkable=True)
self.add_action('roi', 'ROI', 'calculate', tip='Show/Hide ROI Manager', checkable=True)
self.add_action('link_lineouts', 'Link Lineouts', 'link', tip='Link Lineouts',
checkable=True, checked=True, visible=False,
icon_color=self.get_theme().red, icon_checked_color=self.get_theme().green)
self.add_action('isocurve', 'IsoCurve', 'altitude', tip='Show/Hide Isocurve', checkable=True)
self.add_action('aspect_ratio', 'Aspect Ratio', 'aspect_ratio', tip='Fix Aspect Ratio', checkable=True, checked=True)
self.add_action('crosshair', 'CrossHair', 'add_2', tip='Show/Hide data Crosshair', checkable=True)
self.add_action('ROIselect', 'ROI Select', 'select',
tip='Show/Hide ROI selection area', checkable=True)
self.add_action('flip_ud', 'Flip UD', 'flip',
tip='Flip the image up/down', checkable=True,
rotate=90)
self.add_action('flip_lr', 'Flip LR', 'flip',
tip='Flip the image left/right', checkable=True)
self.add_action('rotate', 'Rotate', 'rotate_90_degrees_cw',
tip='Rotate the image', checkable=True)
self.add_action('opposite', 'Opposite', 'remove',
tip='Take the opposite of the image', checkable=True)
self.add_action('legend', 'Legend', 'RGB',
tip='Show the legend', checkable=True)
[docs]
def update_colors(self, colors: list):
for ind, roi_meta in enumerate(self.roi_manager.ROIs):
roi_name = roi_format(roi_meta.index)
self.lineout_viewers[Lineouts.HOR].update_colors(make_dashed_pens(colors[ind]), displayer=roi_name)
self.lineout_viewers[Lineouts.VER].update_colors(make_dashed_pens(colors[ind]), displayer=roi_name)
self.lineout_viewers[Lineouts.INT].update_colors(make_dashed_pens(colors[ind]), displayer=roi_name)
[docs]
def connect_things(self):
self.connect_action('histo', self.histogrammer.activated)
self.connect_action('autolevels', self.histogrammer.set_autolevels)
self.roi_manager.new_ROI_signal.connect(self.update_roi_channels)
self.roi_manager.new_ROI_signal.connect(self.add_roi_displayer)
self.roi_manager.new_ROI_signal.connect(self.lineout_viewers[Lineouts.INT].get_action('clear').click)
self.roi_manager.remove_ROI_signal.connect(self.remove_roi_displayer)
self.roi_manager.color_signal.connect(self.update_colors)
self.connect_action('isocurve', self.get_action('histo').trigger)
self.connect_action('aspect_ratio', self.lock_aspect_ratio)
self.connect_action('histo', self.show_hide_histogram)
self.connect_action('roi', self.show_lineout_widgets)
self.connect_action('roi', self.roi_clicked)
self.connect_action('roi', self.show_hide_link_lineouts)
self.connect_action('ROIselect', lambda: self.show_ROI_select())
self.connect_action('crosshair', self.show_hide_crosshair)
self.connect_action('crosshair', self.show_lineout_widgets)
self.connect_action('crosshair', self.show_hide_link_lineouts)
self.connect_action('legend', self.show_legend)
self.connect_action('link_lineouts', self.link_lineouts)
[docs]
def show_hide_link_lineouts(self):
self.get_action('link_lineouts').setVisible(
self.is_action_checked('roi') or self.is_action_checked('crosshair')
)
[docs]
def show_legend(self, show=True):
self.data_displayer.show_legend(show)
[docs]
def add_roi_displayer(self, roi_index: int):
color = self.roi_manager.get_roi_from_index(roi_index).roi.color()
roi_name = roi_format(roi_index)
self.lineout_viewers[Lineouts.HOR].view.add_data_displayer(roi_name, make_dashed_pens(color))
self.lineout_viewers[Lineouts.VER].view.add_data_displayer(roi_name, make_dashed_pens(color))
self.lineout_viewers[Lineouts.INT].view.add_data_displayer(roi_name, make_dashed_pens(color))
[docs]
def remove_roi_displayer(self, roi_index: int):
roi_name = roi_format(roi_index)
self.lineout_viewers[Lineouts.HOR].view.remove_data_displayer(roi_name)
self.lineout_viewers[Lineouts.VER].view.remove_data_displayer(roi_name)
self.lineout_viewers[Lineouts.INT].view.remove_data_displayer(roi_name)
[docs]
def update_roi_channels(self, roi_index):
"""Update the use_channel setting each time a ROI is added"""
self.roi_manager.update_use_channel(self.data_displayer.labels.copy(),roi_index)
[docs]
def prepare_ui(self):
self.ROIselect.setVisible(False)
self.show_hide_crosshair(False)
self.show_lineout_widgets()
config = Config()
for action_name in ('autolevels', 'auto_levels_sym', 'histo', 'roi', 'isocurve',
'crosshair', 'ROIselect', 'flip_ud', 'flip_lr', 'rotate',
'opposite', 'legend'):
if config('gui', 'viewer', 'viewer2D', action_name):
self.get_action(action_name).trigger()
if not config('gui', 'viewer', 'viewer2D', 'aspect_ratio'):
self.get_action('aspect_ratio').trigger()
[docs]
@Slot(DataRaw)
def display_images(self, datas):
self.data_displayer.update_data(datas)
if self.is_action_checked('isocurve'):
self.isocurver.set_isocurve_data(datas.data[0])
[docs]
def display_roi_lineouts(self, roi_dte: DataToExport):
if len(roi_dte) > 0:
for lineout_type in Lineouts.values():
for displayer_name in self.lineout_viewers[lineout_type].view.other_data_displayers:
dwa = roi_dte.get_data_from_name_origin(lineout_type, displayer_name)
if dwa is not None:
self.lineout_viewers[lineout_type].view.display_data(dwa.deepcopy(),
displayer=displayer_name)
[docs]
def display_crosshair_lineouts(self, crosshair_dte: DataToExport):
for lineout_type in Lineouts.values():
dwa = crosshair_dte.get_data_from_name(lineout_type)
if dwa is not None:
self.lineout_viewers[lineout_type].view.display_data(dwa, displayer='crosshair')
[docs]
@Slot(bool)
def roi_clicked(self, isroichecked=True):
if (config('gui', 'viewer', 'rois_as_popup')
or self.rois_dock is None):
if self.rois_dock is not None:
self.rois_dock.removeWidgets(close=False)
self.rois_dock.setVisible(False)
self.roi_widget.setWindowTitle(f'{self.title} ROIs')
self.roi_widget.setVisible(isroichecked)
self.roi_widget.closeEvent = lambda event: self.set_action_checked('roi', False)
else:
display_in_dock(isroichecked,
self.roi_widget,
self.rois_dock)
for roi_meta in self.roi_manager.ROIs:
roi_meta.roi.setVisible(isroichecked)
[docs]
def get_visible_images(self):
are_items_visible = []
for key in IMAGE_TYPES:
are_items_visible.append(self.is_action_visible(key) and self.is_action_checked(key))
return are_items_visible
[docs]
def notify_visibility_data_displayer(self):
are_items_visible = self.get_visible_images()
self.data_displayer.update_image_visibility(are_items_visible)
[docs]
@Slot(bool)
def show_hide_histogram(self, show=True):
are_items_visible = self.get_visible_images()
self.histogrammer.show_hide_histogram(show, are_items_visible)
[docs]
def get_view_range(self):
return self.image_widget.view.viewRange()
[docs]
def get_data_at(self, name='red', xy=(0, 0)):
return self.data_displayer.get_image(name).get_val_at(xy)
[docs]
def lock_aspect_ratio(self):
lock = self.is_action_checked('aspect_ratio')
x_offset, x_scaling, y_offset, y_scaling = self._get_axis_scaling_offset()
ratio = x_scaling / y_scaling
self.plotitem.vb.setAspectLocked(lock=lock, ratio=ratio)
[docs]
@Slot(int, int)
def move_left_splitter(self, pos, index):
self.splitter_VLeft.blockSignals(True)
self.splitter_VLeft.moveSplitter(pos, index)
self.splitter_VLeft.blockSignals(False)
[docs]
@Slot(int, int)
def move_right_splitter(self, pos, index):
self.splitter_VRight.blockSignals(True)
self.splitter_VRight.moveSplitter(pos, index)
self.splitter_VRight.blockSignals(False)
[docs]
def get_double_clicked(self):
return self.image_widget.view.sig_double_clicked
[docs]
def get_axis(self, position='left') -> AxisItemScaled:
if position not in AXIS_POSITIONS:
raise KeyError(f'{position} is not a possible position for Axis: {AXIS_POSITIONS}')
return self.image_widget.getAxis(position)
@property
def plotitem(self):
return self.image_widget.plotitem
[docs]
def get_image_item(self, color='red'):
return self.data_displayer.get_image(color)
[docs]
def get_crosshair_signal(self):
"""Convenience function from the Crosshair"""
return self.crosshair.crosshair_dragged
[docs]
def get_crosshair_position(self):
"""Convenience function from the Crosshair"""
return self.crosshair.get_positions()
[docs]
def set_crosshair_position(self, *positions):
"""Convenience function from the Crosshair"""
self.crosshair.set_crosshair_position(*positions)
[docs]
@Slot(bool)
def show_hide_crosshair(self, show=True):
self.crosshair.setVisible(show)
self.set_action_visible('position', show)
self.crosshair.setVisible(show)
if show:
self.lineout_viewers[Lineouts.HOR].view.add_data_displayer('crosshair', plot_colors=crosshair_pens)
self.lineout_viewers[Lineouts.VER].view.add_data_displayer('crosshair', plot_colors=crosshair_pens)
self.lineout_viewers[Lineouts.INT].view.add_data_displayer('crosshair', plot_colors=crosshair_pens)
range = self.get_view_range()
self.set_crosshair_position(np.mean(np.array(range[0])), np.mean(np.array(range[0])))
else:
self.lineout_viewers[Lineouts.HOR].view.remove_data_displayer('crosshair')
self.lineout_viewers[Lineouts.VER].view.remove_data_displayer('crosshair')
self.lineout_viewers[Lineouts.INT].view.remove_data_displayer('crosshair')
logger.debug(f'Crosshair visible?: {self.crosshair.isVisible()}')
[docs]
def show_ROI_select(self, pos=None, size=None):
self.ROIselect.setVisible(self.is_action_checked('ROIselect'))
range = self.get_view_range()
size = np.array((range[0][1] - range[0][0], range[1][1] - range[1][0])) * 2/3
self.ROIselect.setSize(size, center=(0.5, 0.5))
self.ROIselect.setPos(np.mean(range, 1)-size/2)
[docs]
def set_image_labels(self, labels: List[str]):
if self.data_displayer.labels != labels:
action_names =['red', 'green', 'blue']
for action_name, label in zip(action_names[:len(labels)], labels):
self.get_action(action_name).setToolTip('Show/Hide'
f' - '
f'{label}')
[docs]
def set_axis_label(self, position, label='', units=''):
"""
Convenience method to set label and unit of any view axes
Parameters
----------
position: (str) any of AXIS_POSITIONS
label: (str) text of the axis label
units: (str) units of the axis label
"""
axis = self.get_axis(position)
axis.setLabel(text=label, units=units)
[docs]
def get_axis_label(self, position):
axis = self.get_axis(position)
return axis.axis_label, axis.axis_units
[docs]
def set_axis_scaling(self, position='top', scaling=1, offset=0, label='', units='index'):
"""
Method used to update the scaling of the right and top axes in order to translate indices to real coordinates
Parameters
----------
position: (str) axis position either one of AXIS_POSITIONS
scaling: (float) scaling of the axis
offset: (float) offset of the axis
label: (str) text of the axis label
units: (str) units of the axis label
"""
if scaling is None:
scaling = 1
if offset is None:
offset = 0
self.get_axis(position).axis_scaling = scaling
self.get_axis(position).axis_offset = offset
self.set_axis_label(position, label=label, units=units)
[docs]
def scale_axis(self, xaxis, yaxis):
"""scale view coordinates from the regular axes to the scaled/offset ones"""
x_offset, x_scaling, y_offset, y_scaling = self._get_axis_scaling_offset()
return xaxis * x_scaling + x_offset, yaxis * y_scaling + y_offset
[docs]
def unscale_axis(self, xaxis, yaxis):
"""scale view coordinates from the scaled/offset axes to the regular ones"""
x_offset, x_scaling, y_offset, y_scaling = self._get_axis_scaling_offset()
return (xaxis - x_offset) / x_scaling, (yaxis - y_offset) / y_scaling
def _get_axis_scaling_offset(self):
x_offset = self.get_axis('bottom').axis_offset
x_scaling = self.get_axis('bottom').axis_scaling
y_offset = self.get_axis('left').axis_offset
y_scaling = self.get_axis('left').axis_scaling
return x_offset, x_scaling, y_offset, y_scaling
[docs]
class Viewer2D(ViewerBase):
"""Object managing plotting and manipulation of 2D data using a View2D"""
def __init__(self, parent: QtWidgets.QWidget = None, title='',
rois_dock: Dock = None):
super().__init__(parent, title)
self.just_init = True
self._datas = None
self.isdata = dict([])
self._is_gradient_manually_set = False
self.view : View2D= View2D(parent, title,
rois_dock = rois_dock)
self.filter_from_rois = Filter2DFromRois(self.view.roi_manager, self.view.data_displayer.get_image('red'),
IMAGE_TYPES)
self.filter_from_rois.register_activation_signal(self.view.get_action('roi').triggered)
self.filter_from_rois.register_target_slot(self.process_roi_lineouts)
self.filter_from_crosshair = Filter2DFromCrosshair(self.view.crosshair, self.view.data_displayer.get_images(),
IMAGE_TYPES)
self.filter_from_crosshair.register_activation_signal(self.view.get_action('crosshair').triggered)
self.filter_from_crosshair.register_target_slot(self.process_crosshair_lineouts)
self.prepare_connect_ui()
[docs]
def move_roi_target(self, pos: Iterable[float] = None, size: Iterable[float] = (1, 1)):
"""move a specific read only ROI at the given position on the viewer"""
self.view.move_scale_roi_target(pos, size)
@property
def crosshair(self):
"""Convenience method """
return self.view.crosshair
@property
def image_widget(self):
"""Convenience method """
return self.view.image_widget
[docs]
def get_data_at(self):
"""Convenience method """
return self.view.get_data_at()
[docs]
def set_crosshair_position(self, xpos, ypos):
"""Convenience method to set the crosshair positions"""
self.view.crosshair.set_crosshair_position(xpos=xpos, ypos=ypos)
[docs]
def activate_roi(self, activate=True):
"""Activate the Roi manager using the corresponding action"""
self.view.set_action_checked('roi', activate)
self.view.get_action('roi').triggered.emit(activate)
[docs]
def roi_changed(self, *args, **kwargs):
self.filter_from_rois.filter_data(self._datas)
[docs]
def crosshair_changed(self):
self.filter_from_crosshair.filter_data(self._datas)
[docs]
def set_gradient(self, image_key, gradient):
"""convenience function"""
self.view.histogrammer.set_gradient(image_key, gradient)
def _show_data(self, data: DataWithAxes, *args, **kwargs):
"""Data to be plotted and eventually filtered using ROI...
Parameters
----------
data: DataWithAxes
"""
if len(data) == 1 and not self._is_gradient_manually_set:
self.set_gradient('red', 'grey')
if len(data) > 3:
logger.warning('Cannot plot on 2D plot more than 3 channels')
data.data = data.data[:3]
self.view.set_image_labels(data.labels)
if data.distribution != self.view.data_displayer.display_type:
self.view.set_image_displayer(data.distribution)
self.filter_from_crosshair.set_graph_items(self.view.data_displayer.get_images())
self.get_axes_from_view(data) # in case axes were not specified into data, one try to get them from the view
self.isdata['red'] = len(data) > 0
self.isdata['green'] = len(data) > 1
self.isdata['blue'] = len(data) > 2
self.update_data()
if self.view.is_action_checked('aspect_ratio'):
self.view.lock_aspect_ratio()
self.set_visible_items()
if not self.view.is_action_checked('roi'):
self.data_to_export_signal.emit(self.data_to_export)
self.autolevels_first()
[docs]
def autolevels_first(self):
if self.just_init and not self.is_action_checked('autolevels'):
self.get_action('autolevels').trigger()
self.update_data()
self.get_action('autolevels').trigger()
self.just_init = False
[docs]
def get_axes_from_view(self, data: DataWithAxes):
"""Obtain axes info from the view
Only for uniform data
"""
if data.distribution == DataDistribution['uniform']:
if data.get_axis_from_index(0)[0] is None:
axis_view = self.view.get_axis('left')
axis = Axis(axis_view.axis_label, units=axis_view.axis_units,
scaling=axis_view.axis_scaling, offset=axis_view.axis_offset, index=0)
axis.create_linear_data(data.shape[0])
data.axes.append(axis)
if data.get_axis_from_index(1)[0] is None:
axis_view = self.view.get_axis('bottom')
axis = Axis(axis_view.axis_label, units=axis_view.axis_units,
scaling=axis_view.axis_scaling, offset=axis_view.axis_offset, index=1)
axis.create_linear_data(data.shape[1])
data.axes.append(axis)
[docs]
def update_data(self):
if self._raw_data is not None:
self._datas = self.set_image_transform()
self.view.display_images(self._datas)
if self._datas.distribution.name == 'uniform':
xaxis = self._datas.get_axis_from_index(1)[0]
yaxis = self._datas.get_axis_from_index(0)[0]
self.x_axis = xaxis
self.y_axis = yaxis
self.view.set_transform(xaxis, yaxis)
else:
self.x_axis = self._datas.get_axis_from_index(0)[0]
self.y_axis = self._datas.get_axis_from_index(0)[1]
if self.view.is_action_checked('roi'):
self.roi_changed()
if self.view.is_action_checked('crosshair'):
self.crosshair_changed()
[docs]
def set_visible_items(self):
for key in IMAGE_TYPES:
self.view.set_action_visible(key, self.isdata[key])
self.view.notify_visibility_data_displayer()
[docs]
def show_roi(self, show=True, show_roi_widget=True):
"""convenience function to control roi"""
if show == (not self.view.is_action_checked('roi')):
self.view.get_action('roi').trigger()
self.view.roi_widget.setVisible(show_roi_widget)
[docs]
def update_crosshair_data(self, crosshair_dte: DataToExport):
try:
posx, posy = self.view.get_crosshair_position()
(posx_scaled, posy_scaled) = self.view.scale_axis(posx, posy)
dat = f'({posx_scaled:.1e}{posy_scaled:.1e})\n'
dwa_int = crosshair_dte.get_data_from_name(Lineouts.INT)
if dwa_int is not None:
for ind_data in range(len(dwa_int)):
dat += f' {dwa_int.labels[ind_data]}:{float(dwa_int[ind_data][0]):.1e}\n'
self.view.set_action_text('position', dat)
except Exception as e:
logger.warning(str(e))
[docs]
def prepare_connect_ui(self):
self.view.ROIselect.sigRegionChangeFinished.connect(self.selected_region_changed)
self.roi_manager.roi_changed.connect(self.roi_changed)
self.view.connect_action('flip_ud', slot=self.update_data)
self.view.connect_action('flip_lr', slot=self.update_data)
self.view.connect_action('rotate', slot=self.update_data)
self.view.connect_action('autolevels', slot=self.update_data)
self.view.connect_action('auto_levels_sym', slot=self.update_data)
self.view.connect_action('opposite', slot=self.update_data)
self.view.connect_action('isocurve', slot=self.update_data)
self.view.histogrammer.gradient_changed.connect(lambda: setattr(self, '_is_gradient_manually_set', True))
# todo : self.view.lineout_plotter.roi_changed.connect(self.roi_changed)
self.view.get_crosshair_signal().connect(self.crosshair_changed)
self.view.get_double_clicked().connect(self.double_clicked)
[docs]
def selected_region_changed(self):
if self.view.is_action_checked('ROIselect'):
pos = self.view.ROIselect.pos()
size = self.view.ROIselect.size()
# self.ROI_select_signal.emit(QtCore.QRectF(pos[0], pos[1], size[0], size[1]))
offset_x = self.view.get_axis('bottom').axis_offset
offset_y = self.view.get_axis('left').axis_offset
self.roi_select_signal.emit(
RoiInfo.info_from_rect_roi(self.view.ROIselect))
[docs]
@Slot(float, float)
def double_clicked(self, posx, posy):
if self.view.is_action_checked('crosshair'):
self.view.crosshair.set_crosshair_position(posx, posy)
self.crosshair_changed()
#scale positions of double_click with respect to real axes
posx, posy = self.view.scale_axis(posx, posy)
self.sig_double_clicked.emit(posx, posy)
@property
def x_axis(self):
return self.view.get_axis('bottom')
@x_axis.setter
def x_axis(self, axis: Axis = None):
if axis is not None:
self.view.set_axis_scaling('bottom', scaling=1, offset=0,
label=axis.label, units=axis.units)
@property
def y_axis(self):
return self.view.get_axis('left')
@y_axis.setter
def y_axis(self, axis: Axis = None):
if axis is not None:
self.view.set_axis_scaling('left', scaling=1, offset=0,
label=axis.label, units=axis.units)
[docs]
@Slot(DataToExport)
def process_crosshair_lineouts(self, dte):
self.view.display_crosshair_lineouts(dte)
self.update_crosshair_data(dte)
self.crosshair_dragged.emit(*self.view.scale_axis(*self.view.crosshair.get_positions()))
[docs]
def process_roi_lineouts(self, roi_dte: DataToExport):
if len(roi_dte) > 0:
self.view.display_roi_lineouts(roi_dte)
roi_dte_bis = roi_dte.deepcopy()
for dwa in roi_dte_bis.data:
if dwa.name == Lineouts.HOR:
dwa.name = f'Hlineout'
elif dwa.name == Lineouts.VER:
dwa.name = f'Vlineout'
elif dwa.name == Lineouts.INT:
dwa.name = f'Integrated'
self.data_to_export.append(roi_dte_bis)
self.measure_data_dict = dict([])
for roi_name in roi_dte_bis.get_origins():
dwa = roi_dte_bis.get_data_from_name_origin(f'Integrated', roi_name)
for ind, data_array in enumerate(dwa.data):
self.measure_data_dict[f'{dwa.labels[ind]}:'] = float(data_array[0])
QtWidgets.QApplication.processEvents()
if not self._display_temporary:
self.data_to_export_signal.emit(self.data_to_export)
self.ROI_changed.emit()
[docs]
def main_spread():
app = QtWidgets.QApplication(sys.argv)
widget = QtWidgets.QWidget()
prog = Viewer2D(widget)
widget.show()
def print_data(data: DataToExport):
print(data)
print('******')
print(data.get_data_from_dim('Data1D'))
prog.data_to_export_signal.connect(print_data)
data_spread = np.load('../../../resources/triangulation_data.npy')
prog.view.get_action('histo').trigger()
prog.view.get_action('autolevels').trigger()
prog.show_data(DataRaw(name='mydata', distribution='spread', data=[data_spread],
axes=[]))
sys.exit(app.exec())
[docs]
def main(data_distribution='uniform'):
"""either 'uniform' or 'spread'"""
app = mkQApp('Viewer2D')
widget = QtWidgets.QWidget()
widget_button = QtWidgets.QWidget()
widget_button.setLayout(QtWidgets.QHBoxLayout())
button = QtWidgets.QPushButton('New Data')
ndata = QtWidgets.QSpinBox()
widget_button.layout().addWidget(button)
widget_button.layout().addWidget(ndata)
def print_data(data: DataToExport):
print(data)
print('******')
print(data.get_data_from_dim('Data1D'))
if data_distribution == 'uniform':
data_to_plot = generate_uniform_data()
elif data_distribution == 'spread':
data_to_plot = generate_uniform_data()
N = 500
sample_x = np.random.randint(0, high=data_to_plot.shape[1], size=(N,))
sample_y = np.random.randint(0, high=data_to_plot.shape[0], size=(N,))
data_spread = np.zeros((N, 3))
for ind in range(N):
data_spread[ind, 2] = data_to_plot.isig[sample_y[ind], sample_x[ind]][0][0]
data_spread[:, 0] = data_to_plot.get_axis_from_index(1)[0].get_data()[sample_x]
data_spread[:, 1] = data_to_plot.get_axis_from_index(0)[0].get_data()[sample_y]
data_to_plot = DataRaw(name='mydata', distribution='spread', data=[data_spread[:, 2]],
nav_indexes=(0,),
axes=[Axis('xaxis', units='xpxl',
data=data_spread[:, 0], index=0, spread_order=0),
Axis('yaxis', units='ypxl',
data=data_spread[:, 1], index=0, spread_order=1)])
prog = Viewer2D(widget)
widget.show()
prog.data_to_export_signal.connect(print_data)
prog.view.get_action('histo').trigger()
prog.view.get_action('autolevels').trigger()
prog.show_data(data_to_plot)
prog.view.show_roi_target(True)
prog.view.move_scale_roi_target((-2, 3), (5, 7))
def print_roi(roi_info):
print(roi_info)
prog.roi_select_signal.connect(print_roi)
button.clicked.connect(lambda: plot_data(prog, ndata.value()))
widget_button.show()
QtWidgets.QApplication.processEvents()
sys.exit(app.exec())
[docs]
def plot_data(viewer2D: Viewer2D, ndata: int = 2):
if ndata > 0:
dwa = generate_uniform_data()
dwa.data = dwa.data[0:ndata]
viewer2D.show_data(dwa)
[docs]
def print_roi_select(rect):
print(rect)
[docs]
def main_view():
app = mkQApp('View2D')
form = QtWidgets.QWidget()
prog = View2D(form)
form.show()
sys.exit(app.exec())
if __name__ == '__main__': # pragma: no cover
#main_view()
main('uniform')
#main('spread')