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.help.URL; import gdsc.core.ij.Utils; import ij.IJ; import ij.ImagePlus; import ij.gui.GenericDialog; import ij.gui.Overlay; import ij.gui.Plot; import ij.gui.PointRoi; import ij.plugin.filter.PlugInFilter; import ij.process.ByteProcessor; import ij.process.ImageProcessor; import ij.process.ShortProcessor; import ij.text.TextWindow; import java.awt.Color; import java.awt.Point; import java.util.ArrayList; import java.util.Arrays; import org.apache.commons.math3.stat.descriptive.DescriptiveStatistics; /** * Finds objects in an image using contiguous pixels of the same value. Locates the closest object to each Find Foci * result held in memory and summarises the counts. */ public class AssignFociToObjects implements PlugInFilter { public static final String TITLE = "Assign Foci"; private static final int flags = DOES_16 + DOES_8G + NO_CHANGES + NO_UNDO; private static String input = ""; private static double radius = 30; private static int minSize = 0; private static int maxSize = 0; private static boolean eightConnected = false; private static boolean removeSmallObjects = true; private static boolean showObjects = false; private static boolean showFoci = false; private static boolean showDistances = false; private static boolean showHistogram = false; private static TextWindow resultsWindow = null; private static TextWindow summaryWindow = null; private static TextWindow distancesWindow = null; private ImagePlus imp; private ArrayList<int[]> results; private static int size = 0; private static ArrayList<Search[]> search = null; private class Search implements Comparable<Search> { int x, y; double d2; public Search(int x, int y, double d2) { this.x = x; this.y = y; this.d2 = d2; } public int compareTo(Search that) { if (this.d2 < that.d2) return -1; if (this.d2 > that.d2) return 1; return 0; } } /* * (non-Javadoc) * * @see ij.plugin.filter.PlugInFilter#setup(java.lang.String, ij.ImagePlus) */ public int setup(String arg, ImagePlus imp) { UsageTracker.recordPlugin(this.getClass(), arg); if (imp == null) return DONE; this.imp = imp; return flags; } /* * (non-Javadoc) * * @see ij.plugin.filter.PlugInFilter#run(ij.process.ImageProcessor) */ public void run(ImageProcessor ip) { if (!showDialog()) return; createDistanceGrid(radius); createResultsTables(); ObjectAnalyzer oa = new ObjectAnalyzer(ip); if (removeSmallObjects) oa.setMinObjectSize(minSize); oa.setEightConnected(eightConnected); int[] objectMask = oa.getObjectMask(); final int maxx = ip.getWidth(); final int maxy = ip.getHeight(); final double maxD2 = radius * radius; // Assign each foci to the nearest object int[] count = new int[oa.getMaxObject() + 1]; int[] found = new int[results.size()]; double[] d2 = new double[found.length]; Arrays.fill(d2, -1); for (int i = 0; i < results.size(); i++) { final int[] result = results.get(i); final int x = result[0]; final int y = result[1]; // Check within the image if (x < 0 || x >= maxx || y < 0 || y >= maxy) continue; int index = y * maxx + x; if (objectMask[index] != 0) { count[objectMask[index]]++; found[i] = objectMask[index]; d2[i] = 0; continue; } // Search for the closest object(s) int[] closestCount = new int[count.length]; // Scan wider and wider from 0,0 until we find an object. for (Search[] next : search) { if (next[0].d2 > maxD2) break; boolean ok = false; for (Search s : next) { final int xx = x + s.x; final int yy = y + s.y; if (xx < 0 || xx >= maxx || yy < 0 || yy >= maxy) continue; index = yy * maxx + xx; if (objectMask[index] != 0) { ok = true; closestCount[objectMask[index]]++; } } if (ok) { // Get the object with the highest count int maxCount = 0; for (int j = 1; j < closestCount.length; j++) if (closestCount[maxCount] < closestCount[j]) maxCount = j; // Assign count[maxCount]++; found[i] = maxCount; d2[i] = next[0].d2; break; } } } double[][] centres = oa.getObjectCentres(); // We must ignore those that are too small/big int[] idMap = new int[count.length]; for (int i = 1; i < count.length; i++) { idMap[i] = i; if (centres[i][2] < minSize || (maxSize != 0 && centres[i][2] > maxSize)) { idMap[i] = -i; } } // TODO - Remove objects from the output image ? showMask(oa, found, idMap); // Show the results DescriptiveStatistics stats = new DescriptiveStatistics(); DescriptiveStatistics stats2 = new DescriptiveStatistics(); StringBuilder sb = new StringBuilder(); for (int i = 1, j = 0; i < count.length; i++) { sb.append(imp.getTitle()); sb.append('\t').append(i); sb.append('\t').append(Utils.rounded(centres[i][0])); sb.append('\t').append(Utils.rounded(centres[i][1])); sb.append('\t').append((int) (centres[i][2])); if (idMap[i] > 0) { // Include this object sb.append("\tTrue"); stats.addValue(count[i]); } else { // Exclude this object sb.append("\tFalse"); stats2.addValue(count[i]); } sb.append('\t').append(count[i]); sb.append('\n'); // Flush before 10 lines to ensure auto-layout of columns if (i >= 9 && j++ == 0) { resultsWindow.append(sb.toString()); sb.setLength(0); } } resultsWindow.append(sb.toString()); // Histogram the count if (showHistogram) { final int max = (int) stats.getMax(); final double[] xValues = new double[max + 1]; final double[] yValues = new double[xValues.length]; for (int i = 1; i < count.length; i++) { if (idMap[i] > 0) yValues[count[i]]++; } double yMax = 0; for (int i = 0; i <= max; i++) { xValues[i] = i; if (yMax < yValues[i]) yMax = yValues[i]; } String title = TITLE + " Histogram"; Plot plot = new Plot(title, "Count", "Frequency", xValues, yValues); plot.setLimits(0, xValues[xValues.length - 1], 0, yMax); plot.addLabel(0, 0, String.format("N = %d, Mean = %s", (int) stats.getSum(), Utils.rounded(stats.getMean()))); plot.draw(); plot.setColor(Color.RED); plot.drawLine(stats.getMean(), 0, stats.getMean(), yMax); Utils.display(title, plot); } // Show the summary sb.setLength(0); sb.append(imp.getTitle()); sb.append('\t').append(oa.getMaxObject()); sb.append('\t').append(stats.getN()); sb.append('\t').append(results.size()); sb.append('\t').append((int) stats.getSum()); sb.append('\t').append((int) stats2.getSum()); sb.append('\t').append(results.size() - (int) (stats.getSum() + stats2.getSum())); sb.append('\t').append(stats.getMin()); sb.append('\t').append(stats.getMax()); sb.append('\t').append(Utils.rounded(stats.getMean())); sb.append('\t').append(Utils.rounded(stats.getPercentile(50))); sb.append('\t').append(Utils.rounded(stats.getStandardDeviation())); summaryWindow.append(sb.toString()); if (!showDistances) return; sb.setLength(0); for (int i = 0, j = 0; i < results.size(); i++) { final int[] result = results.get(i); final int x = result[0]; final int y = result[1]; // Check within the image if (x < 0 || x >= maxx || y < 0 || y >= maxy) continue; sb.append(imp.getTitle()); sb.append('\t').append(i + 1); sb.append('\t').append(x); sb.append('\t').append(y); if (found[i] > 0) { sb.append("\t").append(found[i]); if (idMap[found[i]] > 0) { sb.append("\tTrue\t"); } else { sb.append("\tFalse\t"); } sb.append(Utils.rounded(Math.sqrt(d2[i]))); sb.append('\n'); } else { sb.append("\t\t\t\n"); } // Flush before 10 lines to ensure auto-layout of columns if (i >= 9 && j++ == 0) { distancesWindow.append(sb.toString()); sb.setLength(0); } } distancesWindow.append(sb.toString()); } public boolean showDialog() { ArrayList<int[]> findFociResults = getFindFociResults(); ArrayList<int[]> roiResults = getRoiResults(); if (findFociResults == null && roiResults == null) { IJ.error(TITLE, "No " + FindFoci.TITLE + " results in memory or point ROI on the image"); return false; } GenericDialog gd = new GenericDialog(TITLE); gd.addHelp(URL.FIND_FOCI); String[] options = new String[2]; int count = 0; String msg = "Assign foci to the nearest object\n(Objects will be found in the current image)\nAvailable foci:"; if (findFociResults != null) { msg += "\nFind Foci = " + Utils.pleural(findFociResults.size(), "result"); options[count++] = "Find Foci"; } if (roiResults != null) { msg += "\nROI = " + Utils.pleural(roiResults.size(), "point"); options[count++] = "ROI"; } options = Arrays.copyOf(options, count); gd.addMessage(msg); gd.addChoice("Foci", options, input); gd.addSlider("Radius", 5, 50, radius); gd.addNumericField("Min_size", minSize, 0); gd.addNumericField("Max_size", maxSize, 0); gd.addCheckbox("Eight_connected", eightConnected); gd.addCheckbox("Remove_small_objects", removeSmallObjects); gd.addCheckbox("Show_objects", showObjects); gd.addCheckbox("Show_foci", showFoci); gd.addCheckbox("Show_distances", showDistances); gd.addCheckbox("Show_histogram", showHistogram); gd.showDialog(); if (gd.wasCanceled()) return false; input = gd.getNextChoice(); radius = Math.abs(gd.getNextNumber()); minSize = Math.abs((int) gd.getNextNumber()); maxSize = Math.abs((int) gd.getNextNumber()); eightConnected = gd.getNextBoolean(); removeSmallObjects = gd.getNextBoolean(); showObjects = gd.getNextBoolean(); showFoci = gd.getNextBoolean(); showDistances = gd.getNextBoolean(); showHistogram = gd.getNextBoolean(); // Load objects results = (input.equalsIgnoreCase("ROI")) ? roiResults : findFociResults; if (results == null) { IJ.error(TITLE, "No foci could be loaded"); return false; } return true; } private ArrayList<int[]> getFindFociResults() { if (FindFoci.getResults() == null) return null; ArrayList<int[]> results = new ArrayList<int[]>(FindFoci.getResults().size()); for (FindFociResult result : FindFoci.getResults()) { results.add(new int[] { result.x, result.y }); } return results; } private ArrayList<int[]> getRoiResults() { AssignedPoint[] points = PointManager.extractRoiPoints(imp.getRoi()); if (points.length == 0) return null; ArrayList<int[]> results = new ArrayList<int[]>(points.length); for (AssignedPoint point : points) { results.add(new int[] { point.x, point.y }); } return results; } private static void createDistanceGrid(double radius) { final int n = (int) Math.ceil(radius); final int newSize = 2 * n + 1; if (size >= newSize) { //return; } size = newSize; search = new ArrayList<Search[]>(); AssignFociToObjects instance = new AssignFociToObjects(); Search[] s = new Search[size * size]; double[] tmp = new double[newSize]; for (int y = -n, i = 0; y <= n; y++, i++) tmp[i] = y * y; for (int y = -n, i = 0, index = 0; y <= n; y++, i++) { final double y2 = tmp[i]; for (int x = -n, ii = 0; x <= n; x++, ii++) { s[index++] = instance.new Search(x, y, y2 + tmp[ii]); } } Arrays.sort(s); // Store each increasing search distance as a set // Ignore first record as it is d2==0 double last = -1; int begin = 0, end = -1; for (int i = 1; i < s.length; i++) { if (last != s[i].d2) { if (end != -1) { int length = end - begin + 1; Search[] next = new Search[length]; System.arraycopy(s, begin, next, 0, length); search.add(next); } begin = i; } end = i; last = s[i].d2; } if (end != -1) { int length = end - begin + 1; Search[] next = new Search[length]; System.arraycopy(s, begin, next, 0, length); search.add(next); } } private void createResultsTables() { resultsWindow = createWindow(resultsWindow, "Results", "Image\tObject\tcx\tcy\tSize\tValid\tCount"); summaryWindow = createWindow(summaryWindow, "Summary", "Image\tnObjects\tValid\tnFoci\tIn\tIgnored\tOut\tMin\tMax\tAv\tMed\tSD"); 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 (showDistances) { distancesWindow = createWindow(distancesWindow, "Distances", "Image\tFoci\tx\ty\tObject\tValid\tDistance"); Point p3 = distancesWindow.getLocation(); if (p1.x == p3.x && p1.y == p3.y) { p3.x += 50; p3.y += 50; distancesWindow.setLocation(p3); } } } private TextWindow createWindow(TextWindow window, String title, String header) { if (window == null || !window.isVisible()) window = new TextWindow(TITLE + " " + title, header, "", 800, 400); return window; } private void showMask(ObjectAnalyzer oa, int[] found, int[] idMap) { if (!showObjects) return; int[] objectMask = oa.getObjectMask(); ImageProcessor ip; if (oa.getMaxObject() <= 255) ip = new ByteProcessor(oa.getWidth(), oa.getHeight()); else ip = new ShortProcessor(oa.getWidth(), oa.getHeight()); for (int i = 0; i < objectMask.length; i++) ip.set(i, objectMask[i]); ip.setMinAndMax(0, oa.getMaxObject()); ImagePlus imp = Utils.display(TITLE + " Objects", ip); if (showFoci && found.length > 0) { int[] xin = new int[found.length]; int[] yin = new int[found.length]; int[] xremove = new int[found.length]; int[] yremove = new int[found.length]; int[] xout = new int[found.length]; int[] yout = new int[found.length]; int in = 0, remove = 0, out = 0; for (int i = 0; i < found.length; i++) { final int[] xy = results.get(i); final int id = idMap[found[i]]; if (id > 0) { xin[in] = xy[0]; yin[in++] = xy[1]; } else if (id < 0) { xremove[remove] = xy[0]; yremove[remove++] = xy[1]; } else { xout[out] = xy[0]; yout[out++] = xy[1]; } } Overlay o = new Overlay(); addRoi(xin, yin, in, o, Color.GREEN); addRoi(xremove, yremove, remove, o, Color.YELLOW); addRoi(xout, yout, out, o, Color.RED); imp.setOverlay(o); } } private void addRoi(int[] x, int[] y, int n, Overlay o, Color color) { PointRoi roi = new PointRoi(x, y, n); roi.setShowLabels(false); roi.setFillColor(color); roi.setStrokeColor(color); o.add(roi); } }