package gdsc.foci; import gdsc.UsageTracker; import gdsc.help.URL; import gdsc.core.clustering.Cluster; import gdsc.core.clustering.ClusterPoint; import gdsc.core.clustering.ClusteringAlgorithm; import gdsc.core.clustering.ClusteringEngine; import gdsc.core.ij.IJTrackProgress; 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 ij.IJ; import ij.ImagePlus; import ij.ImageStack; import ij.Prefs; import ij.WindowManager; import ij.gui.DialogListener; import ij.gui.GenericDialog; import ij.gui.Overlay; import ij.gui.PointRoi; import ij.gui.Roi; import ij.plugin.filter.ExtendedPlugInFilter; import ij.plugin.filter.PlugInFilterRunner; import ij.process.ImageProcessor; import ij.process.LUTHelper; import ij.text.TextWindow; import java.awt.AWTEvent; import java.awt.Color; import java.awt.Label; import java.awt.Point; import java.awt.image.ColorModel; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.List; import org.apache.commons.math3.stat.descriptive.DescriptiveStatistics; /** * Performs clustering on the latest Find Foci result held in memory. Optionally can draw the clusters on the Find Foci * output mask if this is selected when the plugin is run. */ public class AssignFociToClusters implements ExtendedPlugInFilter, DialogListener { public static final String TITLE = "Assign Foci To Clusters"; private static int imageFlags = DOES_8G + DOES_16 + SNAPSHOT; private static int noImageFlags = NO_IMAGE_REQUIRED + FINAL_PROCESSING; private static double radius = 100; //@formatter:off private static ClusteringAlgorithm[] algorithms = new ClusteringAlgorithm[] { ClusteringAlgorithm.PARTICLE_SINGLE_LINKAGE, ClusteringAlgorithm.CENTROID_LINKAGE, ClusteringAlgorithm.PARTICLE_CENTROID_LINKAGE, ClusteringAlgorithm.PAIRWISE, ClusteringAlgorithm.PAIRWISE_WITHOUT_NEIGHBOURS }; private static String[] names; static { names = new String[algorithms.length]; for (int i = 0; i < names.length; i++) names[i] = algorithms[i].toString(); } private static int algorithm = 1; private static String[] weights = new String[] { "None", "Pixel count", "Total intensity", "Max Value", "Average intensity", "Total intensity minus background", "Average intensity minus background", "Count above saddle", "Intensity above saddle" }; //@formatter:on private static int weight = 2; private static int minSize = 0; private static boolean showMask = true; private static boolean eliminateEdgeClusters = false; private static int border = 10; private static boolean labelClusters = false; private static boolean filterUsingPointROI = false; private static double filterRadius = 50; private boolean myShowMask = false; private static TextWindow resultsWindow = null; private static TextWindow summaryWindow = null; private static TextWindow matchWindow = null; private static int resultId = 1; private ImagePlus imp; private boolean[] edge = null; private AssignedPoint[] roiPoints; private ArrayList<FindFociResult> results; private ArrayList<Cluster> clusters, minSizeClusters, edgeClusters, filteredClusters; private MatchResult matchResult; private ColorModel cm; private Label label = null; public int setup(String arg, ImagePlus imp) { if (arg.equals("final")) { doClustering(); displayResults(); return DONE; } UsageTracker.recordPlugin(this.getClass(), arg); results = FindFoci.getResults(); if (results == null || results.isEmpty()) { IJ.error(TITLE, "Require " + FindFoci.TITLE + " results in memory"); return DONE; } this.imp = validateInputImage(imp); return (this.imp == null) ? noImageFlags : imageFlags; } public void resetPreview() { if (this.imp != null) { // Reset the preview ImageProcessor ip = this.imp.getProcessor(); ip.setColorModel(cm); ip.reset(); this.imp.setOverlay(null); } } /** * Check if the input image has the same number of non-zero values as the FindFoci results. This means it is a * FindFoci mask for the results. * * @param imp * @return The image if valid */ private ImagePlus validateInputImage(ImagePlus imp) { if (imp == null) return null; if (!FindFoci.isSupported(imp.getBitDepth())) return null; // Find all the mask objects using a stack histogram. ImageStack stack = imp.getImageStack(); int[] h = stack.getProcessor(1).getHistogram(); for (int s = 2; s <= stack.getSize(); s++) { int[] h2 = stack.getProcessor(1).getHistogram(); for (int i = 0; i < h.length; i++) h[i] += h2[i]; } // Correct mask objects should be numbered sequentially from 1. // Find first number that is zero. int size = 1; while (size < h.length) { if (h[size] == 0) break; size++; } size--; // Decrement to find the last non-zero number // Check the FindFoci results have the same number of objects if (size != results.size()) return null; // Check each result matches the image. // Image values correspond to the reverse order of the results. for (int i = 0, id = results.size(); i < results.size(); i++, id--) { final FindFociResult result = results.get(i); final int x = result.x; final int y = result.y; final int z = result.z; try { final int value = stack.getProcessor(z + 1).get(x, y); if (value != id) { System.out.printf("x%d,y%d,z%d %d != %d\n", x, y, z + 1, value, id); return null; } } catch (IllegalArgumentException e) { // The stack is not the correct size System.out.println(e.getMessage()); return null; } } // Store this so it can be reset cm = imp.getProcessor().getColorModel(); // Check for a multi-point ROI roiPoints = PointManager.extractRoiPoints(imp.getRoi()); if (roiPoints.length == 0) roiPoints = null; return imp; } public void run(ImageProcessor ip) { // This will not be called if we selected NO_IMAGE_REQUIRED doClustering(); if (label == null) { // This occurs when the dialog has been closed and the plugin is run. displayResults(); return; } // This occurs when we are supporting a preview if (filteredClusters.isEmpty()) { // No clusters so blank the image for (int i = 0; i < ip.getPixelCount(); i++) ip.set(i, 0); } else { // Create a new mask image colouring the objects from each cluster. // Create a map to convert original foci pixels to clusters. int[] map = new int[results.size() + 1]; for (int i = 0; i < filteredClusters.size(); i++) { for (ClusterPoint p = filteredClusters.get(i).head; p != null; p = p.next) { map[p.id] = i + 1; } } // Update the preview processor with the filteredClusters for (int i = 0; i < ip.getPixelCount(); i++) { if (ip.get(i) != 0) ip.set(i, map[ip.get(i)]); } ip.setColorModel(LUTHelper.getColorModel()); ip.setMinAndMax(0, filteredClusters.size()); labelClusters(imp); } label.setText(Utils.pleural(filteredClusters.size(), "Cluster")); } public void setNPasses(int nPasses) { // Nothing to do } public int showDialog(ImagePlus imp, String command, PlugInFilterRunner pfr) { GenericDialog gd = new GenericDialog(TITLE); gd.addHelp(URL.FIND_FOCI); gd.addMessage(Utils.pleural(results.size(), "result")); gd.addSlider("Radius", 5, 500, radius); gd.addChoice("Algorithm", names, names[algorithm]); gd.addChoice("Weight", weights, weights[weight]); gd.addSlider("Min_size", 1, 20, minSize); if (this.imp != null) { gd.addCheckbox("Show_mask", showMask); gd.addCheckbox("Eliminate_edge_clusters", eliminateEdgeClusters); gd.addSlider("Border", 1, 20, border); gd.addCheckbox("Label_clusters", labelClusters); if (roiPoints != null) { gd.addCheckbox("Filter_using_Point_ROI", filterUsingPointROI); gd.addSlider("Filter_radius", 5, 500, filterRadius); } } // Allow preview gd.addPreviewCheckbox(pfr); gd.addDialogListener(this); gd.addMessage(""); label = (Label) gd.getMessage(); gd.showDialog(); // Disable preview resetPreview(); label = null; if (gd.wasCanceled() || !dialogItemChanged(gd, null)) { return DONE; } return (this.imp == null) ? noImageFlags : imageFlags; } public boolean dialogItemChanged(GenericDialog gd, AWTEvent e) { radius = Math.abs(gd.getNextNumber()); algorithm = gd.getNextChoiceIndex(); weight = gd.getNextChoiceIndex(); minSize = (int) Math.abs(gd.getNextNumber()); if (this.imp != null) { myShowMask = showMask = gd.getNextBoolean(); eliminateEdgeClusters = gd.getNextBoolean(); border = (int) Math.abs(gd.getNextNumber()); if (border < 1) border = 1; labelClusters = gd.getNextBoolean(); if (roiPoints != null) { filterUsingPointROI = gd.getNextBoolean(); filterRadius = Math.abs(gd.getNextNumber()); } } if (!gd.getPreviewCheckbox().getState()) { resetPreview(); } else // We can support a preview without an image if (label != null && imp == null) { noImagePreview(); } return true; } private synchronized void noImagePreview() { doClustering(); label.setText(Utils.pleural(filteredClusters.size(), "Cluster")); } private double lastRadius; private int lastAlgorithm, lastWeight; private int lastMinSize; private boolean lastEliminateEdgeClusters; private int lastBorder; private boolean lastfilterUsingPointROI; private double lastFilterRadius = -1; private void doClustering() { long start = System.currentTimeMillis(); IJ.showStatus("Clustering ..."); if (clusters == null || lastRadius != radius || lastAlgorithm != algorithm || lastWeight != weight) { //System.out.println("clustering 1"); lastRadius = radius; lastAlgorithm = algorithm; lastWeight = weight; minSizeClusters = null; ClusteringEngine e = new ClusteringEngine(Prefs.getThreads(), algorithms[algorithm], new IJTrackProgress()); ArrayList<ClusterPoint> points = getPoints(); clusters = e.findClusters(points, radius); Collections.sort(clusters, new Comparator<Cluster>() { public int compare(Cluster o1, Cluster o2) { if (o1.sumw > o2.sumw) return -1; if (o1.sumw < o2.sumw) return 1; return 0; } }); } if (minSizeClusters == null || lastMinSize != minSize) { minSizeClusters = clusters; edgeClusters = null; if (minSize > 0) { //System.out.println("clustering 2"); minSizeClusters = new ArrayList<Cluster>(clusters.size()); for (Cluster c : clusters) if (c.n >= minSize) minSizeClusters.add(c); } lastMinSize = minSize; } if (edgeClusters == null || lastEliminateEdgeClusters != eliminateEdgeClusters || lastBorder != border) { edgeClusters = minSizeClusters; filteredClusters = null; if (imp != null && eliminateEdgeClusters && border > 0) { //System.out.println("clustering 3"); // Cache the edge particles if (edge == null || lastBorder != border) { ImageStack stack = imp.getImageStack(); edge = new boolean[results.size() + 1]; for (int s = 1; s <= stack.getSize(); s++) { findEdgeObjects(stack.getProcessor(s), edge); } } // Check which clusters contain edge particles edgeClusters = new ArrayList<Cluster>(minSizeClusters.size()); NextCluster: for (Cluster c : minSizeClusters) { for (ClusterPoint p = c.head; p != null; p = p.next) { if (edge[p.id]) { continue NextCluster; } } edgeClusters.add(c); } } lastEliminateEdgeClusters = eliminateEdgeClusters; lastBorder = border; } if (filteredClusters == null || (roiPoints != null && (lastfilterUsingPointROI != filterUsingPointROI || lastFilterRadius != filterRadius))) { if (roiPoints != null && filterUsingPointROI) { if (filteredClusters == null || lastFilterRadius != filterRadius) { lastFilterRadius = filterRadius; //System.out.println("clustering 4"); Coordinate[] actualPoints = roiPoints; Coordinate[] predictedPoints = toCoordinates(edgeClusters); List<Coordinate> TP = null; List<Coordinate> FP = null; List<Coordinate> FN = null; List<PointPair> matches = new ArrayList<PointPair>(edgeClusters.size()); matchResult = MatchCalculator.analyseResults2D(actualPoints, predictedPoints, filterRadius, TP, FP, FN, matches); filteredClusters = new ArrayList<Cluster>(matches.size()); for (PointPair pair : matches) filteredClusters.add(edgeClusters.get(((TimeValuedPoint) pair.getPoint2()).time)); } } else { // No filtering filteredClusters = edgeClusters; lastFilterRadius = -1; } lastfilterUsingPointROI = filterUsingPointROI; } double seconds = (System.currentTimeMillis() - start) / 1000.0; IJ.showStatus(Utils.pleural(filteredClusters.size(), "cluster") + " in " + Utils.rounded(seconds) + " seconds"); } private Coordinate[] toCoordinates(ArrayList<Cluster> clusters) { Coordinate[] coords = new Coordinate[clusters.size()]; for (int i = 0; i < clusters.size(); i++) { Cluster c = clusters.get(i); coords[i] = new TimeValuedPoint((float) c.x, (float) c.y, 0, i, 0); } return coords; } private void displayResults() { if (filteredClusters == null) return; IJ.showStatus("Displaying results ..."); // Options only available if there is an input FindFoci mask image. // Removal of edge clusters will reduce the final number of clusters. if (myShowMask) { // Create a new mask image colouring the objects from each cluster. // Create a map to convert original foci pixels to clusters. int[] map = new int[results.size() + 1]; for (int i = 0; i < filteredClusters.size(); i++) { for (ClusterPoint p = filteredClusters.get(i).head; p != null; p = p.next) { map[p.id] = i + 1; } } ImageStack stack = imp.getImageStack(); ImageStack newStack = new ImageStack(stack.getWidth(), stack.getHeight(), stack.getSize()); for (int s = 1; s <= stack.getSize(); s++) { ImageProcessor ip = stack.getProcessor(s); ImageProcessor ip2 = ip.createProcessor(ip.getWidth(), ip.getHeight()); for (int i = 0; i < ip.getPixelCount(); i++) { if (ip.get(i) != 0) ip2.set(i, map[ip.get(i)]); } newStack.setProcessor(ip2, s); } // Set a colour table if this is a new image. Otherwise the existing one is preserved. ImagePlus clusterImp = WindowManager.getImage(TITLE); if (clusterImp == null) newStack.setColorModel(LUTHelper.getColorModel()); clusterImp = Utils.display(TITLE, newStack); labelClusters(clusterImp); } createResultsTables(); // Show the results final String title = (imp != null) ? imp.getTitle() : "Result " + (resultId++); StringBuilder sb = new StringBuilder(); DescriptiveStatistics stats = new DescriptiveStatistics(); DescriptiveStatistics stats2 = new DescriptiveStatistics(); for (int i = 0; i < filteredClusters.size(); i++) { final Cluster cluster = filteredClusters.get(i); sb.append(title).append('\t'); sb.append(i + 1).append('\t'); sb.append(Utils.rounded(cluster.x)).append('\t'); sb.append(Utils.rounded(cluster.y)).append('\t'); sb.append(Utils.rounded(cluster.n)).append('\t'); sb.append(Utils.rounded(cluster.sumw)).append('\t'); stats.addValue(cluster.n); stats2.addValue(cluster.sumw); sb.append('\n'); // Auto-width adjustment is only performed when number of rows is less than 10 // so do this before it won't work if (i == 9 && resultsWindow.getTextPanel().getLineCount() < 10) { resultsWindow.append(sb.toString()); sb.setLength(0); } } resultsWindow.append(sb.toString()); sb.setLength(0); sb.append(title).append('\t'); sb.append(Utils.rounded(radius)).append('\t'); sb.append(results.size()).append('\t'); sb.append(filteredClusters.size()).append('\t'); sb.append((int) stats.getMin()).append('\t'); sb.append((int) stats.getMax()).append('\t'); sb.append(Utils.rounded(stats.getMean())).append('\t'); sb.append(Utils.rounded(stats2.getMin())).append('\t'); sb.append(Utils.rounded(stats2.getMax())).append('\t'); sb.append(Utils.rounded(stats2.getMean())).append('\t'); summaryWindow.append(sb.toString()); if (matchResult != null) { sb.setLength(0); sb.append(title).append('\t'); sb.append(Utils.rounded(filterRadius)).append('\t'); sb.append(matchResult.getNumberActual()).append('\t'); sb.append(matchResult.getNumberPredicted()).append('\t'); sb.append(matchResult.getTruePositives()).append('\t'); sb.append(matchResult.getFalseNegatives()).append('\t'); sb.append(matchResult.getFalsePositives()).append('\t'); sb.append(Utils.rounded(matchResult.getJaccard())).append('\t'); sb.append(Utils.rounded(matchResult.getRecall())).append('\t'); sb.append(Utils.rounded(matchResult.getPrecision())).append('\t'); sb.append(Utils.rounded(matchResult.getFScore(1))).append('\t'); matchWindow.append(sb.toString()); } IJ.showStatus(""); } private void labelClusters(ImagePlus clusterImp) { Overlay o = null; if (labelClusters) { Roi roi = getClusterRoi(filteredClusters); if (roi != null) { o = new Overlay(roi); o.setStrokeColor(Color.cyan); } } clusterImp.setOverlay(o); } private void findEdgeObjects(ImageProcessor ip, boolean[] edge) { final int w = ip.getWidth(); final int h = ip.getHeight(); for (int i = 0; i < border; i++) { final int top = h - 1 - i; if (top < i) return; for (int x = 0, i1 = i * w, i2 = top * w; x < w; x++, i1++, i2++) { if (ip.get(i1) != 0) edge[ip.get(i1)] = true; if (ip.get(i2) != 0) edge[ip.get(i2)] = true; } } for (int i = 0; i < border; i++) { final int top = w - 1 - i; if (top < i) return; for (int y = border, i1 = border * w + i, i2 = border * w + top; y < w - border; y++, i1 += w, i2 += w) { if (ip.get(i1) != 0) edge[ip.get(i1)] = true; if (ip.get(i2) != 0) edge[ip.get(i2)] = true; } } } private Roi getClusterRoi(ArrayList<Cluster> clusters) { if (clusters == null || clusters.isEmpty()) return null; int nMaxima = clusters.size(); float[] xpoints = new float[nMaxima]; float[] ypoints = new float[nMaxima]; int i = 0; for (Cluster point : clusters) { xpoints[i] = (float) point.x; ypoints[i] = (float) point.y; i++; } PointRoi roi = new PointRoi(xpoints, ypoints, nMaxima); roi.setShowLabels(true); return roi; } private ArrayList<ClusterPoint> getPoints() { if (FindFoci.getResults() == null) return null; ArrayList<ClusterPoint> points = new ArrayList<ClusterPoint>(FindFoci.getResults().size()); // Image values correspond to the reverse order of the results. for (int i = 0, id = results.size(); i < results.size(); i++, id--) { FindFociResult result = results.get(i); points.add(ClusterPoint.newClusterPoint(id, result.x, result.y, getWeight(result))); } return points; } private double getWeight(FindFociResult result) { switch (weight) { //@formatter:off case 0: return 1.0; case 1: return result.count; case 2: return result.totalIntensity; case 3: return result.maxValue; case 4: return result.averageIntensity; case 5: return result.totalIntensityAboveBackground; case 6: return result.averageIntensityAboveBackground; case 7: return result.countAboveSaddle; case 8: return result.intensityAboveSaddle; default: return 1.0; //@formatter:on } } private void createResultsTables() { resultsWindow = createWindow(resultsWindow, "Results", "Title\tCluster\tcx\tcy\tSize\tW", 300); summaryWindow = createWindow(summaryWindow, "Summary", "Title\tRadius\tFoci\tClusters\tMin\tMax\tAv\tMin W\tMax W\tAv W", 300); Point p1 = resultsWindow.getLocation(); Point p2 = summaryWindow.getLocation(); if (p1.x == p2.x && p1.y == p2.y) { p2.y += resultsWindow.getHeight(); summaryWindow.setLocation(p2); } if (matchResult == null) return; matchWindow = createWindow(matchWindow, "Filter Result", "Title\tRadius\tPoints\tClusters\tTP\tFN\tFP\tJaccard\tRecall\tPrecision\tF1", 300); Point p3 = matchWindow.getLocation(); if (p1.x == p3.x && p1.y == p3.y) { p3.y += resultsWindow.getHeight(); matchWindow.setLocation(p3); } if (p2.x == p3.x && p2.y == p3.y) { p3.y += summaryWindow.getHeight(); matchWindow.setLocation(p3); } } private TextWindow createWindow(TextWindow window, String title, String header, int h) { if (window == null || !window.isVisible()) window = new TextWindow(TITLE + " " + title, header, "", 800, h); return window; } }