changeset 0:5396ab665901 draft

planemo upload for repository https://github.com/lldelisle/tools-lldelisle/tree/master/tools/omero_hyperstack_to_gastruloid_measurements commit 4eb6599440c6b220775dc79509983e7549e36e0b
author lldelisle
date Fri, 24 Mar 2023 13:03:39 +0000
parents
children bd5771ff6aa3
files 1-omero_timelapse_image_to_measurements_phase.groovy omero_hyperstack_to_gastruloid_measurements.xml
diffstat 2 files changed, 881 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/1-omero_timelapse_image_to_measurements_phase.groovy	Fri Mar 24 13:03:39 2023 +0000
@@ -0,0 +1,745 @@
+// This macro was written by the BIOP (https://github.com/BIOP)
+// Romain Guiet and Rémy Dornier
+// Lucille Delisle modified to support headless
+// merge the analysis script with templates available at
+// https://github.com/BIOP/OMERO-scripts/tree/main/Fiji
+
+// Last modification: 2023-03-24
+
+/*
+ * = COPYRIGHT =
+ * © All rights reserved. ECOLE POLYTECHNIQUE FEDERALE DE LAUSANNE, Switzerland, BioImaging And Optics Platform (BIOP), 2023
+ *
+ * Licensed under the BSD-3-Clause License:
+ * Redistribution and use in source and binary forms, with or without modification, are permitted provided
+ * that the following conditions are met:
+ * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer
+ *    in the documentation and/or other materials provided with the distribution.
+ * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products
+ *     derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING,
+ * BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+ * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+// This macro will use ilastik to detect ROIs
+// measure and compute elongation index
+
+// The input image(s) may have multiple time stacks
+// It may have multiple channels
+// If multiple channels are present, the LUT will be
+// used to determine which channel should be used.
+
+// This macro works both in headless
+// or GUI
+
+// In both modes,
+// The result table and the result ROI are sent to omero
+// The measures are: Area,Perim.,Circ.,Feret,FeretX,FeretY,FeretAngle,MinFeret,AR,Round,Solidity,Unit,Date,Version,IlastikProject,ProbabilityThreshold,MinSizeParticle,MinDiameter,ClosenessTolerance,MinSimilarity,RadiusMedian,BaseImage,ROI,Time,ROI_type,LargestRadius,SpineLength,ElongationIndex
+
+// LargestRadius and SpineLength are set to 0 if no circle was found.
+// ElongationIndex is set to 0 if a gastruloid was found and to -1 if no gastruloid was found.
+
+// SpineLength is set to 0 and ElongationIndex is set to 1 if a single circle was found.
+
+import ch.epfl.biop.MaxInscribedCircles
+
+import fr.igred.omero.annotations.TableWrapper
+import fr.igred.omero.Client
+import fr.igred.omero.repository.DatasetWrapper
+import fr.igred.omero.repository.ImageWrapper
+import fr.igred.omero.repository.PlateWrapper
+import fr.igred.omero.repository.PixelsWrapper
+import fr.igred.omero.repository.WellWrapper
+import fr.igred.omero.roi.ROIWrapper
+
+import ij.ImagePlus
+import ij.gui.Overlay
+import ij.gui.PolygonRoi
+import ij.gui.Roi
+import ij.IJ
+import ij.io.FileSaver
+import ij.plugin.Concatenator
+import ij.plugin.Duplicator
+import ij.plugin.frame.RoiManager
+import ij.plugin.HyperStackConverter
+import ij.plugin.ImageCalculator
+import ij.Prefs
+import ij.process.FloatPolygon
+import ij.process.ImageProcessor
+
+import java.awt.Color
+import java.awt.GraphicsEnvironment
+import java.io.File
+
+import loci.plugins.in.ImporterOptions
+
+import net.imglib2.img.display.imagej.ImageJFunctions
+
+import omero.model.LengthI
+
+import org.apache.commons.io.FilenameUtils
+import org.apache.commons.io.FileUtils
+import org.ilastik.ilastik4ij.ui.*
+
+def processDataset(Client user_client, DatasetWrapper dataset_wpr,
+                   File ilastik_project, String ilastik_project_type,
+                   Integer ilastik_label_OI,
+                   Double probability_threshold, Double radius_median,
+                   Integer min_size_particle, Boolean get_spine,
+                   Integer minimum_diameter, Integer closeness_tolerance, Double min_similarity,
+                   String ilastik_project_short_name,
+                   File output_directory,
+                   Boolean headless_mode, Boolean debug, String tool_version) {
+    dataset_wpr.getImages(user_client).each{ ImageWrapper img_wpr ->
+        processImage(user_client, img_wpr,
+                     ilastik_project, ilastik_project_type,
+                     ilastik_label_OI, probability_threshold,
+                     radius_median, min_size_particle, get_spine,
+                     minimum_diameter, closeness_tolerance, min_similarity,
+                     ilastik_project_short_name,
+                     output_directory,
+                     headless_mode, debug, tool_version)
+    }
+}
+
+def processSinglePlate(Client user_client, PlateWrapper plate_wpr,
+                       File ilastik_project, String ilastik_project_type,
+                       Integer ilastik_label_OI,
+                       Double probability_threshold, Double radius_median,
+                       Integer min_size_particle, Boolean get_spine,
+                       Integer minimum_diameter, Integer closeness_tolerance, Double min_similarity,
+                       String ilastik_project_short_name,
+                       File output_directory,
+                       Boolean headless_mode, Boolean debug, String tool_version) {
+    plate_wpr.getWells(user_client).each{ well_wpr ->
+        processSingleWell(user_client, well_wpr,
+                     ilastik_project, ilastik_project_type,
+                     ilastik_label_OI, probability_threshold,
+                     radius_median, min_size_particle, get_spine,
+                     minimum_diameter, closeness_tolerance, min_similarity,
+                     ilastik_project_short_name,
+                     output_directory,
+                     headless_mode, debug, tool_version)
+    }
+}
+
+def processSingleWell(Client user_client, WellWrapper well_wpr,
+                      File ilastik_project, String ilastik_project_type,
+                      Integer ilastik_label_OI,
+                      Double probability_threshold, Double radius_median,
+                      Integer min_size_particle, Boolean get_spine,
+                      Integer minimum_diameter, Integer closeness_tolerance, Double min_similarity,
+                      String ilastik_project_short_name,
+                      File output_directory,
+                      Boolean headless_mode, Boolean debug, String tool_version) {
+    well_wpr.getWellSamples().each{
+        processImage(user_client, it.getImage(),
+                     ilastik_project, ilastik_project_type,
+                     ilastik_label_OI, probability_threshold,
+                     radius_median, min_size_particle, get_spine,
+                     minimum_diameter, closeness_tolerance, min_similarity,
+                     ilastik_project_short_name,
+                     output_directory,
+                     headless_mode, debug, tool_version)
+    }
+}
+
+def processImage(Client user_client, ImageWrapper image_wpr,
+                 File ilastik_project, String ilastik_project_type, // String ilastik_strategy,
+                 Integer ilastik_label_OI,
+                 Double probability_threshold, Double radius_median, Integer min_size_particle,
+                 Boolean get_spine,
+                 Integer minimum_diameter, Integer closeness_tolerance, Double min_similarity,
+                 String ilastik_project_short_name,
+                 File output_directory,
+                 Boolean headless_mode, Boolean debug, String tool_version) {
+
+    IJ.run("Close All", "")
+    IJ.run("Clear Results")
+    // Clean ROI manager
+    if (!headless_mode) {
+        rm = new RoiManager()
+        rm = rm.getRoiManager()
+        rm.reset()
+    }
+
+    // Print image information
+    println "\n Image infos"
+    String image_basename = image_wpr.getName()
+    println ("Image_name : " + image_basename + " / id : " + image_wpr.getId())
+    List<DatasetWrapper> dataset_wpr_list = image_wpr.getDatasets(user_client)
+
+    // if the image is part of a dataset
+    if(!dataset_wpr_list.isEmpty()){
+        dataset_wpr_list.each{println("dataset_name : "+it.getName()+" / id : "+it.getId())};
+        image_wpr.getProjects(user_client).each{println("Project_name : "+it.getName()+" / id : "+it.getId())};
+    }
+
+    // if the image is part of a plate
+    else {
+        WellWrapper well_wpr = image_wpr.getWells(user_client).get(0)
+        println ("Well_name : "+well_wpr.getName() +" / id : "+ well_wpr.getId())
+
+        def plate_wpr = image_wpr.getPlates(user_client).get(0)
+        println ("plate_name : "+plate_wpr.getName() + " / id : "+ plate_wpr.getId())
+
+        def screen_wpr = image_wpr.getScreens(user_client).get(0)
+        println ("screen_name : "+screen_wpr.getName() + " / id : "+ screen_wpr.getId())
+    }
+
+    ImagePlus imp = image_wpr.toImagePlus(user_client);
+
+    if (!headless_mode) {
+        imp.show()
+    }
+
+    // get imp info
+    int[] dim_array = imp.getDimensions()
+    int nC = dim_array[2]
+    int nT = dim_array[4]
+
+    int ilastik_input_ch = 1
+    // Find the Greys channel:
+    ImageProcessor ip
+    if (nC > 1) {
+        for (int i = 1; i <= nC; i ++) {
+            imp.setC(i)
+            println "Set channel to "+ i
+            ip = imp.getChannelProcessor()
+            println ip.getLut().toString()
+            // ip.getLut().toString() gives
+            // rgb[0]=black, rgb[255]=white, min=3.0000, max=255.0000
+            // rgb[0]=black, rgb[255]=green, min=1950.0000, max=2835.0000
+            if (ip.getLut().toString().contains("white")) {
+                ilastik_input_ch = i
+                break
+            }
+        }
+    }
+    File output_path = new File (output_directory, image_basename+"_ilastik_" + ilastik_project_short_name + "_output.tif" )
+    ImagePlus predictions_imp
+    FileSaver fs
+    if(output_path.exists()) {
+        println "USING EXISTING ILASTIK OUTPUT"
+        predictions_imp = IJ.openImage( output_path.toString() )
+    } else {
+        /**
+        *  ilastik
+        */
+        println "Starting ilastik"
+
+        // get ilastik predictions for each time point of the Time-lapse but all at the same time
+        ImagePlus ilastik_input_original = new Duplicator().run(imp, ilastik_input_ch, ilastik_input_ch, 1, 1, 1, nT);
+
+        ImagePlus gb_imp = ilastik_input_original.duplicate()
+        IJ.run(gb_imp, "Gaussian Blur...", "sigma=100 stack")
+        ImagePlus ilastik_input = ImageCalculator.run(ilastik_input_original, gb_imp, "Divide create 32-bit stack")
+        if (!headless_mode) {ilastik_input.show()}
+        // can't work without displaying image
+        // IJ.run("Run Pixel Classification Prediction", "projectfilename="+ilastik_project+" inputimage="+ilastik_input.getTitle()+" pixelclassificationtype=Probabilities");
+        //
+        // to use in headless_mode more we need to use a commandservice
+        def predictions_imgPlus
+        if (ilastik_project_type == "Regular") {
+            predictions_imgPlus = cmds.run( IlastikPixelClassificationCommand.class, false,
+                                        'inputImage', ilastik_input,
+                                        'projectFileName', ilastik_project,
+                                        'pixelClassificationType', "Probabilities").get().getOutput("predictions")
+        } else {
+            predictions_imgPlus = cmds.run( IlastikAutoContextCommand.class, false,
+                                        'inputImage', ilastik_input,
+                                        'projectFileName', ilastik_project,
+                                        'AutocontextPredictionType', "Probabilities").get().getOutput("predictions")
+        }
+        // to convert the result to ImagePlus : https://gist.github.com/GenevieveBuckley/460d0abc7c1b13eee983187b955330ba
+        predictions_imp = ImageJFunctions.wrap(predictions_imgPlus, "predictions")
+
+        predictions_imp.setTitle("ilastik_output")
+
+        // save file
+        fs = new FileSaver(predictions_imp)
+        fs.saveAsTiff(output_path.toString() )
+    }
+    if (!headless_mode) {  predictions_imp.show()   }
+
+    /**
+     * From the "ilastik predictions of the Time-lapse" do segmentation and cleaning
+     */
+    // Get only the channel for the gastruloid/background prediction
+    ImagePlus mask_imp = new Duplicator().run(predictions_imp, ilastik_label_OI, ilastik_label_OI, 1, 1, 1, nT);
+    // This title will appear in the result table
+    mask_imp.setTitle(image_basename)
+    // Apply threshold:
+    IJ.setThreshold(mask_imp, probability_threshold, 100.0000);
+    Prefs.blackBackground = true;
+    IJ.run(mask_imp, "Convert to Mask", "method=Default background=Dark black");
+    if (!headless_mode) {  mask_imp.show() }
+
+    // clean the mask a bit
+    // Before we were doing:
+    // IJ.run(mask_ilastik_imp, "Options...", "iterations=10 count=3 black do=Open")
+    // Now:
+    // (Romain proposed 5 as radius_median)
+    println "Smoothing mask"
+
+    // Here I need to check if we first fill holes or first do the median
+    IJ.run(mask_imp, "Median...", "radius=" + radius_median + " stack");
+
+    IJ.run(mask_imp, "Fill Holes", "stack");
+
+    // Get scale from omero
+    PixelsWrapper pixels = image_wpr.getPixels()
+    LengthI pixel_size = pixels.getPixelSizeX()
+    Double scale = pixel_size.getValue()
+    String scale_unit = pixel_size.getUnit().toString()
+
+    // find gastruloids and measure them
+
+    IJ.run("Set Measurements...", "area feret's perimeter shape display redirect=None decimal=3")
+    IJ.run("Set Scale...", "distance=1 known=" + scale + " unit=micron")
+    // Exclude the edge
+    IJ.run(mask_imp, "Analyze Particles...", "size=" + min_size_particle + "-Infinity stack exclude show=Overlay");
+
+    println "Found " + rt.size() + " ROIs"
+
+    // make a "clean" mask
+    newMask_imp = IJ.createImage("CleanMask", "8-bit black", imp.getWidth(), imp.getHeight(), nT);
+    if (nT > 1) {
+        HyperStackConverter.toHyperStack(newMask_imp, 1, 1, nT, "xyctz", "Color");
+    }
+    if (!headless_mode) {newMask_imp.show()}
+
+    Overlay ov = mask_imp.getOverlay()
+    // Let's keep only the largest area for each time:
+    Overlay clean_overlay = new Overlay()
+    Roi largest_roi_inT
+    for (int t=1;t<=nT;t++) {
+        // Don't ask me why we need to refer to Z pos and not T/Frame
+        ArrayList<Roi> all_rois_inT = ov.findAll{ roi -> roi.getZPosition() == t}
+        println "There are " + all_rois_inT.size() + " in time " + t
+        if (all_rois_inT.size() == 0) {
+            // We arbitrary design a ROI of size 1x1
+            largest_roi_inT = new Roi(0,0,1,1)
+            largest_roi_inT.setName("GastruloidNotFound_t" + t)
+        } else {
+            largest_roi_inT = Collections.max(all_rois_inT, Comparator.comparing((roi) -> roi.getStatistics().area ))
+            largest_roi_inT.setName("Gastruloid_t" + t)
+        }
+        // Fill the frame t with the largest_roi_inT
+        newMask_imp.setT(t)
+        Overlay t_ov = new Overlay(largest_roi_inT)
+        t_ov.fill(newMask_imp,  Color.white, Color.black)
+        // Update the position before adding to the clean_overlay
+        largest_roi_inT.setPosition( ilastik_input_ch, 1, t)
+        clean_overlay.add(largest_roi_inT)
+    }
+
+    // Measure this new overlay:
+    rt = clean_overlay.measure(imp)
+
+    assert rt.size() == nT: "Was expecting as many entry as time points"
+
+    // Get Date
+    Date date = new Date()
+    String now = date.format("yyyy-MM-dd_HH-mm")
+
+    // Add Date, version and params
+    for ( int row = 0;row<rt.size();row++) {
+        rt.setValue("Unit", row, scale_unit)
+        rt.setValue("Date", row, now)
+        rt.setValue("Version", row, tool_version)
+        rt.setValue("IlastikProject", row, ilastik_project_short_name)
+        rt.setValue("ProbabilityThreshold", row, probability_threshold)
+        rt.setValue("MinSizeParticle", row, min_size_particle)
+        rt.setValue("MinDiameter", row, minimum_diameter)
+        rt.setValue("ClosenessTolerance", row, closeness_tolerance)
+        rt.setValue("MinSimilarity", row, min_similarity)
+        rt.setValue("RadiusMedian", row, radius_median)
+        String label = rt.getLabel(row)
+        rt.setValue("BaseImage", row, label.split(":")[0])
+        rt.setValue("ROI", row, label.split(":")[1])
+        // In simple-omero-client
+        // Strings that can be converted to double are stored in double
+        // in omero so to create the super_table we need to store all
+        // them as Double:
+        rt.setValue("Time", row, label.split(":")[1].split("_t")[-1] as Double)
+        rt.setValue("ROI_type", row, label.split(":")[1].split("_t")[0])
+    }
+    println "Remove existing ROIs on OMERO"
+    // Remove existing ROIs
+    image_wpr.getROIs(user_client).each{ user_client.delete(it) }
+    println "Store " + clean_overlay.size() + " ROIs on OMERO"
+    // Save ROIs to omero
+    image_wpr.saveROIs(user_client, ROIWrapper.fromImageJ(clean_overlay as List))
+
+    // Get them back with IDs:
+    List<Roi> updatedRois = ROIWrapper.toImageJ(image_wpr.getROIs(user_client), "ROI")
+    if (get_spine) {
+        /**
+        * The MaxInscribedCircles magic is here
+        */
+        isSelectionOnly = false
+        isGetSpine = true
+        appendPositionToName = true
+        MaxInscribedCircles mic = MaxInscribedCircles.builder(newMask_imp)
+            .minimumDiameter(minimum_diameter)
+            .useSelectionOnly(isSelectionOnly)
+            .getSpine(isGetSpine)
+            .spineClosenessTolerance(closeness_tolerance)
+            .spineMinimumSimilarity(min_similarity)
+            .appendPositionToName(appendPositionToName)
+            .build()
+        println "Get spines"
+        mic.process()
+        List<Roi> all_circles = mic.getCircles();
+        List<Roi> all_spines = mic.getSpines();
+
+        /**
+        *  For each Time-point, find the :
+        *  - the largest cicle
+        *  - the spine, and the coordinates of end-points
+        *  Measure distances and inverses spine roi if necessary
+        *  Add value to table with Elongation Index
+        */
+        double pixelWidth = mask_imp.getCalibration().pixelWidth
+
+        for (int row = 0 ; row < rt.size();row++) {
+
+            int t = rt.getValue("Time", row) as int
+            println "#############"+t
+            String roi_type = rt.getStringValue("ROI_type", row)
+
+            if (roi_type == "Gastruloid") {
+                List<Roi> circles_t =  all_circles.findAll{ roi -> roi.getName().endsWith("P_"+t)}
+
+                if (circles_t.size() > 0) {
+                    Roi largestCircle_roi = circles_t[0]
+                    largestCircle_roi.setPosition( ilastik_input_ch, 1, t)
+                    println largestCircle_roi
+                    def xC = largestCircle_roi.x + largestCircle_roi.width/2
+                    def yC = largestCircle_roi.y + largestCircle_roi.height/2
+                    println "Largest Circle center : "  + xC + "," + yC
+                    double circle_roi_radius = largestCircle_roi.width/2
+                    ArrayList<Roi> rois_to_add_to_omero
+                    rt.setValue("LargestRadius", row, circle_roi_radius * pixelWidth)
+                    if (debug) {
+                        circles_t.each{it.setPosition(ilastik_input_ch, 1, t)}
+                        // First put all circles to omero:
+                        image_wpr.saveROIs(user_client, ROIWrapper.fromImageJ(circles_t as List))
+                        if (!headless_mode) {
+                            (circles_t as List).each{ rm.addRoi(it)}
+                        }
+                    } else {
+                        // First put the largest circle to omero:
+                        image_wpr.saveROIs(user_client, ROIWrapper.fromImageJ([largestCircle_roi] as List))
+                        if (!headless_mode) {
+                            rm.addRoi(largestCircle_roi)
+                        }
+                    }
+
+                    // get the Spine, and its points
+                    Roi spine_roi = all_spines.findAll{ roi -> roi.getName().endsWith("P_"+t)}[0]
+                    println "Spine is " + spine_roi
+                    if (spine_roi != null){
+                        //println spine_roi
+                        println "Get points coo"
+                        Double[] spine_xs = spine_roi.getFloatPolygon().xpoints as List
+                        Double[] spine_ys = spine_roi.getFloatPolygon().ypoints as List
+                        //println spine_xs
+                        //println spine_ys
+
+                        // Measure distance between spine end-points and center of the largest circle
+                        Double d1 = Math.sqrt( Math.pow(spine_xs[0]  - xC,2) + Math.pow(spine_ys[0]  - yC,2) )
+                        Double d2 = Math.sqrt( Math.pow(spine_xs[-1] - xC,2) + Math.pow(spine_ys[-1] - yC,2) )
+                        //println d1
+                        //println d2
+
+                        if (d2 < d1){
+                            println "re-orient spine"
+                            // make a new polyline roi from a list of points
+                            spine_name = spine_roi.getName()
+                            spine_roi = new PolygonRoi(spine_xs.reverse() as int[], spine_ys.reverse() as int[], spine_xs.size(), PolygonRoi.POLYLINE)
+                            spine_roi.setName(spine_name)
+                        } else {
+                            println "orientation of spine is ok"
+                        }
+
+                        double line_roi_length = spine_roi.getLength()
+                        rt.setValue("SpineLength", row, line_roi_length * pixelWidth)
+                        rt.setValue("ElongationIndex", row, line_roi_length / (2*circle_roi_radius))
+                        spine_roi.setPosition( ilastik_input_ch, 1, t)
+                        image_wpr.saveROIs(user_client, ROIWrapper.fromImageJ([spine_roi] as List))
+                        if (!headless_mode) {
+                            rm.addRoi(spine_roi)
+                        }
+                    } else {
+                        rt.setValue("SpineLength", row, 0)
+                        rt.setValue("ElongationIndex", row, 1)
+                    }
+                } else {
+                    rt.setValue("LargestRadius", row, 0)
+                    rt.setValue("SpineLength", row, 0)
+                    rt.setValue("ElongationIndex", row, 0)
+                }
+            } else {
+                rt.setValue("LargestRadius", row, 0)
+                rt.setValue("SpineLength", row, 0)
+                rt.setValue("ElongationIndex", row, -1)
+            }
+        }
+    }
+    // get the list of image tables
+    // remove the one with table_name
+    image_wpr.getTables(user_client).each{ TableWrapper t_wpr ->
+        if (t_wpr.getName() == table_name){
+            user_client.delete(t_wpr)
+        }
+    }
+
+    // Create an omero table:
+    println "Create an omero table"
+    TableWrapper table_wpr = new TableWrapper(user_client, rt, image_wpr.getId(), updatedRois, "ROI")
+
+    // upload the table on OMERO
+    table_wpr.setName(table_name)
+    image_wpr.addTable(user_client, table_wpr)
+
+    // add the same infos to the super_table
+    if (super_table == null) {
+        println "super_table is null"
+        super_table = table_wpr
+    } else {
+        println "adding rows"
+        super_table.addRows(user_client, rt, image_wpr.getId(), updatedRois, "ROI")
+    }
+    println super_table.getRowCount()
+    println "Writting measurements to file"
+    rt.save(output_directory.toString() + '/' + image_basename + "__Results.csv" )
+
+    // Put all ROIs in overlay:
+    Overlay global_overlay = new Overlay()
+    ROIWrapper.toImageJ(image_wpr.getROIs(user_client), "ROI").each{
+        global_overlay.add(it)
+    }
+
+    imp.setOverlay(global_overlay)
+
+    // save file
+    fs = new FileSaver(imp)
+    output_path = new File (output_directory, image_basename + ".tiff" )
+    fs.saveAsTiff(output_path.toString() )
+
+    return
+}
+
+// In simple-omero-client
+// Strings that can be converted to double are stored in double
+// In order to build the super_table, tool_version should stay String
+String tool_version = "Phase_v20230324"
+
+// User set variables
+
+#@ String(visibility=MESSAGE, value="Inputs", required=false) msg
+#@ String(label="User name") USERNAME
+#@ String(label="PASSWORD", style='PASSWORD', value="", persist=false) PASSWORD
+#@ String(label="File path with omero credentials") credentials
+#@ String(label="omero host server") host
+#@ Integer(label="omero host server port", value=4064) port
+#@ String(label="Object", choices={"image","dataset","well","plate"}) object_type
+#@ Long(label="ID", value=119273) id
+
+#@ String(visibility=MESSAGE, value="Parameters for segmentation/ROI", required=false) msg2
+#@ File(label="Ilastik project") ilastik_project
+#@ String(label="Ilastik project short name") ilastik_project_short_name
+#@ String(label="Ilastik project type", choices={"Regular", "Auto-context"}, value="Regular") ilastik_project_type
+#@ Integer(label="Ilastik label of interest", min=1, value=1) ilastik_label_OI
+#@ Double(label="Probability threshold for ilastik", min=0, max=1, value=0.65) probability_threshold
+#@ Double(label="Radius for median (=smooth the mask)", min=1, value=20) radius_median
+#@ Integer(label="Minimum surface for Analyze Particle", value=5000) min_size_particle
+
+#@ String(visibility=MESSAGE, value="Parameters for elongation index", required=false) msg3
+#@ Boolean(label="Compute spine", value=true) get_spine
+#@ Integer(label="Minimum diameter of inscribed circles", min=0, value=20) minimum_diameter
+#@ Integer(label="Closeness Tolerance (Spine)", min=0, value=50) closeness_tolerance
+#@ Double(label="Min similarity (Spine)", min=-1, max=1, value=0.1) min_similarity
+
+#@ String(visibility=MESSAGE, value="Parameters for output", required=false) msg4
+#@ File(style = "directory", label="Directory where measures are put") output_directory
+#@ Boolean(label="<html>Run in debug mode<br/>(get all inscribed circles)</html>", value=false) debug
+
+#@ ResultsTable rt
+#@ CommandService cmds
+#@ ConvertService cvts
+#@ DatasetService ds
+#@ DatasetIOService io
+
+// Detect if is headless
+// java.awt.GraphicsEnvironment.checkheadless_mode(GraphicsEnvironment.java:204)
+Boolean headless_mode = GraphicsEnvironment.isHeadless()
+if (headless_mode) {
+    println "Running in headless mode"
+}
+
+// Define rm even if in headless mode
+def rm
+if (!headless_mode){
+    // Reset RoiManager and Result table if not in headless
+    rm = new RoiManager()
+    rm = rm.getRoiManager()
+    rm.reset()
+    // Reset the table
+    rt.reset()
+}
+
+if (PASSWORD == "") {
+    File cred_file = new File(credentials)
+    if (!cred_file.exists()) {
+        throw new Exception("Password or credential file need to be set.")
+    }
+    String creds = FileUtils.readFileToString(cred_file, "UTF-8")
+    if (creds.split("\n").size() < 2) {
+        throw new Exception("Credential file requires 2 lines")
+    }
+    USERNAME = creds.split("\n")[0]
+    PASSWORD = creds.split("\n")[1]
+}
+
+// Connection to server
+Client user_client = new Client()
+user_client.connect(host, port, USERNAME, PASSWORD.toCharArray())
+
+// This super_table is a global variable and will be filled each time an image is processed.
+super_table = null
+// table_name is also a global variable
+table_name = "Measurements_from_Galaxy_phase"
+if (user_client.isConnected()) {
+    println "\nConnected to "+host
+    println "Results will be in " + output_directory.toString()
+
+    try {
+
+        switch (object_type) {
+            case "image":
+                ImageWrapper image_wr
+                try {
+                    image_wpr = user_client.getImage(id)
+                } catch(Exception e) {
+                    throw Exception("Could not retrieve the image, please check the id.")
+                }
+                processImage(user_client, image_wpr,
+                     ilastik_project, ilastik_project_type,
+                     ilastik_label_OI,
+                     probability_threshold, radius_median, min_size_particle,
+                     get_spine, minimum_diameter, closeness_tolerance, min_similarity,
+                     ilastik_project_short_name,
+                     output_directory,
+                     headless_mode, debug, tool_version)
+                break
+            case "dataset":
+                DatasetWrapper dataset_wrp
+                try {
+                    dataset_wrp = user_client.getDataset(id)
+                } catch(Exception e) {
+                    throw Exception("Could not retrieve the dataset, please check the id.")
+                }
+                processDataset(user_client, dataset_wrp,
+                     ilastik_project, ilastik_project_type,
+                     ilastik_label_OI,
+                     probability_threshold, radius_median, min_size_particle,
+                     get_spine, minimum_diameter, closeness_tolerance, min_similarity,
+                     ilastik_project_short_name,
+                     output_directory,
+                     headless_mode, debug, tool_version)
+                // get the list of dataset tables
+                // remove the one with table_name
+                dataset_wrp.getTables(user_client).each{ TableWrapper t_wpr ->
+                    if (t_wpr.getName() == table_name + "_global"){
+                        user_client.delete(t_wpr)
+                    }
+                }
+                // upload the table on OMERO
+                super_table.setName(table_name + "_global")
+                dataset_wrp.addTable(user_client, super_table)
+                break
+            case "well":
+                WellWrapper well_wrp
+                try {
+                    well_wrp = user_client.getWells(id)[0]
+                } catch(Exception e) {
+                    throw Exception("Could not retrieve the well, please check the id.")
+                }
+                processSingleWell(user_client, well_wrp,
+                     ilastik_project, ilastik_project_type,
+                     ilastik_label_OI,
+                     probability_threshold, radius_median, min_size_particle,
+                     get_spine, minimum_diameter, closeness_tolerance, min_similarity,
+                     ilastik_project_short_name,
+                     output_directory,
+                     headless_mode, debug, tool_version)
+                // get the list of well tables
+                // remove the one with table_name
+                well_wrp.getTables(user_client).each{ TableWrapper t_wpr ->
+                    if (t_wpr.getName() == table_name + "_global"){
+                        user_client.delete(t_wpr)
+                    }
+                }
+                // upload the table on OMERO
+                super_table.setName(table_name + "_global")
+                well_wrp.addTable(user_client, super_table)
+                break
+            case "plate":
+                PlateWrapper plate_wrp
+                try {
+                    plate_wrp = user_client.getPlates(id)[0]
+                } catch(Exception e) {
+                    throw Exception("Could not retrieve the plate, please check the id.")
+                }
+                processSinglePlate(user_client, plate_wrp,
+                     ilastik_project, ilastik_project_type,
+                     ilastik_label_OI,
+                     probability_threshold, radius_median, min_size_particle,
+                     get_spine, minimum_diameter, closeness_tolerance, min_similarity,
+                     ilastik_project_short_name,
+                     output_directory,
+                     headless_mode, debug, tool_version)
+                // get the list of well tables
+                // remove the one with table_name
+                plate_wrp.getTables(user_client).each{ TableWrapper t_wpr ->
+                    if (t_wpr.getName() == table_name + "_global"){
+                        user_client.delete(t_wpr)
+                    }
+                }
+                // upload the table on OMERO
+                super_table.setName(table_name + "_global")
+                plate_wrp.addTable(user_client, super_table)
+                break
+        }
+
+    } catch(Exception e) {
+        println("Something went wrong: " + e)
+
+        if (headless_mode){
+            // This is due to Rank Filter + GaussianBlur
+            System.exit(1)
+        }
+    } finally {
+        user_client.disconnect()
+        println "Disonnected " + host
+    }
+    if (headless_mode) {
+        // This is due to Rank Filter + GaussianBlur
+        System.exit(0)
+    }
+
+} else {
+    println "Not able to connect to " + host
+}
+
+return
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/omero_hyperstack_to_gastruloid_measurements.xml	Fri Mar 24 13:03:39 2023 +0000
@@ -0,0 +1,136 @@
+<tool id="omero_hyperstack_to_gastruloid_measurements" name="Omero hyperstack to Gastruloid measurements" profile="20.01" version="@TOOL_VERSION@+galaxy0">
+    <macros>
+        <token name="@TOOL_VERSION@">20230324</token>
+    </macros>
+    <requirements>
+        <requirement type="package" version="20220414">fiji</requirement>
+        <requirement type="package" version="3.7">python</requirement>
+        <requirement type="package" version="2.0.0">fiji-max_inscribed_circles</requirement>
+        <requirement type="package" version="1.8.2">fiji-ilastik</requirement>
+        <requirement type="package" version="5.8.0">fiji-omero_ij</requirement>
+        <requirement type="package" version="5.12.2">fiji-simple_omero_client</requirement>
+    </requirements>
+    <command detect_errors="exit_code"><![CDATA[
+        ## the user wants to use a non-public OMERO instance
+        ## check if credentials are set in the user-preferences, if not warn the user and exit
+        #set $username = $__user__.extra_preferences.get('omero_account|username', "")
+        #set $password = $__user__.extra_preferences.get('omero_account|password', "")
+
+        #if ($username == "" or $password ==""):
+            echo "OMERO connection credentials are empty. Set your credentials via: User -> Preferences -> Manage Information" 1>&2 &&
+            exit 1 &&
+        #end if
+        mkdir output &&
+        ## Because ilastik wants to write to ${HOME}/.cache and ${HOME}/.config
+        export HOME=`pwd` &&
+        ImageJ-ilastik --ij2 --headless --console --run '$__tool_directory__/'1-omero_timelapse_image_to_measurements_phase.groovy 
+            'USERNAME="",PASSWORD="",credentials="${credentials}",host="${omero_host}",port="${omero_port}",object_type="${omero_object.object_type}",id="${omero_object.omero_id}",ilastik_project="${ilastik_project}",ilastik_project_short_name="${ilastik_project.name}",ilastik_project_type="${ilastik_project_type}",ilastik_label_OI="${ilastik_label_OI}",probability_threshold="${probability_threshold}",radius_median="${radius_median}",min_size_particle="${min_size_particle}",get_spine="true",minimum_diameter="${minimum_diameter}",closeness_tolerance="${closeness_tolerance}",min_similarity="${min_similarity}",output_directory="output",debug="${debug}"' > output.log
+        ]]>
+    </command>
+    <configfiles>
+        <configfile name="credentials"><![CDATA[#set $username = $__user__.extra_preferences.get('omero_account|username', "")
+    #set $password = $__user__.extra_preferences.get('omero_account|password', "")
+$username
+$password
+        ]]></configfile>
+    </configfiles>
+    <inputs>
+        <param name="omero_host" type="text" label="OMERO host URL">
+            <validator type="regex" message="Enter a valid host location, for example, your.omero.server">^[a-zA-Z0-9._-]*$</validator>
+            <validator type="expression" message="No two dots (..) allowed">'..' not in value</validator>
+        </param>
+        <param name="omero_port" type="integer" value="4064" label="Omero port" />
+        <conditional name="omero_object">
+            <param name="object_type" type="select" label="Type of object to analyze">
+                <option value="image">Single Omero Image</option>
+                <option value="well">All images of a Well</option>
+                <option value="plate">All images of a Plate</option>
+                <option value="dataset">All images of a Dataset</option>
+            </param>
+            <when value="image">
+                <param name="omero_id" type="integer" value="" label="Image ID on omero" />
+            </when>
+            <when value="well">
+                <param name="omero_id" type="integer" value="" label="Well ID on omero" />
+            </when>
+            <when value="plate">
+                <param name="omero_id" type="integer" value="" label="Plate ID on omero" />
+            </when>
+            <when value="dataset">
+                <param name="omero_id" type="integer" value="" label="Dataset ID on omero" />
+            </when>
+        </conditional>
+        <param name="ilastik_project" type="data" format="h5" label="Ilastik project" />
+        <param name="ilastik_project_type" type="select" label="Type of Ilastik project">
+            <option value="Regular">Regular</option>
+            <option value="Auto-context">Auto-context</option>
+        </param>
+        <param name="ilastik_label_OI" type="integer" value="3" label="Index of label of interest in Ilastik project" />
+        <param name="probability_threshold" type="float" min="0" max="1" value="0.4" label="Probability threshold for ilastik" />
+        <param name="radius_median" type="float" value="20" label="Radius for median (=smooth the mask)" />
+        <param name="min_size_particle" type="integer" min="0" value="5000" label="Minimum surface for Analyze Particle" />
+        <param name="minimum_diameter" type="integer" min="0" value="20" label="Minimum diameter of inscribed circles" />
+        <param name="closeness_tolerance" type="integer" min="0" value="50" label="Closeness Tolerance for the spine" help="Maximum distance between circles along the spine"/>
+        <param name="min_similarity" type="float" min="-1" max="1" value="0.1" label="Min similarity for the spine" help="Close to 0 values allow more U shapes while close to 1 values only allows I shapes" />
+        <param name="debug" type="boolean" truevalue="true" falsevalue="false" checked="false" label="Debug the elongation index" help="This will output all inscribed circles" />
+        <param name="keep_intermediate" type="boolean" checked="false" label="Keep intermediate results (ilastik prediction + tables)" />
+    </inputs>
+
+    <outputs>
+        <data name="logfile" format="txt" from_work_dir="output.log" label="${tool.name} on ID ${omero_object.omero_id} with ${ilastik_project.name}: logfile">
+        </data>
+        <collection name="tables" type="list" label="${tool.name} on ID ${omero_object.omero_id} with ${ilastik_project.name}: Tables">
+            <discover_datasets pattern="(?P&lt;designation&gt;.+)\.csv" directory="output" format="csv"/>
+            <filter>keep_intermediate</filter>
+        </collection>
+        <collection name="hyperstacks_with_overlay" type="list" label="${tool.name} on ID ${omero_object.omero_id} with ${ilastik_project.name}: Hyperstacks">
+            <discover_datasets pattern="(?P&lt;designation&gt;.+)\.tiff" directory="output" format="tiff"/>
+        </collection>
+        <collection name="ilastik_results" type="list" label="${tool.name} on ID ${omero_object.omero_id} with ${ilastik_project.name}: Ilastik">
+            <discover_datasets pattern="(?P&lt;designation&gt;.+)\.tif$" directory="output" format="tiff"/>
+            <filter>keep_intermediate</filter>
+        </collection>
+    </outputs>
+    <help>
+    <![CDATA[
+**Overview**
+
+This tool will get images from omero, use an ilastik project to get propability and generate a mask.
+The potential gastruloids will be indentified by analyze Particles.
+On each ROI, the elongation index will be computed.
+
+**License**
+
+License text::
+
+    // This macro was written by the BIOP (https://github.com/BIOP)
+    // Romain Guiet and Rémy Dornier
+    // Lucille Delisle modified to support headless
+    // merge the analysis script with templates available at
+    // https://github.com/BIOP/OMERO-scripts/tree/main/Fiji
+
+    /*
+    * = COPYRIGHT =
+    * © All rights reserved. ECOLE POLYTECHNIQUE FEDERALE DE LAUSANNE, Switzerland, BioImaging And Optics Platform (BIOP), 2022
+    * 
+    * Licensed under the BSD-3-Clause License:
+    * Redistribution and use in source and binary forms, with or without modification, are permitted provided 
+    * that the following conditions are met:
+    * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
+    * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer 
+    *    in the documentation and/or other materials provided with the distribution.
+    * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products 
+    *     derived from this software without specific prior written permission.
+    * 
+    * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, 
+    * BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 
+    * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 
+    * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 
+    * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, 
+    * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF 
+    * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+    */
+
+]]>
+    </help>
+</tool>