package gdsc.foci; import gdsc.UsageTracker; /*----------------------------------------------------------------------------- * GDSC Plugins for ImageJ * * Copyright (C) 2011 Alex Herbert * Genome Damage and Stability Centre * University of Sussex, UK * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. *---------------------------------------------------------------------------*/ import gdsc.core.ij.Utils; import gdsc.core.match.Coordinate; import gdsc.core.match.MatchResult; import gdsc.core.match.MatchCalculator; import gdsc.core.match.PointPair; import ij.IJ; import ij.ImagePlus; import ij.WindowManager; import ij.gui.GenericDialog; import ij.gui.Overlay; import ij.gui.Plot; import ij.gui.PointRoi; import ij.gui.Roi; import ij.io.OpenDialog; import ij.plugin.PlugIn; import ij.plugin.ZProjector; import ij.process.ImageProcessor; import ij.text.TextWindow; import java.awt.Color; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStreamWriter; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Comparator; import java.util.HashSet; import java.util.LinkedList; import java.util.List; /** * Compares the ROI points on two images and computes the match statistics. * * Can output the matches for each quartile when the points are ranked using their height. Only supports 2D images (no * Z-stacks) but does allow selection of channel/frame that is used for the heights. */ public class MatchPlugin implements PlugIn { /** * Visually impaired safe colour for matches (greenish) */ public static Color MATCH = new Color(0, 158, 115); /** * Visually impaired safe colour for no match 1 (yellowish) */ public static Color UNMATCH1 = new Color(240, 228, 66); /** * Visually impaired safe colour for no match 2 (blueish) */ public static Color UNMATCH2 = new Color(86, 180, 233); private static String TITLE = "Match Calculator"; private static String[] dTypes = new String[] { "Relative", "Absolute" }; private static String title1 = ""; private static String title2 = ""; private static int dType = 0; private static double dThreshold = 0.05; private static boolean overlay = true; private static boolean quartiles = false; private static boolean scatter = true; private static boolean unmatchedDistribution = true; private static boolean matchTable = false; private static boolean saveMatches = false; private static String filename = ""; private static int findFociImageIndex = 0; //@formatter:off private static String[] findFociResult = new String[] { "Intensity", "Intensity above saddle", "Intensity above background", "Count", "Count above saddle", "Max value", "Highest saddle value", }; //@formatter:off private static int findFociResultChoiceIndex = 0; private static boolean writeHeader = true; private static boolean writeUnmatchedHeader = true; private static boolean writeMatchedHeader = true; private static TextWindow resultsWindow = null; private static TextWindow unmatchedWindow = null; private static TextWindow matchedWindow = null; private boolean fileMode = false; private static int channel1 = 1; private static int frame1 = 1; private static int channel2 = 1; private static int frame2 = 1; private String t1 = ""; private String t2 = ""; private Coordinate[] actualPoints = null; private Coordinate[] predictedPoints = null; /* * (non-Javadoc) * * // Build the values to plot * float[] xMatch = new float[matches.size()]; * float[] yMatch = new float[matches.size()]; * float[] xNoMatch = new float[falsePositives.size() + falseNegatives.size()]; * float[] yNoMatch = new float[falsePositives.size() + falseNegatives.size()]; * * int n = 0; * for (PointPair pair : matches) * { * TimeValuedPoint p1 = (TimeValuedPoint) pair.getPoint1(); * TimeValuedPoint p2 = (TimeValuedPoint) pair.getPoint2(); * xMatch[n] = p1.getValue(); // Actual * yMatch[n] = p2.getValue(); // Predicted * n++; * } * n = 0; * // Actual * for (Coordinate point : falseNegatives) * { * TimeValuedPoint p = (TimeValuedPoint) point; * xNoMatch[n++] = p.getValue(); * } * // Predicted * for (Coordinate point : falsePositives) * { * TimeValuedPoint p = (TimeValuedPoint) point; * yNoMatch[n++] = p.getValue(); * } * * @see ij.plugin.PlugIn#run(java.lang.String) */ @SuppressWarnings("unchecked") public void run(String arg) { UsageTracker.recordPlugin(this.getClass(), arg); fileMode = arg.equals("file"); if (!showDialog()) { return; } actualPoints = null; predictedPoints = null; double d = 0; boolean doQuartiles = false; boolean doScatter = false; boolean doUnmatched = false; ImagePlus imp1 = null; ImagePlus imp2 = null; t1 = title1; t2 = title2; if (fileMode) { try { actualPoints = PointManager.loadPoints(title1); predictedPoints = PointManager.loadPoints(title2); } catch (IOException e) { IJ.error("Failed to load the points: " + e.getMessage()); return; } d = dThreshold; } else { imp1 = WindowManager.getImage(title1); imp2 = WindowManager.getImage(title2); if (imp1 != null && imp2 != null) { if (dType == 1) { d = dThreshold; } else { int length1 = Math.min(imp1.getWidth(), imp1.getHeight()); int length2 = Math.min(imp2.getWidth(), imp2.getHeight()); d = Math.ceil(dThreshold * Math.max(length1, length2)); } actualPoints = PointManager.extractRoiPoints(imp1.getRoi()); predictedPoints = PointManager.extractRoiPoints(imp2.getRoi()); boolean canExtractHeights = canExtractHeights(imp1, imp2); doQuartiles = quartiles && canExtractHeights; doScatter = scatter && canExtractHeights; doUnmatched = unmatchedDistribution && canExtractHeights; } if (doQuartiles || doScatter || doUnmatched || matchTable) { // Extract the heights for each point actualPoints = extractHeights(imp1, actualPoints, channel1, frame1); predictedPoints = extractHeights(imp2, predictedPoints, channel2, frame2); } } Object[] points = compareROI(actualPoints, predictedPoints, d, doQuartiles); List<Coordinate> TP = (List<Coordinate>) points[0]; List<Coordinate> FP = (List<Coordinate>) points[1]; List<Coordinate> FN = (List<Coordinate>) points[2]; List<PointPair> matches = (List<PointPair>) points[3]; MatchResult result = (MatchResult) points[4]; if (overlay && !fileMode) { // Imp2 is the predicted, show the overlay on this imp2.setOverlay(null); imp2.saveRoi(); imp2.killRoi(); // Use colour blind friendly colours addOverlay(imp2, TP, MATCH); addOverlay(imp2, FN, UNMATCH1); addOverlay(imp2, FP, UNMATCH2); imp2.updateAndDraw(); } // Output a scatter plot of actual vs predicted if (doScatter) { scatterPlot(imp1, imp2, matches, FP, FN); } // Show analysis of the height distribution of the unmatched points if (doUnmatched) { unmatchedAnalysis(t1, t2, matches, FP, FN); } if (saveMatches) { saveMatches(imp1, imp2, d, matches, FP, FN, result); } if (matchTable) { addIntensityFromFindFoci(matches, FP, FN); showMatches(matches, FP, FN); } } /** * @return True if heights can be extracted from the image */ private boolean canExtractHeights(ImagePlus imp1, ImagePlus imp2) { int[] dim1 = imp1.getDimensions(); int[] dim2 = imp2.getDimensions(); // Uncomment to prevent Z-stacks. Using the z-projection for heights so this should not matter //if (dim1[3] != 1 && dim2[3] != 1) // return false; if ((dim1[2] != 1 || dim1[4] != 1) || (dim2[2] != 1 || dim2[4] != 1)) { // Select channel/frame GenericDialog gd = new GenericDialog("Select Channel/Frame"); gd.addMessage("Stacks detected.\nPlease select the channel/frame."); String[] channels1 = getChannels(imp1); String[] frames1 = getFrames(imp1); String[] channels2 = getChannels(imp2); String[] frames2 = getFrames(imp2); if (channels1.length > 1) gd.addChoice("Image1_Channel", channels1, channels1[channels1.length >= channel1 ? channel1 - 1 : 0]); if (frames1.length > 1) gd.addChoice("Image1_Frame", frames1, frames1[frames1.length >= frame1 ? frame1 - 1 : 0]); if (channels2.length > 1) gd.addChoice("Image2_Channel", channels2, channels2[channels2.length >= channel2 ? channel2 - 1 : 0]); if (frames2.length > 1) gd.addChoice("Image2_Frame", frames2, frames2[frames2.length >= frame2 ? frame2 - 1 : 0]); gd.showDialog(); if (gd.wasCanceled()) return false; // Extract the channel/frame channel1 = (channels1.length > 1) ? gd.getNextChoiceIndex() + 1 : 1; frame1 = (frames1.length > 1) ? gd.getNextChoiceIndex() + 1 : 1; channel2 = (channels2.length > 1) ? gd.getNextChoiceIndex() + 1 : 1; frame2 = (frames2.length > 1) ? gd.getNextChoiceIndex() + 1 : 1; t1 += " c" + channel1 + " t" + frame1; t2 += " c" + channel2 + " t" + frame2; } return true; } private String[] getChannels(ImagePlus imp) { int c = imp.getNChannels(); String[] result = new String[c]; for (int i = 0; i < c; i++) result[i] = Integer.toString(i + 1); return result; } private String[] getFrames(ImagePlus imp) { int c = imp.getNFrames(); String[] result = new String[c]; for (int i = 0; i < c; i++) result[i] = Integer.toString(i + 1); return result; } private TimeValuedPoint[] extractHeights(ImagePlus imp, Coordinate[] actualPoints, int channel, int frame) { // Use maximum intensity projection ImageProcessor ip = Utils.extractTile(imp, frame, channel, ZProjector.MAX_METHOD); //new ImagePlus("height", ip).show(); // Store ID as the time TimeValuedPoint[] newPoints = new TimeValuedPoint[actualPoints.length]; for (int i = 0; i < newPoints.length; i++) { int x = (int) actualPoints[i].getX(); int y = (int) actualPoints[i].getY(); int value = ip.get(x, y); newPoints[i] = new TimeValuedPoint(x, y, 0, i + 1, value); } return newPoints; } /** * Adds an ROI point overlay to the image using the specified colour * * @param imp * @param list * @param color */ public static void addOverlay(ImagePlus imp, List<? extends Coordinate> list, Color color) { if (list.isEmpty()) return; Color strokeColor = color; Color fillColor = color; Overlay o = imp.getOverlay(); PointRoi roi = (PointRoi) PointManager.createROI(list); roi.setStrokeColor(strokeColor); roi.setFillColor(fillColor); roi.setShowLabels(false); if (o == null) { imp.setOverlay(roi, strokeColor, 2, fillColor); } else { o.add(roi); imp.setOverlay(o); } } private boolean showDialog() { String[] items = null; String t1 = title1; String t2 = title2; if (!fileMode) { List<String> imageList = new LinkedList<String>(); for (int id : gdsc.core.ij.Utils.getIDList()) { ImagePlus imp = WindowManager.getImage(id); if (imp != null) { Roi roi = imp.getRoi(); // Allow no ROI => No points if ((roi == null || roi.getType() == Roi.POINT) && !imp.getTitle().startsWith(TITLE)) { imageList.add(imp.getTitle()); } } } if (imageList.size() < 2) { IJ.showMessage(TITLE, "Require 2 images open with point ROI"); return false; } items = imageList.toArray(new String[0]); int index = 0; t1 = (imageList.contains(title1) ? title1 : imageList.get(index++)); t2 = (imageList.contains(title2) ? title2 : imageList.get(index)); } GenericDialog gd = new GenericDialog(TITLE); if (fileMode) { gd.addMessage("Compare the points in two files\nand compute the match statistics"); gd.addStringField("Input_1", t1, 30); gd.addStringField("Input_2", t2, 30); gd.addMessage("Distance between matching points in pixels"); } else { gd.addMessage("Compare the ROI points between 2 images\nand compute the match statistics"); gd.addChoice("Input_1", items, t1); gd.addChoice("Input_2", items, t2); gd.addMessage("Distance between matching points in pixels, or fraction of\n" + "image edge length"); gd.addChoice("Distance_type", dTypes, dTypes[dType]); } gd.addNumericField("Distance", dThreshold, 2); if (!fileMode) { gd.addCheckbox("Overlay", overlay); gd.addCheckbox("Quartiles", quartiles); gd.addCheckbox("Scatter_plot", scatter); gd.addCheckbox("Unmatched_distribution", unmatchedDistribution); gd.addCheckbox("Match_table", matchTable); } gd.addCheckbox("Save_matches", saveMatches); ArrayList<FindFociResult> resultsArray = FindFoci.getResults(); if (resultsArray != null) { String[] imageItems = new String[] { "[None]", "Image1", "Image2" }; gd.addChoice("FindFoci_image", imageItems, imageItems[findFociImageIndex]); gd.addChoice("FindFoci_result", findFociResult, findFociResult[findFociResultChoiceIndex]); } gd.addHelp(gdsc.help.URL.FIND_FOCI); gd.showDialog(); if (gd.wasCanceled()) return false; if (fileMode) { title1 = gd.getNextString(); title2 = gd.getNextString(); } else { title1 = gd.getNextChoice(); title2 = gd.getNextChoice(); dType = gd.getNextChoiceIndex(); } dThreshold = gd.getNextNumber(); if (!fileMode) { overlay = gd.getNextBoolean(); quartiles = gd.getNextBoolean(); scatter = gd.getNextBoolean(); unmatchedDistribution = gd.getNextBoolean(); matchTable = gd.getNextBoolean(); } saveMatches = gd.getNextBoolean(); findFociImageIndex = 0; if (resultsArray != null) { findFociImageIndex = gd.getNextChoiceIndex(); findFociResultChoiceIndex = gd.getNextChoiceIndex(); } return true; } private Object[] compareROI(Coordinate[] actualPoints, Coordinate[] predictedPoints, double dThreshold, boolean doQuartiles) { List<Coordinate> TP = new LinkedList<Coordinate>(); List<Coordinate> FP = new LinkedList<Coordinate>(); List<Coordinate> FN = new LinkedList<Coordinate>(); List<PointPair> matches = new LinkedList<PointPair>(); MatchResult result = MatchCalculator.analyseResults2D(actualPoints, predictedPoints, dThreshold, TP, FP, FN, matches); MatchResult[] qResults = null; if (doQuartiles) qResults = compareQuartiles(actualPoints, predictedPoints, dThreshold); String header = null; if (!java.awt.GraphicsEnvironment.isHeadless()) { if (doQuartiles) { header = createResultsHeader(qResults); Utils.refreshHeadings(resultsWindow, header, true); } if (resultsWindow == null || !resultsWindow.isShowing()) { if (header == null) header = createResultsHeader(qResults); resultsWindow = new TextWindow(TITLE + " Results", header, "", 900, 300); } } else { if (writeHeader) { header = createResultsHeader(qResults); writeHeader = false; IJ.log(header); } } addResult(t1, t2, dThreshold, result, qResults); return new Object[] { TP, FP, FN, matches, result }; } /** * Compare the match results for the points within each height quartile * * @param actualPoints * @param predictedPoints * @param dThreshold * @return An array of 4 quartile results (or null if there are no points) */ private MatchResult[] compareQuartiles(Coordinate[] actualPoints, Coordinate[] predictedPoints, double dThreshold) { TimeValuedPoint[] actualValuedPoints = (TimeValuedPoint[]) actualPoints; TimeValuedPoint[] predictedValuedPoints = (TimeValuedPoint[]) predictedPoints; // Combine points and sort ArrayList<Float> heights = extractHeights(actualValuedPoints, predictedValuedPoints); if (heights.isEmpty()) return null; float[] Q = getQuartiles(heights); // Process each quartile MatchResult[] qResults = new MatchResult[4]; for (int q = 0; q < 4; q++) { TimeValuedPoint[] actual = extractPoints(actualValuedPoints, Q[q], Q[q + 1]); TimeValuedPoint[] predicted = extractPoints(predictedValuedPoints, Q[q], Q[q + 1]); qResults[q] = MatchCalculator.analyseResults2D(actual, predicted, dThreshold); } return qResults; } /** * Extract all the heights from the two sets of valued points */ private ArrayList<Float> extractHeights(TimeValuedPoint[] actualPoints, TimeValuedPoint[] predictedPoints) { HashSet<TimeValuedPoint> nonDuplicates = new HashSet<TimeValuedPoint>(); nonDuplicates.addAll(Arrays.asList(actualPoints)); nonDuplicates.addAll(Arrays.asList(predictedPoints)); ArrayList<Float> heights = new ArrayList<Float>(nonDuplicates.size()); for (TimeValuedPoint p : nonDuplicates) { heights.add(p.getValue()); } Collections.sort(heights); return heights; } /** * Extract the points that are within the specified limits */ private TimeValuedPoint[] extractPoints(TimeValuedPoint[] points, float lower, float upper) { LinkedList<TimeValuedPoint> list = new LinkedList<TimeValuedPoint>(); for (TimeValuedPoint p : points) { if (p.getValue() >= lower && p.getValue() < upper) list.add(p); } return list.toArray(new TimeValuedPoint[list.size()]); } /** * Count the points that are within the specified limits */ private int countPoints(TimeValuedPoint[] points, float lower, float upper) { int n = 0; for (TimeValuedPoint p : points) { if (p.getValue() >= lower && p.getValue() < upper) n++; } return n; } private String createResultsHeader(MatchResult[] qResults) { StringBuilder sb = new StringBuilder(); sb.append("Image 1\t"); sb.append("Image 2\t"); sb.append("Distance (px)\t"); sb.append("N 1\t"); sb.append("N 2\t"); sb.append("Match\t"); sb.append("Unmatch 1\t"); sb.append("Unmatch 2\t"); sb.append("Jaccard\t"); sb.append("Recall 1\t"); sb.append("Recall 2\t"); sb.append("F1"); if (qResults != null) { for (int q = 0; q < 4; q++) addQuartileHeader(sb, "Q" + (q + 1)); } return sb.toString(); } private void addQuartileHeader(StringBuilder sb, String quartile) { sb.append("\t \t"); sb.append(quartile).append(" ").append("N1\t"); sb.append(quartile).append(" ").append("N2\t"); sb.append(quartile).append(" ").append("M\t"); sb.append(quartile).append(" ").append("U1\t"); sb.append(quartile).append(" ").append("U2\t"); sb.append(quartile).append(" ").append("Jaccard\t"); sb.append(quartile).append(" ").append("Recall1\t"); sb.append(quartile).append(" ").append("Recall2\t"); sb.append(quartile).append(" ").append("F1"); } private void addResult(String i1, String i2, double dThrehsold, MatchResult result, MatchResult[] qResults) { StringBuilder sb = new StringBuilder(); sb.append(i1).append("\t"); sb.append(i2).append("\t"); sb.append(IJ.d2s(dThrehsold, 2)).append("\t"); sb.append(result.getNumberActual()).append("\t"); sb.append(result.getNumberPredicted()).append("\t"); sb.append(result.getTruePositives()).append("\t"); sb.append(result.getFalseNegatives()).append("\t"); sb.append(result.getFalsePositives()).append("\t"); sb.append(IJ.d2s(result.getJaccard(), 4)).append("\t"); sb.append(IJ.d2s(result.getRecall(), 4)).append("\t"); sb.append(IJ.d2s(result.getPrecision(), 4)).append("\t"); sb.append(IJ.d2s(result.getFScore(1.0), 4)); if (qResults != null) { for (int q = 0; q < 4; q++) addQuartileResult(sb, qResults[q]); } if (java.awt.GraphicsEnvironment.isHeadless()) { IJ.log(sb.toString()); } else { resultsWindow.append(sb.toString()); } } private void addQuartileResult(StringBuilder sb, MatchResult result) { sb.append("\t \t"); sb.append(result.getNumberActual()).append("\t"); sb.append(result.getNumberPredicted()).append("\t"); sb.append(result.getTruePositives()).append("\t"); sb.append(result.getFalseNegatives()).append("\t"); sb.append(result.getFalsePositives()).append("\t"); sb.append(IJ.d2s(result.getJaccard(), 4)).append("\t"); sb.append(IJ.d2s(result.getRecall(), 4)).append("\t"); sb.append(IJ.d2s(result.getPrecision(), 4)).append("\t"); sb.append(IJ.d2s(result.getFScore(1.0), 4)); } /** * Build a scatter plot of the matches and the false positives/negatives using the image values for the X/Y axes * * @param imp1 * - Actual * @param imp2 * - Predicted * @param matches * @param falsePositives * @param falseNegatives */ private void scatterPlot(ImagePlus imp1, ImagePlus imp2, List<PointPair> matches, List<Coordinate> falsePositives, List<Coordinate> falseNegatives) { if (matches.isEmpty() && falsePositives.isEmpty() && falseNegatives.isEmpty()) return; // Build the values to plot float[] xMatch = new float[matches.size()]; float[] yMatch = new float[matches.size()]; float[] xNoMatch1 = new float[falseNegatives.size()]; float[] yNoMatch1 = new float[falseNegatives.size()]; float[] xNoMatch2 = new float[falsePositives.size()]; float[] yNoMatch2 = new float[falsePositives.size()]; int n = 0; float minimum = Float.POSITIVE_INFINITY, maximum = 0; for (PointPair pair : matches) { TimeValuedPoint p1 = (TimeValuedPoint) pair.getPoint1(); TimeValuedPoint p2 = (TimeValuedPoint) pair.getPoint2(); xMatch[n] = p1.getValue(); // Actual yMatch[n] = p2.getValue(); // Predicted final float max = Math.max(xMatch[n], yMatch[n]); final float min = Math.min(xMatch[n], yMatch[n]); if (maximum < max) maximum = max; if (minimum > min) minimum = min; n++; } n = 0; // Actual for (Coordinate point : falseNegatives) { TimeValuedPoint p = (TimeValuedPoint) point; xNoMatch1[n++] = p.getValue(); if (maximum < p.getValue()) maximum = p.getValue(); minimum = 0; } // Predicted n = 0; for (Coordinate point : falsePositives) { TimeValuedPoint p = (TimeValuedPoint) point; yNoMatch2[n++] = p.getValue(); if (maximum < p.getValue()) maximum = p.getValue(); minimum = 0; } // Create a new plot String title = TITLE + " : " + imp1.getTitle() + " vs " + imp2.getTitle(); Plot plot = new Plot(title, imp1.getTitle(), imp2.getTitle(), (float[]) null, (float[]) null); // Ensure the plot is square float range = maximum - minimum; if (range == 0) range = 10; maximum += range * 0.05; minimum -= range * 0.05; plot.setLimits(minimum, maximum, minimum, maximum); plot.setFrameSize(300, 300); plot.setColor(MATCH); plot.addPoints(xMatch, yMatch, Plot.X); plot.setColor(UNMATCH1); plot.addPoints(xNoMatch1, yNoMatch1, Plot.CROSS); plot.setColor(UNMATCH2); plot.addPoints(xNoMatch2, yNoMatch2, Plot.CROSS); // Find old plot ImagePlus oldPlot = null; for (int id : Utils.getIDList()) { ImagePlus imp = WindowManager.getImage(id); if (imp != null && imp.getTitle().equals(title)) { oldPlot = imp; break; } } // Update plot or draw a new one if (oldPlot != null) { oldPlot.setProcessor(plot.getProcessor()); oldPlot.updateAndDraw(); } else { plot.show(); } } /** * Build a table showing the percentage of unmatched points that fall within each quartile of the matched points * * @param title1 * - Actual * @param title2 * - Predicted * @param matches * @param falsePositives * @param falseNegatives */ private void unmatchedAnalysis(String title1, String title2, List<PointPair> matches, List<Coordinate> falsePositives, List<Coordinate> falseNegatives) { if (matches.isEmpty() && falsePositives.isEmpty() && falseNegatives.isEmpty()) return; // Extract the heights of the matched points. Use the average height of each match. ArrayList<Float> heights = new ArrayList<Float>(matches.size()); for (PointPair pair : matches) { TimeValuedPoint p1 = (TimeValuedPoint) pair.getPoint1(); TimeValuedPoint p2 = (TimeValuedPoint) pair.getPoint2(); heights.add((float)((p1.getValue() + p2.getValue()) / 2.0)); } Collections.sort(heights); // Get the quartile ranges float[] Q = getQuartiles(heights); // Extract the valued points TimeValuedPoint[] actualPoints = extractValuedPoints(falseNegatives); TimeValuedPoint[] predictedPoints = extractValuedPoints(falsePositives); // Count the number of unmatched points from each image in each quartile int[] actualCount = new int[6]; int[] predictedCount = new int[6]; actualCount[0] = countPoints(actualPoints, Float.NEGATIVE_INFINITY, Q[0]); predictedCount[0] = countPoints(predictedPoints, Float.NEGATIVE_INFINITY, Q[0]); for (int q = 0; q < 4; q++) { actualCount[q + 1] = countPoints(actualPoints, Q[q], Q[q + 1]); predictedCount[q + 1] = countPoints(predictedPoints, Q[q], Q[q + 1]); } actualCount[5] = countPoints(actualPoints, Q[4], Float.POSITIVE_INFINITY); predictedCount[5] = countPoints(predictedPoints, Q[4], Float.POSITIVE_INFINITY); // Show a result table String header = "Image 1\tN\t% <Q1\t% Q1\t% Q2\t% Q3\t% Q4\t% >Q4\tImage2\tN\t% <Q1\t% Q1\t% Q2\t% Q3\t% Q4\t% >Q4"; if (!java.awt.GraphicsEnvironment.isHeadless()) { if (unmatchedWindow == null || !unmatchedWindow.isShowing()) { unmatchedWindow = new TextWindow(TITLE + " Unmatched", header, "", 900, 300); } } else { if (writeUnmatchedHeader) { writeUnmatchedHeader = false; IJ.log(header); } } addUnmatchedResult(title1, title2, actualCount, predictedCount); } private float[] getQuartiles(ArrayList<Float> heights) { if (heights.isEmpty()) return new float[] { 0, 0, 0, 0, 0 }; return new float[] { heights.get(0).floatValue(), PointAlignerPlugin.getQuartileBoundary(heights, 0.25), PointAlignerPlugin.getQuartileBoundary(heights, 0.5), PointAlignerPlugin.getQuartileBoundary(heights, 0.75), heights.get(heights.size() - 1).floatValue() + 1 }; } private TimeValuedPoint[] extractValuedPoints(List<Coordinate> list) { TimeValuedPoint[] points = new TimeValuedPoint[list.size()]; int i = 0; for (Coordinate p : list) points[i++] = (TimeValuedPoint) p; return points; } private void addUnmatchedResult(String title1, String title2, int[] actualCount, int[] predictedCount) { StringBuilder sb = new StringBuilder(); addQuartiles(sb, title1, actualCount); sb.append("\t"); addQuartiles(sb, title2, predictedCount); if (java.awt.GraphicsEnvironment.isHeadless()) { IJ.log(sb.toString()); } else { unmatchedWindow.append(sb.toString()); } } private void addQuartiles(StringBuilder sb, String imageTitle, int[] counts) { // Count the total number of create a scale factor to calculate the percentage int total = 0; for (int c : counts) total += c; sb.append(imageTitle).append("\t").append(total); if (total > 0) { double factor = total / 100.0; for (int c : counts) { sb.append("\t"); sb.append(IJ.d2s(c / factor, 1)); } } else { for (int c = counts.length; c-- > 0;) { sb.append("\t-"); } } } /** * Saves the matches and the false positives/negatives to file * * @param imp1 * - Actual * @param imp2 * - Predicted * @param d * @param matches * @param falsePositives * @param falseNegatives * @param result */ private void saveMatches(ImagePlus imp1, ImagePlus imp2, double d, List<PointPair> matches, List<Coordinate> falsePositives, List<Coordinate> falseNegatives, MatchResult result) { if (matches.isEmpty() && falsePositives.isEmpty() && falseNegatives.isEmpty()) return; String[] path = Utils.decodePath(filename); OpenDialog chooser = new OpenDialog("matches_file", path[0], path[1]); if (chooser.getFileName() == null) return; filename = chooser.getDirectory() + chooser.getFileName(); OutputStreamWriter out = null; try { FileOutputStream fos = new FileOutputStream(filename); out = new OutputStreamWriter(fos, "UTF-8"); StringBuilder sb = new StringBuilder(); final String newLine = System.getProperty("line.separator"); sb.append("# Image 1 = ").append(t1).append(newLine); sb.append("# Image 2 = ").append(t2).append(newLine); sb.append("# Distance = ").append(Utils.rounded(d, 2)).append(newLine); sb.append("# N 1 = ").append(result.getNumberActual()).append(newLine); sb.append("# N 2 = ").append(result.getNumberPredicted()).append(newLine); sb.append("# Match = ").append(result.getTruePositives()).append(newLine); sb.append("# Unmatch 1 = ").append(result.getFalseNegatives()).append(newLine); sb.append("# Unmatch 2 = ").append(result.getFalsePositives()).append(newLine); sb.append("# Jaccard = ").append(Utils.rounded(result.getJaccard(), 4)).append(newLine); sb.append("# Recall 1 = ").append(Utils.rounded(result.getRecall(), 4)).append(newLine); sb.append("# Recall 2 = ").append(Utils.rounded(result.getPrecision(), 4)).append(newLine); sb.append("# F-score = ").append(Utils.rounded(result.getFScore(1.0), 4)).append(newLine); sb.append("# X1\tY1\tV1\tX2\tY2\tV2").append(newLine); out.write(sb.toString()); for (PointPair pair : matches) { Coordinate c1 = pair.getPoint1(); Coordinate c2 = pair.getPoint2(); float v1 = 0, v2 = 0; if (pair.getPoint1() instanceof TimeValuedPoint) { TimeValuedPoint p1 = (TimeValuedPoint) c1; TimeValuedPoint p2 = (TimeValuedPoint) c2; v1 = p1.getValue(); // Actual v2 = p2.getValue(); // Predicted } out.write(String.format("%d\t%d\t%.0f\t%d\t%d\t%.0f%s", c1.getX(), c1.getY(), v1, c2.getX(), c2.getY(), v2, newLine)); } // Actual for (Coordinate c : falseNegatives) { float v1 = 0; if (c instanceof TimeValuedPoint) { TimeValuedPoint p = (TimeValuedPoint) c; v1 = p.getValue(); } out.write(String.format("%d\t%d\t%.0f\t0\t0\t0%s", c.getX(), c.getY(), v1, newLine)); } // Predicted for (Coordinate c : falsePositives) { float v1 = 0; if (c instanceof TimeValuedPoint) { TimeValuedPoint p = (TimeValuedPoint) c; v1 = p.getValue(); } out.write(String.format("0\t0\t0\t%d\t%d\t%.0f%s", c.getX(), c.getY(), v1, newLine)); } } catch (Exception e) { IJ.log("Unable to save the matches to file: " + e.getMessage()); } finally { if (out != null) { try { out.close(); } catch (IOException e) { // Ignore } } } } /** * Build a table showing the matched pairs and unmatched points * * @param title1 * - Actual * @param title2 * - Predicted * @param matches * @param falsePositives * @param falseNegatives */ private void showMatches(List<PointPair> matches, List<Coordinate> falsePositives, List<Coordinate> falseNegatives) { if (matches.isEmpty() && falsePositives.isEmpty() && falseNegatives.isEmpty()) return; // Show a result table String header = "Image 1\tId\tX\tY\tImage 2\tId\tX\tY\tDistance"; if (findFociImageIndex > 0) { header += "\tImage " + findFociImageIndex + ": " + findFociResult[findFociResultChoiceIndex]; } if (!java.awt.GraphicsEnvironment.isHeadless()) { if (matchedWindow == null || !matchedWindow.isShowing()) { matchedWindow = new TextWindow(TITLE + " Matched", header, "", 800, 300); } else { Utils.refreshHeadings(matchedWindow, header, true); } } else { if (writeMatchedHeader) { writeMatchedHeader = false; IJ.log(header); } } Collections.sort(matches, new Comparator<PointPair>() { public int compare(PointPair o1, PointPair o2) { TimeValuedPoint p1 = (TimeValuedPoint) o1.getPoint1(); TimeValuedPoint p2 = (TimeValuedPoint) o2.getPoint1(); return (p1.getTime() < p2.getTime()) ? -1 : 1; } }); for (PointPair pair : matches) { int value = -1; if (findFociImageIndex > 0) { TimeValuedPoint point = (TimeValuedPoint) ((findFociImageIndex == 1) ? pair.getPoint1() : pair.getPoint2()); value = (int) point.value; } addMatchedPair(pair.getPoint1(), pair.getPoint2(), pair.getXYZDistance(), value); } TimeValuedPoint[] actualPoints = extractValuedPoints(falseNegatives); TimeValuedPoint[] predictedPoints = extractValuedPoints(falsePositives); Arrays.sort(actualPoints, new Comparator<TimeValuedPoint>() { public int compare(TimeValuedPoint p1, TimeValuedPoint p2) { return (p1.getTime() < p2.getTime()) ? -1 : 1; } }); Arrays.sort(predictedPoints, new Comparator<TimeValuedPoint>() { public int compare(TimeValuedPoint p1, TimeValuedPoint p2) { return (p1.getTime() < p2.getTime()) ? -1 : 1; } }); for (TimeValuedPoint point : actualPoints) { int value = (findFociImageIndex == 1) ? (int) point.value : -1; addMatchedPair(point, null, -1, value); } for (TimeValuedPoint point : predictedPoints) { int value = (findFociImageIndex == 2) ? (int) point.value : -1; addMatchedPair(null, point, -1, value); } } private void addMatchedPair(Coordinate point1, Coordinate point2, double xyzDistance, int value) { StringBuilder sb = new StringBuilder(); addPoint(sb, t1, point1); addPoint(sb, t2, point2); if ((xyzDistance > -1)) sb.append(xyzDistance); else sb.append("-"); if ((value > -1)) sb.append("\t").append(value); if (!java.awt.GraphicsEnvironment.isHeadless()) { matchedWindow.append(sb.toString()); } else { IJ.log(sb.toString()); } } private void addPoint(StringBuilder sb, String title, Coordinate point) { if (point != null) { TimeValuedPoint p = (TimeValuedPoint) point; sb.append(title).append("\t").append(p.getTime()).append("\t").append(p.getX()).append("\t") .append(p.getY()).append("\t"); } else { sb.append("-\t-\t-\t-\t"); } } private void addIntensityFromFindFoci(List<PointPair> matches, List<Coordinate> fP, List<Coordinate> fN) { if (findFociImageIndex == 0) return; ArrayList<FindFociResult> resultsArray = FindFoci.getResults(); // Check the arrays are the correct size if (resultsArray.size() != ((findFociImageIndex == 1) ? actualPoints.length : predictedPoints.length)) { findFociImageIndex = 0; return; } for (PointPair pair : matches) { TimeValuedPoint point = (TimeValuedPoint) ((findFociImageIndex == 1) ? pair.getPoint1() : pair.getPoint2()); point.value = getValue(resultsArray.get(point.time - 1)); } TimeValuedPoint[] points = extractValuedPoints((findFociImageIndex == 1) ? fN : fP); for (TimeValuedPoint point : points) { point.value = getValue(resultsArray.get(point.time - 1)); } } private float getValue(FindFociResult result) { switch (findFociResultChoiceIndex) { //@formatter:off case 0: return (float)result.totalIntensity; case 1: return (float)result.intensityAboveSaddle; case 2: return (float)result.totalIntensityAboveBackground; case 3: return (float)result.count; case 4: return (float)result.countAboveSaddle; case 5: return (float)result.maxValue; case 6: return (float)result.highestSaddleValue; default: return (float) result.totalIntensity; //@formatter:on } } }