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

QWidget(parent: QWidget|None = None, flags: Qt.WindowType = Qt.WindowFlags())

Outputing(napari_viewer, epic)
 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 TrackMate XML", "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_tm_group, save_tm_layout = wid.group_layout( "Save as TrackMate XML" )
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        save_tm_layout.addWidget( self.save_tm_btn )
172        
173        self.save_tm_group.setLayout( save_tm_layout )
174        self.save_tm_group.hide()
175        all_layout.addWidget( self.save_tm_group )
176       
177        ## Save screenshots option
178        current_frame = ut.current_frame( self.epicure.viewer )
179        self.screenshot_group, screenshot_layout = wid.group_layout( "Save screenshot movie" )
180        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" )
181        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" )
182        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" )
183        screenshot_layout.addLayout( sframe_line )
184        screenshot_layout.addLayout( eframe_line )
185        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" )
186
187        screenshot_layout.addWidget(savescreen_btn)
188        self.screenshot_group.setLayout(screenshot_layout)
189        all_layout.addWidget(self.screenshot_group)
190        self.screenshot_group.hide()
191        
192        ## Measure vertex options
193        self.vertex_group, vertices_layout = wid.group_layout( "Measure vertices" )
194        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")
195        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)")
196        vertices_layout.addLayout(radius_line)
197        vertices_layout.addLayout(display_radius_line)
198        self.vertices_btn = wid.add_button( "Measure", self.show_vertices_table, "Measure the vertices (connectivity, intensity)" )
199        vertices_layout.addWidget( self.vertices_btn )
200        self.verticesTable = FeaturesTable(self.viewer, self.epicure)
201        vertices_layout.addWidget(self.verticesTable)
202        self.save_table_vertices = wid.add_button( "Save vertices table", self.save_vertices_table, "Save the current table in a .csv file" )
203        vertices_layout.addWidget(self.save_table_vertices)
204        
205        self.vertex_group.setLayout( vertices_layout )
206        all_layout.addWidget(self.vertex_group)
207        self.vertex_group.hide()
208        
209        ## Finished
210        self.setLayout(all_layout)
211        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
show_scalebar
vertices_btn
verticesTable
save_table_vertices
def get_current_settings(self):
213    def get_current_settings( self ):
214        """ Returns current settings of the widget """
215        disp = {}
216        disp["Apply on"] = self.output_mode.currentText() 
217        disp["Current option"] = self.choose_output.currentText()
218        disp["Show scalebar"] = self.show_scalebar.isChecked()
219        disp = self.cell_features.get_current_settings( disp )
220        disp = self.event_classes.get_current_settings( disp )
221        return disp

Returns current settings of the widget

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

Set the current state of the widget from preferences if any

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

Save screenshots of the current view

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

Check/Uncheck the event in event types list

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

Show selected output panel

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

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

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

Open html table interactive and stats with skrub module

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

Save measures table to file whether it was created or not

def save_table_tracks(self):
327    def save_table_tracks(self):
328        """ Save tracks table to file whether it was created or not """
329        if self.table is None or self.table_selection is None or self.selection_changed() :
330            ut.show_warning("Create/update the table before")
331            return
332        outfile = self.epicure.outname()+"_trackfeatures"+self.get_selection_name()+".xlsx"
333        self.table.to_excel( outfile, sheet_name='EpiCureTrackMeasures' )
334        if self.epicure.verbose > 0:
335            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):
338    def save_one_roi(self, lab):
339        """ Save the Rois of cell with label lab """
340        keep = self.seglayer.data == lab
341        rois = []
342        if np.sum(keep) > 0:
343            ## add 2D case
344            for iframe, frame in enumerate(keep):
345                if np.sum(frame) > 0:
346                    contour = ut.get_contours(frame)
347                    roi = self.create_roi(contour[0], iframe, lab)
348                    rois.append(roi)
349
350        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):
352    def create_roi(self, contour, frame, label):
353        croi = roifile.ImagejRoi()
354        croi.version = 227
355        croi.roitype = roifile.ROI_TYPE(0) ## polygon
356        croi.n_coordinates = len(contour)
357        croi.position = frame + 1
358        croi.t_position = frame+1
359        coords = []
360        cent0 = 0
361        cent1 = 0
362        for cont in contour:
363            coords.append([int(cont[1]), int(cont[0])])
364            cent0 += cont[1]
365            cent1 += cont[0]
366        croi.integer_coordinates = np.array(coords)
367        #croi.top = int(np.min(coords[0]))
368        #croi.left = int(np.min(coords[1]))
369        croi.name = str(frame+1).zfill(4)+'-'+str(int(cent0/len(contour))).zfill(4)+"-"+str(int(cent1/len(contour))).zfill(4)
370        return croi
def save_segmentation(self):
372    def save_segmentation( self ):
373        """ Save current segmentation in selected format """
374        if self.output_mode.currentText() == "Only selected cell": 
375            ## output only the selected cell
376            lab = self.seglayer.selected_label
377            if self.save_choice.currentText() == "ROI":
378                self.save_one_roi(lab)
379                if self.epicure.verbose > 0:
380                    ut.show_info("Cell "+str(lab)+" saved to Fiji ROI")
381                return
382            else:
383                tosave = np.zeros(self.seglayer.data.shape, dtype=self.epicure.dtype)
384                if np.sum(self.seglayer.data==lab) > 0:
385                    tosave[self.seglayer.data==lab] = lab
386                endname = "_"+self.save_choice.currentText()+"_"+str(lab)+".tif"
387        else:
388            ## output all cells
389            if self.output_mode.currentText() == "All cells":
390                if self.save_choice.currentText() == "ROI":
391                    self.save_all_rois()
392                    return
393                tosave = self.seglayer.data
394                endname = "_"+self.save_choice.currentText()+".tif"
395            else:
396                ## or output only selected group
397                group = self.output_mode.currentText()
398                label_group = self.epicure.groups[group]
399                if self.save_choice.currentText() == "ROI":
400                    ncells = 0
401                    for lab in label_group:
402                        self.save_one_roi(lab)
403                        ncells += 1
404                    if self.epicure.verbose > 0:
405                        ut.show_info(str(ncells)+" cells saved to Fiji ROIs")
406                    return
407                tosave = np.zeros(self.seglayer.data.shape, dtype=self.epicure.dtype)
408                endname = "_"+self.save_choice.currentText()+"_"+self.output_mode.currentText()+".tif"
409                for lab in label_group:
410                    tosave[self.seglayer.data==lab] = lab
411        
412        ## save filled image (for label or skeleton) to file
413        outname = os.path.join( self.epicure.outdir, self.epicure.imgname+endname )
414        if self.save_choice.currentText() == "skeleton":
415            parallel = 0
416            if self.epicure.process_parallel:
417                parallel = self.epicure.nparallel
418            tosave = ut.get_skeleton( tosave, viewer=self.viewer, verbose=self.epicure.verbose, parallel=parallel )
419            ut.writeTif( tosave, outname, self.epicure.epi_metadata["ScaleXY"], 'uint8', what="Skeleton" )
420        else:
421            ut.writeTif(tosave, outname, self.epicure.epi_metadata["ScaleXY"], 'float32', what="Segmentation")

Save current segmentation in selected format

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

Save all cells to ROI format

def choose_features(self):
432    def choose_features( self ):
433        """ Pop-up widget to choose the features to measure """
434        self.cell_features.choose()

Pop-up widget to choose the features to measure

def show_vertices_table(self):
436    def show_vertices_table(self):
437        """ Show the measurement of vertices table """
438        self.measure_vertices()
439        self.verticesTable.set_table(self.table)

Show the measurement of vertices table

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

Save vertices table to file whether it was created or not

def measure_vertices(self):
452    def measure_vertices(self):
453        """ Get all vertices (TCJ) and measure their properties """
454        def nb_neighbors(regionmask, labimg):
455            """ Measure the nb of neighbors (labels) around each point """
456            #footprint = disk(radius=8)
457            #dilated = binary_dilation(regionmask, footprint)
458            labels = np.unique(labimg[regionmask]).tolist()
459            nb_nei = len(labels)
460            if 0 in labels:
461                nb_nei = nb_nei - 1
462            return nb_nei 
463
464        self.table = None
465        radius = float(self.vertice_radius.text()) 
466        display_radius = float(self.vertice_display_radius.text()) 
467        ## difference between the measured radius and the displayed radius
468        diff_radius = display_radius - radius
469        if diff_radius < 0:
470            diff_radius = 0
471        parallel = 0
472        if self.epicure.process_parallel:
473            parallel = self.epicure.nparallel
474        ## Get the vertices: junctions of several skeleton lines
475        vertex_img = ut.get_vertices( self.epicure.seg, viewer=None, verbose=self.epicure.verbose, parallel=parallel )
476        vertices_img = np.zeros(vertex_img.shape, dtype=np.int8)
477        ## Individualise, measure, draw
478        for ind, frame in enumerate(vertex_img):
479            props = ut.binary_properties(frame)
480            nvertex = len(props)
481            vertices = []
482            for prop in props:
483                if prop.label > 0:
484                    pt = prop.centroid
485                    vertices.append(pt)
486            vert_img = ut.draw_points(vertices, vertex_img.shape[1:], radius=radius)
487            props = ut.binary_properties(vert_img)
488            if nvertex != len(props):
489                ## one or more vertices had been merged
490                vertices = []
491                for prop in props:
492                    if prop.label > 0:
493                        pt = prop.centroid
494                        vertices.append(pt)
495                vert_img = ut.draw_points(vertices, vertex_img.shape[1:], radius=radius)
496            #vertices_img[ind] = vert_img
497            lbl_img = label(vert_img)
498            int_measures = pand.DataFrame(ut.regionprops_table(lbl_img, self.movlayer.data[ind], properties=["label", "centroid", "intensity_mean"]))
499            ## expand to measure neighbors
500            exp_lbl = ut.touching_labels(lbl_img, expand=3)
501            measures = pand.DataFrame(ut.regionprops_table(exp_lbl, self.epicure.seg[ind], properties=["label"], extra_properties=[nb_neighbors] ))
502            ## Color the vertices by their number of neighbors
503            for lab in measures["label"]:
504                vertices_img[ind][lbl_img==lab] = int(measures.loc[measures["label"]==lab,"nb_neighbors"].iloc[0])
505            df = pand.merge(int_measures, measures, on="label", how="inner")
506            df["Frame"] = ind
507            
508            if self.table is None:
509                self.table = df
510            else:
511                self.table = pand.concat([self.table, df])
512
513            ## Expand for display only
514            vertices_img[ind] = ut.touching_labels(vertices_img[ind], expand=diff_radius)
515
516        ## Display the vertices in a new layer
517        ut.remove_layer(self.viewer, "Vertices") # in case already present
518        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):
520    def measure_features(self):
521        """ Measure features and put them to table """
522        thick = self.epicure.thickness
523
524        def intensity_junction_cytoplasm(regionmask, intensity):
525            """ Measure the intensity only on the contour of regionmask """
526            footprint = disk(radius=thick)
527            inside = binary_erosion(regionmask, footprint)
528            inside_intensity = ut.mean_nonzero(intensity*inside)
529            periph_intensity = ut.mean_nonzero(intensity*(regionmask^inside))
530            return inside_intensity, periph_intensity
531        
532        if self.epicure.verbose > 0:
533            print("Measuring features")
534        #self.viewer.window._status_bar._toggle_activity_dock(True)
535        pb = ut.start_progress( self.viewer, total=2, descr="Measuring cells in all movie" )
536        start_time = time.time()
537        if self.output_mode.currentText() == "Only selected cell": 
538            meas = np.zeros(self.epicure.seglayer.data.shape, self.epicure.dtype)
539            lab = self.epicure.seglayer.selected_label
540            meas[self.epicure.seglayer.data==lab] = lab
541        else:
542            if self.output_mode.currentText() == "All cells": 
543                meas = self.epicure.seglayer.data
544            else:
545                group = self.output_mode.currentText()
546                meas = np.zeros(self.epicure.seglayer.data.shape, self.epicure.dtype)
547                label_group = self.epicure.groups[group]
548                for lab in label_group:
549                    meas[self.epicure.seglayer.data==lab] = lab
550            
551        properties, prop_extra, other_features, int_feat, int_extrafeat = self.cell_features.get_features()
552        do_channels = self.cell_features.get_channels()
553        extra_prop = []
554        if "intensity_junction_cytoplasm" in int_extrafeat:
555            extra_prop = extra_prop + [intensity_junction_cytoplasm]
556
557        extra_properties = []
558        if (do_channels is not None) and ("Movie" in do_channels):
559            properties = properties + int_feat
560            for extra in int_extrafeat:
561                if extra == "intensity_junction_cytoplasm":
562                    extra_properties = extra_properties + [intensity_junction_cytoplasm]
563        
564        pb.update()
565        labgroups = self.epicure.group_of_labels()
566        pb.total = self.epicure.nframes
567        chan_dict = dict()
568        if ( do_channels is not None ):
569            for chan in do_channels:
570                if chan == "Movie":
571                    continue
572                chan_dict[chan] = self.viewer.layers[chan].data
573        seg = self.epicure.seg
574        mov = self.movlayer.data
575        
576        def measure_one_frame_collect( img, frame ):
577            """ Measure on one frame and return a list of dicts for each label """
578            #pb.update()
579            intimg = mov[frame]
580            frame_table = pand.DataFrame( ut.labels_table(img, intensity_image=intimg, properties=properties, extra_properties=extra_properties) )
581            if "group" in other_features:
582                frame_table["group"] = frame_table["label"].map(labgroups).fillna("Ungrouped")
583            frame_table["frame"] = frame
584            
585            # Boundary
586            if "Boundary" in other_features:
587                boundimg = seg[frame]
588                bds = ut.get_boundary_cells(boundimg)
589                frame_table["Boundary"] = frame_table["label"].isin(bds).astype(int)
590            # Border
591            if "Border" in other_features:
592                bds = ut.get_border_cells(img)
593                frame_table["Border"] = frame_table["label"].isin(bds).astype(int)
594            
595            # Intensity features in other channels
596            for chan, intimg_chan in chan_dict.items():
597                intimg_frame = intimg_chan[frame]
598                frame_tab = ut.labels_table(img, intensity_image=intimg_frame, properties=int_feat, extra_properties=extra_prop)
599                for add_prop in int_feat:
600                    frame_table[add_prop+"_"+str(chan)] = frame_tab[add_prop]
601                if "intensity_junction_cytoplasm-0" in frame_tab.keys():
602                    frame_table["intensity_cytoplasm_"+str(chan)] = frame_tab["intensity_junction_cytoplasm-0"]
603                    frame_table["intensity_junction_"+str(chan)] = frame_tab["intensity_junction_cytoplasm-1"]
604            
605            if prop_extra != []:
606                if "shape_index" in prop_extra:
607                    frame_table["shape_index"] = frame_table["perimeter"] / np.sqrt(frame_table["area"])
608                if "roundness" in prop_extra:
609                    frame_table["roundness"] = 4*frame_table["area"] /(np.pi * np.power(frame_table["axis_major_length"],2) )
610                if "aspect_ratio" in prop_extra:
611                    frame_table["aspect_ratio"] = frame_table["axis_major_length"] / frame_table["axis_minor_length"]
612
613            # Neighbor features
614            do_neighbor = "NbNeighbors" in other_features
615            get_neighbor = "Neighbors" in other_features
616            if do_neighbor or get_neighbor:
617                nimg = seg[frame]
618                graph = ut.get_neighbor_graph(nimg, distance=3)
619                all_neighbors = {label: list(graph.adj[label]) for label in graph.nodes}
620                frame_table["neighborlist"] = frame_table["label"].map(lambda l: all_neighbors.get(l, []))
621
622            if do_neighbor:
623                frame_table["NbNeighbors"] = frame_table["neighborlist"].apply(
624                lambda x: len(x) if x else -1
625                )
626            if get_neighbor:
627                frame_table["Neighbors"] = frame_table["neighborlist"].apply(
628                lambda x: "&".join(map(str, x)) if x else ""
629                )
630            if do_neighbor or get_neighbor:
631                frame_table.drop(columns="neighborlist", inplace=True)
632            return pand.DataFrame( frame_table.to_dict(orient="records") )
633
634        if self.epicure.process_parallel:
635            frame_tables = Parallel( n_jobs=self.epicure.nparallel ) ( 
636                delayed( measure_one_frame_collect ) ( frame, iframe ) for iframe, frame in enumerate(meas) )
637        else:
638            frame_tables = [
639                measure_one_frame_collect( frame, iframe )
640                for iframe, frame in (enumerate(meas))
641            ]
642        self.table = pand.concat(frame_tables, ignore_index=True)
643
644        if "intensity_junction_cytoplasm-0" in self.table.columns:
645            self.table = self.table.rename(columns={"intensity_junction_cytoplasm-0": "intensity_cytoplasm", "intensity_junction_cytoplasm-1":"intensity_junction"})
646        self.table_selection = self.selection_choices.index(self.output_mode.currentText())
647        ut.close_progress( self.viewer, pb )
648        #self.viewer.window._status_bar._toggle_activity_dock(False)
649        if self.epicure.verbose > 0:
650            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):
652    def measure_one_frame(self, img, properties, extra_properties, other_features, channels, int_feat, int_extrafeat, frame, labgroups, prop_extra ):
653        """ Measure on one frame """
654        if frame is not None:
655            intimg = self.movlayer.data[frame]
656        else:
657            intimg = self.movlayer.data
658        first = "label" not in self.table.keys()
659        nrows = len(self.table["label"]) if "label" in self.table.keys() else 0
660        
661        ## add the basic label measures
662        frame_table = ut.labels_table( img, intensity_image=intimg, properties=properties, extra_properties=extra_properties )
663        ndata = len(frame_table["label"])
664        for key, value in frame_table.items():
665            if first:
666                self.table[key] = []
667            self.table[key].extend(list(value))
668
669        ## add the frame column
670        if frame is not None:
671            if first:
672                self.table["frame"] = []
673            self.table["frame"].extend([frame]*ndata)
674
675        ## add info of the cell group
676        if "group" in other_features:
677            frame_group = [ labgroups[label] if label in labgroups.keys() else "Ungrouped" for label in frame_table["label"] ]
678            if first:
679                self.table["group"] = []
680            self.table["group"].extend( frame_group )
681
682        ## add the extra shape features
683        if prop_extra != []:
684            if "shape_index" in prop_extra:
685                si = frame_table["perimeter"] /np.sqrt( frame_table["area"] ) 
686                if first:
687                    self.table["shape_index"] = []
688                self.table["shape_index"].extend( si )
689            if "roundness" in prop_extra:
690                rou = 4*frame_table["area"] /(np.pi * np.power(frame_table["axis_major_length"],2) ) 
691                if first:
692                    self.table["roundness"] = []
693                self.table["roundness"].extend( rou )
694            if "aspect_ratio" in prop_extra:
695                ar = list( np.array(frame_table["axis_major_length"])/np.array(frame_table["axis_minor_length"]) )
696                if first:
697                    self.table["aspect_ratio"] = []
698                self.table["aspect_ratio"].extend( ar )
699
700        ### Measure intensity features in other chanels if option is on
701        if (channels is not None):
702            for chan in channels:
703                ## if it's movie, already measured in the general measure
704                if chan == "Movie":
705                    continue
706                ## otherwise, do a new measure on the selected channels
707                if frame is not None:
708                    intimg = self.viewer.layers[chan].data[frame]
709                else:
710                    intimg = self.viewer.layers[chan].data
711                frame_tab = ut.labels_table( img, intensity_image=intimg, properties=int_feat, extra_properties=int_extrafeat )
712                for add_prop in int_feat:
713                    if first:
714                        self.table[add_prop+"_"+chan] = []
715                    self.table[add_prop+"_"+chan].extend( list(frame_tab[add_prop]) )
716                if "intensity_junction_cytoplasm-0" in frame_tab.keys():
717                    if first:
718                        self.table["intensity_cytoplasm_"+chan] = []
719                        self.table["intensity_junction_"+str(chan)] = []
720                    self.table["intensity_cytoplasm_"+chan].extend( list(frame_tab["intensity_junction_cytoplasm-0"]) )
721                    self.table["intensity_junction_"+str(chan)].extend( list(frame_tab["intensity_junction_cytoplasm-1"]) )
722                
723            
724        ## add features of neighbors relationship with graph
725        do_neighbor = "NbNeighbors" in other_features
726        get_neighbor = "Neighbors" in other_features
727        if do_neighbor or get_neighbor:
728            if frame is not None:
729                nimg = self.epicure.seg[frame]
730            else:
731                nimg = self.epicure.seg
732            #start_time = ut.start_time()
733            graph = ut.get_neighbor_graph( nimg, distance=3 )
734            
735            if first:
736                if do_neighbor:
737                    self.table["NbNeighbors"] = []
738                if get_neighbor:
739                    self.table["Neighbors"] = []
740            if do_neighbor:
741                self.table["NbNeighbors"].extend( [-1]*ndata )
742            if get_neighbor:
743                self.table["Neighbors"].extend( [""]*ndata )
744
745            for label in np.unique(frame_table["label"]):
746                if label in graph.nodes:
747                    rlabel = np.where( (frame_table["label"] == label) )[0]
748                    nneighbor = len(graph.adj[label])
749                    for ind in rlabel:
750                        if do_neighbor:
751                            self.table["NbNeighbors"][ind+nrows] = nneighbor
752                        if get_neighbor:
753                            self.table["Neighbors"][ind+nrows] = ""
754                            sep = ""
755                            for key in graph.adj[label].keys():
756                                self.table["Neighbors"][ind+nrows] += sep + str(key)
757                                sep = "&"
758            #ut.show_duration( start_time, "Neighborhoods measured" )
759
760        ## measure cells on boundary    
761        if "Boundary" in other_features:
762            if frame is not None:
763                boundimg = self.epicure.seg[frame]
764            else:
765                boundimg = self.epicure.seg
766            bounds = ut.get_boundary_cells( boundimg )
767            if first:
768                self.table["Boundary"] = []
769            self.table["Boundary"].extend( [0]*ndata )
770            for label in np.unique(frame_table["label"]):
771                if label in bounds:
772                    rlabel = np.where( (frame_table["label"] == label) )[0]
773                    for ind in rlabel:
774                        self.table["Boundary"][ind+nrows] = 1
775        
776        ## measure cells on border  
777        if "Border" in other_features:
778            bounds = ut.get_border_cells( img )
779            if first:
780                self.table["Border"] = []
781            self.table["Border"].extend( [0]*ndata )
782            for label in bounds:
783                rlabel = np.where( (frame_table["label"] == label) )[0]
784                for ind in rlabel:
785                    self.table["Border"][ind+nrows] = 1

Measure on one frame

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

Update the possible selection from group cell list

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

Show the measurement table

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

Scale if necessary the feature values

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

Add the image map of the selected feature

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

Add image layer of values by label

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

Display the cells orientation axis in a new layer

def to_griot(self):
958    def to_griot(self):
959        """ Export current frame to new viewer and makes it ready for Griotte plugin """
960        try:
961            from napari_griottes import make_graph
962        except:
963            ut.show_error("Plugin napari-griottes is not installed")
964            return
965        gview = napari.Viewer()
966        tframe = ut.current_frame(self.viewer)
967        segt = self.epicure.seglayer.data[tframe]
968        touching_frame = self.touching_labels(segt)
969        gview.add_labels(touching_frame, name="TouchingCells", opacity=1)
970        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):
972    def touching_labels(self, labs):
973        """ Dilate labels so that they all touch """
974        from skimage.segmentation import find_boundaries
975        from skimage.morphology import skeletonize
976        from skimage.morphology import binary_closing, binary_opening
977        if self.epicure.verbose > 0:
978            print("********** Generate touching labels image ***********")
979
980        ## skeletonize it
981        skel = skeletonize( binary_closing( find_boundaries(labs), footprint=np.ones((10,10)) ) )
982        ext = np.zeros(labs.shape, dtype="uint8")
983        ext[labs==0] = 1
984        ext = binary_opening(ext, footprint=np.ones((2,2)))
985        newimg = ut.touching_labels(labs, expand=4)
986        newimg[ext>0] = 0
987        return newimg

Dilate labels so that they all touch

def to_ncp(self):
 989    def to_ncp(self):
 990        """ Export current frame to new viewer and makes it ready for napari-cluster-plots plugin """
 991        try:
 992            import napari_skimage_regionprops as nsr
 993        except:
 994            ut.show_error("Plugin napari-skimage-regionprops is not installed")
 995            return
 996        gview = napari.Viewer()
 997        tframe = ut.current_frame(self.viewer)
 998        segt = self.epicure.seglayer.data[tframe]
 999        moviet = self.epicure.viewer.layers["Movie"].data[tframe]
1000        lab = gview.add_labels(segt, name="Segmentation[t="+str(tframe)+"]", blending="additive")
1001        im = gview.add_image(moviet, name="Movie[t="+str(tframe)+"]", blending="additive")
1002        if self.epicure.verbose > 0:
1003            print("Measure features with napari-skimage-regionprops plugin...")
1004        nsr.regionprops_table(im.data, lab.data, size=True, intensity=True, perimeter=True, shape=True, position=True, moments=True, napari_viewer=gview)
1005        try:
1006            import napari_clusters_plotter as ncp
1007        except:
1008            ut.show_error("Plugin napari-clusters-plotter is not installed")
1009            return
1010        gview.window.add_dock_widget( ncp.ClusteringWidget(gview) )
1011        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):
1014    def temporal_graphs_events( self ):
1015        """ New window with temporal graph of event counts """
1016        if self.tplots is not None:
1017            self.tplots.close()
1018        self.tplots = TemporalPlots( self.viewer, self.epicure )
1019        evt_table = self.count_events()
1020        self.tplots.setTable( evt_table )
1021        self.tplots.show()
1022        self.viewer.dims.events.current_step.connect(self.position_verticalline)

New window with temporal graph of event counts

def temporal_graphs(self):
1025    def temporal_graphs(self):
1026        """ New window with temporal graph of the current table selection """
1027        #self.temporal_viewer = napari.Viewer()
1028        self.tplots = TemporalPlots( self.viewer, self.epicure )
1029        self.tplots.setTable(self.table)
1030        self.tplots.show()
1031        #self.plot_wid = self.viewer.window.add_dock_widget( self.tplots, name="Plots" )
1032        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):
1034    def on_close_viewer(self):
1035        """ Temporal plots window is closed """
1036        if self.epicure.verbose > 1:
1037            print("Closed viewer")
1038        self.viewer.dims.events.current_step.disconnect(self.position_verticalline)
1039        self.temporal_viewer = None
1040        self.tplots = None

Temporal plots window is closed

def position_verticalline(self):
1042    def position_verticalline(self):
1043        """ Place the vertical line in the temporal graph to the current frame """
1044        #try:
1045        #    wid = self.tplots
1046        #except:
1047        #    self.on_close_viewer()
1048        if self.tplots is not None:
1049            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):
1053    def show_trackfeature_table(self):
1054        """ Show the measurement of tracks table """
1055        self.measure_track_features()
1056        self.trackTable.set_table( self.table )

Show the measurement of tracks table

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

Measure features of one track

def choose_events(self):
1107    def choose_events( self ):
1108        """ Pop-up widget to choose the event types to measure/export """
1109        self.event_classes.choose()

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

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

Count events of selected types

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

Export events of selected types

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

Export events of selected types in selected format

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

Create a point Fiji ROI

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

Save current segmentation and tracking in TrackMate XML format

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

Choice of features to measure

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

Select all features of type feat

def deselect_all(self, feat):
1289    def deselect_all( self, feat ):
1290        """ Deselect all features of type feat """
1291        if feat == "intensity":
1292            self.deselect_all( "intensity_prop" )
1293            self.deselect_all( "intensity_extra" )
1294            return
1295        if feat == "props":
1296            self.deselect_all( "prop" )
1297            self.deselect_all( "prop_extra" )
1298            return
1299        for featy, feat_val in self.features.items():
1300            if feat_val[1] == feat:
1301                feat_val[0].setChecked( False )

Deselect all features of type feat

def add_feature_group(self, feat_list, feat_type):
1304    def add_feature_group( self, feat_list, feat_type ):
1305        """ Add features to the GUI """
1306        layout = QVBoxLayout()
1307        ncols = 3
1308        for i, feat in enumerate(feat_list):
1309            if i%ncols == 0:
1310                line = QHBoxLayout()
1311            feature_check = wid.add_check( ""+feat, True, None, descr="" )
1312            line.addWidget(feature_check)
1313            self.features[ feat ] = [feature_check, feat_type]
1314            if i%ncols == (ncols-1):
1315                layout.addLayout( line )
1316                line = None
1317        if line is not None:
1318            layout.addLayout( line )
1319        return layout

Add features to the GUI

def close(self):
1322    def close( self ):
1323        """ Close the pop-up window """
1324        self.hide()

Close the pop-up window

def choose(self):
1326    def choose( self ):
1327        """ Show the interface to select the choices """
1328        self.show()

Show the interface to select the choices

def get_current_settings(self, setting):
1330    def get_current_settings( self, setting ):
1331        """ Get current settings of check or not of features """
1332        for feat, feat_cbox in self.features.items():
1333            setting[feat] = feat_cbox[0].isChecked()
1334        return setting

Get current settings of check or not of features

def apply_settings(self, settings):
1336    def apply_settings( self, settings ):
1337        """ Set the checkboxes from preferenced settings """
1338        for feat, checked in settings.items():
1339            if feat in self.features.keys():
1340                self.features[feat][0].setChecked( checked )

Set the checkboxes from preferenced settings

def get_features(self):
1342    def get_features( self ):
1343        """ Returns the list of features to measure """
1344        feats = self.required
1345        feats_extra = []
1346        int_extra_feats = []
1347        int_feats = []
1348        other_feats = []
1349        self.do_intensity = False
1350        for feat, feat_cbox in self.features.items():
1351            if feat_cbox[0].isChecked():
1352                if feat_cbox[1] == "prop":
1353                    feats.append( feat )
1354                if feat_cbox[1] == "prop_extra":
1355                    feats_extra.append( feat )
1356                    if feat == "shape_index":
1357                        if "perimeter" not in feats:
1358                            feats.append("perimeter")
1359                        if "area" not in feats:
1360                            feats.append("area")
1361                    if feat == "roundness":
1362                        if "area" not in feats:
1363                            feats.append("area")
1364                        if "axis_major_length" not in feats:
1365                            feats.append("axis_major_length")
1366                    if feat == "aspect_ratio":
1367                        if "axis_major_length" not in feats:
1368                            feats.append("axis_major_length")
1369                        if "axis_minor_length" not in feats:
1370                            feats.append("axis_minor_length")
1371                if feat_cbox[1] == "other":
1372                    other_feats.append( feat )
1373                if feat_cbox[1] == "intensity_prop":
1374                    int_feats.append( feat )
1375                    self.do_intensity = True
1376                if feat_cbox[1] == "intensity_extra":
1377                    int_extra_feats.append( feat )
1378                    self.do_intensity = True
1379        return feats, feats_extra, other_feats, int_feats, int_extra_feats

Returns the list of features to measure

def get_channels(self):
1381    def get_channels( self ):
1382        """ Returns the list of channels to measure """
1383        if self.do_intensity:
1384            if self.chan_list is not None:
1385                wid_channels = self.chan_list.selectedItems()
1386                channels = []
1387                for chan in wid_channels:
1388                    channels.append( chan.text() )
1389            else:
1390                channels = ["Movie"]
1391            return channels
1392        return None

Returns the list of channels to measure

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

Choice of event types to export/measure

EventClass(epicure)
1396    def __init__( self, epicure ):
1397        super().__init__()
1398        layout = QVBoxLayout()
1399        
1400        self.evt_classes = {}
1401        possible_classes = epicure.event_class
1402        event_layout = self.add_events( possible_classes )
1403        layout.addLayout( event_layout )
1404
1405        bye = wid.add_button( "Ok", self.close, "Close the window" )
1406        layout.addWidget( bye )
1407        self.setLayout( layout )
evt_classes
def add_events(self, event_list):
1409    def add_events( self, event_list ):
1410        """ Add events to the GUI """
1411        layout = QVBoxLayout()
1412        ncols = 3
1413        for i, event in enumerate( event_list ):
1414            if i%ncols == 0:
1415                line = QHBoxLayout()
1416            event_check = wid.add_check_tolayout( line, ""+event, checked=True, descr="")
1417            self.evt_classes[ event ] = [ event_check ]
1418            if i%ncols == (ncols-1):
1419                layout.addLayout( line )
1420                line = None
1421        if line is not None:
1422            layout.addLayout( line )
1423        return layout

Add events to the GUI

def close(self):
1426    def close( self ):
1427        """ Close the pop-up window """
1428        self.hide()

Close the pop-up window

def choose(self):
1430    def choose( self ):
1431        """ Show the interface to select the choices """
1432        self.show()

Show the interface to select the choices

def get_current_settings(self, setting):
1434    def get_current_settings( self, setting ):
1435        """ Get current settings of check or not of features """
1436        for event, event_cbox in self.evt_classes.items():
1437            setting[event] = event_cbox[0].isChecked()
1438        return setting

Get current settings of check or not of features

def apply_settings(self, settings):
1440    def apply_settings( self, settings ):
1441        """ Set the checkboxes from preferenced settings """
1442        for evt, checked in settings.items():
1443            if evt in self.evt_classes.keys():
1444                self.evt_classes[evt][0].setChecked( checked )

Set the checkboxes from preferenced settings

def get_evt_classes(self):
1446    def get_evt_classes( self ):
1447        """ Returns the list of events to measure """
1448        events = []
1449        for evt, evt_cbox in self.evt_classes.items():
1450            if evt_cbox[0].isChecked():
1451                events.append( evt )
1452        return events

Returns the list of events to measure

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

Widget to visualize and interact with the measurement table

FeaturesTable(napari_viewer, epicure)
1457    def __init__(self, napari_viewer, epicure):
1458        super().__init__()
1459        self.viewer = napari_viewer
1460        self.epicure = epicure
1461        self.wid_table = QTableWidget()
1462        self.wid_table.setEditTriggers(QTableWidget.EditTrigger.NoEditTriggers)
1463        self.setLayout(QGridLayout())
1464        self.layout().addWidget(self.wid_table)
1465        self.wid_table.clicked.connect(self.show_label)
1466        self.wid_table.setSortingEnabled(True)
viewer
epicure
wid_table
def show_label(self):
1468    def show_label(self):
1469        """ When click on the table, show selected cell """
1470        if self.wid_table is not None:
1471            row = self.wid_table.currentRow()
1472            self.epicure.seglayer.show_selected_label = False
1473            headers = [self.wid_table.horizontalHeaderItem(ind).text() for ind in range(self.wid_table.columnCount()) ]
1474            labelind = None
1475            if "label" in headers:
1476                labelind = headers.index("label") 
1477            if "Label" in headers:
1478                labelind = headers.index("Label") 
1479            frameind = None
1480            if "frame" in headers:
1481                frameind = headers.index("frame") 
1482            if labelind is not None and labelind >= 0:
1483                lab = int(self.wid_table.item(row, labelind).text())
1484                if frameind is not None:
1485                    ## set current frame to the selected row
1486                    frame = int(self.wid_table.item(row, frameind).text())
1487                    ut.set_frame(self.viewer, frame)
1488                else:
1489                    ## set current frame to the first frame where label or track is present
1490                    frame = self.epicure.tracking.get_first_frame( lab )
1491                    if frame is not None:
1492                        ut.set_frame(self.viewer, frame)
1493                self.epicure.seglayer.selected_label = lab
1494                self.epicure.seglayer.show_selected_label = True

When click on the table, show selected cell

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

Widget to visualize and interact with temporal plots

TemporalPlots(napari_viewer, epicure)
1517    def __init__(self, napari_viewer, epicure):
1518        super().__init__()
1519        self.viewer = napari_viewer
1520        self.epicure = epicure
1521        self.features_list = ["frame"]
1522        self.parameter_gui()
1523        self.vline = None
1524        self.ymin = None
1525        #self.viewer.window.add_dock_widget( self.plot_wid, name="Temporal plot" )
viewer
epicure
features_list
vline
ymin
def parameter_gui(self):
1527    def parameter_gui(self):
1528        """ add widget to choose plotting parameters """
1529        
1530        layout = QVBoxLayout()
1531
1532        ## choice of feature to plot
1533        feat_choice, self.feature_choice = wid.list_line( label="Plot feature", descr="Choose the feature to plot", func=self.plot_feature )
1534        layout.addLayout(feat_choice)
1535        ## option to average by group
1536        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" )
1537        layout.addLayout(ck_line)
1538        ## show the plot
1539        self.plot_wid = self.create_plotwidget()
1540        layout.addWidget(self.plot_wid)
1541        ## save plot or save data of the plot
1542        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" )
1543        self.by_label = wid.add_check( "Arranged data by label", False, None, "Save the data with one column by label" )
1544        line.addWidget( self.by_label )
1545        layout.addLayout( line )
1546        self.setLayout(layout)
1547        self.resize(1000,800)

add widget to choose plotting parameters

def setTable(self, table):
1549    def setTable(self, table):
1550        """ Data table to plot """
1551        self.table = table
1552        self.features_list = self.table.keys()
1553        self.update_feature_list()

Data table to plot

def update_table(self, table):
1555    def update_table(self, table):
1556        """ Update the current plot with the updated table """
1557        self.table = table
1558        curchoice = self.feature_choice.currentText()
1559        self.features_list = self.table.keys()
1560        self.update_feature_list()
1561        if curchoice in self.features_list:
1562            ind = list(self.features_list).index(curchoice)
1563            self.feature_choice.setCurrentIndex(ind)
1564        self.plot_feature()

Update the current plot with the updated table

def update_feature_list(self):
1566    def update_feature_list(self):
1567        """ Update the list of feature in the GUI """
1568        self.feature_choice.clear()
1569        for feat in self.features_list:
1570            self.feature_choice.addItem(feat)
1571        if "division" in self.features_list and "extrusion" in self.features_list:
1572            self.feature_choice.addItem( "division&extrusion" )

Update the list of feature in the GUI

def plot_feature(self):
1574    def plot_feature(self):
1575        """ Plot the selected feature in the temporal graph """
1576        feat = self.feature_choice.currentText()
1577        if feat == "label":
1578            return
1579        if feat == "":
1580            return
1581        if feat == "division&extrusion":
1582            feat = ["division", "extrusion"]
1583        else:
1584            feat = [feat]
1585        
1586        tab = list( zip(self.table["frame"]) )
1587        labname = []
1588        for ft in feat:
1589            tab = [ (*t, v) for t, v in zip( tab, self.table[ft]) ]
1590        tab = [ (*t, v) for t, v in zip( tab, self.table["label"]) ]
1591        labname.append("label")
1592        if "group" in self.table:
1593            tab = [ (*t, v) for t, v in zip( tab, self.table["group"]) ]
1594            labname.append("group")
1595
1596        self.df = pand.DataFrame( tab, columns=["frame"] + feat + labname )
1597        shape = "linear"
1598        if self.smooth.isChecked():
1599            shape = "spline"
1600        if "group" in self.table and self.avg_group.isChecked():
1601            self.dfmean = self.df.groupby(['group', 'frame'])[feat].mean().reset_index()
1602            self.df.columns.name = 'group'
1603            self.fig = px.line( self.dfmean, x='frame', y=feat, color='group', labels={'frame': 'Time (frame)'}, line_shape=shape, render_mode="svg" )
1604        else:
1605            if len( np.unique(self.df["label"]) ) > 1000:
1606                ut.show_warning( "Too many lines to plot; Using a random subset instead" )
1607                subset = sample( np.unique(self.df["label"]).tolist(), 1000)
1608                subdf = self.df[self.df["label"].isin(subset)]
1609                self.fig = px.line( subdf, x="frame", y=feat[0], color="label", labels={'frame': 'Time (frame)'}, line_shape = shape, render_mode="svg")
1610                if len(feat) > 1:
1611                    addfig = px.line(subdf, x="frame", y=feat[1], color="label", line_shape = shape )
1612                    addfig.update_traces( patch={"line": {"dash":"dot"}} )
1613                    self.fig.add_trace( addfig.data[0] )
1614            else:
1615                self.fig = px.line( self.df, x="frame", y=feat[0], color="label", labels={'frame': 'Time (frame)'}, line_shape = shape, render_mode="svg")
1616                if len(feat) > 1:
1617                    addfig = px.line(self.df, x="frame", y=feat[1], color="label", line_shape = shape )
1618                    addfig.update_traces( patch={"line": {"dash":"dot"}} )
1619                    self.fig.add_trace( addfig.data[0] )
1620    
1621        if self.webengine:
1622            self.browser.setHtml( self.fig.to_html(include_plotlyjs='cdn'))
1623        else:
1624            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):
1626    def show_plot_in_browser(self,html):
1627        with tempfile.NamedTemporaryFile(mode='w', suffix='.html', delete=False) as f:
1628            f.write(html)
1629            url = 'file://' + f.name
1630            webbrowser.open(url)
def smooth_df(self, df):
1632    def smooth_df( self, df ):
1633        """ Smooth temporally the dataframe by label or by group """
1634        rollsize = 20
1635        ## average on a smaller scale if only few frames
1636        if np.max( self.table["frame"] ) <= 20:
1637            rollsize = 5    
1638        if feat+"_smooth" in self.df.columns:
1639            feat = feat+"_smooth"
1640        else:
1641            self.df[feat+"_smooth"] = self.df[feat].rolling(rollsize, center=True).mean()
1642            #print(self.df)
1643            feat = feat+"_smooth"

Smooth temporally the dataframe by label or by group

def save_plot_image(self):
1645    def save_plot_image( self ):
1646        """ Save current plot graphic to PNG image """
1647        feat = self.feature_choice.currentText()
1648        outfile = self.epicure.outname()+"_plot_"+feat+".png"
1649        if self.fig is not None:
1650            self.fig.write_image( outfile )
1651        if self.epicure.verbose > 0:
1652            ut.show_info("Measures saved in "+outfile)

Save current plot graphic to PNG image

def save_plot_data(self):
1654    def save_plot_data( self ):
1655        """ Save the raw data to redraw the current plot to csv file """
1656        feat = self.feature_choice.currentText()
1657        outfile = self.epicure.outname()+"_time_"+feat+".csv"
1658        if self.avg_group.isChecked():
1659            data = self.dfmean.reset_index()[["frame", "group", feat]]
1660            if self.by_label.isChecked():
1661                df = pand.pivot_table( data, columns="label", index="frame", values=feat )
1662                df.to_csv( outfile,  sep='\t', header=True, index=True )
1663            else:
1664                data[["frame", "group", feat]].to_csv( outfile,  sep='\t', header=True, index=False )
1665        else:
1666            data = self.df.reset_index()[["frame", "label", feat]]
1667            if self.by_label.isChecked():
1668                df = pand.pivot_table( data, columns="label", index="frame", values=feat )
1669                df.to_csv( outfile,  sep='\t', header=True, index=True )
1670            else:
1671                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):
1673    def move_framepos(self, frame):
1674        """ Move the vertical line showing the current frame position in the main window """
1675        return
1676        #if self.fig is not None:
1677        #    self.fig.add_vline( x=frame, line_dash="dash", line_color="gray" )
1678        #    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):
1682    def import_webengineview(self):
1683        """Return QWebEngineView from whichever Qt is available."""
1684        import importlib
1685        try:
1686            # Fall back to Qt5
1687            mod = importlib.import_module("PyQt5.QtWebEngineWidgets")
1688            self.browser = mod.QWebEngineView(self)
1689            return 
1690        except Exception:
1691            pass
1692        
1693        try:
1694            # Try Qt6 first
1695            view = importlib.import_module("PyQt6.QtWebEngineWidgets")
1696            self.browser = view.QWebEngineView( parent=self )
1697            return  
1698        except Exception as e:
1699            print(e)
1700            pass
1701
1702        raise ImportError(
1703            "No QtWebEngine found. Install PyQt6-WebEngine or PyQtWebEngine."
1704        )

Return QWebEngineView from whichever Qt is available.

def create_plotwidget(self):
1707    def create_plotwidget(self):
1708        """ Create plot window """
1709        try:
1710            self.import_webengineview()
1711            self.webengine = True
1712        except:
1713            self.webengine = False
1714            self.browser = NoEngineViewer()
1715            return self.browser
1716        print(self.webengine)
1717        return self.browser

Create plot window

class NoEngineViewer(PyQt6.QtWidgets.QWidget):
1719class NoEngineViewer(QWidget):
1720    def __init__(self):
1721        super().__init__()
1722        layout = QVBoxLayout()
1723        self.text_browser = QTextBrowser()
1724        self.text_browser.setHtml("<h2>Plots will be redirected to web browser</h2>" \
1725        "" \
1726        "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." \
1727        "" \
1728        "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." \
1729        "")
1730        layout.addWidget(self.text_browser)
1731        self.setLayout(layout)

QWidget(parent: QWidget|None = None, flags: Qt.WindowType = Qt.WindowFlags())

text_browser