epicure.outputing

EpiCure output interface

Handles the onglet Output of EpiCure interface. This panel offers option to export the results in various format or to analyse directly the results in the plugin and display the measures tables, plot and save it.

   1"""
   2    **EpiCure output interface**
   3
   4    Handles the onglet `Output` of EpiCure interface.
   5    This panel offers option to export the results in various format or to analyse directly the results in the plugin and display the measures tables, plot and save it.
   6
   7"""
   8import pandas as pand
   9import numpy as np
  10import roifile
  11from skimage.measure import label
  12import os, time
  13import napari
  14from napari.utils import progress
  15import epicure.Utils as ut
  16import epicure.epiwidgets as wid
  17from epicure.trackmate_export import save_trackmate_xml
  18from epicure.geff_export import save_geff
  19import plotly.express as px
  20from qtpy import QtCore
  21from qtpy.QtCore import Qt
  22from qtpy.QtWidgets import QHBoxLayout, QVBoxLayout, QWidget, QTableWidget, QTableWidgetItem, QGridLayout, QListWidget, QTextBrowser
  23from qtpy.QtWidgets import QAbstractItemView as aiv
  24from random import sample
  25from joblib import Parallel, delayed
  26import webbrowser
  27import tempfile
  28try:
  29    QtCore.QCoreApplication.setAttribute(QtCore.Qt.AA_ShareOpenGLContexts, True)  ## for QtWebEngine import to work on some computers
  30except:
  31    pass
  32
  33from skimage.morphology import disk
  34import skimage
  35if ut.version_above( skimage, "0.25" ):
  36    try:
  37        from skimage.morphology import erosion as binary_erosion 
  38    except:
  39        from skimage.morphology import binary_erosion
  40else:
  41    try:
  42        from skimage.morphology import binary_erosion
  43    except:
  44        from skimage.morphology import erosion as binary_erosion 
  45    
  46
  47class Outputing(QWidget):
  48
  49    def __init__(self, napari_viewer, epic):
  50        """ Initialisation of the interface """
  51        super().__init__()
  52        self.viewer = napari_viewer
  53        self.epicure = epic
  54        self.table = None
  55        self.table_selection = None
  56        self.seglayer = self.viewer.layers["Segmentation"]
  57        self.movlayer = self.viewer.layers["Movie"]
  58        self.selection_choices = ["All cells", "Only selected cell"]
  59        self.output_options = ["", "Export to extern plugins", "Export segmentations", "Measure cell features", "Measure track features", "Export/Measure events", "Save as...", "Save screenshot movie", "Measure vertices"]
  60        self.tplots = None
  61        
  62        chanlist = ["Movie"]
  63        if self.epicure.others is not None:
  64            for chan in self.epicure.others_chanlist:
  65                chanlist.append( "MovieChannel_"+str(chan) )
  66        self.cell_features = CellFeatures( chanlist )
  67        self.event_classes = EventClass( self.epicure ) 
  68        
  69        all_layout = QVBoxLayout()
  70        self.scaled_unit = wid.add_check( "Measures in scaled units", False, check_func=None, descr="Scales the output measures in the given spatio-temporal units (µm, min..)" )
  71        all_layout.addWidget( self.scaled_unit )
  72        self.choose_output = wid.listbox() 
  73        all_layout.addWidget(self.choose_output)
  74        for option in self.output_options:
  75            self.choose_output.addItem(option)
  76        self.choose_output.currentIndexChanged.connect(self.show_output_option)
  77        
  78        ## Choice of active selection
  79        #layout = QVBoxLayout()
  80        selection_layout, self.output_mode = wid.list_line( "Apply on", descr="Choose on which cell(s) to do the action", func=None )
  81        for sel in self.selection_choices:
  82            self.output_mode.addItem(sel)
  83        all_layout.addLayout(selection_layout)
  84       
  85        ## Choice of interface
  86        self.export_group, export_layout = wid.group_layout( "Export to extern plugins" )
  87        griot_btn = wid.add_button( "Current frame to Griottes", self.to_griot, "Launch(in new window) Griottes plugin on current frame" )
  88        export_layout.addWidget(griot_btn)
  89        ncp_btn = wid.add_button( "Current frame to Cluster-Plotter", self.to_ncp, "Launch (in new window) cluster-plotter plugin on current frame" )
  90        export_layout.addWidget(ncp_btn)
  91        self.export_group.setLayout(export_layout)
  92        all_layout.addWidget(self.export_group)
  93        
  94        ## Option to export segmentation results
  95        self.export_seg_group, layout = wid.group_layout(self.output_options[2])
  96        save_line, self.save_choice = wid.button_list( "Save segmentation as", self.save_segmentation, "Save the current segmentation either as ROI, label image or skeleton" ) 
  97        self.save_choice.addItem( "labels" )
  98        self.save_choice.addItem( "ROI" )
  99        self.save_choice.addItem( "skeleton" )
 100        layout.addLayout( save_line )
 101
 102        self.export_seg_group.setLayout(layout)
 103        all_layout.addWidget(self.export_seg_group)
 104
 105        #### Features group
 106        self.feature_group, featlayout = wid.group_layout(self.output_options[3])
 107        
 108        self.choose_features_btn = wid.add_button( "Choose features...", self.choose_features, "Open a window to select the features to measure" )
 109        featlayout.addWidget(self.choose_features_btn)
 110
 111        self.feature_table = wid.add_button( "Create features table", self.show_table, "Measure the selected features and display it as a clickable table" )
 112        featlayout.addWidget(self.feature_table)
 113        self.featTable = FeaturesTable(self.viewer, self.epicure)
 114        featlayout.addWidget(self.featTable)
 115        
 116        ######## Temporal option  
 117        self.temp_graph = wid.add_button( "Table to temporal graphs...", self.temporal_graphs, "Open a plot interface of measured features temporal evolution" )
 118        featlayout.addWidget(self.temp_graph)
 119        self.temp_graph.setEnabled(False)
 120       
 121        ######## Drawing option
 122        featmap, self.show_feature_map = wid.list_line( "Draw feature map:", descr="Add a layer with the cells colored by the selected feature value", func=self.show_feature )
 123        featlayout.addLayout(featmap)
 124        orienbtn = wid.add_button( "Draw cell orientation", self.draw_orientation, "Add a layer with each cell main axis orientation and length " )
 125        featlayout.addWidget( orienbtn )
 126
 127        save_tab_line, self.save_format = wid.button_list( "Save features table", self.save_measure_features, "Save the current table in a .csv file" )
 128        self.save_format.addItem( "csv" )
 129        self.save_format.addItem( "xlsx" )
 130        featlayout.addLayout(save_tab_line)
 131
 132        ## skrub table
 133        self.stat_table = wid.add_button( "Open statistiques table...", self.skrub_features, "Open interactive table with the features statistiques (skrub library)" )
 134        featlayout.addWidget(self.stat_table)
 135        
 136        self.feature_group.setLayout(featlayout)
 137        self.feature_group.hide()
 138        all_layout.addWidget(self.feature_group)
 139
 140        ## Track features
 141        self.trackfeat_group, trackfeatlayout = wid.group_layout(self.output_options[4])
 142        self.trackfeat_table = wid.add_button( "Track features table", self.show_trackfeature_table, "Measure track-related feature and show a table by track" )
 143        trackfeatlayout.addWidget(self.trackfeat_table)
 144        self.trackTable = FeaturesTable(self.viewer, self.epicure)
 145        trackfeatlayout.addWidget(self.trackTable)
 146        self.save_table_track = wid.add_button( "Save track table", self.save_table_tracks, "Save the current table in a .csv file" )
 147        trackfeatlayout.addWidget(self.save_table_track)
 148        
 149        self.trackfeat_group.setLayout(trackfeatlayout)
 150        self.trackfeat_group.hide()
 151        all_layout.addWidget(self.trackfeat_group)
 152
 153        ## Option to export/measure events (Fiji ROI or table), + graphs ?
 154        self.handle_event_group, elayout = wid.group_layout(self.output_options[5])
 155        self.choose_events_btn = wid.add_button( "Choose events...", self.choose_events, "Open a window to select the events to export/measure" )
 156        elayout.addWidget( self.choose_events_btn )
 157        save_evt_line, self.save_evt_choice = wid.button_list( "Export events as", self.export_events, "Save the checked events as Fiji ROIs or .csv table" ) 
 158        self.save_evt_choice.addItem( "Fiji ROI" )
 159        self.save_evt_choice.addItem( "CSV File" )
 160        elayout.addLayout( save_evt_line )
 161        count_evt_btn = wid.add_button( "Count events", self.temporal_graphs_events, descr="Create temporal plot of number of events" )
 162        elayout.addWidget( count_evt_btn )
 163
 164        self.handle_event_group.setLayout( elayout )
 165        self.handle_event_group.hide()
 166        all_layout.addWidget( self.handle_event_group )
 167
 168        ## Save TrackMate XML option
 169        self.save_as_group, save_as_layout = wid.group_layout( "Save as..." )
 170        self.save_tm_btn = wid.add_button( "Save as TrackMate XML", self.save_tm_xml, "Save the current segmentation and the optional tracking in a TrackMate XML file" )
 171        self.save_geff_btn = wid.add_button( "Save as GEFF", self.save_geff, "Save the segmentation and tracks to GEFF" )
 172        save_as_layout.addWidget( self.save_tm_btn )
 173        save_as_layout.addWidget( self.save_geff_btn )
 174        
 175        self.save_as_group.setLayout( save_as_layout )
 176        self.save_as_group.hide()
 177        all_layout.addWidget( self.save_as_group )
 178       
 179        ## Save screenshots option
 180        current_frame = ut.current_frame( self.epicure.viewer )
 181        self.screenshot_group, screenshot_layout = wid.group_layout( "Save screenshot movie" )
 182        self.show_scalebar = wid.add_check_tolayout( screenshot_layout, "With the scale bar", True, check_func=None, descr="Show the scale bar in the screenshots" )
 183        sframe_line, self.sframe = wid.slider_line( "From frame", 0, self.epicure.nframes, 1, value=current_frame, show_value=True, slidefunc=None, descr="Frame from which to start saving screenshots" )
 184        eframe_line, self.eframe = wid.slider_line( "To frame", 0, self.epicure.nframes, 1, value=current_frame+1, show_value=True, slidefunc=None, descr="Frame until which to save screenshots" )
 185        screenshot_layout.addLayout( sframe_line )
 186        screenshot_layout.addLayout( eframe_line )
 187        savescreen_btn = wid.add_button( "Save current view", self.screenshot_movie, "Save the current view (with current display parameters) for frame between the two specified frames in a movie" )
 188
 189        screenshot_layout.addWidget(savescreen_btn)
 190        self.screenshot_group.setLayout(screenshot_layout)
 191        all_layout.addWidget(self.screenshot_group)
 192        self.screenshot_group.hide()
 193        
 194        ## Measure vertex options
 195        self.vertex_group, vertices_layout = wid.group_layout( "Measure vertices" )
 196        radius_line, self.vertice_radius = wid.value_line("Vertex radius", "1.25", descr="Radius of a vertex (TCJ) to consider as one point and measure intensities")
 197        display_radius_line, self.vertice_display_radius = wid.value_line("Display radius", "3", descr="Radius of a vertex for DISPLAY only (size of drawing in the layer)")
 198        vertices_layout.addLayout(radius_line)
 199        vertices_layout.addLayout(display_radius_line)
 200        self.vertices_btn = wid.add_button( "Measure", self.show_vertices_table, "Measure the vertices (connectivity, intensity)" )
 201        vertices_layout.addWidget( self.vertices_btn )
 202        self.verticesTable = FeaturesTable(self.viewer, self.epicure)
 203        vertices_layout.addWidget(self.verticesTable)
 204        self.save_table_vertices = wid.add_button( "Save vertices table", self.save_vertices_table, "Save the current table in a .csv file" )
 205        vertices_layout.addWidget(self.save_table_vertices)
 206        
 207        self.vertex_group.setLayout( vertices_layout )
 208        all_layout.addWidget(self.vertex_group)
 209        self.vertex_group.hide()
 210        
 211        ## Finished
 212        self.setLayout(all_layout)
 213        self.show_output_option()
 214
 215    def get_current_settings( self ):
 216        """ Returns current settings of the widget """
 217        disp = {}
 218        disp["Apply on"] = self.output_mode.currentText() 
 219        disp["Current option"] = self.choose_output.currentText()
 220        disp["Show scalebar"] = self.show_scalebar.isChecked()
 221        disp = self.cell_features.get_current_settings( disp )
 222        disp = self.event_classes.get_current_settings( disp )
 223        return disp
 224
 225    def apply_settings( self, settings ):
 226        """ Set the current state of the widget from preferences if any """
 227        for setting, val in settings.items():
 228            if setting == "Apply on":
 229                self.output_mode.setCurrentText( val )
 230            if setting == "Current option":
 231                self.choose_output.setCurrentText( val )
 232            if setting == "Show scalebar":
 233                self.show_scalebar.setChecked( val )
 234            
 235        self.cell_features.apply_settings( settings )
 236        self.event_classes.apply_settings( settings )
 237
 238    def screenshot_movie( self ):
 239        """ Save screenshots of the current view """
 240        scale_visibility = self.viewer.scale_bar.visible
 241        current_frame = ut.current_frame( self.epicure.viewer )
 242        self.viewer.scale_bar.visible = self.show_scalebar.isChecked()
 243        start_frame = max( self.sframe.value(), 0 )
 244        end_frame = min( self.eframe.value(), self.epicure.nframes )
 245        outname = os.path.join( self.epicure.outdir, self.epicure.imgname+"_screenshots_f"+str(start_frame)+"-"+str(end_frame)+".tif" )
 246        if os.path.exists(outname):
 247            os.remove(outname)
 248        if start_frame > end_frame:
 249            ut.show_warning("From frame > to frame, no screenshot saved")
 250            return
 251        for frame in range(start_frame, end_frame+1):
 252            self.viewer.dims.set_point(0, frame)
 253            shot = self.viewer.screenshot( canvas_only=True, flash=False )
 254            ut.appendToTif( shot, outname )
 255        self.viewer.scale_bar.visible = scale_visibility
 256        self.viewer.dims.set_point(0, current_frame)
 257        ut.show_info( "Screenshot movie saved in "+outname )
 258
 259    def events_select( self, event, check ):
 260        """ Check/Uncheck the event in event types list """
 261        if event in self.event_classes.evt_classes:
 262            self.event_classes.evt_classes[ event ][0].setChecked( check )
 263        else:
 264            print(event+" not found in possible event types to export")
 265
 266    def show_output_option(self):
 267        """ Show selected output panel """
 268        cur_option = self.choose_output.currentText()
 269        self.export_group.setVisible( cur_option == "Export to extern plugins" )
 270        self.export_seg_group.setVisible( cur_option == "Export segmentations" )
 271        self.feature_group.setVisible( cur_option == "Measure cell features" )
 272        self.vertex_group.setVisible( cur_option == "Measure vertices" )
 273        self.trackfeat_group.setVisible( cur_option == "Measure track features" )
 274        self.handle_event_group.setVisible( cur_option == "Export/Measure events" )
 275        self.save_as_group.setVisible( cur_option == "Save as..." )
 276        self.screenshot_group.setVisible( cur_option == "Save screenshot movie" )
 277
 278    def get_current_labels( self ):
 279        """ Get the cell labels to process according to current selection of apply on"""
 280        if self.output_mode.currentText() == "Only selected cell": 
 281            lab = self.epicure.seglayer.selected_label
 282            return [lab]
 283        if self.output_mode.currentText() == "All cells": 
 284            return self.epicure.get_labels()
 285        else:
 286            group = self.output_mode.currentText()
 287            label_group = self.epicure.groups[group]
 288            return label_group
 289
 290            
 291    def get_selection_name(self):
 292        if self.output_mode.currentText() == "Only selected cell": 
 293            lab = self.epicure.seglayer.selected_label
 294            return "_cell_"+str(lab) 
 295        #if self.output_mode.currentText() == "Only checked cells":
 296        #    return "_checked_cells"
 297        if self.output_mode.currentText() == "All cells":
 298            return ""
 299        return "_"+self.output_mode.currentText()
 300
 301    def skrub_features( self ):
 302        """ Open html table interactive and stats with skrub module """
 303        try:
 304            from skrub import TableReport
 305        except:
 306            ut.show_error( "Needs skrub library for this option. Install it (`pip install skrub`) before" )
 307            return
 308        if self.table is None:
 309            ut.show_warning( "Create/update the table before" )
 310            return
 311        report = TableReport( self.table )
 312        report.open()
 313        
 314
 315    def save_measure_features(self):
 316        """ Save measures table to file whether it was created or not """
 317        if self.table is None or self.table_selection is None or self.selection_changed() :
 318            ut.show_warning("Create/update the table before")
 319            return
 320        ext = self.save_format.currentText()
 321        outfile = self.epicure.outname()+"_features"+self.get_selection_name()+"."+ext
 322        if ext == "xlsx":
 323            self.table.to_excel( outfile, sheet_name='EpiCureMeasures' )
 324        else:
 325            self.table.to_csv( outfile, index=False )
 326        if self.epicure.verbose > 0:
 327            ut.show_info("Measures saved in "+outfile)
 328    
 329    def save_table_tracks(self):
 330        """ Save tracks table to file whether it was created or not """
 331        if self.table is None or self.table_selection is None or self.selection_changed() :
 332            ut.show_warning("Create/update the table before")
 333            return
 334        outfile = self.epicure.outname()+"_trackfeatures"+self.get_selection_name()+".xlsx"
 335        self.table.to_excel( outfile, sheet_name='EpiCureTrackMeasures' )
 336        if self.epicure.verbose > 0:
 337            ut.show_info("Track measures saved in "+outfile)
 338
 339
 340    def save_one_roi(self, lab):
 341        """ Save the Rois of cell with label lab """
 342        keep = self.seglayer.data == lab
 343        rois = []
 344        if np.sum(keep) > 0:
 345            ## add 2D case
 346            for iframe, frame in enumerate(keep):
 347                if np.sum(frame) > 0:
 348                    contour = ut.get_contours(frame)
 349                    roi = self.create_roi(contour[0], iframe, lab)
 350                    rois.append(roi)
 351
 352        roifile.roiwrite(self.epicure.outname()+"_rois_cell_"+str(lab)+".zip", rois, mode='w')
 353
 354    def create_roi(self, contour, frame, label):
 355        croi = roifile.ImagejRoi()
 356        croi.version = 227
 357        croi.roitype = roifile.ROI_TYPE(0) ## polygon
 358        croi.n_coordinates = len(contour)
 359        croi.position = frame + 1
 360        croi.t_position = frame+1
 361        coords = []
 362        cent0 = 0
 363        cent1 = 0
 364        for cont in contour:
 365            coords.append([int(cont[1]), int(cont[0])])
 366            cent0 += cont[1]
 367            cent1 += cont[0]
 368        croi.integer_coordinates = np.array(coords)
 369        #croi.top = int(np.min(coords[0]))
 370        #croi.left = int(np.min(coords[1]))
 371        croi.name = str(frame+1).zfill(4)+'-'+str(int(cent0/len(contour))).zfill(4)+"-"+str(int(cent1/len(contour))).zfill(4)
 372        return croi
 373    
 374    def save_segmentation( self ):
 375        """ Save current segmentation in selected format """
 376        if self.output_mode.currentText() == "Only selected cell": 
 377            ## output only the selected cell
 378            lab = self.seglayer.selected_label
 379            if self.save_choice.currentText() == "ROI":
 380                self.save_one_roi(lab)
 381                if self.epicure.verbose > 0:
 382                    ut.show_info("Cell "+str(lab)+" saved to Fiji ROI")
 383                return
 384            else:
 385                tosave = np.zeros(self.seglayer.data.shape, dtype=self.epicure.dtype)
 386                if np.sum(self.seglayer.data==lab) > 0:
 387                    tosave[self.seglayer.data==lab] = lab
 388                endname = "_"+self.save_choice.currentText()+"_"+str(lab)+".tif"
 389        else:
 390            ## output all cells
 391            if self.output_mode.currentText() == "All cells":
 392                if self.save_choice.currentText() == "ROI":
 393                    self.save_all_rois()
 394                    return
 395                tosave = self.seglayer.data
 396                endname = "_"+self.save_choice.currentText()+".tif"
 397            else:
 398                ## or output only selected group
 399                group = self.output_mode.currentText()
 400                label_group = self.epicure.groups[group]
 401                if self.save_choice.currentText() == "ROI":
 402                    ncells = 0
 403                    for lab in label_group:
 404                        self.save_one_roi(lab)
 405                        ncells += 1
 406                    if self.epicure.verbose > 0:
 407                        ut.show_info(str(ncells)+" cells saved to Fiji ROIs")
 408                    return
 409                tosave = np.zeros(self.seglayer.data.shape, dtype=self.epicure.dtype)
 410                endname = "_"+self.save_choice.currentText()+"_"+self.output_mode.currentText()+".tif"
 411                for lab in label_group:
 412                    tosave[self.seglayer.data==lab] = lab
 413        
 414        ## save filled image (for label or skeleton) to file
 415        outname = os.path.join( self.epicure.outdir, self.epicure.imgname+endname )
 416        if self.save_choice.currentText() == "skeleton":
 417            parallel = 0
 418            if self.epicure.process_parallel:
 419                parallel = self.epicure.nparallel
 420            tosave = ut.get_skeleton( tosave, viewer=self.viewer, verbose=self.epicure.verbose, parallel=parallel )
 421            ut.writeTif( tosave, outname, self.epicure.epi_metadata["ScaleXY"], 'uint8', what="Skeleton" )
 422        else:
 423            ut.writeTif(tosave, outname, self.epicure.epi_metadata["ScaleXY"], 'float32', what="Segmentation")
 424                
 425    def save_all_rois( self ):
 426        """ Save all cells to ROI format """
 427        ncells = 0
 428        for lab in np.unique(self.epicure.seglayer.data):
 429            self.save_one_roi(lab)
 430            ncells += 1
 431        if self.epicure.verbose > 0:
 432            ut.show_info(str(ncells)+" cells saved to Fiji ROIs")
 433
 434    def choose_features( self ):
 435        """ Pop-up widget to choose the features to measure """
 436        self.cell_features.choose()
 437    
 438    def show_vertices_table(self):
 439        """ Show the measurement of vertices table """
 440        self.measure_vertices()
 441        self.verticesTable.set_table(self.table)
 442    
 443    def save_vertices_table(self):
 444        """ Save vertices table to file whether it was created or not """
 445        if self.table is None:
 446            ut.show_warning("Create/update the table before")
 447            return
 448        outfile = self.epicure.outname()+"_vertices"+".xlsx"
 449        self.table.to_excel( outfile, sheet_name='EpiCureVerticesMeasures' )
 450        if self.epicure.verbose > 0:
 451            ut.show_info("Vertices measures saved in "+outfile)
 452
 453
 454    def measure_vertices(self):
 455        """ Get all vertices (TCJ) and measure their properties """
 456        def nb_neighbors(regionmask, labimg):
 457            """ Measure the nb of neighbors (labels) around each point """
 458            #footprint = disk(radius=8)
 459            #dilated = binary_dilation(regionmask, footprint)
 460            labels = np.unique(labimg[regionmask]).tolist()
 461            nb_nei = len(labels)
 462            if 0 in labels:
 463                nb_nei = nb_nei - 1
 464            return nb_nei 
 465
 466        self.table = None
 467        radius = float(self.vertice_radius.text()) 
 468        display_radius = float(self.vertice_display_radius.text()) 
 469        ## difference between the measured radius and the displayed radius
 470        diff_radius = display_radius - radius
 471        if diff_radius < 0:
 472            diff_radius = 0
 473        parallel = 0
 474        if self.epicure.process_parallel:
 475            parallel = self.epicure.nparallel
 476        ## Get the vertices: junctions of several skeleton lines
 477        vertex_img = ut.get_vertices( self.epicure.seg, viewer=None, verbose=self.epicure.verbose, parallel=parallel )
 478        vertices_img = np.zeros(vertex_img.shape, dtype=np.int8)
 479        ## Individualise, measure, draw
 480        for ind, frame in enumerate(vertex_img):
 481            props = ut.binary_properties(frame)
 482            nvertex = len(props)
 483            vertices = []
 484            for prop in props:
 485                if prop.label > 0:
 486                    pt = prop.centroid
 487                    vertices.append(pt)
 488            vert_img = ut.draw_points(vertices, vertex_img.shape[1:], radius=radius)
 489            props = ut.binary_properties(vert_img)
 490            if nvertex != len(props):
 491                ## one or more vertices had been merged
 492                vertices = []
 493                for prop in props:
 494                    if prop.label > 0:
 495                        pt = prop.centroid
 496                        vertices.append(pt)
 497                vert_img = ut.draw_points(vertices, vertex_img.shape[1:], radius=radius)
 498            #vertices_img[ind] = vert_img
 499            lbl_img = label(vert_img)
 500            int_measures = pand.DataFrame(ut.regionprops_table(lbl_img, self.movlayer.data[ind], properties=["label", "centroid", "intensity_mean"]))
 501            ## expand to measure neighbors
 502            exp_lbl = ut.touching_labels(lbl_img, expand=3)
 503            measures = pand.DataFrame(ut.regionprops_table(exp_lbl, self.epicure.seg[ind], properties=["label"], extra_properties=[nb_neighbors] ))
 504            ## Color the vertices by their number of neighbors
 505            for lab in measures["label"]:
 506                vertices_img[ind][lbl_img==lab] = int(measures.loc[measures["label"]==lab,"nb_neighbors"].iloc[0])
 507            df = pand.merge(int_measures, measures, on="label", how="inner")
 508            df["Frame"] = ind
 509            
 510            if self.table is None:
 511                self.table = df
 512            else:
 513                self.table = pand.concat([self.table, df])
 514
 515            ## Expand for display only
 516            vertices_img[ind] = ut.touching_labels(vertices_img[ind], expand=diff_radius)
 517
 518        ## Display the vertices in a new layer
 519        ut.remove_layer(self.viewer, "Vertices") # in case already present
 520        self.viewer.add_labels(vertices_img, blending="additive", name="Vertices",  scale=self.viewer.layers["Movie"].scale, opacity=1)
 521
 522    def measure_features(self):
 523        """ Measure features and put them to table """
 524        thick = self.epicure.thickness
 525
 526        def intensity_junction_cytoplasm(regionmask, intensity):
 527            """ Measure the intensity only on the contour of regionmask """
 528            footprint = disk(radius=thick)
 529            inside = binary_erosion(regionmask, footprint)
 530            inside_intensity = ut.mean_nonzero(intensity*inside)
 531            periph_intensity = ut.mean_nonzero(intensity*(regionmask^inside))
 532            return inside_intensity, periph_intensity
 533        
 534        if self.epicure.verbose > 0:
 535            print("Measuring features")
 536        #self.viewer.window._status_bar._toggle_activity_dock(True)
 537        pb = ut.start_progress( self.viewer, total=2, descr="Measuring cells in all movie" )
 538        start_time = time.time()
 539        if self.output_mode.currentText() == "Only selected cell": 
 540            meas = np.zeros(self.epicure.seglayer.data.shape, self.epicure.dtype)
 541            lab = self.epicure.seglayer.selected_label
 542            meas[self.epicure.seglayer.data==lab] = lab
 543        else:
 544            if self.output_mode.currentText() == "All cells": 
 545                meas = self.epicure.seglayer.data
 546            else:
 547                group = self.output_mode.currentText()
 548                meas = np.zeros(self.epicure.seglayer.data.shape, self.epicure.dtype)
 549                label_group = self.epicure.groups[group]
 550                for lab in label_group:
 551                    meas[self.epicure.seglayer.data==lab] = lab
 552            
 553        properties, prop_extra, other_features, int_feat, int_extrafeat = self.cell_features.get_features()
 554        do_channels = self.cell_features.get_channels()
 555        extra_prop = []
 556        if "intensity_junction_cytoplasm" in int_extrafeat:
 557            extra_prop = extra_prop + [intensity_junction_cytoplasm]
 558
 559        extra_properties = []
 560        if (do_channels is not None) and ("Movie" in do_channels):
 561            properties = properties + int_feat
 562            for extra in int_extrafeat:
 563                if extra == "intensity_junction_cytoplasm":
 564                    extra_properties = extra_properties + [intensity_junction_cytoplasm]
 565        
 566        pb.update()
 567        labgroups = self.epicure.group_of_labels()
 568        pb.total = self.epicure.nframes
 569        chan_dict = dict()
 570        if ( do_channels is not None ):
 571            for chan in do_channels:
 572                if chan == "Movie":
 573                    continue
 574                chan_dict[chan] = self.viewer.layers[chan].data
 575        seg = self.epicure.seg
 576        mov = self.movlayer.data
 577        
 578        def measure_one_frame_collect( img, frame ):
 579            """ Measure on one frame and return a list of dicts for each label """
 580            #pb.update()
 581            intimg = mov[frame]
 582            frame_table = pand.DataFrame( ut.labels_table(img, intensity_image=intimg, properties=properties, extra_properties=extra_properties) )
 583            if "group" in other_features:
 584                frame_table["group"] = frame_table["label"].map(labgroups).fillna("Ungrouped")
 585            frame_table["frame"] = frame
 586            
 587            # Boundary
 588            if "Boundary" in other_features:
 589                boundimg = seg[frame]
 590                bds = ut.get_boundary_cells(boundimg)
 591                frame_table["Boundary"] = frame_table["label"].isin(bds).astype(int)
 592            # Border
 593            if "Border" in other_features:
 594                bds = ut.get_border_cells(img)
 595                frame_table["Border"] = frame_table["label"].isin(bds).astype(int)
 596            
 597            # Intensity features in other channels
 598            for chan, intimg_chan in chan_dict.items():
 599                intimg_frame = intimg_chan[frame]
 600                frame_tab = ut.labels_table(img, intensity_image=intimg_frame, properties=int_feat, extra_properties=extra_prop)
 601                for add_prop in int_feat:
 602                    frame_table[add_prop+"_"+str(chan)] = frame_tab[add_prop]
 603                if "intensity_junction_cytoplasm-0" in frame_tab.keys():
 604                    frame_table["intensity_cytoplasm_"+str(chan)] = frame_tab["intensity_junction_cytoplasm-0"]
 605                    frame_table["intensity_junction_"+str(chan)] = frame_tab["intensity_junction_cytoplasm-1"]
 606            
 607            if prop_extra != []:
 608                if "shape_index" in prop_extra:
 609                    frame_table["shape_index"] = frame_table["perimeter"] / np.sqrt(frame_table["area"])
 610                if "roundness" in prop_extra:
 611                    frame_table["roundness"] = 4*frame_table["area"] /(np.pi * np.power(frame_table["axis_major_length"],2) )
 612                if "aspect_ratio" in prop_extra:
 613                    frame_table["aspect_ratio"] = frame_table["axis_major_length"] / frame_table["axis_minor_length"]
 614
 615            # Neighbor features
 616            do_neighbor = "NbNeighbors" in other_features
 617            get_neighbor = "Neighbors" in other_features
 618            if do_neighbor or get_neighbor:
 619                nimg = seg[frame]
 620                graph = ut.get_neighbor_graph(nimg, distance=3)
 621                all_neighbors = {label: list(graph.adj[label]) for label in graph.nodes}
 622                frame_table["neighborlist"] = frame_table["label"].map(lambda l: all_neighbors.get(l, []))
 623
 624            if do_neighbor:
 625                frame_table["NbNeighbors"] = frame_table["neighborlist"].apply(
 626                lambda x: len(x) if x else -1
 627                )
 628            if get_neighbor:
 629                frame_table["Neighbors"] = frame_table["neighborlist"].apply(
 630                lambda x: "&".join(map(str, x)) if x else ""
 631                )
 632            if do_neighbor or get_neighbor:
 633                frame_table.drop(columns="neighborlist", inplace=True)
 634            return pand.DataFrame( frame_table.to_dict(orient="records") )
 635
 636        if self.epicure.process_parallel:
 637            frame_tables = Parallel( n_jobs=self.epicure.nparallel ) ( 
 638                delayed( measure_one_frame_collect ) ( frame, iframe ) for iframe, frame in enumerate(meas) )
 639        else:
 640            frame_tables = [
 641                measure_one_frame_collect( frame, iframe )
 642                for iframe, frame in (enumerate(meas))
 643            ]
 644        self.table = pand.concat(frame_tables, ignore_index=True)
 645
 646        if "intensity_junction_cytoplasm-0" in self.table.columns:
 647            self.table = self.table.rename(columns={"intensity_junction_cytoplasm-0": "intensity_cytoplasm", "intensity_junction_cytoplasm-1":"intensity_junction"})
 648        self.table_selection = self.selection_choices.index(self.output_mode.currentText())
 649        ut.close_progress( self.viewer, pb )
 650        #self.viewer.window._status_bar._toggle_activity_dock(False)
 651        if self.epicure.verbose > 0:
 652            ut.show_info("Features measured in "+"{:.3f}".format((time.time()-start_time)/60)+" min")
 653
 654    def measure_one_frame(self, img, properties, extra_properties, other_features, channels, int_feat, int_extrafeat, frame, labgroups, prop_extra ):
 655        """ Measure on one frame """
 656        if frame is not None:
 657            intimg = self.movlayer.data[frame]
 658        else:
 659            intimg = self.movlayer.data
 660        first = "label" not in self.table.keys()
 661        nrows = len(self.table["label"]) if "label" in self.table.keys() else 0
 662        
 663        ## add the basic label measures
 664        frame_table = ut.labels_table( img, intensity_image=intimg, properties=properties, extra_properties=extra_properties )
 665        ndata = len(frame_table["label"])
 666        for key, value in frame_table.items():
 667            if first:
 668                self.table[key] = []
 669            self.table[key].extend(list(value))
 670
 671        ## add the frame column
 672        if frame is not None:
 673            if first:
 674                self.table["frame"] = []
 675            self.table["frame"].extend([frame]*ndata)
 676
 677        ## add info of the cell group
 678        if "group" in other_features:
 679            frame_group = [ labgroups[label] if label in labgroups.keys() else "Ungrouped" for label in frame_table["label"] ]
 680            if first:
 681                self.table["group"] = []
 682            self.table["group"].extend( frame_group )
 683
 684        ## add the extra shape features
 685        if prop_extra != []:
 686            if "shape_index" in prop_extra:
 687                si = frame_table["perimeter"] /np.sqrt( frame_table["area"] ) 
 688                if first:
 689                    self.table["shape_index"] = []
 690                self.table["shape_index"].extend( si )
 691            if "roundness" in prop_extra:
 692                rou = 4*frame_table["area"] /(np.pi * np.power(frame_table["axis_major_length"],2) ) 
 693                if first:
 694                    self.table["roundness"] = []
 695                self.table["roundness"].extend( rou )
 696            if "aspect_ratio" in prop_extra:
 697                ar = list( np.array(frame_table["axis_major_length"])/np.array(frame_table["axis_minor_length"]) )
 698                if first:
 699                    self.table["aspect_ratio"] = []
 700                self.table["aspect_ratio"].extend( ar )
 701
 702        ### Measure intensity features in other chanels if option is on
 703        if (channels is not None):
 704            for chan in channels:
 705                ## if it's movie, already measured in the general measure
 706                if chan == "Movie":
 707                    continue
 708                ## otherwise, do a new measure on the selected channels
 709                if frame is not None:
 710                    intimg = self.viewer.layers[chan].data[frame]
 711                else:
 712                    intimg = self.viewer.layers[chan].data
 713                frame_tab = ut.labels_table( img, intensity_image=intimg, properties=int_feat, extra_properties=int_extrafeat )
 714                for add_prop in int_feat:
 715                    if first:
 716                        self.table[add_prop+"_"+chan] = []
 717                    self.table[add_prop+"_"+chan].extend( list(frame_tab[add_prop]) )
 718                if "intensity_junction_cytoplasm-0" in frame_tab.keys():
 719                    if first:
 720                        self.table["intensity_cytoplasm_"+chan] = []
 721                        self.table["intensity_junction_"+str(chan)] = []
 722                    self.table["intensity_cytoplasm_"+chan].extend( list(frame_tab["intensity_junction_cytoplasm-0"]) )
 723                    self.table["intensity_junction_"+str(chan)].extend( list(frame_tab["intensity_junction_cytoplasm-1"]) )
 724                
 725            
 726        ## add features of neighbors relationship with graph
 727        do_neighbor = "NbNeighbors" in other_features
 728        get_neighbor = "Neighbors" in other_features
 729        if do_neighbor or get_neighbor:
 730            if frame is not None:
 731                nimg = self.epicure.seg[frame]
 732            else:
 733                nimg = self.epicure.seg
 734            #start_time = ut.start_time()
 735            graph = ut.get_neighbor_graph( nimg, distance=3 )
 736            
 737            if first:
 738                if do_neighbor:
 739                    self.table["NbNeighbors"] = []
 740                if get_neighbor:
 741                    self.table["Neighbors"] = []
 742            if do_neighbor:
 743                self.table["NbNeighbors"].extend( [-1]*ndata )
 744            if get_neighbor:
 745                self.table["Neighbors"].extend( [""]*ndata )
 746
 747            for label in np.unique(frame_table["label"]):
 748                if label in graph.nodes:
 749                    rlabel = np.where( (frame_table["label"] == label) )[0]
 750                    nneighbor = len(graph.adj[label])
 751                    for ind in rlabel:
 752                        if do_neighbor:
 753                            self.table["NbNeighbors"][ind+nrows] = nneighbor
 754                        if get_neighbor:
 755                            self.table["Neighbors"][ind+nrows] = ""
 756                            sep = ""
 757                            for key in graph.adj[label].keys():
 758                                self.table["Neighbors"][ind+nrows] += sep + str(key)
 759                                sep = "&"
 760            #ut.show_duration( start_time, "Neighborhoods measured" )
 761
 762        ## measure cells on boundary    
 763        if "Boundary" in other_features:
 764            if frame is not None:
 765                boundimg = self.epicure.seg[frame]
 766            else:
 767                boundimg = self.epicure.seg
 768            bounds = ut.get_boundary_cells( boundimg )
 769            if first:
 770                self.table["Boundary"] = []
 771            self.table["Boundary"].extend( [0]*ndata )
 772            for label in np.unique(frame_table["label"]):
 773                if label in bounds:
 774                    rlabel = np.where( (frame_table["label"] == label) )[0]
 775                    for ind in rlabel:
 776                        self.table["Boundary"][ind+nrows] = 1
 777        
 778        ## measure cells on border  
 779        if "Border" in other_features:
 780            bounds = ut.get_border_cells( img )
 781            if first:
 782                self.table["Border"] = []
 783            self.table["Border"].extend( [0]*ndata )
 784            for label in bounds:
 785                rlabel = np.where( (frame_table["label"] == label) )[0]
 786                for ind in rlabel:
 787                    self.table["Border"][ind+nrows] = 1
 788
 789        
 790    def selection_changed(self):
 791        if self.table_selection is None:
 792            return True
 793        return self.output_mode.currentText() != self.selection_choices[self.table_selection]
 794
 795    def update_selection_list(self):
 796        """ Update the possible selection from group cell list """
 797        self.selection_choices = ["Only selected cell", "All cells"]
 798        for group in self.epicure.groups.keys():
 799            self.selection_choices.append(group)
 800        self.output_mode.clear()
 801        for sel in self.selection_choices:
 802            self.output_mode.addItem(sel)
 803
 804    def show_table(self):
 805        """ Show the measurement table """
 806        #disable automatic update (slow)
 807        #if self.table is None:
 808            ## create the table and connect action to update it automatically
 809            #self.output_mode.currentIndexChanged.connect(self.show_table)
 810            #self.measure_other_chanels_cbox.stateChanged.connect(self.show_table)
 811            #self.feature_graph_cbox.stateChanged.connect(self.show_table)
 812            #self.feature_intensity_cbox.stateChanged.connect(self.show_table)
 813            #self.feature_shape_cbox.stateChanged.connect(self.show_table)
 814        
 815        ut.set_active_layer( self.viewer, "Segmentation" )
 816        self.show_feature_map.clear()
 817        self.show_feature_map.addItem("")
 818        laynames = [lay.name for lay in self.viewer.layers]
 819        for lay in laynames:
 820            if lay.startswith("Map_"):
 821                ut.remove_layer(self.viewer, lay)
 822        self.measure_features()
 823        featlist = self.table.keys()
 824        ## Scaling the features
 825        if self.scaled_unit.isChecked():
 826            for feat in featlist:
 827                feat_scale, scaled = self.scale_feature( feat, self.table[feat] )
 828                if feat_scale is not None:
 829                    if (feat_scale[0:4] != "Time") and (feat_scale[0:9] != "centroid-"):
 830                        del self.table[feat]
 831                    self.table[feat_scale] = scaled
 832        featlist = self.table.keys()
 833        ## Adding the list to the feature maps
 834        for feat in featlist:
 835            self.show_feature_map.addItem(feat)
 836        self.featTable.set_table(self.table)
 837        self.temp_graph.setEnabled(True)
 838        if self.tplots is not None:
 839            self.tplots.update_table(self.table)
 840
 841    def scale_feature( self, feat, featVals ):
 842        """ Scale if necessary the feature values """
 843        dist_feats = ["centroid-0", "centroid-1", "perimeter", "axis_major_length", "axis_minor_length", "feret_diameter_max", "equivalent_diameter_area" ]
 844        if feat in dist_feats:
 845            return feat+"_"+self.epicure.epi_metadata["UnitXY"], np.array(featVals)*self.epicure.epi_metadata["ScaleXY"]
 846        area_feats = ["area", "area_convex"]
 847        if feat in area_feats:
 848            return feat+"_"+self.epicure.epi_metadata["UnitXY"]+"²", np.array(featVals)*self.epicure.epi_metadata["ScaleXY"] * self.epicure.epi_metadata["ScaleXY"]
 849        if feat == "frame":
 850            return "Time_"+self.epicure.epi_metadata["UnitT"], np.array(featVals)*self.epicure.epi_metadata["ScaleT"]
 851        return None, None
 852
 853
 854    def show_feature(self):
 855        """ Add the image map of the selected feature """
 856        feat = self.show_feature_map.currentText()
 857        if (feat is not None) and (feat != ""):
 858            if feat in self.table.keys():
 859                values = list(self.table[feat])
 860                if feat == "group":
 861                    for i, val in enumerate(values):
 862                        if (val is None) or (val == 'None'):
 863                            values[i] = 0
 864                        else:
 865                            values[i] = list(self.epicure.groups.keys()).index(val) + 1
 866                labels = list(self.table["label"])
 867                frames = None
 868                if "frame" in self.table:
 869                    frames = list(self.table["frame"])
 870                self.draw_map(labels, values, frames, feat)
 871
 872    def draw_map(self, labels, values, frames, featname):
 873        """ Add image layer of values by label """
 874        ## special feature: orientation, draw the axis instead
 875        self.viewer.window._status_bar._toggle_activity_dock(True)
 876        labels = np.array(labels)
 877        values = np.array(values)
 878        frames = np.array(frames)
 879        def map_frame( iframe, segframe ):
 880            """ Draw one frame of the map """
 881            mask = np.where(frames==iframe)[0]
 882            labs = labels[mask]
 883            vals = values[mask]
 884            mapping = np.zeros(segframe.max()+1)
 885            mapping[:] = np.nan
 886            mapping[labs] = vals 
 887            return mapping[segframe] 
 888
 889        if frames is not None:
 890            ## Plotting a movie
 891            if self.epicure.process_parallel:
 892                mapfeat = Parallel( n_jobs=self.epicure.nparallel) (
 893                    delayed ( map_frame )(iframe, frame ) for iframe, frame in enumerate(self.seglayer.data)
 894                )
 895                mapfeat = np.array(mapfeat)
 896            else:
 897                mapfeat = np.empty(self.epicure.seg.shape, dtype="float16")
 898                mapfeat[:] = np.nan
 899                for iframe in np.unique(frames):
 900                    segdata = self.seglayer.data[iframe]
 901                    mapfeat[iframe] = map_frame( iframe, segdata )
 902        else:
 903            mapfeat = np.empty(self.epicure.seg.shape, dtype="float16")
 904            mapfeat[:] = np.nan
 905            for lab, val in progress(zip(labels, values)):
 906                cell = self.seglayer.data==lab
 907                mapfeat[cell] = val
 908        ut.remove_layer(self.viewer, "Map_"+featname)
 909        self.viewer.add_image(mapfeat, name="Map_"+featname, scale=self.viewer.layers["Segmentation"].scale )
 910        self.viewer.window._status_bar._toggle_activity_dock(False)
 911
 912    def draw_orientation( self ):
 913        """ Display the cells orientation axis in a new layer """
 914        ## check that necessary features are measured
 915        ut.remove_layer( self.viewer, "CellOrientation" )
 916        feats = ["centroid-0", "centroid-1", "orientation"]
 917        if self.table is None:
 918            print("Features centroid and orientation necessary to draw orientation, but are not measured yet")
 919            return
 920        for feat in feats:
 921            if feat not in self.table.keys():
 922                print("Feature "+feat+" necessary to draw orientation, but was not measured")
 923                return
 924        ## ok, can work now
 925        self.viewer.window._status_bar._toggle_activity_dock(True)
 926
 927        ## get the coordinates of the axis lines by getting the cell centroid, main orientation
 928        xs = np.array( self.table["centroid-0"] )
 929        ys = np.array( self.table["centroid-1"] )
 930        angles = np.array( self.table["orientation"] )
 931        lens = np.array( [10]*len(angles) )
 932        oriens = np.zeros( (self.epicure.seg.shape), dtype="uint8" )
 933
 934        ## draw axis length depending on the eccentricity
 935        if "eccentricity" in self.table.keys():
 936            lens = np.array(self.table["eccentricity"]*16)             
 937        
 938        if "frame" in self.table:
 939            frames = np.array( self.table["frame"] ).astype(int)
 940        else:
 941            frames = np.array( [0]*len(angles) )
 942
 943        ## draw the lines in between the two extreme points (using Shape layer is too slow on display for big movies)
 944        npts = 30
 945        xmax = oriens.shape[1]-1
 946        ymax = oriens.shape[2]-1
 947        for i in range(npts):
 948            xas = np.clip(xs - lens/2 * np.cos( angles ) * i/float(npts), 0, xmax).astype(int)
 949            xbs = np.clip(xs + lens/2 * np.cos( angles ) * i/float(npts), 0, xmax).astype(int)
 950            yas = np.clip(ys - lens/2 * np.sin( angles ) * i/float(npts), 0, ymax).astype(int)
 951            ybs = np.clip(ys + lens/2 * np.sin( angles ) * i/float(npts), 0, ymax).astype(int)
 952            oriens[ (frames, xas, yas) ] = 255
 953            oriens[ (frames, xbs, ybs) ] = 255
 954        
 955        self.viewer.add_image( oriens, name="CellOrientation", blending="additive", opacity=1, scale=self.viewer.layers["Segmentation"].scale )
 956        self.viewer.window._status_bar._toggle_activity_dock(False)
 957
 958    ################### Export to other plugins
 959
 960    def to_griot(self):
 961        """ Export current frame to new viewer and makes it ready for Griotte plugin """
 962        try:
 963            from napari_griottes import make_graph
 964        except:
 965            ut.show_error("Plugin napari-griottes is not installed")
 966            return
 967        gview = napari.Viewer()
 968        tframe = ut.current_frame(self.viewer)
 969        segt = self.epicure.seglayer.data[tframe]
 970        touching_frame = self.touching_labels(segt)
 971        gview.add_labels(touching_frame, name="TouchingCells", opacity=1)
 972        gview.window.add_dock_widget(make_graph(), name="Griottes")
 973
 974    def touching_labels(self, labs):
 975        """ Dilate labels so that they all touch """
 976        from skimage.segmentation import find_boundaries
 977        from skimage.morphology import skeletonize
 978        from skimage.morphology import binary_closing, binary_opening
 979        if self.epicure.verbose > 0:
 980            print("********** Generate touching labels image ***********")
 981
 982        ## skeletonize it
 983        skel = skeletonize( binary_closing( find_boundaries(labs), footprint=np.ones((10,10)) ) )
 984        ext = np.zeros(labs.shape, dtype="uint8")
 985        ext[labs==0] = 1
 986        ext = binary_opening(ext, footprint=np.ones((2,2)))
 987        newimg = ut.touching_labels(labs, expand=4)
 988        newimg[ext>0] = 0
 989        return newimg
 990    
 991    def to_ncp(self):
 992        """ Export current frame to new viewer and makes it ready for napari-cluster-plots plugin """
 993        try:
 994            import napari_skimage_regionprops as nsr
 995        except:
 996            ut.show_error("Plugin napari-skimage-regionprops is not installed")
 997            return
 998        gview = napari.Viewer()
 999        tframe = ut.current_frame(self.viewer)
1000        segt = self.epicure.seglayer.data[tframe]
1001        moviet = self.epicure.viewer.layers["Movie"].data[tframe]
1002        lab = gview.add_labels(segt, name="Segmentation[t="+str(tframe)+"]", blending="additive")
1003        im = gview.add_image(moviet, name="Movie[t="+str(tframe)+"]", blending="additive")
1004        if self.epicure.verbose > 0:
1005            print("Measure features with napari-skimage-regionprops plugin...")
1006        nsr.regionprops_table(im.data, lab.data, size=True, intensity=True, perimeter=True, shape=True, position=True, moments=True, napari_viewer=gview)
1007        try:
1008            import napari_clusters_plotter as ncp
1009        except:
1010            ut.show_error("Plugin napari-clusters-plotter is not installed")
1011            return
1012        gview.window.add_dock_widget( ncp.ClusteringWidget(gview) )
1013        gview.window.add_dock_widget( ncp.PlotterWidget(gview) )
1014
1015    ################### Temporal graphs
1016    def temporal_graphs_events( self ):
1017        """ New window with temporal graph of event counts """
1018        if self.tplots is not None:
1019            self.tplots.close()
1020        self.tplots = TemporalPlots( self.viewer, self.epicure )
1021        evt_table = self.count_events()
1022        self.tplots.setTable( evt_table )
1023        self.tplots.show()
1024        self.viewer.dims.events.current_step.connect(self.position_verticalline)
1025
1026
1027    def temporal_graphs(self):
1028        """ New window with temporal graph of the current table selection """
1029        #self.temporal_viewer = napari.Viewer()
1030        self.tplots = TemporalPlots( self.viewer, self.epicure )
1031        self.tplots.setTable(self.table)
1032        self.tplots.show()
1033        #self.plot_wid = self.viewer.window.add_dock_widget( self.tplots, name="Plots" )
1034        self.viewer.dims.events.current_step.connect(self.position_verticalline)
1035    
1036    def on_close_viewer(self):
1037        """ Temporal plots window is closed """
1038        if self.epicure.verbose > 1:
1039            print("Closed viewer")
1040        self.viewer.dims.events.current_step.disconnect(self.position_verticalline)
1041        self.temporal_viewer = None
1042        self.tplots = None
1043
1044    def position_verticalline(self):
1045        """ Place the vertical line in the temporal graph to the current frame """
1046        #try:
1047        #    wid = self.tplots
1048        #except:
1049        #    self.on_close_viewer()
1050        if self.tplots is not None:
1051            self.tplots.move_framepos(self.viewer.dims.current_step[0])
1052
1053    ############### track features 
1054
1055    def show_trackfeature_table(self):
1056        """ Show the measurement of tracks table """
1057        self.measure_track_features()
1058        self.trackTable.set_table( self.table )
1059    
1060    def measure_track_features(self):
1061        """ Measure track features and put them to table """
1062        if self.epicure.verbose > 0:
1063            print("Measuring track features")
1064        self.viewer.window._status_bar._toggle_activity_dock(True)
1065        start_time = time.time()
1066
1067        if self.output_mode.currentText() == "Only selected cell": 
1068            track_ids = self.epicure.seglayer.selected_label
1069        else:
1070            if self.output_mode.currentText() == "All cells": 
1071                track_ids = self.epicure.tracking.get_track_list()
1072            else:
1073                group = self.output_mode.currentText()
1074                track_ids = []
1075                label_group = self.epicure.groups[group]
1076                for lab in label_group:
1077                    track_ids.append(lab)
1078            
1079        properties = ["label", "area", "centroid"]
1080        self.table = None
1081
1082        if type(track_ids) == np.ndarray or type(track_ids)==np.array:
1083            track_ids = track_ids.tolist()
1084        if not type(track_ids) == list:
1085            track_ids = [track_ids]
1086
1087        labgroups = self.epicure.group_of_labels()
1088        frame_group = [ labgroups[label] if label in labgroups.keys() else "Ungrouped" for label in track_ids ]
1089        for itrack, track_id in progress(enumerate(track_ids)):
1090            track_frame = self.measure_one_track( track_id )
1091            track_frame["Group"] = frame_group[itrack]
1092            if self.table is None:
1093                self.table = pand.DataFrame([track_frame])
1094            else:
1095                self.table = pand.concat([self.table, pand.DataFrame([track_frame])])
1096
1097        self.table_selection = self.selection_choices.index(self.output_mode.currentText())
1098        self.viewer.window._status_bar._toggle_activity_dock(False)
1099        if self.epicure.verbose > 0:
1100            ut.show_info("Features measured in "+"{:.3f}".format((time.time()-start_time)/60)+" min")
1101
1102    def measure_one_track( self, track_id ):
1103        """ Measure features of one track """
1104        track_features = self.epicure.tracking.measure_track_features( track_id, self.scaled_unit.isChecked() )
1105        return track_features
1106
1107    ############## Events functions
1108
1109    def choose_events( self ):
1110        """ Pop-up widget to choose the event types to measure/export """
1111        self.event_classes.choose()
1112
1113    def count_events( self ):
1114        """ Count events of selected types """
1115        evt_types = self.event_classes.get_evt_classes()
1116        if self.epicure.verbose > 2:
1117            print("Counting events of type "+str(evt_types)+" " )
1118        
1119        ## keep only events related to selected cells
1120        labels = self.get_current_labels()
1121        ## count each type of event
1122        table = np.zeros(  (self.epicure.nframes,len(evt_types)), dtype="uint8" )        
1123        for itype, evt_type in enumerate( evt_types ):
1124            evts = self.epicure.inspecting.get_events_from_type( evt_type )
1125            if len( evts ) > 0:
1126                for evt_sid in evts:
1127                        pos, label = self.epicure.inspecting.get_event_infos( evt_sid )
1128                        if label in labels:
1129                            table[ pos[0], itype ] += 1
1130        df = pand.DataFrame( data=table, columns=evt_types )
1131        df["frame"] = range(len(df))
1132        df["label"] = [0]*len(df)
1133        return df          
1134
1135    def export_events( self ):
1136        """ Export events of selected types """
1137        evt_types = self.event_classes.get_evt_classes()
1138        export_type = self.save_evt_choice.currentText()
1139        if self.epicure.verbose > 2:
1140            print("Exporting events of type "+str(evt_types)+" to "+export_type )
1141        self.export_events_type_format( evt_types, export_type )
1142        
1143    def export_events_type_format( self, evt_types, export_type ):
1144        """ Export events of selected types in selected format """
1145        ## keep only events related to selected cells
1146        labels = self.get_current_labels()
1147        groups = self.epicure.get_groups( labels )
1148        if export_type == "CSV File":
1149            res = pand.DataFrame( columns=["label", "frame", "posY", "posX", "EventClass", "Group"] )  
1150        ## export each type of event in separate files
1151        for itype, evt_type in enumerate( evt_types ):
1152            evts = self.epicure.inspecting.get_events_from_type( evt_type )
1153            if len( evts ) > 0:
1154                rois = [] 
1155                for evt_sid in evts:
1156                    pos, label = self.epicure.inspecting.get_event_infos( evt_sid )
1157                    ind_lab = np.where( labels==label )
1158                    if len( ind_lab[0] ) > 0:
1159                        grp = groups[ int(ind_lab[0][0]) ]
1160                        if export_type == "Fiji ROI":
1161                            roi = self.create_point_roi( pos, itype )
1162                            rois.append( roi )
1163                        if export_type == "CSV File":
1164                            new_event = pand.DataFrame( [[label, pos[0], pos[1], pos[2], evt_type, grp ]], columns=res.columns )
1165                            res = pand.concat( [res, new_event], ignore_index=True )
1166                if export_type == "Fiji ROI":            
1167                    outfile = self.epicure.outname()+"_rois_"+evt_type +""+self.get_selection_name()+".zip" 
1168                    roifile.roiwrite(outfile, rois, mode='w')
1169                    if self.epicure.verbose > 0:
1170                        print( "Events "+str( evt_type )+" saved in ROI file: "+outfile )
1171            ## dont save anything if empty, just print info to user
1172            else:
1173                if self.epicure.verbose > 0:
1174                    print( "No events of type "+str(evt_type)+"" )
1175        
1176        if export_type == "CSV File":            
1177            outfile = self.epicure.outname()+"_events"+self.get_selection_name()+".csv" 
1178            res.to_csv( outfile,  sep='\t', header=True, index=False )
1179            if self.epicure.verbose > 0:
1180                print( "Events data "+" saved in CSV file: "+outfile )
1181
1182
1183    def create_point_roi( self, pos, cat=0 ):
1184        """ Create a point Fiji ROI """
1185        croi = roifile.ImagejRoi()
1186        croi.version = 227
1187        croi.roitype = roifile.ROI_TYPE(10)
1188        croi.name = str(pos[0]+1).zfill(4)+'-'+str(pos[1]).zfill(4)+"-"+str(pos[2]).zfill(4)
1189        croi.n_coordinates = 1
1190        croi.left = int(pos[2])
1191        croi.top = int(pos[1])
1192        croi.z_position = 1
1193        croi.t_position = pos[0]+1
1194        croi.c_position = 1
1195        croi.integer_coordinates = np.array( [[0,0]] )
1196        croi.stroke_width=3
1197        ncolors = 3
1198        if cat%ncolors == 0:  ## color type 0
1199            croi.stroke_color = b'\xff\x00\x00\xff'
1200        if cat%ncolors == 1:  ## color type 1
1201            croi.stroke_color = b'\xff\x00\xff\x00'
1202        if cat%ncolors == 2:  ## color type 2
1203            croi.stroke_color = b'\xff\xff\x00\x00'
1204        return croi
1205
1206    def save_tm_xml( self ):
1207        """ Save current segmentation and tracking in TrackMate XML format """
1208        outname = os.path.join( self.epicure.outdir, self.epicure.imgname+".xml" )
1209        save_trackmate_xml( self.epicure, outname )
1210        if self.epicure.verbose > 0:
1211            ut.show_info("TrackMate XML saved in "+outname)
1212    
1213    def save_geff( self ):
1214        """ Save current segmentation and tracking in GEFF format """
1215        ## save the label segmentation if it's not saved
1216        labelname = os.path.join( self.epicure.outdir, self.epicure.imgname + "_labels.tif" )
1217        ut.writeTif( self.epicure.seg, labelname, self.epicure.epi_metadata["ScaleXY"], "float32", what="Segmentation" )
1218        ## then export the GEFF file
1219        if self.epicure.tracking.graph is None:
1220            self.epicure.tracking.graph = {}
1221        outname = os.path.join( self.epicure.outdir, self.epicure.imgname+".geff" )
1222        save_geff( self.epicure, outname )
1223        if self.epicure.verbose > 0:
1224            ut.show_info("GEFF file saved in "+outname)
1225
1226
1227class CellFeatures(QWidget):
1228    """ Choice of features to measure """
1229    def __init__(self, chanlist):
1230        super().__init__()
1231        layout = QVBoxLayout()
1232        
1233        self.required = ["label"]
1234        self.features = {}
1235        self.chan_list = None
1236        
1237        other_list = ["group", "NbNeighbors", "Neighbors", "Boundary", "Border"]
1238        feat_layout = self.add_feature_group( other_list, "other" )
1239        layout.addLayout( feat_layout )
1240        sel_all_b = wid.add_button( "Select spatial features", lambda: self.select_all("other"), "Select all spatial features" )
1241        desel_all_b = wid.add_button( "Deselect spatial features", lambda: self.deselect_all("other"), "Deselect all spatial features" )
1242        sel_line_b = wid.hlayout()
1243        sel_line_b.addWidget( sel_all_b )
1244        sel_line_b.addWidget( desel_all_b )
1245        layout.addLayout( sel_line_b )
1246
1247
1248        ## Add shape features
1249        shape_list = ["centroid", "area", "area_convex", "axis_major_length", "axis_minor_length", "feret_diameter_max", "equivalent_diameter_area", "eccentricity", "orientation", "perimeter", "solidity"]
1250        other_shape_list = ["shape_index", "roundness", "aspect_ratio"]
1251        feat_layout = self.add_feature_group( shape_list, "prop" )
1252        feat_extra_layout = self.add_feature_group( other_shape_list, "prop_extra" )
1253        layout.addLayout( feat_layout )
1254        layout.addLayout( feat_extra_layout )
1255        sel_all = wid.add_button( "Select morphology features", lambda: self.select_all("props"), "Select all morphology features" )
1256        desel_all = wid.add_button( "Deselect morphology features", lambda: self.deselect_all("props"), "Deselect all morphology features" )
1257        sel_line = wid.hlayout()
1258        sel_line.addWidget( sel_all )
1259        sel_line.addWidget( desel_all )
1260        layout.addLayout( sel_line )
1261
1262        int_lab = wid.label_line( "Intensity features:")
1263        layout.addWidget( int_lab )
1264        intensity_list = ["intensity_mean", "intensity_min", "intensity_max"]
1265        extra_list = ["intensity_junction_cytoplasm"]
1266        feat_layout = self.add_feature_group( intensity_list, "intensity_prop" )
1267        layout.addLayout( feat_layout )
1268        feat_layout = self.add_feature_group( extra_list, "intensity_extra" )
1269        layout.addLayout( feat_layout )
1270        if len(chanlist) > 1:
1271            chan_lab = wid.label_line( "Measure intensity in channels:" )
1272            layout.addWidget( chan_lab )
1273            self.chan_list = QListWidget()
1274            self.chan_list.addItems( chanlist )
1275            self.chan_list.setSelectionMode(aiv.MultiSelection)
1276            self.chan_list.item(0).setSelected(True)
1277            layout.addWidget( self.chan_list )
1278        
1279        sel_all_int = wid.add_button( "Select intensity features", lambda: self.select_all("intensity"), "Select all spatial features" )
1280        desel_all_int = wid.add_button( "Deselect intensity features", lambda: self.deselect_all("intensity"), "Deselect all spatial features" )
1281        sel_line_int = wid.hlayout()
1282        sel_line_int.addWidget( sel_all_int )
1283        sel_line_int.addWidget( desel_all_int )
1284        layout.addLayout( sel_line_int )
1285
1286        bye = wid.add_button( "Ok", self.close, "Close the window" )
1287        layout.addWidget( bye )
1288        self.setLayout( layout )
1289
1290    def select_all( self, feat ):
1291        """ Select all features of type feat """
1292        if feat == "intensity":
1293            self.select_all( "intensity_prop" )
1294            self.select_all( "intensity_extra" )
1295            return
1296        if feat == "props":
1297            self.select_all( "prop" )
1298            self.select_all( "prop_extra" )
1299            return
1300        for featy, feat_val in self.features.items():
1301            if feat_val[1] == feat:
1302                feat_val[0].setChecked( True )
1303    
1304    def deselect_all( self, feat ):
1305        """ Deselect all features of type feat """
1306        if feat == "intensity":
1307            self.deselect_all( "intensity_prop" )
1308            self.deselect_all( "intensity_extra" )
1309            return
1310        if feat == "props":
1311            self.deselect_all( "prop" )
1312            self.deselect_all( "prop_extra" )
1313            return
1314        for featy, feat_val in self.features.items():
1315            if feat_val[1] == feat:
1316                feat_val[0].setChecked( False )
1317
1318
1319    def add_feature_group( self, feat_list, feat_type ):
1320        """ Add features to the GUI """
1321        layout = QVBoxLayout()
1322        ncols = 3
1323        for i, feat in enumerate(feat_list):
1324            if i%ncols == 0:
1325                line = QHBoxLayout()
1326            feature_check = wid.add_check( ""+feat, True, None, descr="" )
1327            line.addWidget(feature_check)
1328            self.features[ feat ] = [feature_check, feat_type]
1329            if i%ncols == (ncols-1):
1330                layout.addLayout( line )
1331                line = None
1332        if line is not None:
1333            layout.addLayout( line )
1334        return layout
1335
1336
1337    def close( self ):
1338        """ Close the pop-up window """
1339        self.hide()
1340
1341    def choose( self ):
1342        """ Show the interface to select the choices """
1343        self.show()
1344
1345    def get_current_settings( self, setting ):
1346        """ Get current settings of check or not of features """
1347        for feat, feat_cbox in self.features.items():
1348            setting[feat] = feat_cbox[0].isChecked()
1349        return setting
1350
1351    def apply_settings( self, settings ):
1352        """ Set the checkboxes from preferenced settings """
1353        for feat, checked in settings.items():
1354            if feat in self.features.keys():
1355                self.features[feat][0].setChecked( checked )
1356        
1357    def get_features( self ):
1358        """ Returns the list of features to measure """
1359        feats = self.required
1360        feats_extra = []
1361        int_extra_feats = []
1362        int_feats = []
1363        other_feats = []
1364        self.do_intensity = False
1365        for feat, feat_cbox in self.features.items():
1366            if feat_cbox[0].isChecked():
1367                if feat_cbox[1] == "prop":
1368                    feats.append( feat )
1369                if feat_cbox[1] == "prop_extra":
1370                    feats_extra.append( feat )
1371                    if feat == "shape_index":
1372                        if "perimeter" not in feats:
1373                            feats.append("perimeter")
1374                        if "area" not in feats:
1375                            feats.append("area")
1376                    if feat == "roundness":
1377                        if "area" not in feats:
1378                            feats.append("area")
1379                        if "axis_major_length" not in feats:
1380                            feats.append("axis_major_length")
1381                    if feat == "aspect_ratio":
1382                        if "axis_major_length" not in feats:
1383                            feats.append("axis_major_length")
1384                        if "axis_minor_length" not in feats:
1385                            feats.append("axis_minor_length")
1386                if feat_cbox[1] == "other":
1387                    other_feats.append( feat )
1388                if feat_cbox[1] == "intensity_prop":
1389                    int_feats.append( feat )
1390                    self.do_intensity = True
1391                if feat_cbox[1] == "intensity_extra":
1392                    int_extra_feats.append( feat )
1393                    self.do_intensity = True
1394        return feats, feats_extra, other_feats, int_feats, int_extra_feats
1395
1396    def get_channels( self ):
1397        """ Returns the list of channels to measure """
1398        if self.do_intensity:
1399            if self.chan_list is not None:
1400                wid_channels = self.chan_list.selectedItems()
1401                channels = []
1402                for chan in wid_channels:
1403                    channels.append( chan.text() )
1404            else:
1405                channels = ["Movie"]
1406            return channels
1407        return None
1408
1409class EventClass( QWidget ):
1410    """ Choice of event types to export/measure """
1411    def __init__( self, epicure ):
1412        super().__init__()
1413        layout = QVBoxLayout()
1414        
1415        self.evt_classes = {}
1416        possible_classes = epicure.event_class
1417        event_layout = self.add_events( possible_classes )
1418        layout.addLayout( event_layout )
1419
1420        bye = wid.add_button( "Ok", self.close, "Close the window" )
1421        layout.addWidget( bye )
1422        self.setLayout( layout )
1423
1424    def add_events( self, event_list ):
1425        """ Add events to the GUI """
1426        layout = QVBoxLayout()
1427        ncols = 3
1428        for i, event in enumerate( event_list ):
1429            if i%ncols == 0:
1430                line = QHBoxLayout()
1431            event_check = wid.add_check_tolayout( line, ""+event, checked=True, descr="")
1432            self.evt_classes[ event ] = [ event_check ]
1433            if i%ncols == (ncols-1):
1434                layout.addLayout( line )
1435                line = None
1436        if line is not None:
1437            layout.addLayout( line )
1438        return layout
1439
1440
1441    def close( self ):
1442        """ Close the pop-up window """
1443        self.hide()
1444
1445    def choose( self ):
1446        """ Show the interface to select the choices """
1447        self.show()
1448
1449    def get_current_settings( self, setting ):
1450        """ Get current settings of check or not of features """
1451        for event, event_cbox in self.evt_classes.items():
1452            setting[event] = event_cbox[0].isChecked()
1453        return setting
1454
1455    def apply_settings( self, settings ):
1456        """ Set the checkboxes from preferenced settings """
1457        for evt, checked in settings.items():
1458            if evt in self.evt_classes.keys():
1459                self.evt_classes[evt][0].setChecked( checked )
1460        
1461    def get_evt_classes( self ):
1462        """ Returns the list of events to measure """
1463        events = []
1464        for evt, evt_cbox in self.evt_classes.items():
1465            if evt_cbox[0].isChecked():
1466                events.append( evt )
1467        return events
1468
1469class FeaturesTable(QWidget):
1470    """ Widget to visualize and interact with the measurement table """
1471
1472    def __init__(self, napari_viewer, epicure):
1473        super().__init__()
1474        self.viewer = napari_viewer
1475        self.epicure = epicure
1476        self.wid_table = QTableWidget()
1477        self.wid_table.setEditTriggers(QTableWidget.EditTrigger.NoEditTriggers)
1478        self.setLayout(QGridLayout())
1479        self.layout().addWidget(self.wid_table)
1480        self.wid_table.clicked.connect(self.show_label)
1481        self.wid_table.setSortingEnabled(True)
1482
1483    def show_label(self):
1484        """ When click on the table, show selected cell """
1485        if self.wid_table is not None:
1486            row = self.wid_table.currentRow()
1487            self.epicure.seglayer.show_selected_label = False
1488            headers = [self.wid_table.horizontalHeaderItem(ind).text() for ind in range(self.wid_table.columnCount()) ]
1489            labelind = None
1490            if "label" in headers:
1491                labelind = headers.index("label") 
1492            if "Label" in headers:
1493                labelind = headers.index("Label") 
1494            frameind = None
1495            if "frame" in headers:
1496                frameind = headers.index("frame") 
1497            if labelind is not None and labelind >= 0:
1498                lab = int(self.wid_table.item(row, labelind).text())
1499                if frameind is not None:
1500                    ## set current frame to the selected row
1501                    frame = int(self.wid_table.item(row, frameind).text())
1502                    ut.set_frame(self.viewer, frame)
1503                else:
1504                    ## set current frame to the first frame where label or track is present
1505                    frame = self.epicure.tracking.get_first_frame( lab )
1506                    if frame is not None:
1507                        ut.set_frame(self.viewer, frame)
1508                self.epicure.seglayer.selected_label = lab
1509                self.epicure.seglayer.show_selected_label = True
1510
1511
1512    def get_features_list(self):
1513        """ Return list of measured features """
1514        return [ self.wid_table.horizontalHeaderItem(ind).text() for ind in range(self.wid_table.columnCount()) ]
1515
1516    def set_table(self, table):
1517        self.wid_table.clear()
1518        self.wid_table.setRowCount(table.shape[0])
1519        self.wid_table.setColumnCount(table.shape[1])
1520
1521        for c, column in enumerate(table.keys()):
1522            column_name = column
1523            self.wid_table.setHorizontalHeaderItem(c, QTableWidgetItem(column_name))
1524            for r, value in enumerate(table.get(column)):
1525                item = QTableWidgetItem()
1526                item.setData( Qt.EditRole, value)
1527                self.wid_table.setItem(r, c, item)
1528
1529class TemporalPlots(QWidget):
1530    """ Widget to visualize and interact with temporal plots """
1531
1532    def __init__(self, napari_viewer, epicure):
1533        super().__init__()
1534        self.viewer = napari_viewer
1535        self.epicure = epicure
1536        self.features_list = ["frame"]
1537        self.parameter_gui()
1538        self.vline = None
1539        self.ymin = None
1540        #self.viewer.window.add_dock_widget( self.plot_wid, name="Temporal plot" )
1541   
1542    def parameter_gui(self):
1543        """ add widget to choose plotting parameters """
1544        
1545        layout = QVBoxLayout()
1546
1547        ## choice of feature to plot
1548        feat_choice, self.feature_choice = wid.list_line( label="Plot feature", descr="Choose the feature to plot", func=self.plot_feature )
1549        layout.addLayout(feat_choice)
1550        ## option to average by group
1551        ck_line, self.avg_group, self.smooth = wid.double_check( "Average by groups", False, self.plot_feature, "Show a line by cell or a line by group", "Smooth lines", False, self.plot_feature, "Smooth temporally (moving average) the plotted lines" )
1552        layout.addLayout(ck_line)
1553        ## show the plot
1554        self.plot_wid = self.create_plotwidget()
1555        layout.addWidget(self.plot_wid)
1556        ## save plot or save data of the plot
1557        line = wid.double_button( "Save plot image", self.save_plot_image, "Save the grapic in a PNG file", "Save plot data", self.save_plot_data, "Save the value used for the plot in .csv file" )
1558        self.by_label = wid.add_check( "Arranged data by label", False, None, "Save the data with one column by label" )
1559        line.addWidget( self.by_label )
1560        layout.addLayout( line )
1561        self.setLayout(layout)
1562        self.resize(1000,800)
1563
1564    def setTable(self, table):
1565        """ Data table to plot """
1566        self.table = table
1567        self.features_list = self.table.keys()
1568        self.update_feature_list()
1569
1570    def update_table(self, table):
1571        """ Update the current plot with the updated table """
1572        self.table = table
1573        curchoice = self.feature_choice.currentText()
1574        self.features_list = self.table.keys()
1575        self.update_feature_list()
1576        if curchoice in self.features_list:
1577            ind = list(self.features_list).index(curchoice)
1578            self.feature_choice.setCurrentIndex(ind)
1579        self.plot_feature()
1580
1581    def update_feature_list(self):
1582        """ Update the list of feature in the GUI """
1583        self.feature_choice.clear()
1584        for feat in self.features_list:
1585            self.feature_choice.addItem(feat)
1586        if "division" in self.features_list and "extrusion" in self.features_list:
1587            self.feature_choice.addItem( "division&extrusion" )
1588    
1589    def plot_feature(self):
1590        """ Plot the selected feature in the temporal graph """
1591        feat = self.feature_choice.currentText()
1592        if feat == "label":
1593            return
1594        if feat == "":
1595            return
1596        if feat == "division&extrusion":
1597            feat = ["division", "extrusion"]
1598        else:
1599            feat = [feat]
1600        
1601        tab = list( zip(self.table["frame"]) )
1602        labname = []
1603        for ft in feat:
1604            tab = [ (*t, v) for t, v in zip( tab, self.table[ft]) ]
1605        tab = [ (*t, v) for t, v in zip( tab, self.table["label"]) ]
1606        labname.append("label")
1607        if "group" in self.table:
1608            tab = [ (*t, v) for t, v in zip( tab, self.table["group"]) ]
1609            labname.append("group")
1610
1611        self.df = pand.DataFrame( tab, columns=["frame"] + feat + labname )
1612        shape = "linear"
1613        if self.smooth.isChecked():
1614            shape = "spline"
1615        if "group" in self.table and self.avg_group.isChecked():
1616            self.dfmean = self.df.groupby(['group', 'frame'])[feat].mean().reset_index()
1617            self.df.columns.name = 'group'
1618            self.fig = px.line( self.dfmean, x='frame', y=feat, color='group', labels={'frame': 'Time (frame)'}, line_shape=shape, render_mode="svg" )
1619        else:
1620            if len( np.unique(self.df["label"]) ) > 1000:
1621                ut.show_warning( "Too many lines to plot; Using a random subset instead" )
1622                subset = sample( np.unique(self.df["label"]).tolist(), 1000)
1623                subdf = self.df[self.df["label"].isin(subset)]
1624                self.fig = px.line( subdf, x="frame", y=feat[0], color="label", labels={'frame': 'Time (frame)'}, line_shape = shape, render_mode="svg")
1625                if len(feat) > 1:
1626                    addfig = px.line(subdf, x="frame", y=feat[1], color="label", line_shape = shape )
1627                    addfig.update_traces( patch={"line": {"dash":"dot"}} )
1628                    self.fig.add_trace( addfig.data[0] )
1629            else:
1630                self.fig = px.line( self.df, x="frame", y=feat[0], color="label", labels={'frame': 'Time (frame)'}, line_shape = shape, render_mode="svg")
1631                if len(feat) > 1:
1632                    addfig = px.line(self.df, x="frame", y=feat[1], color="label", line_shape = shape )
1633                    addfig.update_traces( patch={"line": {"dash":"dot"}} )
1634                    self.fig.add_trace( addfig.data[0] )
1635    
1636        if self.webengine:
1637            self.browser.setHtml( self.fig.to_html(include_plotlyjs='cdn'))
1638        else:
1639            self.show_plot_in_browser( self.fig.to_html(include_plotlyjs='cdn'))
1640        
1641    def show_plot_in_browser(self,html):
1642        with tempfile.NamedTemporaryFile(mode='w', suffix='.html', delete=False) as f:
1643            f.write(html)
1644            url = 'file://' + f.name
1645            webbrowser.open(url)
1646
1647    def smooth_df( self, df ):
1648        """ Smooth temporally the dataframe by label or by group """
1649        rollsize = 20
1650        ## average on a smaller scale if only few frames
1651        if np.max( self.table["frame"] ) <= 20:
1652            rollsize = 5    
1653        if feat+"_smooth" in self.df.columns:
1654            feat = feat+"_smooth"
1655        else:
1656            self.df[feat+"_smooth"] = self.df[feat].rolling(rollsize, center=True).mean()
1657            #print(self.df)
1658            feat = feat+"_smooth"
1659
1660    def save_plot_image( self ):
1661        """ Save current plot graphic to PNG image """
1662        feat = self.feature_choice.currentText()
1663        outfile = self.epicure.outname()+"_plot_"+feat+".png"
1664        if self.fig is not None:
1665            self.fig.write_image( outfile )
1666        if self.epicure.verbose > 0:
1667            ut.show_info("Measures saved in "+outfile)
1668
1669    def save_plot_data( self ):
1670        """ Save the raw data to redraw the current plot to csv file """
1671        feat = self.feature_choice.currentText()
1672        outfile = self.epicure.outname()+"_time_"+feat+".csv"
1673        if self.avg_group.isChecked():
1674            data = self.dfmean.reset_index()[["frame", "group", feat]]
1675            if self.by_label.isChecked():
1676                df = pand.pivot_table( data, columns="label", index="frame", values=feat )
1677                df.to_csv( outfile,  sep='\t', header=True, index=True )
1678            else:
1679                data[["frame", "group", feat]].to_csv( outfile,  sep='\t', header=True, index=False )
1680        else:
1681            data = self.df.reset_index()[["frame", "label", feat]]
1682            if self.by_label.isChecked():
1683                df = pand.pivot_table( data, columns="label", index="frame", values=feat )
1684                df.to_csv( outfile,  sep='\t', header=True, index=True )
1685            else:
1686                data[["frame", "label", feat]].to_csv( outfile,  sep='\t', header=True, index=False )
1687
1688    def move_framepos(self, frame):
1689        """ Move the vertical line showing the current frame position in the main window """
1690        return
1691        #if self.fig is not None:
1692        #    self.fig.add_vline( x=frame, line_dash="dash", line_color="gray" )
1693        #    self.browser.setHtml( self.fig.to_html(include_plotlyjs='cdn'))
1694
1695
1696
1697    def import_webengineview(self):
1698        """Return QWebEngineView from whichever Qt is available."""
1699        import importlib
1700        try:
1701            # Fall back to Qt5
1702            mod = importlib.import_module("PyQt5.QtWebEngineWidgets")
1703            self.browser = mod.QWebEngineView(self)
1704            return 
1705        except Exception:
1706            pass
1707        
1708        try:
1709            # Try Qt6 first
1710            view = importlib.import_module("PyQt6.QtWebEngineWidgets")
1711            self.browser = view.QWebEngineView( parent=self )
1712            return  
1713        except Exception as e:
1714            print(e)
1715            pass
1716
1717        raise ImportError(
1718            "No QtWebEngine found. Install PyQt6-WebEngine or PyQtWebEngine."
1719        )
1720
1721
1722    def create_plotwidget(self):
1723        """ Create plot window """
1724        try:
1725            self.import_webengineview()
1726            self.webengine = True
1727        except:
1728            self.webengine = False
1729            self.browser = NoEngineViewer()
1730            return self.browser
1731        print(self.webengine)
1732        return self.browser
1733
1734class NoEngineViewer(QWidget):
1735    def __init__(self):
1736        super().__init__()
1737        layout = QVBoxLayout()
1738        self.text_browser = QTextBrowser()
1739        self.text_browser.setHtml("<h2>Plots will be redirected to web browser</h2>" \
1740        "" \
1741        "Your napari installation is using pyside6 that doesn't have the necessary dependency to show the plot in this window interactively. To have this option, reinstall napari with pyqt5 or pyqt6." \
1742        "" \
1743        "Otherwise, you can still see the plot, it will open in your web browser, but will be slower to display, reload the web page if nothing appears." \
1744        "")
1745        layout.addWidget(self.text_browser)
1746        self.setLayout(layout)
class Outputing(PyQt6.QtWidgets.QWidget):
  48class Outputing(QWidget):
  49
  50    def __init__(self, napari_viewer, epic):
  51        """ Initialisation of the interface """
  52        super().__init__()
  53        self.viewer = napari_viewer
  54        self.epicure = epic
  55        self.table = None
  56        self.table_selection = None
  57        self.seglayer = self.viewer.layers["Segmentation"]
  58        self.movlayer = self.viewer.layers["Movie"]
  59        self.selection_choices = ["All cells", "Only selected cell"]
  60        self.output_options = ["", "Export to extern plugins", "Export segmentations", "Measure cell features", "Measure track features", "Export/Measure events", "Save as...", "Save screenshot movie", "Measure vertices"]
  61        self.tplots = None
  62        
  63        chanlist = ["Movie"]
  64        if self.epicure.others is not None:
  65            for chan in self.epicure.others_chanlist:
  66                chanlist.append( "MovieChannel_"+str(chan) )
  67        self.cell_features = CellFeatures( chanlist )
  68        self.event_classes = EventClass( self.epicure ) 
  69        
  70        all_layout = QVBoxLayout()
  71        self.scaled_unit = wid.add_check( "Measures in scaled units", False, check_func=None, descr="Scales the output measures in the given spatio-temporal units (µm, min..)" )
  72        all_layout.addWidget( self.scaled_unit )
  73        self.choose_output = wid.listbox() 
  74        all_layout.addWidget(self.choose_output)
  75        for option in self.output_options:
  76            self.choose_output.addItem(option)
  77        self.choose_output.currentIndexChanged.connect(self.show_output_option)
  78        
  79        ## Choice of active selection
  80        #layout = QVBoxLayout()
  81        selection_layout, self.output_mode = wid.list_line( "Apply on", descr="Choose on which cell(s) to do the action", func=None )
  82        for sel in self.selection_choices:
  83            self.output_mode.addItem(sel)
  84        all_layout.addLayout(selection_layout)
  85       
  86        ## Choice of interface
  87        self.export_group, export_layout = wid.group_layout( "Export to extern plugins" )
  88        griot_btn = wid.add_button( "Current frame to Griottes", self.to_griot, "Launch(in new window) Griottes plugin on current frame" )
  89        export_layout.addWidget(griot_btn)
  90        ncp_btn = wid.add_button( "Current frame to Cluster-Plotter", self.to_ncp, "Launch (in new window) cluster-plotter plugin on current frame" )
  91        export_layout.addWidget(ncp_btn)
  92        self.export_group.setLayout(export_layout)
  93        all_layout.addWidget(self.export_group)
  94        
  95        ## Option to export segmentation results
  96        self.export_seg_group, layout = wid.group_layout(self.output_options[2])
  97        save_line, self.save_choice = wid.button_list( "Save segmentation as", self.save_segmentation, "Save the current segmentation either as ROI, label image or skeleton" ) 
  98        self.save_choice.addItem( "labels" )
  99        self.save_choice.addItem( "ROI" )
 100        self.save_choice.addItem( "skeleton" )
 101        layout.addLayout( save_line )
 102
 103        self.export_seg_group.setLayout(layout)
 104        all_layout.addWidget(self.export_seg_group)
 105
 106        #### Features group
 107        self.feature_group, featlayout = wid.group_layout(self.output_options[3])
 108        
 109        self.choose_features_btn = wid.add_button( "Choose features...", self.choose_features, "Open a window to select the features to measure" )
 110        featlayout.addWidget(self.choose_features_btn)
 111
 112        self.feature_table = wid.add_button( "Create features table", self.show_table, "Measure the selected features and display it as a clickable table" )
 113        featlayout.addWidget(self.feature_table)
 114        self.featTable = FeaturesTable(self.viewer, self.epicure)
 115        featlayout.addWidget(self.featTable)
 116        
 117        ######## Temporal option  
 118        self.temp_graph = wid.add_button( "Table to temporal graphs...", self.temporal_graphs, "Open a plot interface of measured features temporal evolution" )
 119        featlayout.addWidget(self.temp_graph)
 120        self.temp_graph.setEnabled(False)
 121       
 122        ######## Drawing option
 123        featmap, self.show_feature_map = wid.list_line( "Draw feature map:", descr="Add a layer with the cells colored by the selected feature value", func=self.show_feature )
 124        featlayout.addLayout(featmap)
 125        orienbtn = wid.add_button( "Draw cell orientation", self.draw_orientation, "Add a layer with each cell main axis orientation and length " )
 126        featlayout.addWidget( orienbtn )
 127
 128        save_tab_line, self.save_format = wid.button_list( "Save features table", self.save_measure_features, "Save the current table in a .csv file" )
 129        self.save_format.addItem( "csv" )
 130        self.save_format.addItem( "xlsx" )
 131        featlayout.addLayout(save_tab_line)
 132
 133        ## skrub table
 134        self.stat_table = wid.add_button( "Open statistiques table...", self.skrub_features, "Open interactive table with the features statistiques (skrub library)" )
 135        featlayout.addWidget(self.stat_table)
 136        
 137        self.feature_group.setLayout(featlayout)
 138        self.feature_group.hide()
 139        all_layout.addWidget(self.feature_group)
 140
 141        ## Track features
 142        self.trackfeat_group, trackfeatlayout = wid.group_layout(self.output_options[4])
 143        self.trackfeat_table = wid.add_button( "Track features table", self.show_trackfeature_table, "Measure track-related feature and show a table by track" )
 144        trackfeatlayout.addWidget(self.trackfeat_table)
 145        self.trackTable = FeaturesTable(self.viewer, self.epicure)
 146        trackfeatlayout.addWidget(self.trackTable)
 147        self.save_table_track = wid.add_button( "Save track table", self.save_table_tracks, "Save the current table in a .csv file" )
 148        trackfeatlayout.addWidget(self.save_table_track)
 149        
 150        self.trackfeat_group.setLayout(trackfeatlayout)
 151        self.trackfeat_group.hide()
 152        all_layout.addWidget(self.trackfeat_group)
 153
 154        ## Option to export/measure events (Fiji ROI or table), + graphs ?
 155        self.handle_event_group, elayout = wid.group_layout(self.output_options[5])
 156        self.choose_events_btn = wid.add_button( "Choose events...", self.choose_events, "Open a window to select the events to export/measure" )
 157        elayout.addWidget( self.choose_events_btn )
 158        save_evt_line, self.save_evt_choice = wid.button_list( "Export events as", self.export_events, "Save the checked events as Fiji ROIs or .csv table" ) 
 159        self.save_evt_choice.addItem( "Fiji ROI" )
 160        self.save_evt_choice.addItem( "CSV File" )
 161        elayout.addLayout( save_evt_line )
 162        count_evt_btn = wid.add_button( "Count events", self.temporal_graphs_events, descr="Create temporal plot of number of events" )
 163        elayout.addWidget( count_evt_btn )
 164
 165        self.handle_event_group.setLayout( elayout )
 166        self.handle_event_group.hide()
 167        all_layout.addWidget( self.handle_event_group )
 168
 169        ## Save TrackMate XML option
 170        self.save_as_group, save_as_layout = wid.group_layout( "Save as..." )
 171        self.save_tm_btn = wid.add_button( "Save as TrackMate XML", self.save_tm_xml, "Save the current segmentation and the optional tracking in a TrackMate XML file" )
 172        self.save_geff_btn = wid.add_button( "Save as GEFF", self.save_geff, "Save the segmentation and tracks to GEFF" )
 173        save_as_layout.addWidget( self.save_tm_btn )
 174        save_as_layout.addWidget( self.save_geff_btn )
 175        
 176        self.save_as_group.setLayout( save_as_layout )
 177        self.save_as_group.hide()
 178        all_layout.addWidget( self.save_as_group )
 179       
 180        ## Save screenshots option
 181        current_frame = ut.current_frame( self.epicure.viewer )
 182        self.screenshot_group, screenshot_layout = wid.group_layout( "Save screenshot movie" )
 183        self.show_scalebar = wid.add_check_tolayout( screenshot_layout, "With the scale bar", True, check_func=None, descr="Show the scale bar in the screenshots" )
 184        sframe_line, self.sframe = wid.slider_line( "From frame", 0, self.epicure.nframes, 1, value=current_frame, show_value=True, slidefunc=None, descr="Frame from which to start saving screenshots" )
 185        eframe_line, self.eframe = wid.slider_line( "To frame", 0, self.epicure.nframes, 1, value=current_frame+1, show_value=True, slidefunc=None, descr="Frame until which to save screenshots" )
 186        screenshot_layout.addLayout( sframe_line )
 187        screenshot_layout.addLayout( eframe_line )
 188        savescreen_btn = wid.add_button( "Save current view", self.screenshot_movie, "Save the current view (with current display parameters) for frame between the two specified frames in a movie" )
 189
 190        screenshot_layout.addWidget(savescreen_btn)
 191        self.screenshot_group.setLayout(screenshot_layout)
 192        all_layout.addWidget(self.screenshot_group)
 193        self.screenshot_group.hide()
 194        
 195        ## Measure vertex options
 196        self.vertex_group, vertices_layout = wid.group_layout( "Measure vertices" )
 197        radius_line, self.vertice_radius = wid.value_line("Vertex radius", "1.25", descr="Radius of a vertex (TCJ) to consider as one point and measure intensities")
 198        display_radius_line, self.vertice_display_radius = wid.value_line("Display radius", "3", descr="Radius of a vertex for DISPLAY only (size of drawing in the layer)")
 199        vertices_layout.addLayout(radius_line)
 200        vertices_layout.addLayout(display_radius_line)
 201        self.vertices_btn = wid.add_button( "Measure", self.show_vertices_table, "Measure the vertices (connectivity, intensity)" )
 202        vertices_layout.addWidget( self.vertices_btn )
 203        self.verticesTable = FeaturesTable(self.viewer, self.epicure)
 204        vertices_layout.addWidget(self.verticesTable)
 205        self.save_table_vertices = wid.add_button( "Save vertices table", self.save_vertices_table, "Save the current table in a .csv file" )
 206        vertices_layout.addWidget(self.save_table_vertices)
 207        
 208        self.vertex_group.setLayout( vertices_layout )
 209        all_layout.addWidget(self.vertex_group)
 210        self.vertex_group.hide()
 211        
 212        ## Finished
 213        self.setLayout(all_layout)
 214        self.show_output_option()
 215
 216    def get_current_settings( self ):
 217        """ Returns current settings of the widget """
 218        disp = {}
 219        disp["Apply on"] = self.output_mode.currentText() 
 220        disp["Current option"] = self.choose_output.currentText()
 221        disp["Show scalebar"] = self.show_scalebar.isChecked()
 222        disp = self.cell_features.get_current_settings( disp )
 223        disp = self.event_classes.get_current_settings( disp )
 224        return disp
 225
 226    def apply_settings( self, settings ):
 227        """ Set the current state of the widget from preferences if any """
 228        for setting, val in settings.items():
 229            if setting == "Apply on":
 230                self.output_mode.setCurrentText( val )
 231            if setting == "Current option":
 232                self.choose_output.setCurrentText( val )
 233            if setting == "Show scalebar":
 234                self.show_scalebar.setChecked( val )
 235            
 236        self.cell_features.apply_settings( settings )
 237        self.event_classes.apply_settings( settings )
 238
 239    def screenshot_movie( self ):
 240        """ Save screenshots of the current view """
 241        scale_visibility = self.viewer.scale_bar.visible
 242        current_frame = ut.current_frame( self.epicure.viewer )
 243        self.viewer.scale_bar.visible = self.show_scalebar.isChecked()
 244        start_frame = max( self.sframe.value(), 0 )
 245        end_frame = min( self.eframe.value(), self.epicure.nframes )
 246        outname = os.path.join( self.epicure.outdir, self.epicure.imgname+"_screenshots_f"+str(start_frame)+"-"+str(end_frame)+".tif" )
 247        if os.path.exists(outname):
 248            os.remove(outname)
 249        if start_frame > end_frame:
 250            ut.show_warning("From frame > to frame, no screenshot saved")
 251            return
 252        for frame in range(start_frame, end_frame+1):
 253            self.viewer.dims.set_point(0, frame)
 254            shot = self.viewer.screenshot( canvas_only=True, flash=False )
 255            ut.appendToTif( shot, outname )
 256        self.viewer.scale_bar.visible = scale_visibility
 257        self.viewer.dims.set_point(0, current_frame)
 258        ut.show_info( "Screenshot movie saved in "+outname )
 259
 260    def events_select( self, event, check ):
 261        """ Check/Uncheck the event in event types list """
 262        if event in self.event_classes.evt_classes:
 263            self.event_classes.evt_classes[ event ][0].setChecked( check )
 264        else:
 265            print(event+" not found in possible event types to export")
 266
 267    def show_output_option(self):
 268        """ Show selected output panel """
 269        cur_option = self.choose_output.currentText()
 270        self.export_group.setVisible( cur_option == "Export to extern plugins" )
 271        self.export_seg_group.setVisible( cur_option == "Export segmentations" )
 272        self.feature_group.setVisible( cur_option == "Measure cell features" )
 273        self.vertex_group.setVisible( cur_option == "Measure vertices" )
 274        self.trackfeat_group.setVisible( cur_option == "Measure track features" )
 275        self.handle_event_group.setVisible( cur_option == "Export/Measure events" )
 276        self.save_as_group.setVisible( cur_option == "Save as..." )
 277        self.screenshot_group.setVisible( cur_option == "Save screenshot movie" )
 278
 279    def get_current_labels( self ):
 280        """ Get the cell labels to process according to current selection of apply on"""
 281        if self.output_mode.currentText() == "Only selected cell": 
 282            lab = self.epicure.seglayer.selected_label
 283            return [lab]
 284        if self.output_mode.currentText() == "All cells": 
 285            return self.epicure.get_labels()
 286        else:
 287            group = self.output_mode.currentText()
 288            label_group = self.epicure.groups[group]
 289            return label_group
 290
 291            
 292    def get_selection_name(self):
 293        if self.output_mode.currentText() == "Only selected cell": 
 294            lab = self.epicure.seglayer.selected_label
 295            return "_cell_"+str(lab) 
 296        #if self.output_mode.currentText() == "Only checked cells":
 297        #    return "_checked_cells"
 298        if self.output_mode.currentText() == "All cells":
 299            return ""
 300        return "_"+self.output_mode.currentText()
 301
 302    def skrub_features( self ):
 303        """ Open html table interactive and stats with skrub module """
 304        try:
 305            from skrub import TableReport
 306        except:
 307            ut.show_error( "Needs skrub library for this option. Install it (`pip install skrub`) before" )
 308            return
 309        if self.table is None:
 310            ut.show_warning( "Create/update the table before" )
 311            return
 312        report = TableReport( self.table )
 313        report.open()
 314        
 315
 316    def save_measure_features(self):
 317        """ Save measures table to file whether it was created or not """
 318        if self.table is None or self.table_selection is None or self.selection_changed() :
 319            ut.show_warning("Create/update the table before")
 320            return
 321        ext = self.save_format.currentText()
 322        outfile = self.epicure.outname()+"_features"+self.get_selection_name()+"."+ext
 323        if ext == "xlsx":
 324            self.table.to_excel( outfile, sheet_name='EpiCureMeasures' )
 325        else:
 326            self.table.to_csv( outfile, index=False )
 327        if self.epicure.verbose > 0:
 328            ut.show_info("Measures saved in "+outfile)
 329    
 330    def save_table_tracks(self):
 331        """ Save tracks table to file whether it was created or not """
 332        if self.table is None or self.table_selection is None or self.selection_changed() :
 333            ut.show_warning("Create/update the table before")
 334            return
 335        outfile = self.epicure.outname()+"_trackfeatures"+self.get_selection_name()+".xlsx"
 336        self.table.to_excel( outfile, sheet_name='EpiCureTrackMeasures' )
 337        if self.epicure.verbose > 0:
 338            ut.show_info("Track measures saved in "+outfile)
 339
 340
 341    def save_one_roi(self, lab):
 342        """ Save the Rois of cell with label lab """
 343        keep = self.seglayer.data == lab
 344        rois = []
 345        if np.sum(keep) > 0:
 346            ## add 2D case
 347            for iframe, frame in enumerate(keep):
 348                if np.sum(frame) > 0:
 349                    contour = ut.get_contours(frame)
 350                    roi = self.create_roi(contour[0], iframe, lab)
 351                    rois.append(roi)
 352
 353        roifile.roiwrite(self.epicure.outname()+"_rois_cell_"+str(lab)+".zip", rois, mode='w')
 354
 355    def create_roi(self, contour, frame, label):
 356        croi = roifile.ImagejRoi()
 357        croi.version = 227
 358        croi.roitype = roifile.ROI_TYPE(0) ## polygon
 359        croi.n_coordinates = len(contour)
 360        croi.position = frame + 1
 361        croi.t_position = frame+1
 362        coords = []
 363        cent0 = 0
 364        cent1 = 0
 365        for cont in contour:
 366            coords.append([int(cont[1]), int(cont[0])])
 367            cent0 += cont[1]
 368            cent1 += cont[0]
 369        croi.integer_coordinates = np.array(coords)
 370        #croi.top = int(np.min(coords[0]))
 371        #croi.left = int(np.min(coords[1]))
 372        croi.name = str(frame+1).zfill(4)+'-'+str(int(cent0/len(contour))).zfill(4)+"-"+str(int(cent1/len(contour))).zfill(4)
 373        return croi
 374    
 375    def save_segmentation( self ):
 376        """ Save current segmentation in selected format """
 377        if self.output_mode.currentText() == "Only selected cell": 
 378            ## output only the selected cell
 379            lab = self.seglayer.selected_label
 380            if self.save_choice.currentText() == "ROI":
 381                self.save_one_roi(lab)
 382                if self.epicure.verbose > 0:
 383                    ut.show_info("Cell "+str(lab)+" saved to Fiji ROI")
 384                return
 385            else:
 386                tosave = np.zeros(self.seglayer.data.shape, dtype=self.epicure.dtype)
 387                if np.sum(self.seglayer.data==lab) > 0:
 388                    tosave[self.seglayer.data==lab] = lab
 389                endname = "_"+self.save_choice.currentText()+"_"+str(lab)+".tif"
 390        else:
 391            ## output all cells
 392            if self.output_mode.currentText() == "All cells":
 393                if self.save_choice.currentText() == "ROI":
 394                    self.save_all_rois()
 395                    return
 396                tosave = self.seglayer.data
 397                endname = "_"+self.save_choice.currentText()+".tif"
 398            else:
 399                ## or output only selected group
 400                group = self.output_mode.currentText()
 401                label_group = self.epicure.groups[group]
 402                if self.save_choice.currentText() == "ROI":
 403                    ncells = 0
 404                    for lab in label_group:
 405                        self.save_one_roi(lab)
 406                        ncells += 1
 407                    if self.epicure.verbose > 0:
 408                        ut.show_info(str(ncells)+" cells saved to Fiji ROIs")
 409                    return
 410                tosave = np.zeros(self.seglayer.data.shape, dtype=self.epicure.dtype)
 411                endname = "_"+self.save_choice.currentText()+"_"+self.output_mode.currentText()+".tif"
 412                for lab in label_group:
 413                    tosave[self.seglayer.data==lab] = lab
 414        
 415        ## save filled image (for label or skeleton) to file
 416        outname = os.path.join( self.epicure.outdir, self.epicure.imgname+endname )
 417        if self.save_choice.currentText() == "skeleton":
 418            parallel = 0
 419            if self.epicure.process_parallel:
 420                parallel = self.epicure.nparallel
 421            tosave = ut.get_skeleton( tosave, viewer=self.viewer, verbose=self.epicure.verbose, parallel=parallel )
 422            ut.writeTif( tosave, outname, self.epicure.epi_metadata["ScaleXY"], 'uint8', what="Skeleton" )
 423        else:
 424            ut.writeTif(tosave, outname, self.epicure.epi_metadata["ScaleXY"], 'float32', what="Segmentation")
 425                
 426    def save_all_rois( self ):
 427        """ Save all cells to ROI format """
 428        ncells = 0
 429        for lab in np.unique(self.epicure.seglayer.data):
 430            self.save_one_roi(lab)
 431            ncells += 1
 432        if self.epicure.verbose > 0:
 433            ut.show_info(str(ncells)+" cells saved to Fiji ROIs")
 434
 435    def choose_features( self ):
 436        """ Pop-up widget to choose the features to measure """
 437        self.cell_features.choose()
 438    
 439    def show_vertices_table(self):
 440        """ Show the measurement of vertices table """
 441        self.measure_vertices()
 442        self.verticesTable.set_table(self.table)
 443    
 444    def save_vertices_table(self):
 445        """ Save vertices table to file whether it was created or not """
 446        if self.table is None:
 447            ut.show_warning("Create/update the table before")
 448            return
 449        outfile = self.epicure.outname()+"_vertices"+".xlsx"
 450        self.table.to_excel( outfile, sheet_name='EpiCureVerticesMeasures' )
 451        if self.epicure.verbose > 0:
 452            ut.show_info("Vertices measures saved in "+outfile)
 453
 454
 455    def measure_vertices(self):
 456        """ Get all vertices (TCJ) and measure their properties """
 457        def nb_neighbors(regionmask, labimg):
 458            """ Measure the nb of neighbors (labels) around each point """
 459            #footprint = disk(radius=8)
 460            #dilated = binary_dilation(regionmask, footprint)
 461            labels = np.unique(labimg[regionmask]).tolist()
 462            nb_nei = len(labels)
 463            if 0 in labels:
 464                nb_nei = nb_nei - 1
 465            return nb_nei 
 466
 467        self.table = None
 468        radius = float(self.vertice_radius.text()) 
 469        display_radius = float(self.vertice_display_radius.text()) 
 470        ## difference between the measured radius and the displayed radius
 471        diff_radius = display_radius - radius
 472        if diff_radius < 0:
 473            diff_radius = 0
 474        parallel = 0
 475        if self.epicure.process_parallel:
 476            parallel = self.epicure.nparallel
 477        ## Get the vertices: junctions of several skeleton lines
 478        vertex_img = ut.get_vertices( self.epicure.seg, viewer=None, verbose=self.epicure.verbose, parallel=parallel )
 479        vertices_img = np.zeros(vertex_img.shape, dtype=np.int8)
 480        ## Individualise, measure, draw
 481        for ind, frame in enumerate(vertex_img):
 482            props = ut.binary_properties(frame)
 483            nvertex = len(props)
 484            vertices = []
 485            for prop in props:
 486                if prop.label > 0:
 487                    pt = prop.centroid
 488                    vertices.append(pt)
 489            vert_img = ut.draw_points(vertices, vertex_img.shape[1:], radius=radius)
 490            props = ut.binary_properties(vert_img)
 491            if nvertex != len(props):
 492                ## one or more vertices had been merged
 493                vertices = []
 494                for prop in props:
 495                    if prop.label > 0:
 496                        pt = prop.centroid
 497                        vertices.append(pt)
 498                vert_img = ut.draw_points(vertices, vertex_img.shape[1:], radius=radius)
 499            #vertices_img[ind] = vert_img
 500            lbl_img = label(vert_img)
 501            int_measures = pand.DataFrame(ut.regionprops_table(lbl_img, self.movlayer.data[ind], properties=["label", "centroid", "intensity_mean"]))
 502            ## expand to measure neighbors
 503            exp_lbl = ut.touching_labels(lbl_img, expand=3)
 504            measures = pand.DataFrame(ut.regionprops_table(exp_lbl, self.epicure.seg[ind], properties=["label"], extra_properties=[nb_neighbors] ))
 505            ## Color the vertices by their number of neighbors
 506            for lab in measures["label"]:
 507                vertices_img[ind][lbl_img==lab] = int(measures.loc[measures["label"]==lab,"nb_neighbors"].iloc[0])
 508            df = pand.merge(int_measures, measures, on="label", how="inner")
 509            df["Frame"] = ind
 510            
 511            if self.table is None:
 512                self.table = df
 513            else:
 514                self.table = pand.concat([self.table, df])
 515
 516            ## Expand for display only
 517            vertices_img[ind] = ut.touching_labels(vertices_img[ind], expand=diff_radius)
 518
 519        ## Display the vertices in a new layer
 520        ut.remove_layer(self.viewer, "Vertices") # in case already present
 521        self.viewer.add_labels(vertices_img, blending="additive", name="Vertices",  scale=self.viewer.layers["Movie"].scale, opacity=1)
 522
 523    def measure_features(self):
 524        """ Measure features and put them to table """
 525        thick = self.epicure.thickness
 526
 527        def intensity_junction_cytoplasm(regionmask, intensity):
 528            """ Measure the intensity only on the contour of regionmask """
 529            footprint = disk(radius=thick)
 530            inside = binary_erosion(regionmask, footprint)
 531            inside_intensity = ut.mean_nonzero(intensity*inside)
 532            periph_intensity = ut.mean_nonzero(intensity*(regionmask^inside))
 533            return inside_intensity, periph_intensity
 534        
 535        if self.epicure.verbose > 0:
 536            print("Measuring features")
 537        #self.viewer.window._status_bar._toggle_activity_dock(True)
 538        pb = ut.start_progress( self.viewer, total=2, descr="Measuring cells in all movie" )
 539        start_time = time.time()
 540        if self.output_mode.currentText() == "Only selected cell": 
 541            meas = np.zeros(self.epicure.seglayer.data.shape, self.epicure.dtype)
 542            lab = self.epicure.seglayer.selected_label
 543            meas[self.epicure.seglayer.data==lab] = lab
 544        else:
 545            if self.output_mode.currentText() == "All cells": 
 546                meas = self.epicure.seglayer.data
 547            else:
 548                group = self.output_mode.currentText()
 549                meas = np.zeros(self.epicure.seglayer.data.shape, self.epicure.dtype)
 550                label_group = self.epicure.groups[group]
 551                for lab in label_group:
 552                    meas[self.epicure.seglayer.data==lab] = lab
 553            
 554        properties, prop_extra, other_features, int_feat, int_extrafeat = self.cell_features.get_features()
 555        do_channels = self.cell_features.get_channels()
 556        extra_prop = []
 557        if "intensity_junction_cytoplasm" in int_extrafeat:
 558            extra_prop = extra_prop + [intensity_junction_cytoplasm]
 559
 560        extra_properties = []
 561        if (do_channels is not None) and ("Movie" in do_channels):
 562            properties = properties + int_feat
 563            for extra in int_extrafeat:
 564                if extra == "intensity_junction_cytoplasm":
 565                    extra_properties = extra_properties + [intensity_junction_cytoplasm]
 566        
 567        pb.update()
 568        labgroups = self.epicure.group_of_labels()
 569        pb.total = self.epicure.nframes
 570        chan_dict = dict()
 571        if ( do_channels is not None ):
 572            for chan in do_channels:
 573                if chan == "Movie":
 574                    continue
 575                chan_dict[chan] = self.viewer.layers[chan].data
 576        seg = self.epicure.seg
 577        mov = self.movlayer.data
 578        
 579        def measure_one_frame_collect( img, frame ):
 580            """ Measure on one frame and return a list of dicts for each label """
 581            #pb.update()
 582            intimg = mov[frame]
 583            frame_table = pand.DataFrame( ut.labels_table(img, intensity_image=intimg, properties=properties, extra_properties=extra_properties) )
 584            if "group" in other_features:
 585                frame_table["group"] = frame_table["label"].map(labgroups).fillna("Ungrouped")
 586            frame_table["frame"] = frame
 587            
 588            # Boundary
 589            if "Boundary" in other_features:
 590                boundimg = seg[frame]
 591                bds = ut.get_boundary_cells(boundimg)
 592                frame_table["Boundary"] = frame_table["label"].isin(bds).astype(int)
 593            # Border
 594            if "Border" in other_features:
 595                bds = ut.get_border_cells(img)
 596                frame_table["Border"] = frame_table["label"].isin(bds).astype(int)
 597            
 598            # Intensity features in other channels
 599            for chan, intimg_chan in chan_dict.items():
 600                intimg_frame = intimg_chan[frame]
 601                frame_tab = ut.labels_table(img, intensity_image=intimg_frame, properties=int_feat, extra_properties=extra_prop)
 602                for add_prop in int_feat:
 603                    frame_table[add_prop+"_"+str(chan)] = frame_tab[add_prop]
 604                if "intensity_junction_cytoplasm-0" in frame_tab.keys():
 605                    frame_table["intensity_cytoplasm_"+str(chan)] = frame_tab["intensity_junction_cytoplasm-0"]
 606                    frame_table["intensity_junction_"+str(chan)] = frame_tab["intensity_junction_cytoplasm-1"]
 607            
 608            if prop_extra != []:
 609                if "shape_index" in prop_extra:
 610                    frame_table["shape_index"] = frame_table["perimeter"] / np.sqrt(frame_table["area"])
 611                if "roundness" in prop_extra:
 612                    frame_table["roundness"] = 4*frame_table["area"] /(np.pi * np.power(frame_table["axis_major_length"],2) )
 613                if "aspect_ratio" in prop_extra:
 614                    frame_table["aspect_ratio"] = frame_table["axis_major_length"] / frame_table["axis_minor_length"]
 615
 616            # Neighbor features
 617            do_neighbor = "NbNeighbors" in other_features
 618            get_neighbor = "Neighbors" in other_features
 619            if do_neighbor or get_neighbor:
 620                nimg = seg[frame]
 621                graph = ut.get_neighbor_graph(nimg, distance=3)
 622                all_neighbors = {label: list(graph.adj[label]) for label in graph.nodes}
 623                frame_table["neighborlist"] = frame_table["label"].map(lambda l: all_neighbors.get(l, []))
 624
 625            if do_neighbor:
 626                frame_table["NbNeighbors"] = frame_table["neighborlist"].apply(
 627                lambda x: len(x) if x else -1
 628                )
 629            if get_neighbor:
 630                frame_table["Neighbors"] = frame_table["neighborlist"].apply(
 631                lambda x: "&".join(map(str, x)) if x else ""
 632                )
 633            if do_neighbor or get_neighbor:
 634                frame_table.drop(columns="neighborlist", inplace=True)
 635            return pand.DataFrame( frame_table.to_dict(orient="records") )
 636
 637        if self.epicure.process_parallel:
 638            frame_tables = Parallel( n_jobs=self.epicure.nparallel ) ( 
 639                delayed( measure_one_frame_collect ) ( frame, iframe ) for iframe, frame in enumerate(meas) )
 640        else:
 641            frame_tables = [
 642                measure_one_frame_collect( frame, iframe )
 643                for iframe, frame in (enumerate(meas))
 644            ]
 645        self.table = pand.concat(frame_tables, ignore_index=True)
 646
 647        if "intensity_junction_cytoplasm-0" in self.table.columns:
 648            self.table = self.table.rename(columns={"intensity_junction_cytoplasm-0": "intensity_cytoplasm", "intensity_junction_cytoplasm-1":"intensity_junction"})
 649        self.table_selection = self.selection_choices.index(self.output_mode.currentText())
 650        ut.close_progress( self.viewer, pb )
 651        #self.viewer.window._status_bar._toggle_activity_dock(False)
 652        if self.epicure.verbose > 0:
 653            ut.show_info("Features measured in "+"{:.3f}".format((time.time()-start_time)/60)+" min")
 654
 655    def measure_one_frame(self, img, properties, extra_properties, other_features, channels, int_feat, int_extrafeat, frame, labgroups, prop_extra ):
 656        """ Measure on one frame """
 657        if frame is not None:
 658            intimg = self.movlayer.data[frame]
 659        else:
 660            intimg = self.movlayer.data
 661        first = "label" not in self.table.keys()
 662        nrows = len(self.table["label"]) if "label" in self.table.keys() else 0
 663        
 664        ## add the basic label measures
 665        frame_table = ut.labels_table( img, intensity_image=intimg, properties=properties, extra_properties=extra_properties )
 666        ndata = len(frame_table["label"])
 667        for key, value in frame_table.items():
 668            if first:
 669                self.table[key] = []
 670            self.table[key].extend(list(value))
 671
 672        ## add the frame column
 673        if frame is not None:
 674            if first:
 675                self.table["frame"] = []
 676            self.table["frame"].extend([frame]*ndata)
 677
 678        ## add info of the cell group
 679        if "group" in other_features:
 680            frame_group = [ labgroups[label] if label in labgroups.keys() else "Ungrouped" for label in frame_table["label"] ]
 681            if first:
 682                self.table["group"] = []
 683            self.table["group"].extend( frame_group )
 684
 685        ## add the extra shape features
 686        if prop_extra != []:
 687            if "shape_index" in prop_extra:
 688                si = frame_table["perimeter"] /np.sqrt( frame_table["area"] ) 
 689                if first:
 690                    self.table["shape_index"] = []
 691                self.table["shape_index"].extend( si )
 692            if "roundness" in prop_extra:
 693                rou = 4*frame_table["area"] /(np.pi * np.power(frame_table["axis_major_length"],2) ) 
 694                if first:
 695                    self.table["roundness"] = []
 696                self.table["roundness"].extend( rou )
 697            if "aspect_ratio" in prop_extra:
 698                ar = list( np.array(frame_table["axis_major_length"])/np.array(frame_table["axis_minor_length"]) )
 699                if first:
 700                    self.table["aspect_ratio"] = []
 701                self.table["aspect_ratio"].extend( ar )
 702
 703        ### Measure intensity features in other chanels if option is on
 704        if (channels is not None):
 705            for chan in channels:
 706                ## if it's movie, already measured in the general measure
 707                if chan == "Movie":
 708                    continue
 709                ## otherwise, do a new measure on the selected channels
 710                if frame is not None:
 711                    intimg = self.viewer.layers[chan].data[frame]
 712                else:
 713                    intimg = self.viewer.layers[chan].data
 714                frame_tab = ut.labels_table( img, intensity_image=intimg, properties=int_feat, extra_properties=int_extrafeat )
 715                for add_prop in int_feat:
 716                    if first:
 717                        self.table[add_prop+"_"+chan] = []
 718                    self.table[add_prop+"_"+chan].extend( list(frame_tab[add_prop]) )
 719                if "intensity_junction_cytoplasm-0" in frame_tab.keys():
 720                    if first:
 721                        self.table["intensity_cytoplasm_"+chan] = []
 722                        self.table["intensity_junction_"+str(chan)] = []
 723                    self.table["intensity_cytoplasm_"+chan].extend( list(frame_tab["intensity_junction_cytoplasm-0"]) )
 724                    self.table["intensity_junction_"+str(chan)].extend( list(frame_tab["intensity_junction_cytoplasm-1"]) )
 725                
 726            
 727        ## add features of neighbors relationship with graph
 728        do_neighbor = "NbNeighbors" in other_features
 729        get_neighbor = "Neighbors" in other_features
 730        if do_neighbor or get_neighbor:
 731            if frame is not None:
 732                nimg = self.epicure.seg[frame]
 733            else:
 734                nimg = self.epicure.seg
 735            #start_time = ut.start_time()
 736            graph = ut.get_neighbor_graph( nimg, distance=3 )
 737            
 738            if first:
 739                if do_neighbor:
 740                    self.table["NbNeighbors"] = []
 741                if get_neighbor:
 742                    self.table["Neighbors"] = []
 743            if do_neighbor:
 744                self.table["NbNeighbors"].extend( [-1]*ndata )
 745            if get_neighbor:
 746                self.table["Neighbors"].extend( [""]*ndata )
 747
 748            for label in np.unique(frame_table["label"]):
 749                if label in graph.nodes:
 750                    rlabel = np.where( (frame_table["label"] == label) )[0]
 751                    nneighbor = len(graph.adj[label])
 752                    for ind in rlabel:
 753                        if do_neighbor:
 754                            self.table["NbNeighbors"][ind+nrows] = nneighbor
 755                        if get_neighbor:
 756                            self.table["Neighbors"][ind+nrows] = ""
 757                            sep = ""
 758                            for key in graph.adj[label].keys():
 759                                self.table["Neighbors"][ind+nrows] += sep + str(key)
 760                                sep = "&"
 761            #ut.show_duration( start_time, "Neighborhoods measured" )
 762
 763        ## measure cells on boundary    
 764        if "Boundary" in other_features:
 765            if frame is not None:
 766                boundimg = self.epicure.seg[frame]
 767            else:
 768                boundimg = self.epicure.seg
 769            bounds = ut.get_boundary_cells( boundimg )
 770            if first:
 771                self.table["Boundary"] = []
 772            self.table["Boundary"].extend( [0]*ndata )
 773            for label in np.unique(frame_table["label"]):
 774                if label in bounds:
 775                    rlabel = np.where( (frame_table["label"] == label) )[0]
 776                    for ind in rlabel:
 777                        self.table["Boundary"][ind+nrows] = 1
 778        
 779        ## measure cells on border  
 780        if "Border" in other_features:
 781            bounds = ut.get_border_cells( img )
 782            if first:
 783                self.table["Border"] = []
 784            self.table["Border"].extend( [0]*ndata )
 785            for label in bounds:
 786                rlabel = np.where( (frame_table["label"] == label) )[0]
 787                for ind in rlabel:
 788                    self.table["Border"][ind+nrows] = 1
 789
 790        
 791    def selection_changed(self):
 792        if self.table_selection is None:
 793            return True
 794        return self.output_mode.currentText() != self.selection_choices[self.table_selection]
 795
 796    def update_selection_list(self):
 797        """ Update the possible selection from group cell list """
 798        self.selection_choices = ["Only selected cell", "All cells"]
 799        for group in self.epicure.groups.keys():
 800            self.selection_choices.append(group)
 801        self.output_mode.clear()
 802        for sel in self.selection_choices:
 803            self.output_mode.addItem(sel)
 804
 805    def show_table(self):
 806        """ Show the measurement table """
 807        #disable automatic update (slow)
 808        #if self.table is None:
 809            ## create the table and connect action to update it automatically
 810            #self.output_mode.currentIndexChanged.connect(self.show_table)
 811            #self.measure_other_chanels_cbox.stateChanged.connect(self.show_table)
 812            #self.feature_graph_cbox.stateChanged.connect(self.show_table)
 813            #self.feature_intensity_cbox.stateChanged.connect(self.show_table)
 814            #self.feature_shape_cbox.stateChanged.connect(self.show_table)
 815        
 816        ut.set_active_layer( self.viewer, "Segmentation" )
 817        self.show_feature_map.clear()
 818        self.show_feature_map.addItem("")
 819        laynames = [lay.name for lay in self.viewer.layers]
 820        for lay in laynames:
 821            if lay.startswith("Map_"):
 822                ut.remove_layer(self.viewer, lay)
 823        self.measure_features()
 824        featlist = self.table.keys()
 825        ## Scaling the features
 826        if self.scaled_unit.isChecked():
 827            for feat in featlist:
 828                feat_scale, scaled = self.scale_feature( feat, self.table[feat] )
 829                if feat_scale is not None:
 830                    if (feat_scale[0:4] != "Time") and (feat_scale[0:9] != "centroid-"):
 831                        del self.table[feat]
 832                    self.table[feat_scale] = scaled
 833        featlist = self.table.keys()
 834        ## Adding the list to the feature maps
 835        for feat in featlist:
 836            self.show_feature_map.addItem(feat)
 837        self.featTable.set_table(self.table)
 838        self.temp_graph.setEnabled(True)
 839        if self.tplots is not None:
 840            self.tplots.update_table(self.table)
 841
 842    def scale_feature( self, feat, featVals ):
 843        """ Scale if necessary the feature values """
 844        dist_feats = ["centroid-0", "centroid-1", "perimeter", "axis_major_length", "axis_minor_length", "feret_diameter_max", "equivalent_diameter_area" ]
 845        if feat in dist_feats:
 846            return feat+"_"+self.epicure.epi_metadata["UnitXY"], np.array(featVals)*self.epicure.epi_metadata["ScaleXY"]
 847        area_feats = ["area", "area_convex"]
 848        if feat in area_feats:
 849            return feat+"_"+self.epicure.epi_metadata["UnitXY"]+"²", np.array(featVals)*self.epicure.epi_metadata["ScaleXY"] * self.epicure.epi_metadata["ScaleXY"]
 850        if feat == "frame":
 851            return "Time_"+self.epicure.epi_metadata["UnitT"], np.array(featVals)*self.epicure.epi_metadata["ScaleT"]
 852        return None, None
 853
 854
 855    def show_feature(self):
 856        """ Add the image map of the selected feature """
 857        feat = self.show_feature_map.currentText()
 858        if (feat is not None) and (feat != ""):
 859            if feat in self.table.keys():
 860                values = list(self.table[feat])
 861                if feat == "group":
 862                    for i, val in enumerate(values):
 863                        if (val is None) or (val == 'None'):
 864                            values[i] = 0
 865                        else:
 866                            values[i] = list(self.epicure.groups.keys()).index(val) + 1
 867                labels = list(self.table["label"])
 868                frames = None
 869                if "frame" in self.table:
 870                    frames = list(self.table["frame"])
 871                self.draw_map(labels, values, frames, feat)
 872
 873    def draw_map(self, labels, values, frames, featname):
 874        """ Add image layer of values by label """
 875        ## special feature: orientation, draw the axis instead
 876        self.viewer.window._status_bar._toggle_activity_dock(True)
 877        labels = np.array(labels)
 878        values = np.array(values)
 879        frames = np.array(frames)
 880        def map_frame( iframe, segframe ):
 881            """ Draw one frame of the map """
 882            mask = np.where(frames==iframe)[0]
 883            labs = labels[mask]
 884            vals = values[mask]
 885            mapping = np.zeros(segframe.max()+1)
 886            mapping[:] = np.nan
 887            mapping[labs] = vals 
 888            return mapping[segframe] 
 889
 890        if frames is not None:
 891            ## Plotting a movie
 892            if self.epicure.process_parallel:
 893                mapfeat = Parallel( n_jobs=self.epicure.nparallel) (
 894                    delayed ( map_frame )(iframe, frame ) for iframe, frame in enumerate(self.seglayer.data)
 895                )
 896                mapfeat = np.array(mapfeat)
 897            else:
 898                mapfeat = np.empty(self.epicure.seg.shape, dtype="float16")
 899                mapfeat[:] = np.nan
 900                for iframe in np.unique(frames):
 901                    segdata = self.seglayer.data[iframe]
 902                    mapfeat[iframe] = map_frame( iframe, segdata )
 903        else:
 904            mapfeat = np.empty(self.epicure.seg.shape, dtype="float16")
 905            mapfeat[:] = np.nan
 906            for lab, val in progress(zip(labels, values)):
 907                cell = self.seglayer.data==lab
 908                mapfeat[cell] = val
 909        ut.remove_layer(self.viewer, "Map_"+featname)
 910        self.viewer.add_image(mapfeat, name="Map_"+featname, scale=self.viewer.layers["Segmentation"].scale )
 911        self.viewer.window._status_bar._toggle_activity_dock(False)
 912
 913    def draw_orientation( self ):
 914        """ Display the cells orientation axis in a new layer """
 915        ## check that necessary features are measured
 916        ut.remove_layer( self.viewer, "CellOrientation" )
 917        feats = ["centroid-0", "centroid-1", "orientation"]
 918        if self.table is None:
 919            print("Features centroid and orientation necessary to draw orientation, but are not measured yet")
 920            return
 921        for feat in feats:
 922            if feat not in self.table.keys():
 923                print("Feature "+feat+" necessary to draw orientation, but was not measured")
 924                return
 925        ## ok, can work now
 926        self.viewer.window._status_bar._toggle_activity_dock(True)
 927
 928        ## get the coordinates of the axis lines by getting the cell centroid, main orientation
 929        xs = np.array( self.table["centroid-0"] )
 930        ys = np.array( self.table["centroid-1"] )
 931        angles = np.array( self.table["orientation"] )
 932        lens = np.array( [10]*len(angles) )
 933        oriens = np.zeros( (self.epicure.seg.shape), dtype="uint8" )
 934
 935        ## draw axis length depending on the eccentricity
 936        if "eccentricity" in self.table.keys():
 937            lens = np.array(self.table["eccentricity"]*16)             
 938        
 939        if "frame" in self.table:
 940            frames = np.array( self.table["frame"] ).astype(int)
 941        else:
 942            frames = np.array( [0]*len(angles) )
 943
 944        ## draw the lines in between the two extreme points (using Shape layer is too slow on display for big movies)
 945        npts = 30
 946        xmax = oriens.shape[1]-1
 947        ymax = oriens.shape[2]-1
 948        for i in range(npts):
 949            xas = np.clip(xs - lens/2 * np.cos( angles ) * i/float(npts), 0, xmax).astype(int)
 950            xbs = np.clip(xs + lens/2 * np.cos( angles ) * i/float(npts), 0, xmax).astype(int)
 951            yas = np.clip(ys - lens/2 * np.sin( angles ) * i/float(npts), 0, ymax).astype(int)
 952            ybs = np.clip(ys + lens/2 * np.sin( angles ) * i/float(npts), 0, ymax).astype(int)
 953            oriens[ (frames, xas, yas) ] = 255
 954            oriens[ (frames, xbs, ybs) ] = 255
 955        
 956        self.viewer.add_image( oriens, name="CellOrientation", blending="additive", opacity=1, scale=self.viewer.layers["Segmentation"].scale )
 957        self.viewer.window._status_bar._toggle_activity_dock(False)
 958
 959    ################### Export to other plugins
 960
 961    def to_griot(self):
 962        """ Export current frame to new viewer and makes it ready for Griotte plugin """
 963        try:
 964            from napari_griottes import make_graph
 965        except:
 966            ut.show_error("Plugin napari-griottes is not installed")
 967            return
 968        gview = napari.Viewer()
 969        tframe = ut.current_frame(self.viewer)
 970        segt = self.epicure.seglayer.data[tframe]
 971        touching_frame = self.touching_labels(segt)
 972        gview.add_labels(touching_frame, name="TouchingCells", opacity=1)
 973        gview.window.add_dock_widget(make_graph(), name="Griottes")
 974
 975    def touching_labels(self, labs):
 976        """ Dilate labels so that they all touch """
 977        from skimage.segmentation import find_boundaries
 978        from skimage.morphology import skeletonize
 979        from skimage.morphology import binary_closing, binary_opening
 980        if self.epicure.verbose > 0:
 981            print("********** Generate touching labels image ***********")
 982
 983        ## skeletonize it
 984        skel = skeletonize( binary_closing( find_boundaries(labs), footprint=np.ones((10,10)) ) )
 985        ext = np.zeros(labs.shape, dtype="uint8")
 986        ext[labs==0] = 1
 987        ext = binary_opening(ext, footprint=np.ones((2,2)))
 988        newimg = ut.touching_labels(labs, expand=4)
 989        newimg[ext>0] = 0
 990        return newimg
 991    
 992    def to_ncp(self):
 993        """ Export current frame to new viewer and makes it ready for napari-cluster-plots plugin """
 994        try:
 995            import napari_skimage_regionprops as nsr
 996        except:
 997            ut.show_error("Plugin napari-skimage-regionprops is not installed")
 998            return
 999        gview = napari.Viewer()
1000        tframe = ut.current_frame(self.viewer)
1001        segt = self.epicure.seglayer.data[tframe]
1002        moviet = self.epicure.viewer.layers["Movie"].data[tframe]
1003        lab = gview.add_labels(segt, name="Segmentation[t="+str(tframe)+"]", blending="additive")
1004        im = gview.add_image(moviet, name="Movie[t="+str(tframe)+"]", blending="additive")
1005        if self.epicure.verbose > 0:
1006            print("Measure features with napari-skimage-regionprops plugin...")
1007        nsr.regionprops_table(im.data, lab.data, size=True, intensity=True, perimeter=True, shape=True, position=True, moments=True, napari_viewer=gview)
1008        try:
1009            import napari_clusters_plotter as ncp
1010        except:
1011            ut.show_error("Plugin napari-clusters-plotter is not installed")
1012            return
1013        gview.window.add_dock_widget( ncp.ClusteringWidget(gview) )
1014        gview.window.add_dock_widget( ncp.PlotterWidget(gview) )
1015
1016    ################### Temporal graphs
1017    def temporal_graphs_events( self ):
1018        """ New window with temporal graph of event counts """
1019        if self.tplots is not None:
1020            self.tplots.close()
1021        self.tplots = TemporalPlots( self.viewer, self.epicure )
1022        evt_table = self.count_events()
1023        self.tplots.setTable( evt_table )
1024        self.tplots.show()
1025        self.viewer.dims.events.current_step.connect(self.position_verticalline)
1026
1027
1028    def temporal_graphs(self):
1029        """ New window with temporal graph of the current table selection """
1030        #self.temporal_viewer = napari.Viewer()
1031        self.tplots = TemporalPlots( self.viewer, self.epicure )
1032        self.tplots.setTable(self.table)
1033        self.tplots.show()
1034        #self.plot_wid = self.viewer.window.add_dock_widget( self.tplots, name="Plots" )
1035        self.viewer.dims.events.current_step.connect(self.position_verticalline)
1036    
1037    def on_close_viewer(self):
1038        """ Temporal plots window is closed """
1039        if self.epicure.verbose > 1:
1040            print("Closed viewer")
1041        self.viewer.dims.events.current_step.disconnect(self.position_verticalline)
1042        self.temporal_viewer = None
1043        self.tplots = None
1044
1045    def position_verticalline(self):
1046        """ Place the vertical line in the temporal graph to the current frame """
1047        #try:
1048        #    wid = self.tplots
1049        #except:
1050        #    self.on_close_viewer()
1051        if self.tplots is not None:
1052            self.tplots.move_framepos(self.viewer.dims.current_step[0])
1053
1054    ############### track features 
1055
1056    def show_trackfeature_table(self):
1057        """ Show the measurement of tracks table """
1058        self.measure_track_features()
1059        self.trackTable.set_table( self.table )
1060    
1061    def measure_track_features(self):
1062        """ Measure track features and put them to table """
1063        if self.epicure.verbose > 0:
1064            print("Measuring track features")
1065        self.viewer.window._status_bar._toggle_activity_dock(True)
1066        start_time = time.time()
1067
1068        if self.output_mode.currentText() == "Only selected cell": 
1069            track_ids = self.epicure.seglayer.selected_label
1070        else:
1071            if self.output_mode.currentText() == "All cells": 
1072                track_ids = self.epicure.tracking.get_track_list()
1073            else:
1074                group = self.output_mode.currentText()
1075                track_ids = []
1076                label_group = self.epicure.groups[group]
1077                for lab in label_group:
1078                    track_ids.append(lab)
1079            
1080        properties = ["label", "area", "centroid"]
1081        self.table = None
1082
1083        if type(track_ids) == np.ndarray or type(track_ids)==np.array:
1084            track_ids = track_ids.tolist()
1085        if not type(track_ids) == list:
1086            track_ids = [track_ids]
1087
1088        labgroups = self.epicure.group_of_labels()
1089        frame_group = [ labgroups[label] if label in labgroups.keys() else "Ungrouped" for label in track_ids ]
1090        for itrack, track_id in progress(enumerate(track_ids)):
1091            track_frame = self.measure_one_track( track_id )
1092            track_frame["Group"] = frame_group[itrack]
1093            if self.table is None:
1094                self.table = pand.DataFrame([track_frame])
1095            else:
1096                self.table = pand.concat([self.table, pand.DataFrame([track_frame])])
1097
1098        self.table_selection = self.selection_choices.index(self.output_mode.currentText())
1099        self.viewer.window._status_bar._toggle_activity_dock(False)
1100        if self.epicure.verbose > 0:
1101            ut.show_info("Features measured in "+"{:.3f}".format((time.time()-start_time)/60)+" min")
1102
1103    def measure_one_track( self, track_id ):
1104        """ Measure features of one track """
1105        track_features = self.epicure.tracking.measure_track_features( track_id, self.scaled_unit.isChecked() )
1106        return track_features
1107
1108    ############## Events functions
1109
1110    def choose_events( self ):
1111        """ Pop-up widget to choose the event types to measure/export """
1112        self.event_classes.choose()
1113
1114    def count_events( self ):
1115        """ Count events of selected types """
1116        evt_types = self.event_classes.get_evt_classes()
1117        if self.epicure.verbose > 2:
1118            print("Counting events of type "+str(evt_types)+" " )
1119        
1120        ## keep only events related to selected cells
1121        labels = self.get_current_labels()
1122        ## count each type of event
1123        table = np.zeros(  (self.epicure.nframes,len(evt_types)), dtype="uint8" )        
1124        for itype, evt_type in enumerate( evt_types ):
1125            evts = self.epicure.inspecting.get_events_from_type( evt_type )
1126            if len( evts ) > 0:
1127                for evt_sid in evts:
1128                        pos, label = self.epicure.inspecting.get_event_infos( evt_sid )
1129                        if label in labels:
1130                            table[ pos[0], itype ] += 1
1131        df = pand.DataFrame( data=table, columns=evt_types )
1132        df["frame"] = range(len(df))
1133        df["label"] = [0]*len(df)
1134        return df          
1135
1136    def export_events( self ):
1137        """ Export events of selected types """
1138        evt_types = self.event_classes.get_evt_classes()
1139        export_type = self.save_evt_choice.currentText()
1140        if self.epicure.verbose > 2:
1141            print("Exporting events of type "+str(evt_types)+" to "+export_type )
1142        self.export_events_type_format( evt_types, export_type )
1143        
1144    def export_events_type_format( self, evt_types, export_type ):
1145        """ Export events of selected types in selected format """
1146        ## keep only events related to selected cells
1147        labels = self.get_current_labels()
1148        groups = self.epicure.get_groups( labels )
1149        if export_type == "CSV File":
1150            res = pand.DataFrame( columns=["label", "frame", "posY", "posX", "EventClass", "Group"] )  
1151        ## export each type of event in separate files
1152        for itype, evt_type in enumerate( evt_types ):
1153            evts = self.epicure.inspecting.get_events_from_type( evt_type )
1154            if len( evts ) > 0:
1155                rois = [] 
1156                for evt_sid in evts:
1157                    pos, label = self.epicure.inspecting.get_event_infos( evt_sid )
1158                    ind_lab = np.where( labels==label )
1159                    if len( ind_lab[0] ) > 0:
1160                        grp = groups[ int(ind_lab[0][0]) ]
1161                        if export_type == "Fiji ROI":
1162                            roi = self.create_point_roi( pos, itype )
1163                            rois.append( roi )
1164                        if export_type == "CSV File":
1165                            new_event = pand.DataFrame( [[label, pos[0], pos[1], pos[2], evt_type, grp ]], columns=res.columns )
1166                            res = pand.concat( [res, new_event], ignore_index=True )
1167                if export_type == "Fiji ROI":            
1168                    outfile = self.epicure.outname()+"_rois_"+evt_type +""+self.get_selection_name()+".zip" 
1169                    roifile.roiwrite(outfile, rois, mode='w')
1170                    if self.epicure.verbose > 0:
1171                        print( "Events "+str( evt_type )+" saved in ROI file: "+outfile )
1172            ## dont save anything if empty, just print info to user
1173            else:
1174                if self.epicure.verbose > 0:
1175                    print( "No events of type "+str(evt_type)+"" )
1176        
1177        if export_type == "CSV File":            
1178            outfile = self.epicure.outname()+"_events"+self.get_selection_name()+".csv" 
1179            res.to_csv( outfile,  sep='\t', header=True, index=False )
1180            if self.epicure.verbose > 0:
1181                print( "Events data "+" saved in CSV file: "+outfile )
1182
1183
1184    def create_point_roi( self, pos, cat=0 ):
1185        """ Create a point Fiji ROI """
1186        croi = roifile.ImagejRoi()
1187        croi.version = 227
1188        croi.roitype = roifile.ROI_TYPE(10)
1189        croi.name = str(pos[0]+1).zfill(4)+'-'+str(pos[1]).zfill(4)+"-"+str(pos[2]).zfill(4)
1190        croi.n_coordinates = 1
1191        croi.left = int(pos[2])
1192        croi.top = int(pos[1])
1193        croi.z_position = 1
1194        croi.t_position = pos[0]+1
1195        croi.c_position = 1
1196        croi.integer_coordinates = np.array( [[0,0]] )
1197        croi.stroke_width=3
1198        ncolors = 3
1199        if cat%ncolors == 0:  ## color type 0
1200            croi.stroke_color = b'\xff\x00\x00\xff'
1201        if cat%ncolors == 1:  ## color type 1
1202            croi.stroke_color = b'\xff\x00\xff\x00'
1203        if cat%ncolors == 2:  ## color type 2
1204            croi.stroke_color = b'\xff\xff\x00\x00'
1205        return croi
1206
1207    def save_tm_xml( self ):
1208        """ Save current segmentation and tracking in TrackMate XML format """
1209        outname = os.path.join( self.epicure.outdir, self.epicure.imgname+".xml" )
1210        save_trackmate_xml( self.epicure, outname )
1211        if self.epicure.verbose > 0:
1212            ut.show_info("TrackMate XML saved in "+outname)
1213    
1214    def save_geff( self ):
1215        """ Save current segmentation and tracking in GEFF format """
1216        ## save the label segmentation if it's not saved
1217        labelname = os.path.join( self.epicure.outdir, self.epicure.imgname + "_labels.tif" )
1218        ut.writeTif( self.epicure.seg, labelname, self.epicure.epi_metadata["ScaleXY"], "float32", what="Segmentation" )
1219        ## then export the GEFF file
1220        if self.epicure.tracking.graph is None:
1221            self.epicure.tracking.graph = {}
1222        outname = os.path.join( self.epicure.outdir, self.epicure.imgname+".geff" )
1223        save_geff( self.epicure, outname )
1224        if self.epicure.verbose > 0:
1225            ut.show_info("GEFF file saved in "+outname)

QWidget(parent: Optional[QWidget] = None, flags: Qt.WindowType = Qt.WindowFlags())

Outputing(napari_viewer, epic)
 50    def __init__(self, napari_viewer, epic):
 51        """ Initialisation of the interface """
 52        super().__init__()
 53        self.viewer = napari_viewer
 54        self.epicure = epic
 55        self.table = None
 56        self.table_selection = None
 57        self.seglayer = self.viewer.layers["Segmentation"]
 58        self.movlayer = self.viewer.layers["Movie"]
 59        self.selection_choices = ["All cells", "Only selected cell"]
 60        self.output_options = ["", "Export to extern plugins", "Export segmentations", "Measure cell features", "Measure track features", "Export/Measure events", "Save as...", "Save screenshot movie", "Measure vertices"]
 61        self.tplots = None
 62        
 63        chanlist = ["Movie"]
 64        if self.epicure.others is not None:
 65            for chan in self.epicure.others_chanlist:
 66                chanlist.append( "MovieChannel_"+str(chan) )
 67        self.cell_features = CellFeatures( chanlist )
 68        self.event_classes = EventClass( self.epicure ) 
 69        
 70        all_layout = QVBoxLayout()
 71        self.scaled_unit = wid.add_check( "Measures in scaled units", False, check_func=None, descr="Scales the output measures in the given spatio-temporal units (µm, min..)" )
 72        all_layout.addWidget( self.scaled_unit )
 73        self.choose_output = wid.listbox() 
 74        all_layout.addWidget(self.choose_output)
 75        for option in self.output_options:
 76            self.choose_output.addItem(option)
 77        self.choose_output.currentIndexChanged.connect(self.show_output_option)
 78        
 79        ## Choice of active selection
 80        #layout = QVBoxLayout()
 81        selection_layout, self.output_mode = wid.list_line( "Apply on", descr="Choose on which cell(s) to do the action", func=None )
 82        for sel in self.selection_choices:
 83            self.output_mode.addItem(sel)
 84        all_layout.addLayout(selection_layout)
 85       
 86        ## Choice of interface
 87        self.export_group, export_layout = wid.group_layout( "Export to extern plugins" )
 88        griot_btn = wid.add_button( "Current frame to Griottes", self.to_griot, "Launch(in new window) Griottes plugin on current frame" )
 89        export_layout.addWidget(griot_btn)
 90        ncp_btn = wid.add_button( "Current frame to Cluster-Plotter", self.to_ncp, "Launch (in new window) cluster-plotter plugin on current frame" )
 91        export_layout.addWidget(ncp_btn)
 92        self.export_group.setLayout(export_layout)
 93        all_layout.addWidget(self.export_group)
 94        
 95        ## Option to export segmentation results
 96        self.export_seg_group, layout = wid.group_layout(self.output_options[2])
 97        save_line, self.save_choice = wid.button_list( "Save segmentation as", self.save_segmentation, "Save the current segmentation either as ROI, label image or skeleton" ) 
 98        self.save_choice.addItem( "labels" )
 99        self.save_choice.addItem( "ROI" )
100        self.save_choice.addItem( "skeleton" )
101        layout.addLayout( save_line )
102
103        self.export_seg_group.setLayout(layout)
104        all_layout.addWidget(self.export_seg_group)
105
106        #### Features group
107        self.feature_group, featlayout = wid.group_layout(self.output_options[3])
108        
109        self.choose_features_btn = wid.add_button( "Choose features...", self.choose_features, "Open a window to select the features to measure" )
110        featlayout.addWidget(self.choose_features_btn)
111
112        self.feature_table = wid.add_button( "Create features table", self.show_table, "Measure the selected features and display it as a clickable table" )
113        featlayout.addWidget(self.feature_table)
114        self.featTable = FeaturesTable(self.viewer, self.epicure)
115        featlayout.addWidget(self.featTable)
116        
117        ######## Temporal option  
118        self.temp_graph = wid.add_button( "Table to temporal graphs...", self.temporal_graphs, "Open a plot interface of measured features temporal evolution" )
119        featlayout.addWidget(self.temp_graph)
120        self.temp_graph.setEnabled(False)
121       
122        ######## Drawing option
123        featmap, self.show_feature_map = wid.list_line( "Draw feature map:", descr="Add a layer with the cells colored by the selected feature value", func=self.show_feature )
124        featlayout.addLayout(featmap)
125        orienbtn = wid.add_button( "Draw cell orientation", self.draw_orientation, "Add a layer with each cell main axis orientation and length " )
126        featlayout.addWidget( orienbtn )
127
128        save_tab_line, self.save_format = wid.button_list( "Save features table", self.save_measure_features, "Save the current table in a .csv file" )
129        self.save_format.addItem( "csv" )
130        self.save_format.addItem( "xlsx" )
131        featlayout.addLayout(save_tab_line)
132
133        ## skrub table
134        self.stat_table = wid.add_button( "Open statistiques table...", self.skrub_features, "Open interactive table with the features statistiques (skrub library)" )
135        featlayout.addWidget(self.stat_table)
136        
137        self.feature_group.setLayout(featlayout)
138        self.feature_group.hide()
139        all_layout.addWidget(self.feature_group)
140
141        ## Track features
142        self.trackfeat_group, trackfeatlayout = wid.group_layout(self.output_options[4])
143        self.trackfeat_table = wid.add_button( "Track features table", self.show_trackfeature_table, "Measure track-related feature and show a table by track" )
144        trackfeatlayout.addWidget(self.trackfeat_table)
145        self.trackTable = FeaturesTable(self.viewer, self.epicure)
146        trackfeatlayout.addWidget(self.trackTable)
147        self.save_table_track = wid.add_button( "Save track table", self.save_table_tracks, "Save the current table in a .csv file" )
148        trackfeatlayout.addWidget(self.save_table_track)
149        
150        self.trackfeat_group.setLayout(trackfeatlayout)
151        self.trackfeat_group.hide()
152        all_layout.addWidget(self.trackfeat_group)
153
154        ## Option to export/measure events (Fiji ROI or table), + graphs ?
155        self.handle_event_group, elayout = wid.group_layout(self.output_options[5])
156        self.choose_events_btn = wid.add_button( "Choose events...", self.choose_events, "Open a window to select the events to export/measure" )
157        elayout.addWidget( self.choose_events_btn )
158        save_evt_line, self.save_evt_choice = wid.button_list( "Export events as", self.export_events, "Save the checked events as Fiji ROIs or .csv table" ) 
159        self.save_evt_choice.addItem( "Fiji ROI" )
160        self.save_evt_choice.addItem( "CSV File" )
161        elayout.addLayout( save_evt_line )
162        count_evt_btn = wid.add_button( "Count events", self.temporal_graphs_events, descr="Create temporal plot of number of events" )
163        elayout.addWidget( count_evt_btn )
164
165        self.handle_event_group.setLayout( elayout )
166        self.handle_event_group.hide()
167        all_layout.addWidget( self.handle_event_group )
168
169        ## Save TrackMate XML option
170        self.save_as_group, save_as_layout = wid.group_layout( "Save as..." )
171        self.save_tm_btn = wid.add_button( "Save as TrackMate XML", self.save_tm_xml, "Save the current segmentation and the optional tracking in a TrackMate XML file" )
172        self.save_geff_btn = wid.add_button( "Save as GEFF", self.save_geff, "Save the segmentation and tracks to GEFF" )
173        save_as_layout.addWidget( self.save_tm_btn )
174        save_as_layout.addWidget( self.save_geff_btn )
175        
176        self.save_as_group.setLayout( save_as_layout )
177        self.save_as_group.hide()
178        all_layout.addWidget( self.save_as_group )
179       
180        ## Save screenshots option
181        current_frame = ut.current_frame( self.epicure.viewer )
182        self.screenshot_group, screenshot_layout = wid.group_layout( "Save screenshot movie" )
183        self.show_scalebar = wid.add_check_tolayout( screenshot_layout, "With the scale bar", True, check_func=None, descr="Show the scale bar in the screenshots" )
184        sframe_line, self.sframe = wid.slider_line( "From frame", 0, self.epicure.nframes, 1, value=current_frame, show_value=True, slidefunc=None, descr="Frame from which to start saving screenshots" )
185        eframe_line, self.eframe = wid.slider_line( "To frame", 0, self.epicure.nframes, 1, value=current_frame+1, show_value=True, slidefunc=None, descr="Frame until which to save screenshots" )
186        screenshot_layout.addLayout( sframe_line )
187        screenshot_layout.addLayout( eframe_line )
188        savescreen_btn = wid.add_button( "Save current view", self.screenshot_movie, "Save the current view (with current display parameters) for frame between the two specified frames in a movie" )
189
190        screenshot_layout.addWidget(savescreen_btn)
191        self.screenshot_group.setLayout(screenshot_layout)
192        all_layout.addWidget(self.screenshot_group)
193        self.screenshot_group.hide()
194        
195        ## Measure vertex options
196        self.vertex_group, vertices_layout = wid.group_layout( "Measure vertices" )
197        radius_line, self.vertice_radius = wid.value_line("Vertex radius", "1.25", descr="Radius of a vertex (TCJ) to consider as one point and measure intensities")
198        display_radius_line, self.vertice_display_radius = wid.value_line("Display radius", "3", descr="Radius of a vertex for DISPLAY only (size of drawing in the layer)")
199        vertices_layout.addLayout(radius_line)
200        vertices_layout.addLayout(display_radius_line)
201        self.vertices_btn = wid.add_button( "Measure", self.show_vertices_table, "Measure the vertices (connectivity, intensity)" )
202        vertices_layout.addWidget( self.vertices_btn )
203        self.verticesTable = FeaturesTable(self.viewer, self.epicure)
204        vertices_layout.addWidget(self.verticesTable)
205        self.save_table_vertices = wid.add_button( "Save vertices table", self.save_vertices_table, "Save the current table in a .csv file" )
206        vertices_layout.addWidget(self.save_table_vertices)
207        
208        self.vertex_group.setLayout( vertices_layout )
209        all_layout.addWidget(self.vertex_group)
210        self.vertex_group.hide()
211        
212        ## Finished
213        self.setLayout(all_layout)
214        self.show_output_option()

Initialisation of the interface

viewer
epicure
table
table_selection
seglayer
movlayer
selection_choices
output_options
tplots
cell_features
event_classes
scaled_unit
choose_output
choose_features_btn
feature_table
featTable
temp_graph
stat_table
trackfeat_table
trackTable
save_table_track
choose_events_btn
save_tm_btn
save_geff_btn
show_scalebar
vertices_btn
verticesTable
save_table_vertices
def get_current_settings(self):
216    def get_current_settings( self ):
217        """ Returns current settings of the widget """
218        disp = {}
219        disp["Apply on"] = self.output_mode.currentText() 
220        disp["Current option"] = self.choose_output.currentText()
221        disp["Show scalebar"] = self.show_scalebar.isChecked()
222        disp = self.cell_features.get_current_settings( disp )
223        disp = self.event_classes.get_current_settings( disp )
224        return disp

Returns current settings of the widget

def apply_settings(self, settings):
226    def apply_settings( self, settings ):
227        """ Set the current state of the widget from preferences if any """
228        for setting, val in settings.items():
229            if setting == "Apply on":
230                self.output_mode.setCurrentText( val )
231            if setting == "Current option":
232                self.choose_output.setCurrentText( val )
233            if setting == "Show scalebar":
234                self.show_scalebar.setChecked( val )
235            
236        self.cell_features.apply_settings( settings )
237        self.event_classes.apply_settings( settings )

Set the current state of the widget from preferences if any

def screenshot_movie(self):
239    def screenshot_movie( self ):
240        """ Save screenshots of the current view """
241        scale_visibility = self.viewer.scale_bar.visible
242        current_frame = ut.current_frame( self.epicure.viewer )
243        self.viewer.scale_bar.visible = self.show_scalebar.isChecked()
244        start_frame = max( self.sframe.value(), 0 )
245        end_frame = min( self.eframe.value(), self.epicure.nframes )
246        outname = os.path.join( self.epicure.outdir, self.epicure.imgname+"_screenshots_f"+str(start_frame)+"-"+str(end_frame)+".tif" )
247        if os.path.exists(outname):
248            os.remove(outname)
249        if start_frame > end_frame:
250            ut.show_warning("From frame > to frame, no screenshot saved")
251            return
252        for frame in range(start_frame, end_frame+1):
253            self.viewer.dims.set_point(0, frame)
254            shot = self.viewer.screenshot( canvas_only=True, flash=False )
255            ut.appendToTif( shot, outname )
256        self.viewer.scale_bar.visible = scale_visibility
257        self.viewer.dims.set_point(0, current_frame)
258        ut.show_info( "Screenshot movie saved in "+outname )

Save screenshots of the current view

def events_select(self, event, check):
260    def events_select( self, event, check ):
261        """ Check/Uncheck the event in event types list """
262        if event in self.event_classes.evt_classes:
263            self.event_classes.evt_classes[ event ][0].setChecked( check )
264        else:
265            print(event+" not found in possible event types to export")

Check/Uncheck the event in event types list

def show_output_option(self):
267    def show_output_option(self):
268        """ Show selected output panel """
269        cur_option = self.choose_output.currentText()
270        self.export_group.setVisible( cur_option == "Export to extern plugins" )
271        self.export_seg_group.setVisible( cur_option == "Export segmentations" )
272        self.feature_group.setVisible( cur_option == "Measure cell features" )
273        self.vertex_group.setVisible( cur_option == "Measure vertices" )
274        self.trackfeat_group.setVisible( cur_option == "Measure track features" )
275        self.handle_event_group.setVisible( cur_option == "Export/Measure events" )
276        self.save_as_group.setVisible( cur_option == "Save as..." )
277        self.screenshot_group.setVisible( cur_option == "Save screenshot movie" )

Show selected output panel

def get_current_labels(self):
279    def get_current_labels( self ):
280        """ Get the cell labels to process according to current selection of apply on"""
281        if self.output_mode.currentText() == "Only selected cell": 
282            lab = self.epicure.seglayer.selected_label
283            return [lab]
284        if self.output_mode.currentText() == "All cells": 
285            return self.epicure.get_labels()
286        else:
287            group = self.output_mode.currentText()
288            label_group = self.epicure.groups[group]
289            return label_group

Get the cell labels to process according to current selection of apply on

def get_selection_name(self):
292    def get_selection_name(self):
293        if self.output_mode.currentText() == "Only selected cell": 
294            lab = self.epicure.seglayer.selected_label
295            return "_cell_"+str(lab) 
296        #if self.output_mode.currentText() == "Only checked cells":
297        #    return "_checked_cells"
298        if self.output_mode.currentText() == "All cells":
299            return ""
300        return "_"+self.output_mode.currentText()
def skrub_features(self):
302    def skrub_features( self ):
303        """ Open html table interactive and stats with skrub module """
304        try:
305            from skrub import TableReport
306        except:
307            ut.show_error( "Needs skrub library for this option. Install it (`pip install skrub`) before" )
308            return
309        if self.table is None:
310            ut.show_warning( "Create/update the table before" )
311            return
312        report = TableReport( self.table )
313        report.open()

Open html table interactive and stats with skrub module

def save_measure_features(self):
316    def save_measure_features(self):
317        """ Save measures table to file whether it was created or not """
318        if self.table is None or self.table_selection is None or self.selection_changed() :
319            ut.show_warning("Create/update the table before")
320            return
321        ext = self.save_format.currentText()
322        outfile = self.epicure.outname()+"_features"+self.get_selection_name()+"."+ext
323        if ext == "xlsx":
324            self.table.to_excel( outfile, sheet_name='EpiCureMeasures' )
325        else:
326            self.table.to_csv( outfile, index=False )
327        if self.epicure.verbose > 0:
328            ut.show_info("Measures saved in "+outfile)

Save measures table to file whether it was created or not

def save_table_tracks(self):
330    def save_table_tracks(self):
331        """ Save tracks table to file whether it was created or not """
332        if self.table is None or self.table_selection is None or self.selection_changed() :
333            ut.show_warning("Create/update the table before")
334            return
335        outfile = self.epicure.outname()+"_trackfeatures"+self.get_selection_name()+".xlsx"
336        self.table.to_excel( outfile, sheet_name='EpiCureTrackMeasures' )
337        if self.epicure.verbose > 0:
338            ut.show_info("Track measures saved in "+outfile)

Save tracks table to file whether it was created or not

def save_one_roi(self, lab):
341    def save_one_roi(self, lab):
342        """ Save the Rois of cell with label lab """
343        keep = self.seglayer.data == lab
344        rois = []
345        if np.sum(keep) > 0:
346            ## add 2D case
347            for iframe, frame in enumerate(keep):
348                if np.sum(frame) > 0:
349                    contour = ut.get_contours(frame)
350                    roi = self.create_roi(contour[0], iframe, lab)
351                    rois.append(roi)
352
353        roifile.roiwrite(self.epicure.outname()+"_rois_cell_"+str(lab)+".zip", rois, mode='w')

Save the Rois of cell with label lab

def create_roi(self, contour, frame, label):
355    def create_roi(self, contour, frame, label):
356        croi = roifile.ImagejRoi()
357        croi.version = 227
358        croi.roitype = roifile.ROI_TYPE(0) ## polygon
359        croi.n_coordinates = len(contour)
360        croi.position = frame + 1
361        croi.t_position = frame+1
362        coords = []
363        cent0 = 0
364        cent1 = 0
365        for cont in contour:
366            coords.append([int(cont[1]), int(cont[0])])
367            cent0 += cont[1]
368            cent1 += cont[0]
369        croi.integer_coordinates = np.array(coords)
370        #croi.top = int(np.min(coords[0]))
371        #croi.left = int(np.min(coords[1]))
372        croi.name = str(frame+1).zfill(4)+'-'+str(int(cent0/len(contour))).zfill(4)+"-"+str(int(cent1/len(contour))).zfill(4)
373        return croi
def save_segmentation(self):
375    def save_segmentation( self ):
376        """ Save current segmentation in selected format """
377        if self.output_mode.currentText() == "Only selected cell": 
378            ## output only the selected cell
379            lab = self.seglayer.selected_label
380            if self.save_choice.currentText() == "ROI":
381                self.save_one_roi(lab)
382                if self.epicure.verbose > 0:
383                    ut.show_info("Cell "+str(lab)+" saved to Fiji ROI")
384                return
385            else:
386                tosave = np.zeros(self.seglayer.data.shape, dtype=self.epicure.dtype)
387                if np.sum(self.seglayer.data==lab) > 0:
388                    tosave[self.seglayer.data==lab] = lab
389                endname = "_"+self.save_choice.currentText()+"_"+str(lab)+".tif"
390        else:
391            ## output all cells
392            if self.output_mode.currentText() == "All cells":
393                if self.save_choice.currentText() == "ROI":
394                    self.save_all_rois()
395                    return
396                tosave = self.seglayer.data
397                endname = "_"+self.save_choice.currentText()+".tif"
398            else:
399                ## or output only selected group
400                group = self.output_mode.currentText()
401                label_group = self.epicure.groups[group]
402                if self.save_choice.currentText() == "ROI":
403                    ncells = 0
404                    for lab in label_group:
405                        self.save_one_roi(lab)
406                        ncells += 1
407                    if self.epicure.verbose > 0:
408                        ut.show_info(str(ncells)+" cells saved to Fiji ROIs")
409                    return
410                tosave = np.zeros(self.seglayer.data.shape, dtype=self.epicure.dtype)
411                endname = "_"+self.save_choice.currentText()+"_"+self.output_mode.currentText()+".tif"
412                for lab in label_group:
413                    tosave[self.seglayer.data==lab] = lab
414        
415        ## save filled image (for label or skeleton) to file
416        outname = os.path.join( self.epicure.outdir, self.epicure.imgname+endname )
417        if self.save_choice.currentText() == "skeleton":
418            parallel = 0
419            if self.epicure.process_parallel:
420                parallel = self.epicure.nparallel
421            tosave = ut.get_skeleton( tosave, viewer=self.viewer, verbose=self.epicure.verbose, parallel=parallel )
422            ut.writeTif( tosave, outname, self.epicure.epi_metadata["ScaleXY"], 'uint8', what="Skeleton" )
423        else:
424            ut.writeTif(tosave, outname, self.epicure.epi_metadata["ScaleXY"], 'float32', what="Segmentation")

Save current segmentation in selected format

def save_all_rois(self):
426    def save_all_rois( self ):
427        """ Save all cells to ROI format """
428        ncells = 0
429        for lab in np.unique(self.epicure.seglayer.data):
430            self.save_one_roi(lab)
431            ncells += 1
432        if self.epicure.verbose > 0:
433            ut.show_info(str(ncells)+" cells saved to Fiji ROIs")

Save all cells to ROI format

def choose_features(self):
435    def choose_features( self ):
436        """ Pop-up widget to choose the features to measure """
437        self.cell_features.choose()

Pop-up widget to choose the features to measure

def show_vertices_table(self):
439    def show_vertices_table(self):
440        """ Show the measurement of vertices table """
441        self.measure_vertices()
442        self.verticesTable.set_table(self.table)

Show the measurement of vertices table

def save_vertices_table(self):
444    def save_vertices_table(self):
445        """ Save vertices table to file whether it was created or not """
446        if self.table is None:
447            ut.show_warning("Create/update the table before")
448            return
449        outfile = self.epicure.outname()+"_vertices"+".xlsx"
450        self.table.to_excel( outfile, sheet_name='EpiCureVerticesMeasures' )
451        if self.epicure.verbose > 0:
452            ut.show_info("Vertices measures saved in "+outfile)

Save vertices table to file whether it was created or not

def measure_vertices(self):
455    def measure_vertices(self):
456        """ Get all vertices (TCJ) and measure their properties """
457        def nb_neighbors(regionmask, labimg):
458            """ Measure the nb of neighbors (labels) around each point """
459            #footprint = disk(radius=8)
460            #dilated = binary_dilation(regionmask, footprint)
461            labels = np.unique(labimg[regionmask]).tolist()
462            nb_nei = len(labels)
463            if 0 in labels:
464                nb_nei = nb_nei - 1
465            return nb_nei 
466
467        self.table = None
468        radius = float(self.vertice_radius.text()) 
469        display_radius = float(self.vertice_display_radius.text()) 
470        ## difference between the measured radius and the displayed radius
471        diff_radius = display_radius - radius
472        if diff_radius < 0:
473            diff_radius = 0
474        parallel = 0
475        if self.epicure.process_parallel:
476            parallel = self.epicure.nparallel
477        ## Get the vertices: junctions of several skeleton lines
478        vertex_img = ut.get_vertices( self.epicure.seg, viewer=None, verbose=self.epicure.verbose, parallel=parallel )
479        vertices_img = np.zeros(vertex_img.shape, dtype=np.int8)
480        ## Individualise, measure, draw
481        for ind, frame in enumerate(vertex_img):
482            props = ut.binary_properties(frame)
483            nvertex = len(props)
484            vertices = []
485            for prop in props:
486                if prop.label > 0:
487                    pt = prop.centroid
488                    vertices.append(pt)
489            vert_img = ut.draw_points(vertices, vertex_img.shape[1:], radius=radius)
490            props = ut.binary_properties(vert_img)
491            if nvertex != len(props):
492                ## one or more vertices had been merged
493                vertices = []
494                for prop in props:
495                    if prop.label > 0:
496                        pt = prop.centroid
497                        vertices.append(pt)
498                vert_img = ut.draw_points(vertices, vertex_img.shape[1:], radius=radius)
499            #vertices_img[ind] = vert_img
500            lbl_img = label(vert_img)
501            int_measures = pand.DataFrame(ut.regionprops_table(lbl_img, self.movlayer.data[ind], properties=["label", "centroid", "intensity_mean"]))
502            ## expand to measure neighbors
503            exp_lbl = ut.touching_labels(lbl_img, expand=3)
504            measures = pand.DataFrame(ut.regionprops_table(exp_lbl, self.epicure.seg[ind], properties=["label"], extra_properties=[nb_neighbors] ))
505            ## Color the vertices by their number of neighbors
506            for lab in measures["label"]:
507                vertices_img[ind][lbl_img==lab] = int(measures.loc[measures["label"]==lab,"nb_neighbors"].iloc[0])
508            df = pand.merge(int_measures, measures, on="label", how="inner")
509            df["Frame"] = ind
510            
511            if self.table is None:
512                self.table = df
513            else:
514                self.table = pand.concat([self.table, df])
515
516            ## Expand for display only
517            vertices_img[ind] = ut.touching_labels(vertices_img[ind], expand=diff_radius)
518
519        ## Display the vertices in a new layer
520        ut.remove_layer(self.viewer, "Vertices") # in case already present
521        self.viewer.add_labels(vertices_img, blending="additive", name="Vertices",  scale=self.viewer.layers["Movie"].scale, opacity=1)

Get all vertices (TCJ) and measure their properties

def measure_features(self):
523    def measure_features(self):
524        """ Measure features and put them to table """
525        thick = self.epicure.thickness
526
527        def intensity_junction_cytoplasm(regionmask, intensity):
528            """ Measure the intensity only on the contour of regionmask """
529            footprint = disk(radius=thick)
530            inside = binary_erosion(regionmask, footprint)
531            inside_intensity = ut.mean_nonzero(intensity*inside)
532            periph_intensity = ut.mean_nonzero(intensity*(regionmask^inside))
533            return inside_intensity, periph_intensity
534        
535        if self.epicure.verbose > 0:
536            print("Measuring features")
537        #self.viewer.window._status_bar._toggle_activity_dock(True)
538        pb = ut.start_progress( self.viewer, total=2, descr="Measuring cells in all movie" )
539        start_time = time.time()
540        if self.output_mode.currentText() == "Only selected cell": 
541            meas = np.zeros(self.epicure.seglayer.data.shape, self.epicure.dtype)
542            lab = self.epicure.seglayer.selected_label
543            meas[self.epicure.seglayer.data==lab] = lab
544        else:
545            if self.output_mode.currentText() == "All cells": 
546                meas = self.epicure.seglayer.data
547            else:
548                group = self.output_mode.currentText()
549                meas = np.zeros(self.epicure.seglayer.data.shape, self.epicure.dtype)
550                label_group = self.epicure.groups[group]
551                for lab in label_group:
552                    meas[self.epicure.seglayer.data==lab] = lab
553            
554        properties, prop_extra, other_features, int_feat, int_extrafeat = self.cell_features.get_features()
555        do_channels = self.cell_features.get_channels()
556        extra_prop = []
557        if "intensity_junction_cytoplasm" in int_extrafeat:
558            extra_prop = extra_prop + [intensity_junction_cytoplasm]
559
560        extra_properties = []
561        if (do_channels is not None) and ("Movie" in do_channels):
562            properties = properties + int_feat
563            for extra in int_extrafeat:
564                if extra == "intensity_junction_cytoplasm":
565                    extra_properties = extra_properties + [intensity_junction_cytoplasm]
566        
567        pb.update()
568        labgroups = self.epicure.group_of_labels()
569        pb.total = self.epicure.nframes
570        chan_dict = dict()
571        if ( do_channels is not None ):
572            for chan in do_channels:
573                if chan == "Movie":
574                    continue
575                chan_dict[chan] = self.viewer.layers[chan].data
576        seg = self.epicure.seg
577        mov = self.movlayer.data
578        
579        def measure_one_frame_collect( img, frame ):
580            """ Measure on one frame and return a list of dicts for each label """
581            #pb.update()
582            intimg = mov[frame]
583            frame_table = pand.DataFrame( ut.labels_table(img, intensity_image=intimg, properties=properties, extra_properties=extra_properties) )
584            if "group" in other_features:
585                frame_table["group"] = frame_table["label"].map(labgroups).fillna("Ungrouped")
586            frame_table["frame"] = frame
587            
588            # Boundary
589            if "Boundary" in other_features:
590                boundimg = seg[frame]
591                bds = ut.get_boundary_cells(boundimg)
592                frame_table["Boundary"] = frame_table["label"].isin(bds).astype(int)
593            # Border
594            if "Border" in other_features:
595                bds = ut.get_border_cells(img)
596                frame_table["Border"] = frame_table["label"].isin(bds).astype(int)
597            
598            # Intensity features in other channels
599            for chan, intimg_chan in chan_dict.items():
600                intimg_frame = intimg_chan[frame]
601                frame_tab = ut.labels_table(img, intensity_image=intimg_frame, properties=int_feat, extra_properties=extra_prop)
602                for add_prop in int_feat:
603                    frame_table[add_prop+"_"+str(chan)] = frame_tab[add_prop]
604                if "intensity_junction_cytoplasm-0" in frame_tab.keys():
605                    frame_table["intensity_cytoplasm_"+str(chan)] = frame_tab["intensity_junction_cytoplasm-0"]
606                    frame_table["intensity_junction_"+str(chan)] = frame_tab["intensity_junction_cytoplasm-1"]
607            
608            if prop_extra != []:
609                if "shape_index" in prop_extra:
610                    frame_table["shape_index"] = frame_table["perimeter"] / np.sqrt(frame_table["area"])
611                if "roundness" in prop_extra:
612                    frame_table["roundness"] = 4*frame_table["area"] /(np.pi * np.power(frame_table["axis_major_length"],2) )
613                if "aspect_ratio" in prop_extra:
614                    frame_table["aspect_ratio"] = frame_table["axis_major_length"] / frame_table["axis_minor_length"]
615
616            # Neighbor features
617            do_neighbor = "NbNeighbors" in other_features
618            get_neighbor = "Neighbors" in other_features
619            if do_neighbor or get_neighbor:
620                nimg = seg[frame]
621                graph = ut.get_neighbor_graph(nimg, distance=3)
622                all_neighbors = {label: list(graph.adj[label]) for label in graph.nodes}
623                frame_table["neighborlist"] = frame_table["label"].map(lambda l: all_neighbors.get(l, []))
624
625            if do_neighbor:
626                frame_table["NbNeighbors"] = frame_table["neighborlist"].apply(
627                lambda x: len(x) if x else -1
628                )
629            if get_neighbor:
630                frame_table["Neighbors"] = frame_table["neighborlist"].apply(
631                lambda x: "&".join(map(str, x)) if x else ""
632                )
633            if do_neighbor or get_neighbor:
634                frame_table.drop(columns="neighborlist", inplace=True)
635            return pand.DataFrame( frame_table.to_dict(orient="records") )
636
637        if self.epicure.process_parallel:
638            frame_tables = Parallel( n_jobs=self.epicure.nparallel ) ( 
639                delayed( measure_one_frame_collect ) ( frame, iframe ) for iframe, frame in enumerate(meas) )
640        else:
641            frame_tables = [
642                measure_one_frame_collect( frame, iframe )
643                for iframe, frame in (enumerate(meas))
644            ]
645        self.table = pand.concat(frame_tables, ignore_index=True)
646
647        if "intensity_junction_cytoplasm-0" in self.table.columns:
648            self.table = self.table.rename(columns={"intensity_junction_cytoplasm-0": "intensity_cytoplasm", "intensity_junction_cytoplasm-1":"intensity_junction"})
649        self.table_selection = self.selection_choices.index(self.output_mode.currentText())
650        ut.close_progress( self.viewer, pb )
651        #self.viewer.window._status_bar._toggle_activity_dock(False)
652        if self.epicure.verbose > 0:
653            ut.show_info("Features measured in "+"{:.3f}".format((time.time()-start_time)/60)+" min")

Measure features and put them to table

def measure_one_frame( self, img, properties, extra_properties, other_features, channels, int_feat, int_extrafeat, frame, labgroups, prop_extra):
655    def measure_one_frame(self, img, properties, extra_properties, other_features, channels, int_feat, int_extrafeat, frame, labgroups, prop_extra ):
656        """ Measure on one frame """
657        if frame is not None:
658            intimg = self.movlayer.data[frame]
659        else:
660            intimg = self.movlayer.data
661        first = "label" not in self.table.keys()
662        nrows = len(self.table["label"]) if "label" in self.table.keys() else 0
663        
664        ## add the basic label measures
665        frame_table = ut.labels_table( img, intensity_image=intimg, properties=properties, extra_properties=extra_properties )
666        ndata = len(frame_table["label"])
667        for key, value in frame_table.items():
668            if first:
669                self.table[key] = []
670            self.table[key].extend(list(value))
671
672        ## add the frame column
673        if frame is not None:
674            if first:
675                self.table["frame"] = []
676            self.table["frame"].extend([frame]*ndata)
677
678        ## add info of the cell group
679        if "group" in other_features:
680            frame_group = [ labgroups[label] if label in labgroups.keys() else "Ungrouped" for label in frame_table["label"] ]
681            if first:
682                self.table["group"] = []
683            self.table["group"].extend( frame_group )
684
685        ## add the extra shape features
686        if prop_extra != []:
687            if "shape_index" in prop_extra:
688                si = frame_table["perimeter"] /np.sqrt( frame_table["area"] ) 
689                if first:
690                    self.table["shape_index"] = []
691                self.table["shape_index"].extend( si )
692            if "roundness" in prop_extra:
693                rou = 4*frame_table["area"] /(np.pi * np.power(frame_table["axis_major_length"],2) ) 
694                if first:
695                    self.table["roundness"] = []
696                self.table["roundness"].extend( rou )
697            if "aspect_ratio" in prop_extra:
698                ar = list( np.array(frame_table["axis_major_length"])/np.array(frame_table["axis_minor_length"]) )
699                if first:
700                    self.table["aspect_ratio"] = []
701                self.table["aspect_ratio"].extend( ar )
702
703        ### Measure intensity features in other chanels if option is on
704        if (channels is not None):
705            for chan in channels:
706                ## if it's movie, already measured in the general measure
707                if chan == "Movie":
708                    continue
709                ## otherwise, do a new measure on the selected channels
710                if frame is not None:
711                    intimg = self.viewer.layers[chan].data[frame]
712                else:
713                    intimg = self.viewer.layers[chan].data
714                frame_tab = ut.labels_table( img, intensity_image=intimg, properties=int_feat, extra_properties=int_extrafeat )
715                for add_prop in int_feat:
716                    if first:
717                        self.table[add_prop+"_"+chan] = []
718                    self.table[add_prop+"_"+chan].extend( list(frame_tab[add_prop]) )
719                if "intensity_junction_cytoplasm-0" in frame_tab.keys():
720                    if first:
721                        self.table["intensity_cytoplasm_"+chan] = []
722                        self.table["intensity_junction_"+str(chan)] = []
723                    self.table["intensity_cytoplasm_"+chan].extend( list(frame_tab["intensity_junction_cytoplasm-0"]) )
724                    self.table["intensity_junction_"+str(chan)].extend( list(frame_tab["intensity_junction_cytoplasm-1"]) )
725                
726            
727        ## add features of neighbors relationship with graph
728        do_neighbor = "NbNeighbors" in other_features
729        get_neighbor = "Neighbors" in other_features
730        if do_neighbor or get_neighbor:
731            if frame is not None:
732                nimg = self.epicure.seg[frame]
733            else:
734                nimg = self.epicure.seg
735            #start_time = ut.start_time()
736            graph = ut.get_neighbor_graph( nimg, distance=3 )
737            
738            if first:
739                if do_neighbor:
740                    self.table["NbNeighbors"] = []
741                if get_neighbor:
742                    self.table["Neighbors"] = []
743            if do_neighbor:
744                self.table["NbNeighbors"].extend( [-1]*ndata )
745            if get_neighbor:
746                self.table["Neighbors"].extend( [""]*ndata )
747
748            for label in np.unique(frame_table["label"]):
749                if label in graph.nodes:
750                    rlabel = np.where( (frame_table["label"] == label) )[0]
751                    nneighbor = len(graph.adj[label])
752                    for ind in rlabel:
753                        if do_neighbor:
754                            self.table["NbNeighbors"][ind+nrows] = nneighbor
755                        if get_neighbor:
756                            self.table["Neighbors"][ind+nrows] = ""
757                            sep = ""
758                            for key in graph.adj[label].keys():
759                                self.table["Neighbors"][ind+nrows] += sep + str(key)
760                                sep = "&"
761            #ut.show_duration( start_time, "Neighborhoods measured" )
762
763        ## measure cells on boundary    
764        if "Boundary" in other_features:
765            if frame is not None:
766                boundimg = self.epicure.seg[frame]
767            else:
768                boundimg = self.epicure.seg
769            bounds = ut.get_boundary_cells( boundimg )
770            if first:
771                self.table["Boundary"] = []
772            self.table["Boundary"].extend( [0]*ndata )
773            for label in np.unique(frame_table["label"]):
774                if label in bounds:
775                    rlabel = np.where( (frame_table["label"] == label) )[0]
776                    for ind in rlabel:
777                        self.table["Boundary"][ind+nrows] = 1
778        
779        ## measure cells on border  
780        if "Border" in other_features:
781            bounds = ut.get_border_cells( img )
782            if first:
783                self.table["Border"] = []
784            self.table["Border"].extend( [0]*ndata )
785            for label in bounds:
786                rlabel = np.where( (frame_table["label"] == label) )[0]
787                for ind in rlabel:
788                    self.table["Border"][ind+nrows] = 1

Measure on one frame

def selection_changed(self):
791    def selection_changed(self):
792        if self.table_selection is None:
793            return True
794        return self.output_mode.currentText() != self.selection_choices[self.table_selection]
def update_selection_list(self):
796    def update_selection_list(self):
797        """ Update the possible selection from group cell list """
798        self.selection_choices = ["Only selected cell", "All cells"]
799        for group in self.epicure.groups.keys():
800            self.selection_choices.append(group)
801        self.output_mode.clear()
802        for sel in self.selection_choices:
803            self.output_mode.addItem(sel)

Update the possible selection from group cell list

def show_table(self):
805    def show_table(self):
806        """ Show the measurement table """
807        #disable automatic update (slow)
808        #if self.table is None:
809            ## create the table and connect action to update it automatically
810            #self.output_mode.currentIndexChanged.connect(self.show_table)
811            #self.measure_other_chanels_cbox.stateChanged.connect(self.show_table)
812            #self.feature_graph_cbox.stateChanged.connect(self.show_table)
813            #self.feature_intensity_cbox.stateChanged.connect(self.show_table)
814            #self.feature_shape_cbox.stateChanged.connect(self.show_table)
815        
816        ut.set_active_layer( self.viewer, "Segmentation" )
817        self.show_feature_map.clear()
818        self.show_feature_map.addItem("")
819        laynames = [lay.name for lay in self.viewer.layers]
820        for lay in laynames:
821            if lay.startswith("Map_"):
822                ut.remove_layer(self.viewer, lay)
823        self.measure_features()
824        featlist = self.table.keys()
825        ## Scaling the features
826        if self.scaled_unit.isChecked():
827            for feat in featlist:
828                feat_scale, scaled = self.scale_feature( feat, self.table[feat] )
829                if feat_scale is not None:
830                    if (feat_scale[0:4] != "Time") and (feat_scale[0:9] != "centroid-"):
831                        del self.table[feat]
832                    self.table[feat_scale] = scaled
833        featlist = self.table.keys()
834        ## Adding the list to the feature maps
835        for feat in featlist:
836            self.show_feature_map.addItem(feat)
837        self.featTable.set_table(self.table)
838        self.temp_graph.setEnabled(True)
839        if self.tplots is not None:
840            self.tplots.update_table(self.table)

Show the measurement table

def scale_feature(self, feat, featVals):
842    def scale_feature( self, feat, featVals ):
843        """ Scale if necessary the feature values """
844        dist_feats = ["centroid-0", "centroid-1", "perimeter", "axis_major_length", "axis_minor_length", "feret_diameter_max", "equivalent_diameter_area" ]
845        if feat in dist_feats:
846            return feat+"_"+self.epicure.epi_metadata["UnitXY"], np.array(featVals)*self.epicure.epi_metadata["ScaleXY"]
847        area_feats = ["area", "area_convex"]
848        if feat in area_feats:
849            return feat+"_"+self.epicure.epi_metadata["UnitXY"]+"²", np.array(featVals)*self.epicure.epi_metadata["ScaleXY"] * self.epicure.epi_metadata["ScaleXY"]
850        if feat == "frame":
851            return "Time_"+self.epicure.epi_metadata["UnitT"], np.array(featVals)*self.epicure.epi_metadata["ScaleT"]
852        return None, None

Scale if necessary the feature values

def show_feature(self):
855    def show_feature(self):
856        """ Add the image map of the selected feature """
857        feat = self.show_feature_map.currentText()
858        if (feat is not None) and (feat != ""):
859            if feat in self.table.keys():
860                values = list(self.table[feat])
861                if feat == "group":
862                    for i, val in enumerate(values):
863                        if (val is None) or (val == 'None'):
864                            values[i] = 0
865                        else:
866                            values[i] = list(self.epicure.groups.keys()).index(val) + 1
867                labels = list(self.table["label"])
868                frames = None
869                if "frame" in self.table:
870                    frames = list(self.table["frame"])
871                self.draw_map(labels, values, frames, feat)

Add the image map of the selected feature

def draw_map(self, labels, values, frames, featname):
873    def draw_map(self, labels, values, frames, featname):
874        """ Add image layer of values by label """
875        ## special feature: orientation, draw the axis instead
876        self.viewer.window._status_bar._toggle_activity_dock(True)
877        labels = np.array(labels)
878        values = np.array(values)
879        frames = np.array(frames)
880        def map_frame( iframe, segframe ):
881            """ Draw one frame of the map """
882            mask = np.where(frames==iframe)[0]
883            labs = labels[mask]
884            vals = values[mask]
885            mapping = np.zeros(segframe.max()+1)
886            mapping[:] = np.nan
887            mapping[labs] = vals 
888            return mapping[segframe] 
889
890        if frames is not None:
891            ## Plotting a movie
892            if self.epicure.process_parallel:
893                mapfeat = Parallel( n_jobs=self.epicure.nparallel) (
894                    delayed ( map_frame )(iframe, frame ) for iframe, frame in enumerate(self.seglayer.data)
895                )
896                mapfeat = np.array(mapfeat)
897            else:
898                mapfeat = np.empty(self.epicure.seg.shape, dtype="float16")
899                mapfeat[:] = np.nan
900                for iframe in np.unique(frames):
901                    segdata = self.seglayer.data[iframe]
902                    mapfeat[iframe] = map_frame( iframe, segdata )
903        else:
904            mapfeat = np.empty(self.epicure.seg.shape, dtype="float16")
905            mapfeat[:] = np.nan
906            for lab, val in progress(zip(labels, values)):
907                cell = self.seglayer.data==lab
908                mapfeat[cell] = val
909        ut.remove_layer(self.viewer, "Map_"+featname)
910        self.viewer.add_image(mapfeat, name="Map_"+featname, scale=self.viewer.layers["Segmentation"].scale )
911        self.viewer.window._status_bar._toggle_activity_dock(False)

Add image layer of values by label

def draw_orientation(self):
913    def draw_orientation( self ):
914        """ Display the cells orientation axis in a new layer """
915        ## check that necessary features are measured
916        ut.remove_layer( self.viewer, "CellOrientation" )
917        feats = ["centroid-0", "centroid-1", "orientation"]
918        if self.table is None:
919            print("Features centroid and orientation necessary to draw orientation, but are not measured yet")
920            return
921        for feat in feats:
922            if feat not in self.table.keys():
923                print("Feature "+feat+" necessary to draw orientation, but was not measured")
924                return
925        ## ok, can work now
926        self.viewer.window._status_bar._toggle_activity_dock(True)
927
928        ## get the coordinates of the axis lines by getting the cell centroid, main orientation
929        xs = np.array( self.table["centroid-0"] )
930        ys = np.array( self.table["centroid-1"] )
931        angles = np.array( self.table["orientation"] )
932        lens = np.array( [10]*len(angles) )
933        oriens = np.zeros( (self.epicure.seg.shape), dtype="uint8" )
934
935        ## draw axis length depending on the eccentricity
936        if "eccentricity" in self.table.keys():
937            lens = np.array(self.table["eccentricity"]*16)             
938        
939        if "frame" in self.table:
940            frames = np.array( self.table["frame"] ).astype(int)
941        else:
942            frames = np.array( [0]*len(angles) )
943
944        ## draw the lines in between the two extreme points (using Shape layer is too slow on display for big movies)
945        npts = 30
946        xmax = oriens.shape[1]-1
947        ymax = oriens.shape[2]-1
948        for i in range(npts):
949            xas = np.clip(xs - lens/2 * np.cos( angles ) * i/float(npts), 0, xmax).astype(int)
950            xbs = np.clip(xs + lens/2 * np.cos( angles ) * i/float(npts), 0, xmax).astype(int)
951            yas = np.clip(ys - lens/2 * np.sin( angles ) * i/float(npts), 0, ymax).astype(int)
952            ybs = np.clip(ys + lens/2 * np.sin( angles ) * i/float(npts), 0, ymax).astype(int)
953            oriens[ (frames, xas, yas) ] = 255
954            oriens[ (frames, xbs, ybs) ] = 255
955        
956        self.viewer.add_image( oriens, name="CellOrientation", blending="additive", opacity=1, scale=self.viewer.layers["Segmentation"].scale )
957        self.viewer.window._status_bar._toggle_activity_dock(False)

Display the cells orientation axis in a new layer

def to_griot(self):
961    def to_griot(self):
962        """ Export current frame to new viewer and makes it ready for Griotte plugin """
963        try:
964            from napari_griottes import make_graph
965        except:
966            ut.show_error("Plugin napari-griottes is not installed")
967            return
968        gview = napari.Viewer()
969        tframe = ut.current_frame(self.viewer)
970        segt = self.epicure.seglayer.data[tframe]
971        touching_frame = self.touching_labels(segt)
972        gview.add_labels(touching_frame, name="TouchingCells", opacity=1)
973        gview.window.add_dock_widget(make_graph(), name="Griottes")

Export current frame to new viewer and makes it ready for Griotte plugin

def touching_labels(self, labs):
975    def touching_labels(self, labs):
976        """ Dilate labels so that they all touch """
977        from skimage.segmentation import find_boundaries
978        from skimage.morphology import skeletonize
979        from skimage.morphology import binary_closing, binary_opening
980        if self.epicure.verbose > 0:
981            print("********** Generate touching labels image ***********")
982
983        ## skeletonize it
984        skel = skeletonize( binary_closing( find_boundaries(labs), footprint=np.ones((10,10)) ) )
985        ext = np.zeros(labs.shape, dtype="uint8")
986        ext[labs==0] = 1
987        ext = binary_opening(ext, footprint=np.ones((2,2)))
988        newimg = ut.touching_labels(labs, expand=4)
989        newimg[ext>0] = 0
990        return newimg

Dilate labels so that they all touch

def to_ncp(self):
 992    def to_ncp(self):
 993        """ Export current frame to new viewer and makes it ready for napari-cluster-plots plugin """
 994        try:
 995            import napari_skimage_regionprops as nsr
 996        except:
 997            ut.show_error("Plugin napari-skimage-regionprops is not installed")
 998            return
 999        gview = napari.Viewer()
1000        tframe = ut.current_frame(self.viewer)
1001        segt = self.epicure.seglayer.data[tframe]
1002        moviet = self.epicure.viewer.layers["Movie"].data[tframe]
1003        lab = gview.add_labels(segt, name="Segmentation[t="+str(tframe)+"]", blending="additive")
1004        im = gview.add_image(moviet, name="Movie[t="+str(tframe)+"]", blending="additive")
1005        if self.epicure.verbose > 0:
1006            print("Measure features with napari-skimage-regionprops plugin...")
1007        nsr.regionprops_table(im.data, lab.data, size=True, intensity=True, perimeter=True, shape=True, position=True, moments=True, napari_viewer=gview)
1008        try:
1009            import napari_clusters_plotter as ncp
1010        except:
1011            ut.show_error("Plugin napari-clusters-plotter is not installed")
1012            return
1013        gview.window.add_dock_widget( ncp.ClusteringWidget(gview) )
1014        gview.window.add_dock_widget( ncp.PlotterWidget(gview) )

Export current frame to new viewer and makes it ready for napari-cluster-plots plugin

def temporal_graphs_events(self):
1017    def temporal_graphs_events( self ):
1018        """ New window with temporal graph of event counts """
1019        if self.tplots is not None:
1020            self.tplots.close()
1021        self.tplots = TemporalPlots( self.viewer, self.epicure )
1022        evt_table = self.count_events()
1023        self.tplots.setTable( evt_table )
1024        self.tplots.show()
1025        self.viewer.dims.events.current_step.connect(self.position_verticalline)

New window with temporal graph of event counts

def temporal_graphs(self):
1028    def temporal_graphs(self):
1029        """ New window with temporal graph of the current table selection """
1030        #self.temporal_viewer = napari.Viewer()
1031        self.tplots = TemporalPlots( self.viewer, self.epicure )
1032        self.tplots.setTable(self.table)
1033        self.tplots.show()
1034        #self.plot_wid = self.viewer.window.add_dock_widget( self.tplots, name="Plots" )
1035        self.viewer.dims.events.current_step.connect(self.position_verticalline)

New window with temporal graph of the current table selection

def on_close_viewer(self):
1037    def on_close_viewer(self):
1038        """ Temporal plots window is closed """
1039        if self.epicure.verbose > 1:
1040            print("Closed viewer")
1041        self.viewer.dims.events.current_step.disconnect(self.position_verticalline)
1042        self.temporal_viewer = None
1043        self.tplots = None

Temporal plots window is closed

def position_verticalline(self):
1045    def position_verticalline(self):
1046        """ Place the vertical line in the temporal graph to the current frame """
1047        #try:
1048        #    wid = self.tplots
1049        #except:
1050        #    self.on_close_viewer()
1051        if self.tplots is not None:
1052            self.tplots.move_framepos(self.viewer.dims.current_step[0])

Place the vertical line in the temporal graph to the current frame

def show_trackfeature_table(self):
1056    def show_trackfeature_table(self):
1057        """ Show the measurement of tracks table """
1058        self.measure_track_features()
1059        self.trackTable.set_table( self.table )

Show the measurement of tracks table

def measure_track_features(self):
1061    def measure_track_features(self):
1062        """ Measure track features and put them to table """
1063        if self.epicure.verbose > 0:
1064            print("Measuring track features")
1065        self.viewer.window._status_bar._toggle_activity_dock(True)
1066        start_time = time.time()
1067
1068        if self.output_mode.currentText() == "Only selected cell": 
1069            track_ids = self.epicure.seglayer.selected_label
1070        else:
1071            if self.output_mode.currentText() == "All cells": 
1072                track_ids = self.epicure.tracking.get_track_list()
1073            else:
1074                group = self.output_mode.currentText()
1075                track_ids = []
1076                label_group = self.epicure.groups[group]
1077                for lab in label_group:
1078                    track_ids.append(lab)
1079            
1080        properties = ["label", "area", "centroid"]
1081        self.table = None
1082
1083        if type(track_ids) == np.ndarray or type(track_ids)==np.array:
1084            track_ids = track_ids.tolist()
1085        if not type(track_ids) == list:
1086            track_ids = [track_ids]
1087
1088        labgroups = self.epicure.group_of_labels()
1089        frame_group = [ labgroups[label] if label in labgroups.keys() else "Ungrouped" for label in track_ids ]
1090        for itrack, track_id in progress(enumerate(track_ids)):
1091            track_frame = self.measure_one_track( track_id )
1092            track_frame["Group"] = frame_group[itrack]
1093            if self.table is None:
1094                self.table = pand.DataFrame([track_frame])
1095            else:
1096                self.table = pand.concat([self.table, pand.DataFrame([track_frame])])
1097
1098        self.table_selection = self.selection_choices.index(self.output_mode.currentText())
1099        self.viewer.window._status_bar._toggle_activity_dock(False)
1100        if self.epicure.verbose > 0:
1101            ut.show_info("Features measured in "+"{:.3f}".format((time.time()-start_time)/60)+" min")

Measure track features and put them to table

def measure_one_track(self, track_id):
1103    def measure_one_track( self, track_id ):
1104        """ Measure features of one track """
1105        track_features = self.epicure.tracking.measure_track_features( track_id, self.scaled_unit.isChecked() )
1106        return track_features

Measure features of one track

def choose_events(self):
1110    def choose_events( self ):
1111        """ Pop-up widget to choose the event types to measure/export """
1112        self.event_classes.choose()

Pop-up widget to choose the event types to measure/export

def count_events(self):
1114    def count_events( self ):
1115        """ Count events of selected types """
1116        evt_types = self.event_classes.get_evt_classes()
1117        if self.epicure.verbose > 2:
1118            print("Counting events of type "+str(evt_types)+" " )
1119        
1120        ## keep only events related to selected cells
1121        labels = self.get_current_labels()
1122        ## count each type of event
1123        table = np.zeros(  (self.epicure.nframes,len(evt_types)), dtype="uint8" )        
1124        for itype, evt_type in enumerate( evt_types ):
1125            evts = self.epicure.inspecting.get_events_from_type( evt_type )
1126            if len( evts ) > 0:
1127                for evt_sid in evts:
1128                        pos, label = self.epicure.inspecting.get_event_infos( evt_sid )
1129                        if label in labels:
1130                            table[ pos[0], itype ] += 1
1131        df = pand.DataFrame( data=table, columns=evt_types )
1132        df["frame"] = range(len(df))
1133        df["label"] = [0]*len(df)
1134        return df          

Count events of selected types

def export_events(self):
1136    def export_events( self ):
1137        """ Export events of selected types """
1138        evt_types = self.event_classes.get_evt_classes()
1139        export_type = self.save_evt_choice.currentText()
1140        if self.epicure.verbose > 2:
1141            print("Exporting events of type "+str(evt_types)+" to "+export_type )
1142        self.export_events_type_format( evt_types, export_type )

Export events of selected types

def export_events_type_format(self, evt_types, export_type):
1144    def export_events_type_format( self, evt_types, export_type ):
1145        """ Export events of selected types in selected format """
1146        ## keep only events related to selected cells
1147        labels = self.get_current_labels()
1148        groups = self.epicure.get_groups( labels )
1149        if export_type == "CSV File":
1150            res = pand.DataFrame( columns=["label", "frame", "posY", "posX", "EventClass", "Group"] )  
1151        ## export each type of event in separate files
1152        for itype, evt_type in enumerate( evt_types ):
1153            evts = self.epicure.inspecting.get_events_from_type( evt_type )
1154            if len( evts ) > 0:
1155                rois = [] 
1156                for evt_sid in evts:
1157                    pos, label = self.epicure.inspecting.get_event_infos( evt_sid )
1158                    ind_lab = np.where( labels==label )
1159                    if len( ind_lab[0] ) > 0:
1160                        grp = groups[ int(ind_lab[0][0]) ]
1161                        if export_type == "Fiji ROI":
1162                            roi = self.create_point_roi( pos, itype )
1163                            rois.append( roi )
1164                        if export_type == "CSV File":
1165                            new_event = pand.DataFrame( [[label, pos[0], pos[1], pos[2], evt_type, grp ]], columns=res.columns )
1166                            res = pand.concat( [res, new_event], ignore_index=True )
1167                if export_type == "Fiji ROI":            
1168                    outfile = self.epicure.outname()+"_rois_"+evt_type +""+self.get_selection_name()+".zip" 
1169                    roifile.roiwrite(outfile, rois, mode='w')
1170                    if self.epicure.verbose > 0:
1171                        print( "Events "+str( evt_type )+" saved in ROI file: "+outfile )
1172            ## dont save anything if empty, just print info to user
1173            else:
1174                if self.epicure.verbose > 0:
1175                    print( "No events of type "+str(evt_type)+"" )
1176        
1177        if export_type == "CSV File":            
1178            outfile = self.epicure.outname()+"_events"+self.get_selection_name()+".csv" 
1179            res.to_csv( outfile,  sep='\t', header=True, index=False )
1180            if self.epicure.verbose > 0:
1181                print( "Events data "+" saved in CSV file: "+outfile )

Export events of selected types in selected format

def create_point_roi(self, pos, cat=0):
1184    def create_point_roi( self, pos, cat=0 ):
1185        """ Create a point Fiji ROI """
1186        croi = roifile.ImagejRoi()
1187        croi.version = 227
1188        croi.roitype = roifile.ROI_TYPE(10)
1189        croi.name = str(pos[0]+1).zfill(4)+'-'+str(pos[1]).zfill(4)+"-"+str(pos[2]).zfill(4)
1190        croi.n_coordinates = 1
1191        croi.left = int(pos[2])
1192        croi.top = int(pos[1])
1193        croi.z_position = 1
1194        croi.t_position = pos[0]+1
1195        croi.c_position = 1
1196        croi.integer_coordinates = np.array( [[0,0]] )
1197        croi.stroke_width=3
1198        ncolors = 3
1199        if cat%ncolors == 0:  ## color type 0
1200            croi.stroke_color = b'\xff\x00\x00\xff'
1201        if cat%ncolors == 1:  ## color type 1
1202            croi.stroke_color = b'\xff\x00\xff\x00'
1203        if cat%ncolors == 2:  ## color type 2
1204            croi.stroke_color = b'\xff\xff\x00\x00'
1205        return croi

Create a point Fiji ROI

def save_tm_xml(self):
1207    def save_tm_xml( self ):
1208        """ Save current segmentation and tracking in TrackMate XML format """
1209        outname = os.path.join( self.epicure.outdir, self.epicure.imgname+".xml" )
1210        save_trackmate_xml( self.epicure, outname )
1211        if self.epicure.verbose > 0:
1212            ut.show_info("TrackMate XML saved in "+outname)

Save current segmentation and tracking in TrackMate XML format

def save_geff(self):
1214    def save_geff( self ):
1215        """ Save current segmentation and tracking in GEFF format """
1216        ## save the label segmentation if it's not saved
1217        labelname = os.path.join( self.epicure.outdir, self.epicure.imgname + "_labels.tif" )
1218        ut.writeTif( self.epicure.seg, labelname, self.epicure.epi_metadata["ScaleXY"], "float32", what="Segmentation" )
1219        ## then export the GEFF file
1220        if self.epicure.tracking.graph is None:
1221            self.epicure.tracking.graph = {}
1222        outname = os.path.join( self.epicure.outdir, self.epicure.imgname+".geff" )
1223        save_geff( self.epicure, outname )
1224        if self.epicure.verbose > 0:
1225            ut.show_info("GEFF file saved in "+outname)

Save current segmentation and tracking in GEFF format

class CellFeatures(PyQt6.QtWidgets.QWidget):
1228class CellFeatures(QWidget):
1229    """ Choice of features to measure """
1230    def __init__(self, chanlist):
1231        super().__init__()
1232        layout = QVBoxLayout()
1233        
1234        self.required = ["label"]
1235        self.features = {}
1236        self.chan_list = None
1237        
1238        other_list = ["group", "NbNeighbors", "Neighbors", "Boundary", "Border"]
1239        feat_layout = self.add_feature_group( other_list, "other" )
1240        layout.addLayout( feat_layout )
1241        sel_all_b = wid.add_button( "Select spatial features", lambda: self.select_all("other"), "Select all spatial features" )
1242        desel_all_b = wid.add_button( "Deselect spatial features", lambda: self.deselect_all("other"), "Deselect all spatial features" )
1243        sel_line_b = wid.hlayout()
1244        sel_line_b.addWidget( sel_all_b )
1245        sel_line_b.addWidget( desel_all_b )
1246        layout.addLayout( sel_line_b )
1247
1248
1249        ## Add shape features
1250        shape_list = ["centroid", "area", "area_convex", "axis_major_length", "axis_minor_length", "feret_diameter_max", "equivalent_diameter_area", "eccentricity", "orientation", "perimeter", "solidity"]
1251        other_shape_list = ["shape_index", "roundness", "aspect_ratio"]
1252        feat_layout = self.add_feature_group( shape_list, "prop" )
1253        feat_extra_layout = self.add_feature_group( other_shape_list, "prop_extra" )
1254        layout.addLayout( feat_layout )
1255        layout.addLayout( feat_extra_layout )
1256        sel_all = wid.add_button( "Select morphology features", lambda: self.select_all("props"), "Select all morphology features" )
1257        desel_all = wid.add_button( "Deselect morphology features", lambda: self.deselect_all("props"), "Deselect all morphology features" )
1258        sel_line = wid.hlayout()
1259        sel_line.addWidget( sel_all )
1260        sel_line.addWidget( desel_all )
1261        layout.addLayout( sel_line )
1262
1263        int_lab = wid.label_line( "Intensity features:")
1264        layout.addWidget( int_lab )
1265        intensity_list = ["intensity_mean", "intensity_min", "intensity_max"]
1266        extra_list = ["intensity_junction_cytoplasm"]
1267        feat_layout = self.add_feature_group( intensity_list, "intensity_prop" )
1268        layout.addLayout( feat_layout )
1269        feat_layout = self.add_feature_group( extra_list, "intensity_extra" )
1270        layout.addLayout( feat_layout )
1271        if len(chanlist) > 1:
1272            chan_lab = wid.label_line( "Measure intensity in channels:" )
1273            layout.addWidget( chan_lab )
1274            self.chan_list = QListWidget()
1275            self.chan_list.addItems( chanlist )
1276            self.chan_list.setSelectionMode(aiv.MultiSelection)
1277            self.chan_list.item(0).setSelected(True)
1278            layout.addWidget( self.chan_list )
1279        
1280        sel_all_int = wid.add_button( "Select intensity features", lambda: self.select_all("intensity"), "Select all spatial features" )
1281        desel_all_int = wid.add_button( "Deselect intensity features", lambda: self.deselect_all("intensity"), "Deselect all spatial features" )
1282        sel_line_int = wid.hlayout()
1283        sel_line_int.addWidget( sel_all_int )
1284        sel_line_int.addWidget( desel_all_int )
1285        layout.addLayout( sel_line_int )
1286
1287        bye = wid.add_button( "Ok", self.close, "Close the window" )
1288        layout.addWidget( bye )
1289        self.setLayout( layout )
1290
1291    def select_all( self, feat ):
1292        """ Select all features of type feat """
1293        if feat == "intensity":
1294            self.select_all( "intensity_prop" )
1295            self.select_all( "intensity_extra" )
1296            return
1297        if feat == "props":
1298            self.select_all( "prop" )
1299            self.select_all( "prop_extra" )
1300            return
1301        for featy, feat_val in self.features.items():
1302            if feat_val[1] == feat:
1303                feat_val[0].setChecked( True )
1304    
1305    def deselect_all( self, feat ):
1306        """ Deselect all features of type feat """
1307        if feat == "intensity":
1308            self.deselect_all( "intensity_prop" )
1309            self.deselect_all( "intensity_extra" )
1310            return
1311        if feat == "props":
1312            self.deselect_all( "prop" )
1313            self.deselect_all( "prop_extra" )
1314            return
1315        for featy, feat_val in self.features.items():
1316            if feat_val[1] == feat:
1317                feat_val[0].setChecked( False )
1318
1319
1320    def add_feature_group( self, feat_list, feat_type ):
1321        """ Add features to the GUI """
1322        layout = QVBoxLayout()
1323        ncols = 3
1324        for i, feat in enumerate(feat_list):
1325            if i%ncols == 0:
1326                line = QHBoxLayout()
1327            feature_check = wid.add_check( ""+feat, True, None, descr="" )
1328            line.addWidget(feature_check)
1329            self.features[ feat ] = [feature_check, feat_type]
1330            if i%ncols == (ncols-1):
1331                layout.addLayout( line )
1332                line = None
1333        if line is not None:
1334            layout.addLayout( line )
1335        return layout
1336
1337
1338    def close( self ):
1339        """ Close the pop-up window """
1340        self.hide()
1341
1342    def choose( self ):
1343        """ Show the interface to select the choices """
1344        self.show()
1345
1346    def get_current_settings( self, setting ):
1347        """ Get current settings of check or not of features """
1348        for feat, feat_cbox in self.features.items():
1349            setting[feat] = feat_cbox[0].isChecked()
1350        return setting
1351
1352    def apply_settings( self, settings ):
1353        """ Set the checkboxes from preferenced settings """
1354        for feat, checked in settings.items():
1355            if feat in self.features.keys():
1356                self.features[feat][0].setChecked( checked )
1357        
1358    def get_features( self ):
1359        """ Returns the list of features to measure """
1360        feats = self.required
1361        feats_extra = []
1362        int_extra_feats = []
1363        int_feats = []
1364        other_feats = []
1365        self.do_intensity = False
1366        for feat, feat_cbox in self.features.items():
1367            if feat_cbox[0].isChecked():
1368                if feat_cbox[1] == "prop":
1369                    feats.append( feat )
1370                if feat_cbox[1] == "prop_extra":
1371                    feats_extra.append( feat )
1372                    if feat == "shape_index":
1373                        if "perimeter" not in feats:
1374                            feats.append("perimeter")
1375                        if "area" not in feats:
1376                            feats.append("area")
1377                    if feat == "roundness":
1378                        if "area" not in feats:
1379                            feats.append("area")
1380                        if "axis_major_length" not in feats:
1381                            feats.append("axis_major_length")
1382                    if feat == "aspect_ratio":
1383                        if "axis_major_length" not in feats:
1384                            feats.append("axis_major_length")
1385                        if "axis_minor_length" not in feats:
1386                            feats.append("axis_minor_length")
1387                if feat_cbox[1] == "other":
1388                    other_feats.append( feat )
1389                if feat_cbox[1] == "intensity_prop":
1390                    int_feats.append( feat )
1391                    self.do_intensity = True
1392                if feat_cbox[1] == "intensity_extra":
1393                    int_extra_feats.append( feat )
1394                    self.do_intensity = True
1395        return feats, feats_extra, other_feats, int_feats, int_extra_feats
1396
1397    def get_channels( self ):
1398        """ Returns the list of channels to measure """
1399        if self.do_intensity:
1400            if self.chan_list is not None:
1401                wid_channels = self.chan_list.selectedItems()
1402                channels = []
1403                for chan in wid_channels:
1404                    channels.append( chan.text() )
1405            else:
1406                channels = ["Movie"]
1407            return channels
1408        return None

Choice of features to measure

CellFeatures(chanlist)
1230    def __init__(self, chanlist):
1231        super().__init__()
1232        layout = QVBoxLayout()
1233        
1234        self.required = ["label"]
1235        self.features = {}
1236        self.chan_list = None
1237        
1238        other_list = ["group", "NbNeighbors", "Neighbors", "Boundary", "Border"]
1239        feat_layout = self.add_feature_group( other_list, "other" )
1240        layout.addLayout( feat_layout )
1241        sel_all_b = wid.add_button( "Select spatial features", lambda: self.select_all("other"), "Select all spatial features" )
1242        desel_all_b = wid.add_button( "Deselect spatial features", lambda: self.deselect_all("other"), "Deselect all spatial features" )
1243        sel_line_b = wid.hlayout()
1244        sel_line_b.addWidget( sel_all_b )
1245        sel_line_b.addWidget( desel_all_b )
1246        layout.addLayout( sel_line_b )
1247
1248
1249        ## Add shape features
1250        shape_list = ["centroid", "area", "area_convex", "axis_major_length", "axis_minor_length", "feret_diameter_max", "equivalent_diameter_area", "eccentricity", "orientation", "perimeter", "solidity"]
1251        other_shape_list = ["shape_index", "roundness", "aspect_ratio"]
1252        feat_layout = self.add_feature_group( shape_list, "prop" )
1253        feat_extra_layout = self.add_feature_group( other_shape_list, "prop_extra" )
1254        layout.addLayout( feat_layout )
1255        layout.addLayout( feat_extra_layout )
1256        sel_all = wid.add_button( "Select morphology features", lambda: self.select_all("props"), "Select all morphology features" )
1257        desel_all = wid.add_button( "Deselect morphology features", lambda: self.deselect_all("props"), "Deselect all morphology features" )
1258        sel_line = wid.hlayout()
1259        sel_line.addWidget( sel_all )
1260        sel_line.addWidget( desel_all )
1261        layout.addLayout( sel_line )
1262
1263        int_lab = wid.label_line( "Intensity features:")
1264        layout.addWidget( int_lab )
1265        intensity_list = ["intensity_mean", "intensity_min", "intensity_max"]
1266        extra_list = ["intensity_junction_cytoplasm"]
1267        feat_layout = self.add_feature_group( intensity_list, "intensity_prop" )
1268        layout.addLayout( feat_layout )
1269        feat_layout = self.add_feature_group( extra_list, "intensity_extra" )
1270        layout.addLayout( feat_layout )
1271        if len(chanlist) > 1:
1272            chan_lab = wid.label_line( "Measure intensity in channels:" )
1273            layout.addWidget( chan_lab )
1274            self.chan_list = QListWidget()
1275            self.chan_list.addItems( chanlist )
1276            self.chan_list.setSelectionMode(aiv.MultiSelection)
1277            self.chan_list.item(0).setSelected(True)
1278            layout.addWidget( self.chan_list )
1279        
1280        sel_all_int = wid.add_button( "Select intensity features", lambda: self.select_all("intensity"), "Select all spatial features" )
1281        desel_all_int = wid.add_button( "Deselect intensity features", lambda: self.deselect_all("intensity"), "Deselect all spatial features" )
1282        sel_line_int = wid.hlayout()
1283        sel_line_int.addWidget( sel_all_int )
1284        sel_line_int.addWidget( desel_all_int )
1285        layout.addLayout( sel_line_int )
1286
1287        bye = wid.add_button( "Ok", self.close, "Close the window" )
1288        layout.addWidget( bye )
1289        self.setLayout( layout )
required
features
chan_list
def select_all(self, feat):
1291    def select_all( self, feat ):
1292        """ Select all features of type feat """
1293        if feat == "intensity":
1294            self.select_all( "intensity_prop" )
1295            self.select_all( "intensity_extra" )
1296            return
1297        if feat == "props":
1298            self.select_all( "prop" )
1299            self.select_all( "prop_extra" )
1300            return
1301        for featy, feat_val in self.features.items():
1302            if feat_val[1] == feat:
1303                feat_val[0].setChecked( True )

Select all features of type feat

def deselect_all(self, feat):
1305    def deselect_all( self, feat ):
1306        """ Deselect all features of type feat """
1307        if feat == "intensity":
1308            self.deselect_all( "intensity_prop" )
1309            self.deselect_all( "intensity_extra" )
1310            return
1311        if feat == "props":
1312            self.deselect_all( "prop" )
1313            self.deselect_all( "prop_extra" )
1314            return
1315        for featy, feat_val in self.features.items():
1316            if feat_val[1] == feat:
1317                feat_val[0].setChecked( False )

Deselect all features of type feat

def add_feature_group(self, feat_list, feat_type):
1320    def add_feature_group( self, feat_list, feat_type ):
1321        """ Add features to the GUI """
1322        layout = QVBoxLayout()
1323        ncols = 3
1324        for i, feat in enumerate(feat_list):
1325            if i%ncols == 0:
1326                line = QHBoxLayout()
1327            feature_check = wid.add_check( ""+feat, True, None, descr="" )
1328            line.addWidget(feature_check)
1329            self.features[ feat ] = [feature_check, feat_type]
1330            if i%ncols == (ncols-1):
1331                layout.addLayout( line )
1332                line = None
1333        if line is not None:
1334            layout.addLayout( line )
1335        return layout

Add features to the GUI

def close(self):
1338    def close( self ):
1339        """ Close the pop-up window """
1340        self.hide()

Close the pop-up window

def choose(self):
1342    def choose( self ):
1343        """ Show the interface to select the choices """
1344        self.show()

Show the interface to select the choices

def get_current_settings(self, setting):
1346    def get_current_settings( self, setting ):
1347        """ Get current settings of check or not of features """
1348        for feat, feat_cbox in self.features.items():
1349            setting[feat] = feat_cbox[0].isChecked()
1350        return setting

Get current settings of check or not of features

def apply_settings(self, settings):
1352    def apply_settings( self, settings ):
1353        """ Set the checkboxes from preferenced settings """
1354        for feat, checked in settings.items():
1355            if feat in self.features.keys():
1356                self.features[feat][0].setChecked( checked )

Set the checkboxes from preferenced settings

def get_features(self):
1358    def get_features( self ):
1359        """ Returns the list of features to measure """
1360        feats = self.required
1361        feats_extra = []
1362        int_extra_feats = []
1363        int_feats = []
1364        other_feats = []
1365        self.do_intensity = False
1366        for feat, feat_cbox in self.features.items():
1367            if feat_cbox[0].isChecked():
1368                if feat_cbox[1] == "prop":
1369                    feats.append( feat )
1370                if feat_cbox[1] == "prop_extra":
1371                    feats_extra.append( feat )
1372                    if feat == "shape_index":
1373                        if "perimeter" not in feats:
1374                            feats.append("perimeter")
1375                        if "area" not in feats:
1376                            feats.append("area")
1377                    if feat == "roundness":
1378                        if "area" not in feats:
1379                            feats.append("area")
1380                        if "axis_major_length" not in feats:
1381                            feats.append("axis_major_length")
1382                    if feat == "aspect_ratio":
1383                        if "axis_major_length" not in feats:
1384                            feats.append("axis_major_length")
1385                        if "axis_minor_length" not in feats:
1386                            feats.append("axis_minor_length")
1387                if feat_cbox[1] == "other":
1388                    other_feats.append( feat )
1389                if feat_cbox[1] == "intensity_prop":
1390                    int_feats.append( feat )
1391                    self.do_intensity = True
1392                if feat_cbox[1] == "intensity_extra":
1393                    int_extra_feats.append( feat )
1394                    self.do_intensity = True
1395        return feats, feats_extra, other_feats, int_feats, int_extra_feats

Returns the list of features to measure

def get_channels(self):
1397    def get_channels( self ):
1398        """ Returns the list of channels to measure """
1399        if self.do_intensity:
1400            if self.chan_list is not None:
1401                wid_channels = self.chan_list.selectedItems()
1402                channels = []
1403                for chan in wid_channels:
1404                    channels.append( chan.text() )
1405            else:
1406                channels = ["Movie"]
1407            return channels
1408        return None

Returns the list of channels to measure

class EventClass(PyQt6.QtWidgets.QWidget):
1410class EventClass( QWidget ):
1411    """ Choice of event types to export/measure """
1412    def __init__( self, epicure ):
1413        super().__init__()
1414        layout = QVBoxLayout()
1415        
1416        self.evt_classes = {}
1417        possible_classes = epicure.event_class
1418        event_layout = self.add_events( possible_classes )
1419        layout.addLayout( event_layout )
1420
1421        bye = wid.add_button( "Ok", self.close, "Close the window" )
1422        layout.addWidget( bye )
1423        self.setLayout( layout )
1424
1425    def add_events( self, event_list ):
1426        """ Add events to the GUI """
1427        layout = QVBoxLayout()
1428        ncols = 3
1429        for i, event in enumerate( event_list ):
1430            if i%ncols == 0:
1431                line = QHBoxLayout()
1432            event_check = wid.add_check_tolayout( line, ""+event, checked=True, descr="")
1433            self.evt_classes[ event ] = [ event_check ]
1434            if i%ncols == (ncols-1):
1435                layout.addLayout( line )
1436                line = None
1437        if line is not None:
1438            layout.addLayout( line )
1439        return layout
1440
1441
1442    def close( self ):
1443        """ Close the pop-up window """
1444        self.hide()
1445
1446    def choose( self ):
1447        """ Show the interface to select the choices """
1448        self.show()
1449
1450    def get_current_settings( self, setting ):
1451        """ Get current settings of check or not of features """
1452        for event, event_cbox in self.evt_classes.items():
1453            setting[event] = event_cbox[0].isChecked()
1454        return setting
1455
1456    def apply_settings( self, settings ):
1457        """ Set the checkboxes from preferenced settings """
1458        for evt, checked in settings.items():
1459            if evt in self.evt_classes.keys():
1460                self.evt_classes[evt][0].setChecked( checked )
1461        
1462    def get_evt_classes( self ):
1463        """ Returns the list of events to measure """
1464        events = []
1465        for evt, evt_cbox in self.evt_classes.items():
1466            if evt_cbox[0].isChecked():
1467                events.append( evt )
1468        return events

Choice of event types to export/measure

EventClass(epicure)
1412    def __init__( self, epicure ):
1413        super().__init__()
1414        layout = QVBoxLayout()
1415        
1416        self.evt_classes = {}
1417        possible_classes = epicure.event_class
1418        event_layout = self.add_events( possible_classes )
1419        layout.addLayout( event_layout )
1420
1421        bye = wid.add_button( "Ok", self.close, "Close the window" )
1422        layout.addWidget( bye )
1423        self.setLayout( layout )
evt_classes
def add_events(self, event_list):
1425    def add_events( self, event_list ):
1426        """ Add events to the GUI """
1427        layout = QVBoxLayout()
1428        ncols = 3
1429        for i, event in enumerate( event_list ):
1430            if i%ncols == 0:
1431                line = QHBoxLayout()
1432            event_check = wid.add_check_tolayout( line, ""+event, checked=True, descr="")
1433            self.evt_classes[ event ] = [ event_check ]
1434            if i%ncols == (ncols-1):
1435                layout.addLayout( line )
1436                line = None
1437        if line is not None:
1438            layout.addLayout( line )
1439        return layout

Add events to the GUI

def close(self):
1442    def close( self ):
1443        """ Close the pop-up window """
1444        self.hide()

Close the pop-up window

def choose(self):
1446    def choose( self ):
1447        """ Show the interface to select the choices """
1448        self.show()

Show the interface to select the choices

def get_current_settings(self, setting):
1450    def get_current_settings( self, setting ):
1451        """ Get current settings of check or not of features """
1452        for event, event_cbox in self.evt_classes.items():
1453            setting[event] = event_cbox[0].isChecked()
1454        return setting

Get current settings of check or not of features

def apply_settings(self, settings):
1456    def apply_settings( self, settings ):
1457        """ Set the checkboxes from preferenced settings """
1458        for evt, checked in settings.items():
1459            if evt in self.evt_classes.keys():
1460                self.evt_classes[evt][0].setChecked( checked )

Set the checkboxes from preferenced settings

def get_evt_classes(self):
1462    def get_evt_classes( self ):
1463        """ Returns the list of events to measure """
1464        events = []
1465        for evt, evt_cbox in self.evt_classes.items():
1466            if evt_cbox[0].isChecked():
1467                events.append( evt )
1468        return events

Returns the list of events to measure

class FeaturesTable(PyQt6.QtWidgets.QWidget):
1470class FeaturesTable(QWidget):
1471    """ Widget to visualize and interact with the measurement table """
1472
1473    def __init__(self, napari_viewer, epicure):
1474        super().__init__()
1475        self.viewer = napari_viewer
1476        self.epicure = epicure
1477        self.wid_table = QTableWidget()
1478        self.wid_table.setEditTriggers(QTableWidget.EditTrigger.NoEditTriggers)
1479        self.setLayout(QGridLayout())
1480        self.layout().addWidget(self.wid_table)
1481        self.wid_table.clicked.connect(self.show_label)
1482        self.wid_table.setSortingEnabled(True)
1483
1484    def show_label(self):
1485        """ When click on the table, show selected cell """
1486        if self.wid_table is not None:
1487            row = self.wid_table.currentRow()
1488            self.epicure.seglayer.show_selected_label = False
1489            headers = [self.wid_table.horizontalHeaderItem(ind).text() for ind in range(self.wid_table.columnCount()) ]
1490            labelind = None
1491            if "label" in headers:
1492                labelind = headers.index("label") 
1493            if "Label" in headers:
1494                labelind = headers.index("Label") 
1495            frameind = None
1496            if "frame" in headers:
1497                frameind = headers.index("frame") 
1498            if labelind is not None and labelind >= 0:
1499                lab = int(self.wid_table.item(row, labelind).text())
1500                if frameind is not None:
1501                    ## set current frame to the selected row
1502                    frame = int(self.wid_table.item(row, frameind).text())
1503                    ut.set_frame(self.viewer, frame)
1504                else:
1505                    ## set current frame to the first frame where label or track is present
1506                    frame = self.epicure.tracking.get_first_frame( lab )
1507                    if frame is not None:
1508                        ut.set_frame(self.viewer, frame)
1509                self.epicure.seglayer.selected_label = lab
1510                self.epicure.seglayer.show_selected_label = True
1511
1512
1513    def get_features_list(self):
1514        """ Return list of measured features """
1515        return [ self.wid_table.horizontalHeaderItem(ind).text() for ind in range(self.wid_table.columnCount()) ]
1516
1517    def set_table(self, table):
1518        self.wid_table.clear()
1519        self.wid_table.setRowCount(table.shape[0])
1520        self.wid_table.setColumnCount(table.shape[1])
1521
1522        for c, column in enumerate(table.keys()):
1523            column_name = column
1524            self.wid_table.setHorizontalHeaderItem(c, QTableWidgetItem(column_name))
1525            for r, value in enumerate(table.get(column)):
1526                item = QTableWidgetItem()
1527                item.setData( Qt.EditRole, value)
1528                self.wid_table.setItem(r, c, item)

Widget to visualize and interact with the measurement table

FeaturesTable(napari_viewer, epicure)
1473    def __init__(self, napari_viewer, epicure):
1474        super().__init__()
1475        self.viewer = napari_viewer
1476        self.epicure = epicure
1477        self.wid_table = QTableWidget()
1478        self.wid_table.setEditTriggers(QTableWidget.EditTrigger.NoEditTriggers)
1479        self.setLayout(QGridLayout())
1480        self.layout().addWidget(self.wid_table)
1481        self.wid_table.clicked.connect(self.show_label)
1482        self.wid_table.setSortingEnabled(True)
viewer
epicure
wid_table
def show_label(self):
1484    def show_label(self):
1485        """ When click on the table, show selected cell """
1486        if self.wid_table is not None:
1487            row = self.wid_table.currentRow()
1488            self.epicure.seglayer.show_selected_label = False
1489            headers = [self.wid_table.horizontalHeaderItem(ind).text() for ind in range(self.wid_table.columnCount()) ]
1490            labelind = None
1491            if "label" in headers:
1492                labelind = headers.index("label") 
1493            if "Label" in headers:
1494                labelind = headers.index("Label") 
1495            frameind = None
1496            if "frame" in headers:
1497                frameind = headers.index("frame") 
1498            if labelind is not None and labelind >= 0:
1499                lab = int(self.wid_table.item(row, labelind).text())
1500                if frameind is not None:
1501                    ## set current frame to the selected row
1502                    frame = int(self.wid_table.item(row, frameind).text())
1503                    ut.set_frame(self.viewer, frame)
1504                else:
1505                    ## set current frame to the first frame where label or track is present
1506                    frame = self.epicure.tracking.get_first_frame( lab )
1507                    if frame is not None:
1508                        ut.set_frame(self.viewer, frame)
1509                self.epicure.seglayer.selected_label = lab
1510                self.epicure.seglayer.show_selected_label = True

When click on the table, show selected cell

def get_features_list(self):
1513    def get_features_list(self):
1514        """ Return list of measured features """
1515        return [ self.wid_table.horizontalHeaderItem(ind).text() for ind in range(self.wid_table.columnCount()) ]

Return list of measured features

def set_table(self, table):
1517    def set_table(self, table):
1518        self.wid_table.clear()
1519        self.wid_table.setRowCount(table.shape[0])
1520        self.wid_table.setColumnCount(table.shape[1])
1521
1522        for c, column in enumerate(table.keys()):
1523            column_name = column
1524            self.wid_table.setHorizontalHeaderItem(c, QTableWidgetItem(column_name))
1525            for r, value in enumerate(table.get(column)):
1526                item = QTableWidgetItem()
1527                item.setData( Qt.EditRole, value)
1528                self.wid_table.setItem(r, c, item)
class TemporalPlots(PyQt6.QtWidgets.QWidget):
1530class TemporalPlots(QWidget):
1531    """ Widget to visualize and interact with temporal plots """
1532
1533    def __init__(self, napari_viewer, epicure):
1534        super().__init__()
1535        self.viewer = napari_viewer
1536        self.epicure = epicure
1537        self.features_list = ["frame"]
1538        self.parameter_gui()
1539        self.vline = None
1540        self.ymin = None
1541        #self.viewer.window.add_dock_widget( self.plot_wid, name="Temporal plot" )
1542   
1543    def parameter_gui(self):
1544        """ add widget to choose plotting parameters """
1545        
1546        layout = QVBoxLayout()
1547
1548        ## choice of feature to plot
1549        feat_choice, self.feature_choice = wid.list_line( label="Plot feature", descr="Choose the feature to plot", func=self.plot_feature )
1550        layout.addLayout(feat_choice)
1551        ## option to average by group
1552        ck_line, self.avg_group, self.smooth = wid.double_check( "Average by groups", False, self.plot_feature, "Show a line by cell or a line by group", "Smooth lines", False, self.plot_feature, "Smooth temporally (moving average) the plotted lines" )
1553        layout.addLayout(ck_line)
1554        ## show the plot
1555        self.plot_wid = self.create_plotwidget()
1556        layout.addWidget(self.plot_wid)
1557        ## save plot or save data of the plot
1558        line = wid.double_button( "Save plot image", self.save_plot_image, "Save the grapic in a PNG file", "Save plot data", self.save_plot_data, "Save the value used for the plot in .csv file" )
1559        self.by_label = wid.add_check( "Arranged data by label", False, None, "Save the data with one column by label" )
1560        line.addWidget( self.by_label )
1561        layout.addLayout( line )
1562        self.setLayout(layout)
1563        self.resize(1000,800)
1564
1565    def setTable(self, table):
1566        """ Data table to plot """
1567        self.table = table
1568        self.features_list = self.table.keys()
1569        self.update_feature_list()
1570
1571    def update_table(self, table):
1572        """ Update the current plot with the updated table """
1573        self.table = table
1574        curchoice = self.feature_choice.currentText()
1575        self.features_list = self.table.keys()
1576        self.update_feature_list()
1577        if curchoice in self.features_list:
1578            ind = list(self.features_list).index(curchoice)
1579            self.feature_choice.setCurrentIndex(ind)
1580        self.plot_feature()
1581
1582    def update_feature_list(self):
1583        """ Update the list of feature in the GUI """
1584        self.feature_choice.clear()
1585        for feat in self.features_list:
1586            self.feature_choice.addItem(feat)
1587        if "division" in self.features_list and "extrusion" in self.features_list:
1588            self.feature_choice.addItem( "division&extrusion" )
1589    
1590    def plot_feature(self):
1591        """ Plot the selected feature in the temporal graph """
1592        feat = self.feature_choice.currentText()
1593        if feat == "label":
1594            return
1595        if feat == "":
1596            return
1597        if feat == "division&extrusion":
1598            feat = ["division", "extrusion"]
1599        else:
1600            feat = [feat]
1601        
1602        tab = list( zip(self.table["frame"]) )
1603        labname = []
1604        for ft in feat:
1605            tab = [ (*t, v) for t, v in zip( tab, self.table[ft]) ]
1606        tab = [ (*t, v) for t, v in zip( tab, self.table["label"]) ]
1607        labname.append("label")
1608        if "group" in self.table:
1609            tab = [ (*t, v) for t, v in zip( tab, self.table["group"]) ]
1610            labname.append("group")
1611
1612        self.df = pand.DataFrame( tab, columns=["frame"] + feat + labname )
1613        shape = "linear"
1614        if self.smooth.isChecked():
1615            shape = "spline"
1616        if "group" in self.table and self.avg_group.isChecked():
1617            self.dfmean = self.df.groupby(['group', 'frame'])[feat].mean().reset_index()
1618            self.df.columns.name = 'group'
1619            self.fig = px.line( self.dfmean, x='frame', y=feat, color='group', labels={'frame': 'Time (frame)'}, line_shape=shape, render_mode="svg" )
1620        else:
1621            if len( np.unique(self.df["label"]) ) > 1000:
1622                ut.show_warning( "Too many lines to plot; Using a random subset instead" )
1623                subset = sample( np.unique(self.df["label"]).tolist(), 1000)
1624                subdf = self.df[self.df["label"].isin(subset)]
1625                self.fig = px.line( subdf, x="frame", y=feat[0], color="label", labels={'frame': 'Time (frame)'}, line_shape = shape, render_mode="svg")
1626                if len(feat) > 1:
1627                    addfig = px.line(subdf, x="frame", y=feat[1], color="label", line_shape = shape )
1628                    addfig.update_traces( patch={"line": {"dash":"dot"}} )
1629                    self.fig.add_trace( addfig.data[0] )
1630            else:
1631                self.fig = px.line( self.df, x="frame", y=feat[0], color="label", labels={'frame': 'Time (frame)'}, line_shape = shape, render_mode="svg")
1632                if len(feat) > 1:
1633                    addfig = px.line(self.df, x="frame", y=feat[1], color="label", line_shape = shape )
1634                    addfig.update_traces( patch={"line": {"dash":"dot"}} )
1635                    self.fig.add_trace( addfig.data[0] )
1636    
1637        if self.webengine:
1638            self.browser.setHtml( self.fig.to_html(include_plotlyjs='cdn'))
1639        else:
1640            self.show_plot_in_browser( self.fig.to_html(include_plotlyjs='cdn'))
1641        
1642    def show_plot_in_browser(self,html):
1643        with tempfile.NamedTemporaryFile(mode='w', suffix='.html', delete=False) as f:
1644            f.write(html)
1645            url = 'file://' + f.name
1646            webbrowser.open(url)
1647
1648    def smooth_df( self, df ):
1649        """ Smooth temporally the dataframe by label or by group """
1650        rollsize = 20
1651        ## average on a smaller scale if only few frames
1652        if np.max( self.table["frame"] ) <= 20:
1653            rollsize = 5    
1654        if feat+"_smooth" in self.df.columns:
1655            feat = feat+"_smooth"
1656        else:
1657            self.df[feat+"_smooth"] = self.df[feat].rolling(rollsize, center=True).mean()
1658            #print(self.df)
1659            feat = feat+"_smooth"
1660
1661    def save_plot_image( self ):
1662        """ Save current plot graphic to PNG image """
1663        feat = self.feature_choice.currentText()
1664        outfile = self.epicure.outname()+"_plot_"+feat+".png"
1665        if self.fig is not None:
1666            self.fig.write_image( outfile )
1667        if self.epicure.verbose > 0:
1668            ut.show_info("Measures saved in "+outfile)
1669
1670    def save_plot_data( self ):
1671        """ Save the raw data to redraw the current plot to csv file """
1672        feat = self.feature_choice.currentText()
1673        outfile = self.epicure.outname()+"_time_"+feat+".csv"
1674        if self.avg_group.isChecked():
1675            data = self.dfmean.reset_index()[["frame", "group", feat]]
1676            if self.by_label.isChecked():
1677                df = pand.pivot_table( data, columns="label", index="frame", values=feat )
1678                df.to_csv( outfile,  sep='\t', header=True, index=True )
1679            else:
1680                data[["frame", "group", feat]].to_csv( outfile,  sep='\t', header=True, index=False )
1681        else:
1682            data = self.df.reset_index()[["frame", "label", feat]]
1683            if self.by_label.isChecked():
1684                df = pand.pivot_table( data, columns="label", index="frame", values=feat )
1685                df.to_csv( outfile,  sep='\t', header=True, index=True )
1686            else:
1687                data[["frame", "label", feat]].to_csv( outfile,  sep='\t', header=True, index=False )
1688
1689    def move_framepos(self, frame):
1690        """ Move the vertical line showing the current frame position in the main window """
1691        return
1692        #if self.fig is not None:
1693        #    self.fig.add_vline( x=frame, line_dash="dash", line_color="gray" )
1694        #    self.browser.setHtml( self.fig.to_html(include_plotlyjs='cdn'))
1695
1696
1697
1698    def import_webengineview(self):
1699        """Return QWebEngineView from whichever Qt is available."""
1700        import importlib
1701        try:
1702            # Fall back to Qt5
1703            mod = importlib.import_module("PyQt5.QtWebEngineWidgets")
1704            self.browser = mod.QWebEngineView(self)
1705            return 
1706        except Exception:
1707            pass
1708        
1709        try:
1710            # Try Qt6 first
1711            view = importlib.import_module("PyQt6.QtWebEngineWidgets")
1712            self.browser = view.QWebEngineView( parent=self )
1713            return  
1714        except Exception as e:
1715            print(e)
1716            pass
1717
1718        raise ImportError(
1719            "No QtWebEngine found. Install PyQt6-WebEngine or PyQtWebEngine."
1720        )
1721
1722
1723    def create_plotwidget(self):
1724        """ Create plot window """
1725        try:
1726            self.import_webengineview()
1727            self.webengine = True
1728        except:
1729            self.webengine = False
1730            self.browser = NoEngineViewer()
1731            return self.browser
1732        print(self.webengine)
1733        return self.browser

Widget to visualize and interact with temporal plots

TemporalPlots(napari_viewer, epicure)
1533    def __init__(self, napari_viewer, epicure):
1534        super().__init__()
1535        self.viewer = napari_viewer
1536        self.epicure = epicure
1537        self.features_list = ["frame"]
1538        self.parameter_gui()
1539        self.vline = None
1540        self.ymin = None
1541        #self.viewer.window.add_dock_widget( self.plot_wid, name="Temporal plot" )
viewer
epicure
features_list
vline
ymin
def parameter_gui(self):
1543    def parameter_gui(self):
1544        """ add widget to choose plotting parameters """
1545        
1546        layout = QVBoxLayout()
1547
1548        ## choice of feature to plot
1549        feat_choice, self.feature_choice = wid.list_line( label="Plot feature", descr="Choose the feature to plot", func=self.plot_feature )
1550        layout.addLayout(feat_choice)
1551        ## option to average by group
1552        ck_line, self.avg_group, self.smooth = wid.double_check( "Average by groups", False, self.plot_feature, "Show a line by cell or a line by group", "Smooth lines", False, self.plot_feature, "Smooth temporally (moving average) the plotted lines" )
1553        layout.addLayout(ck_line)
1554        ## show the plot
1555        self.plot_wid = self.create_plotwidget()
1556        layout.addWidget(self.plot_wid)
1557        ## save plot or save data of the plot
1558        line = wid.double_button( "Save plot image", self.save_plot_image, "Save the grapic in a PNG file", "Save plot data", self.save_plot_data, "Save the value used for the plot in .csv file" )
1559        self.by_label = wid.add_check( "Arranged data by label", False, None, "Save the data with one column by label" )
1560        line.addWidget( self.by_label )
1561        layout.addLayout( line )
1562        self.setLayout(layout)
1563        self.resize(1000,800)

add widget to choose plotting parameters

def setTable(self, table):
1565    def setTable(self, table):
1566        """ Data table to plot """
1567        self.table = table
1568        self.features_list = self.table.keys()
1569        self.update_feature_list()

Data table to plot

def update_table(self, table):
1571    def update_table(self, table):
1572        """ Update the current plot with the updated table """
1573        self.table = table
1574        curchoice = self.feature_choice.currentText()
1575        self.features_list = self.table.keys()
1576        self.update_feature_list()
1577        if curchoice in self.features_list:
1578            ind = list(self.features_list).index(curchoice)
1579            self.feature_choice.setCurrentIndex(ind)
1580        self.plot_feature()

Update the current plot with the updated table

def update_feature_list(self):
1582    def update_feature_list(self):
1583        """ Update the list of feature in the GUI """
1584        self.feature_choice.clear()
1585        for feat in self.features_list:
1586            self.feature_choice.addItem(feat)
1587        if "division" in self.features_list and "extrusion" in self.features_list:
1588            self.feature_choice.addItem( "division&extrusion" )

Update the list of feature in the GUI

def plot_feature(self):
1590    def plot_feature(self):
1591        """ Plot the selected feature in the temporal graph """
1592        feat = self.feature_choice.currentText()
1593        if feat == "label":
1594            return
1595        if feat == "":
1596            return
1597        if feat == "division&extrusion":
1598            feat = ["division", "extrusion"]
1599        else:
1600            feat = [feat]
1601        
1602        tab = list( zip(self.table["frame"]) )
1603        labname = []
1604        for ft in feat:
1605            tab = [ (*t, v) for t, v in zip( tab, self.table[ft]) ]
1606        tab = [ (*t, v) for t, v in zip( tab, self.table["label"]) ]
1607        labname.append("label")
1608        if "group" in self.table:
1609            tab = [ (*t, v) for t, v in zip( tab, self.table["group"]) ]
1610            labname.append("group")
1611
1612        self.df = pand.DataFrame( tab, columns=["frame"] + feat + labname )
1613        shape = "linear"
1614        if self.smooth.isChecked():
1615            shape = "spline"
1616        if "group" in self.table and self.avg_group.isChecked():
1617            self.dfmean = self.df.groupby(['group', 'frame'])[feat].mean().reset_index()
1618            self.df.columns.name = 'group'
1619            self.fig = px.line( self.dfmean, x='frame', y=feat, color='group', labels={'frame': 'Time (frame)'}, line_shape=shape, render_mode="svg" )
1620        else:
1621            if len( np.unique(self.df["label"]) ) > 1000:
1622                ut.show_warning( "Too many lines to plot; Using a random subset instead" )
1623                subset = sample( np.unique(self.df["label"]).tolist(), 1000)
1624                subdf = self.df[self.df["label"].isin(subset)]
1625                self.fig = px.line( subdf, x="frame", y=feat[0], color="label", labels={'frame': 'Time (frame)'}, line_shape = shape, render_mode="svg")
1626                if len(feat) > 1:
1627                    addfig = px.line(subdf, x="frame", y=feat[1], color="label", line_shape = shape )
1628                    addfig.update_traces( patch={"line": {"dash":"dot"}} )
1629                    self.fig.add_trace( addfig.data[0] )
1630            else:
1631                self.fig = px.line( self.df, x="frame", y=feat[0], color="label", labels={'frame': 'Time (frame)'}, line_shape = shape, render_mode="svg")
1632                if len(feat) > 1:
1633                    addfig = px.line(self.df, x="frame", y=feat[1], color="label", line_shape = shape )
1634                    addfig.update_traces( patch={"line": {"dash":"dot"}} )
1635                    self.fig.add_trace( addfig.data[0] )
1636    
1637        if self.webengine:
1638            self.browser.setHtml( self.fig.to_html(include_plotlyjs='cdn'))
1639        else:
1640            self.show_plot_in_browser( self.fig.to_html(include_plotlyjs='cdn'))

Plot the selected feature in the temporal graph

def show_plot_in_browser(self, html):
1642    def show_plot_in_browser(self,html):
1643        with tempfile.NamedTemporaryFile(mode='w', suffix='.html', delete=False) as f:
1644            f.write(html)
1645            url = 'file://' + f.name
1646            webbrowser.open(url)
def smooth_df(self, df):
1648    def smooth_df( self, df ):
1649        """ Smooth temporally the dataframe by label or by group """
1650        rollsize = 20
1651        ## average on a smaller scale if only few frames
1652        if np.max( self.table["frame"] ) <= 20:
1653            rollsize = 5    
1654        if feat+"_smooth" in self.df.columns:
1655            feat = feat+"_smooth"
1656        else:
1657            self.df[feat+"_smooth"] = self.df[feat].rolling(rollsize, center=True).mean()
1658            #print(self.df)
1659            feat = feat+"_smooth"

Smooth temporally the dataframe by label or by group

def save_plot_image(self):
1661    def save_plot_image( self ):
1662        """ Save current plot graphic to PNG image """
1663        feat = self.feature_choice.currentText()
1664        outfile = self.epicure.outname()+"_plot_"+feat+".png"
1665        if self.fig is not None:
1666            self.fig.write_image( outfile )
1667        if self.epicure.verbose > 0:
1668            ut.show_info("Measures saved in "+outfile)

Save current plot graphic to PNG image

def save_plot_data(self):
1670    def save_plot_data( self ):
1671        """ Save the raw data to redraw the current plot to csv file """
1672        feat = self.feature_choice.currentText()
1673        outfile = self.epicure.outname()+"_time_"+feat+".csv"
1674        if self.avg_group.isChecked():
1675            data = self.dfmean.reset_index()[["frame", "group", feat]]
1676            if self.by_label.isChecked():
1677                df = pand.pivot_table( data, columns="label", index="frame", values=feat )
1678                df.to_csv( outfile,  sep='\t', header=True, index=True )
1679            else:
1680                data[["frame", "group", feat]].to_csv( outfile,  sep='\t', header=True, index=False )
1681        else:
1682            data = self.df.reset_index()[["frame", "label", feat]]
1683            if self.by_label.isChecked():
1684                df = pand.pivot_table( data, columns="label", index="frame", values=feat )
1685                df.to_csv( outfile,  sep='\t', header=True, index=True )
1686            else:
1687                data[["frame", "label", feat]].to_csv( outfile,  sep='\t', header=True, index=False )

Save the raw data to redraw the current plot to csv file

def move_framepos(self, frame):
1689    def move_framepos(self, frame):
1690        """ Move the vertical line showing the current frame position in the main window """
1691        return
1692        #if self.fig is not None:
1693        #    self.fig.add_vline( x=frame, line_dash="dash", line_color="gray" )
1694        #    self.browser.setHtml( self.fig.to_html(include_plotlyjs='cdn'))

Move the vertical line showing the current frame position in the main window

def import_webengineview(self):
1698    def import_webengineview(self):
1699        """Return QWebEngineView from whichever Qt is available."""
1700        import importlib
1701        try:
1702            # Fall back to Qt5
1703            mod = importlib.import_module("PyQt5.QtWebEngineWidgets")
1704            self.browser = mod.QWebEngineView(self)
1705            return 
1706        except Exception:
1707            pass
1708        
1709        try:
1710            # Try Qt6 first
1711            view = importlib.import_module("PyQt6.QtWebEngineWidgets")
1712            self.browser = view.QWebEngineView( parent=self )
1713            return  
1714        except Exception as e:
1715            print(e)
1716            pass
1717
1718        raise ImportError(
1719            "No QtWebEngine found. Install PyQt6-WebEngine or PyQtWebEngine."
1720        )

Return QWebEngineView from whichever Qt is available.

def create_plotwidget(self):
1723    def create_plotwidget(self):
1724        """ Create plot window """
1725        try:
1726            self.import_webengineview()
1727            self.webengine = True
1728        except:
1729            self.webengine = False
1730            self.browser = NoEngineViewer()
1731            return self.browser
1732        print(self.webengine)
1733        return self.browser

Create plot window

class NoEngineViewer(PyQt6.QtWidgets.QWidget):
1735class NoEngineViewer(QWidget):
1736    def __init__(self):
1737        super().__init__()
1738        layout = QVBoxLayout()
1739        self.text_browser = QTextBrowser()
1740        self.text_browser.setHtml("<h2>Plots will be redirected to web browser</h2>" \
1741        "" \
1742        "Your napari installation is using pyside6 that doesn't have the necessary dependency to show the plot in this window interactively. To have this option, reinstall napari with pyqt5 or pyqt6." \
1743        "" \
1744        "Otherwise, you can still see the plot, it will open in your web browser, but will be slower to display, reload the web page if nothing appears." \
1745        "")
1746        layout.addWidget(self.text_browser)
1747        self.setLayout(layout)

QWidget(parent: Optional[QWidget] = None, flags: Qt.WindowType = Qt.WindowFlags())

text_browser