From 6eae5605b03428cae0141feb87164c56f9f6919e Mon Sep 17 00:00:00 2001 From: Amandine Tournay <amandine.tournay@pasteur.fr> Date: Tue, 9 Mar 2021 18:58:55 +0100 Subject: [PATCH] Added project --- .gitignore | 6 + pom.xml | 98 ++++ .../roi/quantify/PathIntensityProfiler.java | 446 ++++++++++++++++++ 3 files changed, 550 insertions(+) create mode 100644 .gitignore create mode 100644 pom.xml create mode 100644 src/main/java/plugins/stef/roi/quantify/PathIntensityProfiler.java diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3d47f98 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +.idea/ +target/ +.settings/ +*.iml +.project +.classpath \ No newline at end of file diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..7900d80 --- /dev/null +++ b/pom.xml @@ -0,0 +1,98 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project xmlns="http://maven.apache.org/POM/4.0.0" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + + <!-- Inherited Icy Parent POM --> + <parent> + <groupId>org.bioimageanalysis.icy</groupId> + <artifactId>parent-pom-plugin</artifactId> + <version>1.0.3</version> + </parent> + + <!-- Project Information --> + <artifactId>path-intensity-profiler</artifactId> + <version>1.2.1.</version> + + <packaging>jar</packaging> + + <name>Path Intensity Profiler</name> + <description> + This plugin will compute the intensity profile of ROI contour along Z (3D) and T (timelaps) dimension. It generates a workbook document containing all the intensity values which can be used to display graph or be saved as XLSX or CSV format. + The plugin is compatible with Protocols. + </description> + <url>http://icy.bioimageanalysis.org/plugin/path-intensity-profil/</url> + <inceptionYear>2020</inceptionYear> + + <organization> + <name>Institut Pasteur</name> + <url>https://pasteur.fr</url> + </organization> + + <licenses> + <license> + <name>GNU GPLv3</name> + <url>https://www.gnu.org/licenses/gpl-3.0.en.html</url> + <distribution>repo</distribution> + </license> + </licenses> + + <developers> + <developer> + <id>sdallongeville</id> + <name>Stéphane Dallongeville</name> + <url>https://research.pasteur.fr/fr/member/stephane-dallongeville/</url> + <roles> + <role>founder</role> + <role>lead</role> + <role>architect</role> + <role>developer</role> + <role>debugger</role> + <role>tester</role> + <role>maintainer</role> + <role>support</role> + </roles> + </developer> + </developers> + + <!-- Project properties --> + <properties> + + </properties> + + <!-- Project build configuration --> + <build> + + </build> + + <!-- List of project's dependencies --> + <dependencies> + <!-- The core of Icy --> + <dependency> + <groupId>org.bioimageanalysis.icy</groupId> + <artifactId>icy-kernel</artifactId> + </dependency> + + <!-- The EzPlug library, simplifies writing UI for Icy plugins. --> + <dependency> + <groupId>org.bioimageanalysis.icy</groupId> + <artifactId>ezplug</artifactId> + </dependency> + + <dependency> + <groupId>org.bioimageanalysis.icy</groupId> + <artifactId>workbooks</artifactId> + <version>3.4.10</version> + </dependency> + </dependencies> + + <!-- Icy Maven repository (to find parent POM) --> + <repositories> + <repository> + <id>icy</id> + <name>Icy's Nexus</name> + <url>https://icy-nexus.pasteur.fr/repository/Icy/</url> + </repository> + </repositories> +</project> diff --git a/src/main/java/plugins/stef/roi/quantify/PathIntensityProfiler.java b/src/main/java/plugins/stef/roi/quantify/PathIntensityProfiler.java new file mode 100644 index 0000000..8e07b21 --- /dev/null +++ b/src/main/java/plugins/stef/roi/quantify/PathIntensityProfiler.java @@ -0,0 +1,446 @@ +package plugins.stef.roi.quantify; + +import java.awt.Dimension; +import java.awt.Point; +import java.awt.geom.Point2D; +import java.util.ArrayList; +import java.util.List; + +import javax.swing.JComponent; + +import org.apache.poi.xssf.usermodel.XSSFWorkbook; + +import icy.gui.dialog.MessageDialog; +import icy.math.Line3DIterator; +import icy.math.MathUtil; +import icy.roi.BooleanMask2D; +import icy.roi.ROI; +import icy.roi.ROI2D; +import icy.roi.ROI3D; +import icy.sequence.Sequence; +import icy.type.collection.CollectionUtil; +import icy.type.geom.Line3D; +import icy.type.point.Point3D; +import icy.type.point.Point4D; +import icy.type.rectangle.Rectangle5D; +import plugins.adufour.blocks.lang.Block; +import plugins.adufour.blocks.util.VarList; +import plugins.adufour.ezplug.EzGUI; +import plugins.adufour.ezplug.EzPlug; +import plugins.adufour.ezplug.EzStoppable; +import plugins.adufour.ezplug.EzVarBoolean; +import plugins.adufour.vars.gui.swing.WorkbookEditor; +import plugins.adufour.vars.lang.VarROIArray; +import plugins.adufour.vars.lang.VarSequence; +import plugins.adufour.vars.lang.VarWorkbook; +import plugins.adufour.workbooks.IcySpreadSheet; +import plugins.adufour.workbooks.Workbooks; +import plugins.adufour.workbooks.Workbooks.WorkbookFormat; +import plugins.kernel.roi.roi2d.ROI2DLine; +import plugins.kernel.roi.roi2d.ROI2DPoint; +import plugins.kernel.roi.roi2d.ROI2DPolyLine; +import plugins.kernel.roi.roi2d.ROI2DPolygon; +import plugins.kernel.roi.roi2d.ROI2DRectangle; +import plugins.kernel.roi.roi2d.ROI2DShape; +import plugins.kernel.roi.roi3d.ROI3DLine; +import plugins.kernel.roi.roi3d.ROI3DPoint; +import plugins.kernel.roi.roi3d.ROI3DPolyLine; +import plugins.kernel.roi.roi3d.ROI3DShape; + +/** + * Path profiler plugin class.<br> + * Computes intensity profil along path (contour for area ROI) for the given list of ROI and return result in XLSX workbook format + * where we have one sheet per ROI. + * + * @author Stephane + */ +public class PathIntensityProfiler extends EzPlug implements Block, EzStoppable +{ + // VAR + public final VarSequence varSequence; + public final VarROIArray varRois; + public final EzVarBoolean varRealUnit; + public final VarWorkbook varWorkbook; + + public PathIntensityProfiler() + { + super(); + + varSequence = new VarSequence("Sequence", null); + varRois = new VarROIArray("Roi(s)"); + varRealUnit = new EzVarBoolean("Use real unit", false); + varWorkbook = new VarWorkbook("WorkBook", Workbooks.createEmptyWorkbook(WorkbookFormat.XLSX)); + } + + @Override + protected void initialize() + { + final WorkbookEditor viewer = (WorkbookEditor) varWorkbook.createVarViewer(); + + viewer.setEnabled(true); + viewer.setFirstRowAsHeader(true); + + final JComponent jc = viewer.getEditorComponent(); + jc.setPreferredSize(new Dimension(400, 300)); + + addComponent(jc); + addEzComponent(varRealUnit); + } + + @Override + public void declareInput(VarList inputMap) + { + inputMap.add("sequence", varSequence); + inputMap.add("rois", varRois); + inputMap.add("realUnit", varRealUnit.getVariable()); + } + + @Override + public void declareOutput(VarList outputMap) + { + outputMap.add("workbook", varWorkbook); + } + + @Override + public void clean() + { + // set empty workbook to release resources + varWorkbook.setValue(Workbooks.createEmptyWorkbook(WorkbookFormat.XLSX)); + } + + @Override + protected void execute() + { + // set empty workbook by default (also release resources from previous run) + varWorkbook.setValue(Workbooks.createEmptyWorkbook(WorkbookFormat.XLSX)); + + // interactive mode (not in protocol) + if (!isHeadLess()) + { + final Sequence seq = getActiveSequence(); + + // set variables + if (seq != null) + { + varSequence.setValue(seq); + varRois.setValue(seq.getROIs().toArray(new ROI[0])); + } + else + { + // inform user + MessageDialog.showDialog("You need to open an image containing ROI(s).", + MessageDialog.INFORMATION_MESSAGE); + return; + } + } + + final Sequence sequence = varSequence.getValue(); + + if (sequence != null) + { + final ROI[] rois = varRois.getValue(); + final List<ROI> validRois = new ArrayList<ROI>(); + + // we can only compute path intensity on path ROI or 2D ROI + for (ROI roi : rois) + { + if (roi instanceof ROI2D) + validRois.add(roi); + // else if ((roi instanceof ROI3DPoint) || (roi instanceof ROI3DLine) || (roi instanceof ROI3DPolyLine)) + else if (roi instanceof ROI3D) + validRois.add(roi); + } + + if (validRois.size() == 0) + { + // inform user + if (!isHeadLess()) + MessageDialog.showDialog( + "The selected Sequence doesn't contain any ROI where we can compute intensity profil.", + MessageDialog.INFORMATION_MESSAGE); + return; + } + + // interactive mode + if (!isHeadLess()) + getUI().setProgressBarMessage("Computing..."); + + try + { + // set result + varWorkbook.setValue( + getPathIntensityProfil(sequence, validRois, varRealUnit.getValue().booleanValue(), getUI())); + } + finally + { + // interactive mode + if (!isHeadLess()) + { + getUI().setProgressBarMessage("Done"); + getUI().setProgressBarValue(0); + } + } + } + } + + /** + * Computes intensity profil along path (contour for area ROI) for the given list of ROI. + * + * @return result in a XLSX workbook where we have one sheet per ROI. + */ + private static XSSFWorkbook getPathIntensityProfil(Sequence sequence, List<ROI> rois, boolean useRealUnit, EzGUI ui) + { + final XSSFWorkbook result = (XSSFWorkbook) Workbooks.createEmptyWorkbook(WorkbookFormat.XLSX); + + // nothing to do + if (rois.isEmpty()) + return result; + + final int sizeT = sequence.getSizeT(); + final int sizeZ = sequence.getSizeZ(); + final int sizeC = sequence.getSizeC(); + + final Point4D.Double unitScale = new Point4D.Double(); + + if (useRealUnit) + { + unitScale.x = sequence.getPixelSizeX(); + unitScale.y = sequence.getPixelSizeY(); + unitScale.z = sequence.getPixelSizeZ(); + unitScale.t = sequence.getTimeInterval(); + } + else + { + unitScale.x = 1d; + unitScale.y = 1d; + unitScale.z = 1d; + unitScale.t = 1d; + } + + final double progressRatio = rois.size() * sizeT * sizeZ * sizeC; + + int roiIndex = 0; + // can process now + for (ROI roi : rois) + { + // create a sheet for each ROI + final IcySpreadSheet sh = new IcySpreadSheet( + result.createSheet(roi.getName() + String.format(" - S%04d", Integer.valueOf(roiIndex)))); + + // write columns headers + int indCol = 0; + sh.setValue(0, indCol++, "Point #"); + sh.setValue(0, indCol++, "X" + (useRealUnit ? " (um)" : "")); + sh.setValue(0, indCol++, "Y" + (useRealUnit ? " (um)" : "")); + sh.setValue(0, indCol++, "Z" + (useRealUnit ? " (um)" : "")); + sh.setValue(0, indCol++, "T" + (useRealUnit ? " (s)" : "")); + for (int c = 0; c < sizeC; c++) + sh.setValue(0, indCol++, "ch " + c + " (" + sequence.getChannelName(c) + ")"); + + // ROI bounds + final Rectangle5D roiBounds = roi.getBounds5D(); + // start + int row = 1; + + for (int t = 0; t < sizeT; t++) + { + // in case of 2D ROI, we need to handle the specific T = -1 (ALL) + if (roi instanceof ROI2D) + { + final int roiT = ((ROI2D) roi).getT(); + + // ROI not on current T position ? --> next + if ((roiT != -1) && (roiT != t)) + continue; + } + // in case of 3D ROI, we need to handle the specific T = -1 (ALL) + else if (roi instanceof ROI3D) + { + final int roiT = ((ROI3D) roi).getT(); + + // ROI not on current T position ? --> next + if ((roiT != -1) && (roiT != t)) + continue; + } + // ROI does not contain current T position ? --> next + else if ((roiBounds.getMinT() > t) || (roiBounds.getMaxT() < t)) + continue; + + for (int z = 0; z < sizeZ; z++) + { + // in case of 2D ROI, we need to handle the specific Z = -1 (ALL) + if (roi instanceof ROI2D) + { + final int roiZ = ((ROI2D) roi).getZ(); + + // ROI not on current Z position ? --> next + if ((roiZ != -1) && (roiZ != z)) + continue; + } + // otherwise we just need 1 iteration + else if (z > 0) + continue; + + // interrupt processing if wanted + if (Thread.currentThread().isInterrupted()) + return result; + + // point index + int ptIndex = 0; + + for (int c = 0, zStartRow = row; c < sizeC; c++) + { + // ROI does not contain current C position ? --> next + if ((roiBounds.getMinC() > c) || (roiBounds.getMaxC() < c)) + continue; + + // display progress + if (ui != null) + { + // compute progress bar avoid integer type loss + double progress = roiIndex; + progress *= sizeT * sizeZ * sizeC; + progress += t * sizeZ * sizeC; + progress += z * sizeC; + progress += c; + progress /= progressRatio; + + ui.setProgressBarValue(progress); + } + + // rewind to original Z start row when multi channel + if (c > 0) + row = zStartRow; + + final List pts; + boolean interpolate = true; + + // 3D line path ROI ? + if ((roi instanceof ROI3DPoint) || (roi instanceof ROI3DLine) || (roi instanceof ROI3DPolyLine)) + pts = ((ROI3DShape) roi).getPoints(); + // 2D straight line path / area ROI (we shouldn't consider Arc / Ellipse shape ROI here) ? + else if ((roi instanceof ROI2DPoint) || (roi instanceof ROI2DLine) + || (roi instanceof ROI2DPolyLine) || (roi instanceof ROI2DPolygon) + || (roi instanceof ROI2DRectangle)) + pts = ((ROI2DShape) roi).getPoints(); + else + { + // initialize + pts = new ArrayList(); + // no link here + interpolate = false; + + // 2D ROI so we can use the contour of the mask + if (roi instanceof ROI2D) + { + /// iterate over all component (separate object if any) + for (BooleanMask2D mask : ((ROI2D) roi).getBooleanMask(true).getComponents()) + // add points in contour order + pts.addAll(mask.getConnectedContourPoints()); + } + // 3D ROI so we can use the surface area points + else if (roi instanceof ROI3D) + { + // add points in whatever order + pts.addAll( + CollectionUtil.asList(((ROI3D) roi).getBooleanMask(true).getContourPoints())); + } + // not supported + else + { + + } + } + + // control points that need interpolation in-between ? + if (interpolate) + { + // get the first obj + Point3D startPt = getPoint3D(pts.get(0), z); + + // specific case where we have only a single point + if (pts.size() == 1) + writeRow(sh, row++, ptIndex++, t, c, sequence, startPt, unitScale); + else + { + // otherwise we use a special way to interpolate through 2 controls points + for (int i = 1; i < pts.size(); i++) + { + final Point3D endPt = getPoint3D(pts.get(i), z); + // get line iterator + final Line3DIterator lineIt = new Line3DIterator(new Line3D(startPt, endPt), 1d, + false); + + // iterate over line points + while (lineIt.hasNext()) + writeRow(sh, row++, ptIndex++, t, c, sequence, lineIt.next(), unitScale); + + // done --> go next control point + startPt = endPt; + } + } + } + // contour points ? + else + { + for (int i = 0; i < pts.size(); i++) + writeRow(sh, row++, ptIndex++, t, c, sequence, getPoint3D(pts.get(i), z), unitScale); + } + } + } + } + + roiIndex++; + } + + return result; + + } + + /** + * Computes intensity profil along path (contour for area ROI) for the given list of ROI.<br> + * + * @return result in a XLSX workbook where we have one sheet per ROI. + */ + public static XSSFWorkbook getPathIntensityProfil(Sequence sequence, List<ROI> rois, boolean useRealUnit) + { + return getPathIntensityProfil(sequence, rois, useRealUnit, null); + } + + private static Point3D getPoint3D(Object obj, int curZ) + { + final Point3D result; + + if (obj instanceof Point) + { + final Point pt2d = (Point) obj; + result = new Point3D.Double(pt2d.getX(), pt2d.getY(), curZ); + } + else if (obj instanceof Point2D) + { + final Point2D pt2d = (Point2D) obj; + result = new Point3D.Double(pt2d.getX(), pt2d.getY(), curZ); + } + else if (obj instanceof Point3D) + result = (Point3D) obj; + else + result = null; + + return result; + } + + private static void writeRow(IcySpreadSheet sh, int row, int ind, int t, int c, Sequence sequence, Point3D pt, + Point4D unitScale) + { + sh.setValue(row, 0, Integer.valueOf(ind)); + sh.setValue(row, 1, Double.valueOf(MathUtil.roundSignificant(pt.getX() * unitScale.getX(), 5, true))); + sh.setValue(row, 2, Double.valueOf(MathUtil.roundSignificant(pt.getY() * unitScale.getY(), 5, true))); + sh.setValue(row, 3, Double.valueOf(MathUtil.roundSignificant(pt.getZ() * unitScale.getZ(), 5, true))); + if (unitScale.getT() == 1d) + sh.setValue(row, 4, Integer.valueOf(t)); + else + sh.setValue(row, 4, Double.valueOf(MathUtil.roundSignificant(t * unitScale.getT(), 5, true))); + sh.setValue(row, 5 + c, Double.valueOf(MathUtil + .roundSignificant(sequence.getDataInterpolated(t, pt.getZ(), c, pt.getY(), pt.getX()), 5, true))); + } +} -- GitLab