package gdsc.smlm.ij.plugins; /*----------------------------------------------------------------------------- * 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.smlm.ij.plugins.ResultsManager.InputSource; import gdsc.smlm.ij.utils.CoordinateProvider; import gdsc.smlm.ij.utils.ImageROIPainter; import gdsc.core.ij.Utils; import gdsc.core.match.Coordinate; import gdsc.core.match.MatchCalculator; import gdsc.core.match.MatchResult; import gdsc.core.match.PointPair; import gdsc.core.match.Pulse; import gdsc.smlm.results.MemoryPeakResults; import gdsc.smlm.results.PeakResult; import ij.IJ; import ij.gui.GenericDialog; import ij.plugin.PlugIn; import ij.text.TextWindow; import java.awt.Point; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import org.apache.commons.math3.util.FastMath; /** * Compares the coordinates in sets of traced results and computes the match statistics. */ public class TraceMatchCalculator implements PlugIn, CoordinateProvider { private static String TITLE = "Trace Match Calculator"; private static String inputOption1 = ""; private static String inputOption2 = ""; private static String inputOption3 = ""; private static double dThreshold = 1; private static double beta = 4; private static boolean showPairs = false; private static String[] SORT_OPTIONS = new String[] { "Score", "Time" }; private static int sortIndex = 1; private static boolean writeHeader = true; private static TextWindow resultsWindow = null; private static TextWindow pairsWindow = null; private static TextWindow triplesWindow = null; private static ImageROIPainter pairPainter = null, triplePainter = null; /* * (non-Javadoc) * * @see ij.plugin.PlugIn#run(java.lang.String) */ public void run(String arg) { SMLMUsageTracker.recordPlugin(this.getClass(), arg); if (MemoryPeakResults.isMemoryEmpty()) { IJ.error(TITLE, "No localisations in memory"); return; } if (!showDialog()) return; // Load the results MemoryPeakResults results1 = ResultsManager.loadInputResults(inputOption1, false); MemoryPeakResults results2 = ResultsManager.loadInputResults(inputOption2, false); MemoryPeakResults results3 = ResultsManager.loadInputResults(inputOption3, false); if (results1 == null || results1.size() == 0 || results2 == null || results2.size() == 0) { IJ.error(TITLE, "No results could be loaded"); IJ.showStatus(""); return; } final long start = System.nanoTime(); compareCoordinates(results1, results2, results3, dThreshold); double seconds = (System.nanoTime() - start) / 1000000000.0; IJ.showStatus(String.format("%s = %ss", TITLE, Utils.rounded(seconds, 4))); } private boolean showDialog() { GenericDialog gd = new GenericDialog(TITLE); gd.addMessage("Compare the points in two results sets\nand compute the match statistics"); ResultsManager.addInput(gd, "Results1", inputOption1, InputSource.MEMORY_MULTI_FRAME); ResultsManager.addInput(gd, "Results2", inputOption2, InputSource.MEMORY_MULTI_FRAME); ResultsManager.addInput(gd, "Results3", inputOption3, InputSource.NONE, InputSource.MEMORY_MULTI_FRAME); gd.addNumericField("Distance", dThreshold, 2); gd.addNumericField("Beta", beta, 2); gd.addCheckbox("Show_pairs", showPairs); gd.addChoice("Sort_pairs", SORT_OPTIONS, SORT_OPTIONS[sortIndex]); gd.showDialog(); if (gd.wasCanceled()) return false; inputOption1 = gd.getNextChoice(); inputOption2 = gd.getNextChoice(); inputOption3 = gd.getNextChoice(); dThreshold = gd.getNextNumber(); beta = gd.getNextNumber(); showPairs = gd.getNextBoolean(); sortIndex = gd.getNextChoiceIndex(); // Check arguments try { Parameters.isAboveZero("Distance threshold", dThreshold); Parameters.isPositive("Beta", beta); } catch (IllegalArgumentException e) { IJ.error(TITLE, e.getMessage()); return false; } return true; } private void compareCoordinates(MemoryPeakResults results1, MemoryPeakResults results2, MemoryPeakResults results3, double dThreshold) { Pulse[] p1 = extractPulses(results1); Pulse[] p2 = extractPulses(results2); Pulse[] p3 = extractPulses(results3); List<Pulse> TP = null; List<Pulse> FP = null; List<Pulse> FN = null; List<PointPair> pairs = null; List<Pulse> TP2 = null; List<Pulse> FP2 = null; List<Pulse> FN2 = null; List<PointPair> pairs2 = null; if (showPairs) { pairs = new LinkedList<PointPair>(); FP = new LinkedList<Pulse>(); FN = new LinkedList<Pulse>(); pairs2 = new LinkedList<PointPair>(); FP2 = new LinkedList<Pulse>(); FN2 = new LinkedList<Pulse>(); } MatchResult result = MatchCalculator.analyseResults2D(p1, p2, dThreshold, TP, FP, FN, pairs); MatchResult result2 = MatchCalculator.analyseResults2D(p1, p3, dThreshold, TP2, FP2, FN2, pairs2); // Create output if (!java.awt.GraphicsEnvironment.isHeadless()) { if (resultsWindow == null || !resultsWindow.isShowing()) { resultsWindow = new TextWindow(TITLE + " Results", createResultsHeader(), "", 900, 300); } if (showPairs) { if (p3 == null) { // Produce a pairs output if (pairsWindow == null || !pairsWindow.isShowing()) { pairsWindow = new TextWindow(TITLE + " Pairs", createPairsHeader(), "", 900, 300); Point p = resultsWindow.getLocation(); p.y += resultsWindow.getHeight(); pairsWindow.setLocation(p); pairPainter = new ImageROIPainter(pairsWindow.getTextPanel(), results1.getSource().getOriginal().getName(), this); } pairsWindow.getTextPanel().clear(); pairPainter.setTitle(results1.getSource().getOriginal().getName()); // Add the unmatched points for (Coordinate c : FN) pairs.add(new PointPair(c, null)); for (Coordinate c : FP) pairs.add(new PointPair(null, c)); List<? extends PointPair> sortedPairs = sort(pairs); for (PointPair pair : sortedPairs) addPairResult(pair); } else { // Produce a triple output if (triplesWindow == null || !triplesWindow.isShowing()) { triplesWindow = new TextWindow(TITLE + " Triples", createTriplesHeader(), "", 900, 300); Point p = resultsWindow.getLocation(); p.y += resultsWindow.getHeight(); triplesWindow.setLocation(p); triplePainter = new ImageROIPainter(triplesWindow.getTextPanel(), results1.getSource() .getName(), this); } triplesWindow.getTextPanel().clear(); triplePainter.setTitle(results1.getSource().getOriginal().getName()); HashMap<Pulse, Triple> map = new HashMap<Pulse, Triple>(); ArrayList<Triple> triples = new ArrayList<Triple>(pairs.size()); for (PointPair pair : pairs) { Pulse p = (Pulse) pair.getPoint1(); Triple t = new Triple(p, (Pulse) pair.getPoint2(), null); triples.add(t); map.put(p, t); } // Complete the reference set of points for (Coordinate c : FN) { Pulse p = (Pulse) c; Triple t = new Triple(p, null, null); triples.add(t); map.put(p, t); } // Add the unmatched points for (Coordinate c : FP) triples.add(new Triple(null, (Pulse) c, null)); for (Coordinate c : FP2) triples.add(new Triple(null, null, (Pulse) c)); // Add the results from the second match for (PointPair pair : pairs2) { Pulse p = (Pulse) pair.getPoint1(); Pulse pp = (Pulse) pair.getPoint2(); Triple t = map.get(p); if (t != null) { t.p3 = pp; } else { triples.add(new Triple(null, null, pp)); } } List<? extends Triple> sortedTriples = sort(triples); for (Triple t : sortedTriples) addTripleResult(t); } } } else { if (writeHeader) { writeHeader = false; IJ.log(createResultsHeader()); } } addResult(inputOption1, inputOption2, dThreshold, result); if (p3 != null) addResult(inputOption1, inputOption3, dThreshold, result2); } private Pulse[] extractPulses(MemoryPeakResults results) { if (results == null) return null; Pulse[] pulses = new Pulse[results.size()]; int i = 0; for (PeakResult p : results.getResults()) { pulses[i++] = new Pulse(p.getXPosition(), p.getYPosition(), p.getFrame(), p.getEndFrame()); } return pulses; } private String createResultsHeader() { StringBuilder sb = new StringBuilder(); sb.append("Image 1\t"); sb.append("Image 2\t"); sb.append("Distance (px)\t"); sb.append("N\t"); sb.append("TP\t"); sb.append("FP\t"); sb.append("FN\t"); sb.append("Jaccard\t"); sb.append("Score\t"); sb.append("Precision\t"); sb.append("Recall\t"); sb.append("F0.5\t"); sb.append("F1\t"); sb.append("F2\t"); sb.append("F-beta"); return sb.toString(); } private void addResult(String i1, String i2, double dThrehsold, MatchResult result) { 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.getNumberPredicted()).append("\t"); sb.append(result.getTruePositives()).append("\t"); sb.append(result.getFalsePositives()).append("\t"); sb.append(result.getFalseNegatives()).append("\t"); sb.append(IJ.d2s(result.getJaccard(), 4)).append("\t"); sb.append(IJ.d2s(result.getRMSD(), 4)).append("\t"); sb.append(IJ.d2s(result.getPrecision(), 4)).append("\t"); sb.append(IJ.d2s(result.getRecall(), 4)).append("\t"); sb.append(IJ.d2s(result.getFScore(0.5), 4)).append("\t"); sb.append(IJ.d2s(result.getFScore(1.0), 4)).append("\t"); sb.append(IJ.d2s(result.getFScore(2.0), 4)).append("\t"); sb.append(IJ.d2s(result.getFScore(beta), 4)); if (java.awt.GraphicsEnvironment.isHeadless()) { IJ.log(sb.toString()); } else { resultsWindow.append(sb.toString()); } } private String createPairsHeader() { StringBuilder sb = new StringBuilder(); sb.append("Start1\t"); sb.append("End2\t"); sb.append("X1\t"); sb.append("Y1\t"); sb.append("Z1\t"); sb.append("Start2\t"); sb.append("End2\t"); sb.append("X2\t"); sb.append("Y2\t"); sb.append("Z2\t"); sb.append("Distance\t"); sb.append("Score\t"); return sb.toString(); } private void addPairResult(PointPair pair) { StringBuilder sb = new StringBuilder(); Pulse p1 = (Pulse) pair.getPoint1(); Pulse p2 = (Pulse) pair.getPoint2(); addPoint(sb, p1); addResult(sb, p1, p2); pairsWindow.append(sb.toString()); } private void addPoint(StringBuilder sb, Pulse p) { if (p == null) { sb.append("-\t-\t-\t-\t-\t"); } else { sb.append(p.getStart()).append("\t"); sb.append(p.getEnd()).append("\t"); sb.append(IJ.d2s(p.getX())).append("\t"); sb.append(IJ.d2s(p.getY())).append("\t"); sb.append(IJ.d2s(p.getZ())).append("\t"); } } private String createTriplesHeader() { StringBuilder sb = new StringBuilder(); sb.append("Start1\t"); sb.append("End2\t"); sb.append("X1\t"); sb.append("Y1\t"); sb.append("Z1\t"); sb.append("Start2\t"); sb.append("End2\t"); sb.append("X2\t"); sb.append("Y2\t"); sb.append("Z2\t"); sb.append("Distance\t"); sb.append("Score\t"); sb.append("Start3\t"); sb.append("End3\t"); sb.append("X3\t"); sb.append("Y3\t"); sb.append("Z3\t"); sb.append("Distance2\t"); sb.append("Score2\t"); return sb.toString(); } /* * (non-Javadoc) * * @see gdsc.smlm.ij.utils.CoordinateProvider#getCoordinates(java.lang.String) */ public double[] getCoordinates(String line) { // Extract the startT and x,y coordinates from the first pulse in the line final int[] index = { 0, 5, 12 }; String[] fields = line.split("\t"); for (int i : index) { if (i < fields.length) { if (fields[i].equals("-")) continue; int startT = Integer.valueOf(fields[i]); double x = Double.valueOf(fields[i + 2]); double y = Double.valueOf(fields[i + 3]); return new double[] { startT, x, y }; } } return null; } private void addTripleResult(Triple triple) { StringBuilder sb = new StringBuilder(); Pulse p1 = triple.p1; Pulse p2 = triple.p2; Pulse p3 = triple.p3; addPoint(sb, p1); addResult(sb, p1, p2); addResult(sb, p1, p3); triplesWindow.append(sb.toString()); } private void addResult(StringBuilder sb, Pulse p1, Pulse p2) { addPoint(sb, p2); PointPair pair = new PointPair(p1, p2); double d = pair.getXYDistance(); if (d >= 0) sb.append(Utils.rounded(d, 4)).append("\t"); else sb.append("-\t"); if (p1 != null && p2 != null) sb.append(Utils.rounded(p1.score(p2, d * d, dThreshold), 4)).append("\t"); else sb.append("-\t"); } private List<? extends PointPair> sort(List<PointPair> pairs) { switch (sortIndex) { case 1: // Sort by time ArrayList<TimeComparablePointPair> newPairs = new ArrayList<TimeComparablePointPair>(pairs.size()); for (PointPair pair : pairs) { newPairs.add(new TimeComparablePointPair(pair)); } Collections.sort(newPairs); return newPairs; default: // Already sorted by score return pairs; } } private List<? extends Triple> sort(ArrayList<Triple> triples) { if (sortIndex == 1) { List<TimeComparableTriple> sorted = new ArrayList<TimeComparableTriple>(triples.size()); for (Triple t : triples) sorted.add(new TimeComparableTriple(t)); Collections.sort(sorted); return sorted; } else { List<ScoreComparableTriple> sorted = new ArrayList<ScoreComparableTriple>(triples.size()); for (Triple t : triples) sorted.add(new ScoreComparableTriple(t)); Collections.sort(sorted); return sorted; } } private class TimeComparablePointPair extends PointPair implements Comparable<TimeComparablePointPair> { int startT = Integer.MAX_VALUE; public Pulse p1, p2; public TimeComparablePointPair(PointPair pair) { super(pair.getPoint1(), pair.getPoint2()); p1 = (Pulse) pair.getPoint1(); p2 = (Pulse) pair.getPoint2(); if (p2 != null) startT = p2.getStart(); if (p1 != null) startT = p1.getStart(); } /* * (non-Javadoc) * * @see java.lang.Comparable#compareTo(java.lang.Object) */ public int compareTo(TimeComparablePointPair o) { // Significance of points are: p1, p2, p3 // Sort by the earliest start time of the most significant point. if (startT == o.startT) { // Sort using the significant points first. int result = comparePulsesStartT(p1, o.p1); if (result != 0) return result; result = comparePulsesStartT(p2, o.p2); if (result != 0) return result; result = comparePulsesEndT(p1, o.p1); if (result != 0) return result; result = comparePulsesEndT(p2, o.p2); if (result != 0) return result; // Sort using coords result = comparePulsesCoords(p1, o.p1); if (result != 0) return result; return comparePulsesCoords(p2, o.p2); } return startT - o.startT; } } private static int comparePulsesStartT(Pulse p1, Pulse p2) { // Data for a point always beats no data if (p1 == null) return (p2 == null) ? 0 : 1; if (p2 == null) return -1; return p1.getStart() - p2.getStart(); } private static int comparePulsesEndT(Pulse p1, Pulse p2) { // Data for a point always beats no data if (p1 == null) return (p2 == null) ? 0 : 1; if (p2 == null) return -1; return p1.getEnd() - p2.getEnd(); } private static int comparePulsesCoords(Pulse p1, Pulse p2) { // Data for a point always beats no data if (p1 == null) return (p2 == null) ? 0 : 1; if (p2 == null) return -1; if (p1.getX() < p2.getX()) return -1; if (p1.getX() > p2.getX()) return 1; if (p1.getY() < p2.getY()) return -1; if (p1.getY() > p2.getY()) return 1; return 0; } private class Triple { public Pulse p1, p2, p3; public Triple(Pulse p1, Pulse p2, Pulse p3) { this.p1 = p1; this.p2 = p2; this.p3 = p3; } public Triple(Triple t) { this.p1 = t.p1; this.p2 = t.p2; this.p3 = t.p3; } } private class TimeComparableTriple extends Triple implements Comparable<TimeComparableTriple> { int startT = Integer.MAX_VALUE; public TimeComparableTriple(Triple t) { super(t); if (p3 != null) startT = p3.getStart(); if (p2 != null) startT = p2.getStart(); if (p1 != null) startT = p1.getStart(); } /* * (non-Javadoc) * * @see java.lang.Comparable#compareTo(java.lang.Object) */ public int compareTo(TimeComparableTriple o) { // Significance of points are: p1, p2, p3 // Sort by the earliest start time of the most significant point. if (startT == o.startT) { // Sort using the significant points first. //int result = comparePulsesStartT(p1, o.p1); //if (result != 0) // return result; //result = comparePulsesStartT(p2, o.p2); //if (result != 0) // return result; //result = comparePulsesStartT(p3, o.p3); //if (result != 0) // return result; //result = comparePulsesEndT(p1, o.p1); //if (result != 0) // return result; //result = comparePulsesEndT(p2, o.p2); //if (result != 0) // return result; //return comparePulsesEndT(p3, o.p3); // Make the order the same as for the pairs table int result = comparePulsesStartT(p1, o.p1); if (result != 0) return result; result = comparePulsesStartT(p2, o.p2); if (result != 0) return result; result = comparePulsesEndT(p1, o.p1); if (result != 0) return result; result = comparePulsesEndT(p2, o.p2); if (result != 0) return result; // Then sort using the third result = comparePulsesStartT(p3, o.p3); if (result != 0) return result; result = comparePulsesEndT(p3, o.p3); if (result != 0) return result; // Sort using coords result = comparePulsesCoords(p1, o.p1); if (result != 0) return result; result = comparePulsesCoords(p2, o.p2); if (result != 0) return result; return comparePulsesCoords(p3, o.p3); } return startT - o.startT; } } private class ScoreComparableTriple extends Triple implements Comparable<ScoreComparableTriple> { double score = 0; public ScoreComparableTriple(Triple t) { super(t); if (p1 != null) { if (p2 != null) score = p1.score(p2, dThreshold); if (p3 != null) score = FastMath.max(score, p1.score(p2, dThreshold)); } } /* * (non-Javadoc) * * @see java.lang.Comparable#compareTo(java.lang.Object) */ public int compareTo(ScoreComparableTriple o) { if (score > o.score) return -1; if (score < o.score) return 1; return 0; } } }