# Copyright (c) DataLab Platform Developers, BSD 3-Clause license, see LICENSE file."""ROI editor==========The :mod:`cdl.core.gui.roieditor` module provides the ROI editor widgetsfor signals and images.Signal ROI editor-----------------.. autoclass:: SignalROIEditor :members:Image ROI editor----------------.. autoclass:: ImageROIEditor :members:"""# pylint: disable=invalid-name # Allows short reference names like x, y, ...from__future__importannotationsimportabcfromtypingimportTYPE_CHECKING,Generic,TypeVar,Unionfromguidata.configtoolsimportget_iconfromguidata.qthelpersimportadd_actions,create_actionfromplotpy.builderimportmakefromplotpy.interfacesimportIImageItemTypefromplotpy.itemsimport(AnnotatedCircle,AnnotatedPolygon,AnnotatedRectangle,CurveItem,MaskedImageItem,ObjectInfo,XRangeSelection,)fromplotpy.plotimportPlotDialog,PlotManagerfromplotpy.toolsimportCircleTool,HRangeTool,PolygonTool,RectangleToolfromqtpyimportQtCoreasQCfromqtpyimportQtWidgetsasQWimportcdl.objasdlofromcdl.configimportConf,_fromcdl.core.model.baseimport(TypeObj,TypePlotItem,TypeROI,TypeROIItem,configure_roi_item,)fromcdl.core.model.imageimportCircularROI,PolygonalROI,RectangularROIfromcdl.core.model.signalimportSegmentROIfromcdl.envimportexecenvfromcdl.objimportImageObj,ImageROI,SignalObj,SignalROIifTYPE_CHECKING:fromplotpy.plotimportBasePlotfromplotpy.tools.baseimportInteractiveTooldefplot_item_to_single_roi(item:XRangeSelection|AnnotatedRectangle|AnnotatedCircle|AnnotatedPolygon,)->SegmentROI|CircularROI|RectangularROI|PolygonalROI:"""Factory function to create a single ROI object from plot item Args: item: Plot item Returns: Single ROI object """ifisinstance(item,XRangeSelection):cls=SegmentROIelifisinstance(item,AnnotatedRectangle):cls=RectangularROIelifisinstance(item,AnnotatedCircle):cls=CircularROIelifisinstance(item,AnnotatedPolygon):cls=PolygonalROIelse:raiseTypeError(f"Unsupported ROI item type: {type(item)}")returncls.from_plot_item(item)ROI_EDITOR_TOOLBAR_ID="roi_editor_toolbar"defconfigure_roi_item_in_tool(shape,obj:dlo.SignalObj|dlo.ImageObj)->None:"""Configure ROI item in tool"""fmt=obj.get_metadata_option("format")configure_roi_item(shape,fmt,lbl=True,editable=True,option=obj.PREFIX)deftool_activate(tool:InteractiveTool)->None:"""Tool activate"""plot=tool.get_active_plot()plot.select_some_items([])# Deselect all itemstool.activate()deftool_setup_shape(shape:TypeROIItem,obj:dlo.SignalObj|dlo.ImageObj)->None:"""Tool setup shape"""shape.setTitle("ROI")configure_roi_item_in_tool(shape,obj)classROISegmentTool(HRangeTool):"""ROI segment tool"""TITLE=_("Add ROI")ICON="signal_roi.svg"def__init__(self,manager:PlotManager,obj:dlo.SignalObj)->None:super().__init__(manager,switch_to_default_tool=True,toolbar_id=ROI_EDITOR_TOOLBAR_ID)self.roi=SegmentROI([0,1],False)self.obj=objself.activate=tool_activatedefcreate_shape(self)->XRangeSelection:"""Create shape"""shape=self.roi.to_plot_item(self.obj)configure_roi_item_in_tool(shape,self.obj)shape.selected=TruereturnshapeclassROIRectangleTool(RectangleTool):"""ROI rectangle tool"""TITLE=_("Add rectangular ROI")ICON="roi_new_rectangle.svg"def__init__(self,manager:PlotManager,obj:dlo.ImageObj)->None:super().__init__(manager,switch_to_default_tool=True,toolbar_id=ROI_EDITOR_TOOLBAR_ID,setup_shape_cb=tool_setup_shape,)self.roi=RectangularROI([0,0,1,1],True)self.obj=objself.activate=tool_activatedefcreate_shape(self)->tuple[AnnotatedRectangle,int,int]:"""Create shape"""item=self.roi.to_plot_item(self.obj)returnitem,0,2defsetup_shape(self,shape:AnnotatedRectangle)->None:"""Setup shape"""tool_setup_shape(shape,self.obj)classROICircleTool(CircleTool):"""ROI circle tool"""TITLE=_("Add circular ROI")ICON="roi_new_circle.svg"def__init__(self,manager:PlotManager,obj:dlo.ImageObj)->None:super().__init__(manager,switch_to_default_tool=True,toolbar_id=ROI_EDITOR_TOOLBAR_ID,setup_shape_cb=tool_setup_shape,)self.roi=CircularROI([0,0,1],True)self.obj=objself.activate=tool_activatedefcreate_shape(self)->tuple[AnnotatedCircle,int,int]:"""Create shape"""item=self.roi.to_plot_item(self.obj)returnitem,0,1defsetup_shape(self,shape:AnnotatedCircle)->None:"""Setup shape"""tool_setup_shape(shape,self.obj)classROIPolygonTool(PolygonTool):"""ROI polygon tool"""TITLE=_("Add polygonal ROI")ICON="roi_new_polygon.svg"def__init__(self,manager:PlotManager,obj:dlo.ImageObj)->None:super().__init__(manager,switch_to_default_tool=True,toolbar_id=ROI_EDITOR_TOOLBAR_ID,setup_shape_cb=tool_setup_shape,)self.roi=PolygonalROI([[0,0],[1,0],[1,1],[0,1]],True)self.obj=objself.activate=tool_activatedefcreate_shape(self)->tuple[AnnotatedPolygon,int,int]:"""Create shape"""returnself.roi.to_plot_item(self.obj)defsetup_shape(self,shape:AnnotatedPolygon)->None:"""Setup shape"""tool_setup_shape(shape,self.obj)TypeROIEditor=TypeVar("TypeROIEditor",bound="BaseROIEditor")classBaseROIEditorMeta(type(QW.QWidget),abc.ABCMeta):"""Mixed metaclass to avoid conflicts"""classBaseROIEditor(QW.QWidget,Generic[TypeObj,TypeROI,TypePlotItem,TypeROIItem],metaclass=BaseROIEditorMeta,):"""ROI Editor"""ICON_NAME=NoneOBJ_NAME=NoneROI_ITEM_TYPES=()def__init__(self,parent:PlotDialog,obj:TypeObj,extract:bool,item:TypePlotItem|None=None,)->None:super().__init__(parent)self.plot_dialog=parentparent.accepted.connect(self.dialog_accepted)self.plot=parent.get_plot()self.toolbar=QW.QToolBar(self)self.obj=objself.extract=extractself.__modified:bool|None=Noneroi=obj.roiifroiisNone:roi=self.get_obj_roi_class()()self.__roi:TypeROI=roifmt=obj.get_metadata_option("format")self.roi_items:list[TypeROIItem]=list(self.__roi.iterate_roi_items(obj,fmt,True,True))self.add_tools_to_plot_dialog()item=obj.make_item()ifitemisNoneelseitemitem.set_selectable(False)item.set_readonly(True)self.plot.add_item(item)forroi_iteminself.roi_items:self.plot.add_item(roi_item)self.plot.set_active_item(roi_item)self.remove_all_action:QW.QAction|None=Noneself.singleobj_btn:QW.QToolButton|None=Noneself.setup_widget()# force update of ROI titles and remove_all_btn stateself.items_changed(self.plot)self.plot.SIG_ITEMS_CHANGED.connect(self.items_changed)self.plot.SIG_ITEM_REMOVED.connect(self.item_removed)self.plot.SIG_RANGE_CHANGED.connect(lambda_rng,_min,_max:self.item_moved())self.plot.SIG_ANNOTATION_CHANGED.connect(lambda_plt:self.item_moved())# In "extract mode", the dialog box OK button should always been enabled# when at least one ROI is defined,# whereas in non-extract mode (when editing ROIs) the OK button is by default# disabled (until ROI data is modified)ifextract:self.modified=len(self.roi_items)>0else:self.modified=False@abc.abstractmethoddefget_obj_roi_class(self)->type[TypeROI]:"""Get object ROI class"""@abc.abstractmethoddefadd_tools_to_plot_dialog(self)->None:"""Add tools to plot dialog"""@propertydefmodified(self)->bool:"""Return dialog modified state"""returnself.__modified@modified.setterdefmodified(self,value:bool)->None:"""Set dialog modified state"""self.__modified=valueifself.extract:# In "extract mode", OK button is enabled when at least one ROI is definedvalue=valueandlen(self.roi_items)>0self.plot_dialog.button_box.button(QW.QDialogButtonBox.Ok).setEnabled(value)defdialog_accepted(self)->None:"""Parent dialog was accepted: updating ROI Editor data"""self.__roi.empty()forroi_iteminself.roi_items:self.__roi.add_roi(plot_item_to_single_roi(roi_item))ifself.singleobj_btnisnotNone:singleobj=self.singleobj_btn.isChecked()self.__roi.singleobj=singleobjConf.proc.extract_roi_singleobj.set(singleobj)defget_roieditor_results(self)->tuple[TypeROI,bool]:"""Get ROI editor results Returns: A tuple containing the ROI data parameters and ROI modified state. ROI modified state is True if the ROI data has been modified within the dialog box. """returnself.__roi,self.modifieddefcreate_actions(self)->list[QW.QAction]:"""Create actions"""self.remove_all_action=create_action(self,_("Remove all ROIs"),icon=get_icon("roi_delete.svg"),triggered=self.remove_all_rois,)return[None,self.remove_all_action]defsetup_widget(self)->None:"""Setup ROI editor widget"""layout=QW.QHBoxLayout()self.toolbar.setToolButtonStyle(QC.Qt.ToolButtonTextUnderIcon)add_actions(self.toolbar,self.create_actions())layout.addWidget(self.toolbar)ifself.extract:self.singleobj_btn=QW.QCheckBox(_("Extract all ROIs into a single %s object")%self.OBJ_NAME,self,)layout.addWidget(self.singleobj_btn)self.singleobj_btn.setChecked(self.__roi.singleobj)layout.addStretch()self.setLayout(layout)defremove_all_rois(self)->None:"""Remove all ROIs"""if(execenv.unattendedorQW.QMessageBox.question(self,_("Remove all ROIs"),_("Are you sure you want to remove all ROIs?"),)==QW.QMessageBox.Yes):self.plot.del_items(self.roi_items)@abc.abstractmethoddefupdate_roi_titles(self)->None:"""Update ROI annotation titles"""defupdate_roi_items(self)->None:"""Update ROI items"""old_nb_items=len(self.roi_items)self.roi_items=[itemforiteminself.plot.get_items()ifisinstance(item,self.ROI_ITEM_TYPES)]self.plot.select_some_items([])self.update_roi_titles()ifold_nb_items!=len(self.roi_items):self.modified=Truedefitems_changed(self,_plot:BasePlot)->None:"""Items have changed"""self.update_roi_items()self.remove_all_action.setEnabled(len(self.roi_items)>0)defitem_removed(self,item)->None:"""Item was removed. Since all items are read-only except ROIs... this must be an ROI."""assertiteminself.roi_itemsself.update_roi_items()self.modified=Truedefitem_moved(self)->None:"""ROI plot item has just been moved"""self.modified=TrueclassROIRangeInfo(ObjectInfo):"""ObjectInfo for ROI selection"""def__init__(self,roi_items)->None:self.roi_items=roi_itemsdefget_text(self)->str:textlist=[]forindex,roi_iteminenumerate(self.roi_items):x0,x1=roi_item.get_range()textlist.append(f"ROI{index:02d}: {x0} ≤ x ≤ {x1}")return"<br>".join(textlist)
[docs]classSignalROIEditor(BaseROIEditor[SignalObj,SignalROI,CurveItem,XRangeSelection]):"""Signal ROI Editor"""ICON_NAME="signal_roi.svg"OBJ_NAME=_("signal")ROI_ITEM_TYPES=(XRangeSelection,)
[docs]defget_obj_roi_class(self)->type[SignalROI]:"""Get object ROI class"""returnSignalROI
[docs]defadd_tools_to_plot_dialog(self)->None:"""Add tools to plot dialog"""mgr=self.plot_dialog.get_manager()mgr.add_toolbar(self.toolbar,ROI_EDITOR_TOOLBAR_ID)mgr.add_tool(ROISegmentTool,self.obj)
[docs]defsetup_widget(self)->None:"""Setup ROI editor widget"""super().setup_widget()info=ROIRangeInfo(self.roi_items)info_label=make.info_label("BL",info,title=_("Regions of interest"))self.plot.add_item(info_label)self.info_label=info_label
[docs]defupdate_roi_titles(self):"""Update ROI annotation titles"""super().update_roi_titles()self.info_label.update_text()
[docs]classImageROIEditor(BaseROIEditor[ImageObj,ImageROI,MaskedImageItem,# `Union` is mandatory here for Python 3.9-3.10 compatibility:Union[AnnotatedPolygon,AnnotatedRectangle,AnnotatedCircle],]):"""Image ROI Editor"""ICON_NAME="image_roi.svg"OBJ_NAME=_("image")ROI_ITEM_TYPES=(AnnotatedRectangle,AnnotatedCircle,AnnotatedPolygon)
[docs]defget_obj_roi_class(self)->type[ImageROI]:"""Get object ROI class"""returnImageROI
[docs]defadd_tools_to_plot_dialog(self)->None:"""Add tools to plot dialog"""mgr=self.plot_dialog.get_manager()mgr.add_toolbar(self.toolbar,ROI_EDITOR_TOOLBAR_ID)mgr.add_tool(ROIRectangleTool,self.obj)mgr.add_tool(ROICircleTool,self.obj)mgr.add_tool(ROIPolygonTool,self.obj)
[docs]defsetup_widget(self)->None:"""Setup ROI editor widget"""super().setup_widget()item:MaskedImageItem=self.plot.get_items(item_type=IImageItemType)[0]item.set_mask_visible(False)
[docs]defupdate_roi_titles(self)->None:"""Update ROI annotation titles"""super().update_roi_titles()forindex,roi_iteminenumerate(self.roi_items):roi_item:AnnotatedRectangle|AnnotatedCircle|AnnotatedPolygonroi_item.annotationparam.title=f"ROI{index:02d}"roi_item.annotationparam.update_item(roi_item)