import dataclasses
from collections import OrderedDict
from pathlib import Path
import sys
from typing import List, Union, Dict
import numpy as np
from qtpy import QtCore, QtWidgets
from qtpy.QtCore import QObject, Slot, Signal,QSignalBlocker
from qtpy.QtGui import QIcon, QPixmap
from pyqtgraph.parametertree.parameterTypes.basetypes import GroupParameter
from pymodaq_utils.utils import find_objects_in_list_from_attr_name_val
from pymodaq_utils.logger import get_module_name, set_logger
from pymodaq_utils.config import GlobalConfig as Config
from pymodaq_data.plotting.utils import PlotColors
from pymodaq_data.post_treatment.process_to_scalar import DataProcessorFactory
from pymodaq_gui.parameter.pymodaq_ptypes import registerParameterType
from pymodaq_gui.parameter import utils as putils
from pymodaq_gui.parameter import ParameterTree, Parameter
from pymodaq_gui.managers.action_manager import QAction
from pymodaq_gui.managers.parameter_manager import ParameterManager
from pymodaq_gui.utils.utils import first_available_integer
from pymodaq_gui.plotting.items.roi_sync import (roi_format, ROISync, RoiParameter,
ROIFactory, ROI, ROIDim)
data_processors = DataProcessorFactory()
logger = set_logger(get_module_name(__file__))
config = Config()
plot_colors = PlotColors()
ROI_NAME_PREFIX = 'ROI_'
ROI2D_TYPES = ROIFactory.get_descriptors_from_dimensionality(ROIDim.ROI2D)
[docs]
class ROIScalableGroup(GroupParameter):
def __init__(self, roi_dim=ROIDim.ROI1D, **opts):
opts['type'] = 'scalable_roigroup_parameter'
opts['addText'] = "Add"
self.roi_dim = roi_dim
if roi_dim == ROIDim.ROI2D:
opts['addList'] = ROI2D_TYPES
# self.color_list = ROIManager.color_list
super().__init__(**opts)
[docs]
def addNew(self, descriptor=''):
name_prefix = ROI_NAME_PREFIX
child_indexes = [int(par.name()[len(name_prefix) + 1:]) for par in self.children()]
if not child_indexes:
newindex = 0
else:
newindex = max(child_indexes) + 1
self.addChild(RoiParameter(self.roi_dim, descriptor, newindex))
registerParameterType('scalable_roigroup_parameter', ROIScalableGroup)
[docs]
class ROIParameterManager(ParameterManager):
def __init__(self, roi_dim=ROIDim.ROI1D):
super().__init__(settings_name='roi_parameters',
action_list=("save", "update", "load", "clear", "search"))
self.roi_dim = roi_dim
self.settings.addChild(ROIScalableGroup(self.roi_dim,
name='rois',
title='ROIs'))
@property
def rois_setting(self) -> ROIScalableGroup:
return self._settings.child('rois')
[docs]
def clear_settings_slot(self):
try:
for child in self.rois_setting.children():
child.remove()
except KeyError:
pass
[docs]
def set_settings(self, settings: Union[Parameter, List[Dict[str, str]], Path]):
""" If empty create the standard structure otherwise empty the ROIs and readd them one by one
for the ROI to reflects the param changes"""
if not hasattr(self, '_settings') or self._settings is None or 'rois' not in [child.name() for child in self._settings.children()]:
super().set_settings(settings)
else:
settings = self.create_parameter(settings)
self.clear_settings_slot()
for child in settings.child('rois').children():
self.rois_setting.addChild(child.saveState())
[docs]
class ROIViewerManager(ROIParameterManager, QtCore.QObject):
new_ROI_signal = Signal(int)
remove_ROI_signal = Signal(int)
# roi_value_changed = Signal(str, tuple)
color_signal = Signal(list)
# roi_update_children = Signal(list)
roi_changed = Signal()
# color_list = np.array(plot_colors)
params = []
def __init__(self, view_box=None, roi_dim=ROIDim.ROI1D):
QtCore.QObject.__init__(self)
ROIParameterManager.__init__(self, roi_dim)
self.view_box: ViewBox = view_box # a viewbox to add ROI into!
self._ROIs: list[ROIMeta] = []
@property
def roiwidget(self):
""" For backcompatibility """
return self.settings_tree
[docs]
def emit_colors(self):
colors = [roi_meta.param['color'] for roi_meta in self._ROIs]
for color in colors:
color.setAlpha(255)
self.color_signal.emit(colors)
@property
def ROIs(self) -> list[ROIMeta]:
return self._ROIs
def __len__(self):
return len(self._ROIs)
[docs]
def get_roi_from_index(self, index: int) -> ROIMeta:
return find_objects_in_list_from_attr_name_val(self._ROIs, 'index', index)[0]
[docs]
def add_roi_programmatically(self, descriptor: str = ROI2D_TYPES[0]):
self.rois_setting.addNew(descriptor)
[docs]
def remove_roi_programmatically(self, index: int):
self.rois_setting.removeChild(self.get_roi_from_index(index).param)
[docs]
def get_ROI_indexes(self):
return [roi.index for roi in self.ROIs]
[docs]
def child_added(self, param: Parameter, data: tuple[RoiParameter, int]):
if data[0].parent() is self.rois_setting:
self.create_and_add(data[0])
[docs]
def create_and_add(self, param: RoiParameter):
roi_meta = ROIMeta(param)
self._ROIs.append(roi_meta)
self.view_box.addItem(roi_meta.roi)
roi_meta.roi.sigRegionChangeFinished.connect(lambda: self.roi_changed.emit())
roi_meta.roi.sigRemoveRequested.connect(lambda: self.remove_ROI(roi_meta))
roi_meta.roi.sigCopyRequested.connect(lambda: self.copy_ROI(roi_meta))
self.new_ROI_signal.emit(param.index)
self.roi_changed.emit()
[docs]
def value_changed(self, param: Parameter):
if param.name() == 'color':
self.emit_colors()
elif param.name() == 'process_data' and isinstance(param.parent(), RoiParameter):
# activate/deactivate the processing of this ROI (read by the filters through roi.compute)
self.get_roi_from_index(param.parent().index).roi.compute = param.value()
self.roi_changed.emit()
[docs]
def param_deleted(self, param: RoiParameter):
roi_meta = find_objects_in_list_from_attr_name_val(
self._ROIs, 'param', param)[0]
self._ROIs.remove(roi_meta)
self.view_box.removeItem(roi_meta.roi)
self.remove_ROI_signal.emit(roi_meta.index)
[docs]
def expand_roi_tree(self, roi):
# Expand roi tree when roi gets double selected
param = self.rois_setting.child(roi_format(roi.index))
isExpanded = not param.opts['expanded']
param.setOpts(expanded=isExpanded)
[docs]
def remove_ROI(self, roi_meta: ROIMeta):
self.remove_roi_programmatically(roi_meta.index)
[docs]
def copy_ROI(self, roi_meta: ROIMeta):
"""Method to copy a ROI and add it to the parameter tree and to the viewer widget
The method extracts the parameters of the copied ROI, create a new parameter, a new ROI and update it with the settings from the copied parameter
Args:
roi (ROI): the ROI to be copied
"""
index = first_available_integer(self.get_ROI_indexes())
#Copy parameter and edit name
param_roi = roi_meta.param
param_state = param_roi.saveState() # Transforming parameter in dict
param_state['name'] = roi_format(index) # Changing name
param_state['index'] = index
param_state['title'] = roi_format(index)
param = RoiParameter(param_roi.roi_dim, descriptor=param_roi.descriptor, index=index)
param.restoreState(param_state)
self.rois_setting.addChild(param)
[docs]
def update_use_channel(self, channels: List[str], index=None):
"""Function to update the selected channels. If no index is given, the channels are applied to all ROIs.
Args:
channels (List[str]): channels list from a viewer
index (int, optional): ROI index. Defaults to None.
"""
if index is not None:
param = self.rois_setting.child(roi_format(index), 'use_channel')
param.setValue(dict(all_items=channels,
selected=channels))
else:
for ind in range(len(self)):
param = self.rois_setting.child(roi_format(ind), 'use_channel')
param.setValue(dict(all_items=channels,
selected=channels))
[docs]
def set_roi(self, roi_params, roi_params_new):
for child, new_child in zip(roi_params, roi_params_new):
if new_child.value():
child.setValue(new_child.value())
self.set_roi(child.children(), new_child.children())
if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)
from pymodaq_gui.plotting.widgets import ImageWidget
from pyqtgraph import PlotWidget, ViewBox
im = ImageWidget()
im = PlotWidget()
prog = ROIViewerManager(im, ROIDim.ROI2D)
widget = QtWidgets.QWidget()
layout = QtWidgets.QHBoxLayout()
widget.setLayout(layout)
layout.addWidget(im)
layout.addWidget(prog.settings_tree)
widget.show()
prog.add_roi_programmatically(ROI2D_TYPES[0])
prog.add_roi_programmatically(ROI2D_TYPES[1])
prog.add_roi_programmatically(ROI2D_TYPES[2])
sys.exit(app.exec())