package ij.plugin.filter; import gdsc.core.ij.Utils; import ij.IJ; import ij.ImagePlus; import ij.Macro; import ij.Prefs; import ij.WindowManager; import ij.gui.GenericDialog; import ij.gui.Roi; import ij.macro.Interpreter; import ij.measure.ResultsTable; import ij.plugin.frame.RoiManager; import ij.process.ColorProcessor; import ij.process.FloatProcessor; import ij.process.ImageProcessor; import ij.process.ImageStatistics; import ij.process.ShortProcessor; import ij.text.TextPanel; import ij.text.TextWindow; import java.awt.Frame; import java.io.BufferedWriter; import java.io.FileOutputStream; import java.io.OutputStreamWriter; import java.lang.reflect.Field; import java.util.Arrays; import java.util.HashMap; import java.util.LinkedList; import java.util.List; /*----------------------------------------------------------------------------- * GDSC Plugins for ImageJ * * Copyright (C) 2015 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. *---------------------------------------------------------------------------*/ /** * Extend the ImageJ Particle Analyser to allow the particles to be obtained from an input mask with objects * assigned using contiguous pixels with a unique value. If blank pixels exist between two objects with the same pixel * value then they will be treated as separate objects. * <p> * Adds an option to select the redirection image for particle analysis. This can be none. * <p> * If the input image is a mask then the functionality is the same as the original ParticleAnalyzer class. * <p> * Note: This class used to extend the default ImageJ ParticleAnalyzer. However the Java inheritance method invocation * system would not call the MaskParticleAnalyzer.analyseParticle(...) method. This may be due to it being a default * (package) scoped method. It works on the Linux JVM but not on Windows. It would call the protected/public methods * that had been overridden, just not the default scope method. I have thus changed it to extend a copy of the ImageJ * ParticleAnalyzer. This can be updated with new version from the ImageJ source code as appropriate and default scoped * methods set to protected. */ public class MaskParticleAnalyzer extends ParticleAnalyzerCopy { private static String redirectTitle = ""; private static boolean particleSummary = false; private static boolean saveHistogram = false; private static String histogramFile = ""; private ImagePlus restoreRedirectImp; private BufferedWriter out = null; private HashMap<Double, int[]> summaryHistogram = null; private boolean useGetPixelValue; private float[] image; private float value; private boolean noThreshold = false; private double dmin, dmax; // Methods to allow the Analyzer class package level fields to be set. // This is not possible on the Windows JVM but is OK on linux. static Field firstParticle, lastParticle; static { try { firstParticle = Analyzer.class.getDeclaredField("firstParticle"); firstParticle.setAccessible(true); lastParticle = Analyzer.class.getDeclaredField("lastParticle"); lastParticle.setAccessible(true); } catch (Throwable e) { // Reflection has failed firstParticle = lastParticle = null; } } static void setAnalyzerFirstParticle(int value) { //Analyzer.firstParticle = value; if (firstParticle != null) { try { firstParticle.set(Analyzer.class, value); //IJ.log("Set firstParticle to "+value); } catch (Throwable e) { // Reflection has failed firstParticle = null; } } } static void setAnalyzerLastParticle(int value) { //Analyzer.lastParticle = value; if (lastParticle != null) { try { lastParticle.set(Analyzer.class, value); //IJ.log("Set lastParticle to "+value); } catch (Throwable e) { // Reflection has failed lastParticle = null; } } } public int setup(String arg, ImagePlus imp) { int flags = FINAL_PROCESSING; if (imp != null) { if ("final".equals(arg)) { if (noThreshold) { imp.getProcessor().resetThreshold(); imp.setDisplayRange(dmin, dmax); imp.updateAndDraw(); } Analyzer.setRedirectImage(restoreRedirectImp); close(out); if (particleSummary) createSummary(); return DONE; } dmin = imp.getDisplayRangeMin(); dmax = imp.getDisplayRangeMax(); noThreshold = isNoThreshold(imp); // The plugin will be run on a thresholded/mask image to define particles. // Choose the redirect image to sample the pixels from. int[] idList = Utils.getIDList(); String[] list = new String[idList.length + 1]; list[0] = "[None]"; int count = 1; for (int id : idList) { ImagePlus imp2 = WindowManager.getImage(id); if (imp2 == null || imp2.getWidth() != imp.getWidth() || imp2.getHeight() != imp.getHeight()) continue; if (imp2.getID() == imp.getID()) continue; list[count++] = imp2.getTitle(); } list = Arrays.copyOf(list, count); GenericDialog gd = new GenericDialog("Mask Particle Analyzer..."); gd.addMessage("Analyses objects in an image.\n \nObjects are defined with contiguous pixels of the same value.\nIgnore pixels outside any configured thresholds."); gd.addChoice("Redirect_image", list, redirectTitle); gd.addCheckbox("Particle_summary", particleSummary); gd.addCheckbox("Save_histogram", saveHistogram); if (noThreshold) gd.addMessage("Warning: The image is not thresholded / 8-bit binary mask.\nContinuing will use the min/max values in the image which\nmay produce many objects."); gd.addHelp(gdsc.help.URL.FIND_FOCI); gd.showDialog(); if (gd.wasCanceled()) return DONE; int index = gd.getNextChoiceIndex(); redirectTitle = list[index]; particleSummary = gd.getNextBoolean(); if (particleSummary) summaryHistogram = new HashMap<Double, int[]>(); saveHistogram = gd.getNextBoolean(); if (saveHistogram) { histogramFile = Utils.getFilename("Histogram_file", histogramFile); if (histogramFile != null) { int i = histogramFile.lastIndexOf('.'); if (i == -1) histogramFile += ".txt"; out = createOutput(histogramFile); if (out == null) return DONE; } } if (Analyzer.isRedirectImage()) { // Get the current redirect image using reflection since we just want to restore it // and do not want errors from image size mismatch in Analyzer.getRedirectImage(imp); try { Field field = Analyzer.class.getDeclaredField("redirectTarget"); field.setAccessible(true); int redirectTarget = (Integer) field.get(Analyzer.class); restoreRedirectImp = WindowManager.getImage(redirectTarget); //if (restoreRedirectImp != null) // System.out.println("Redirect image = " + restoreRedirectImp.getTitle()); } catch (Throwable e) { // Reflection has failed } } ImagePlus redirectImp = (index > 0) ? WindowManager.getImage(redirectTitle) : null; Analyzer.setRedirectImage(redirectImp); useGetPixelValue = imp.getProcessor() instanceof ColorProcessor; image = new float[imp.getWidth() * imp.getHeight()]; if (noThreshold) { cache(imp.getProcessor()); float min = Float.POSITIVE_INFINITY; float max = Float.NEGATIVE_INFINITY; for (int i = 1; i < image.length; i++) { if (image[i] != 0) { if (min > image[i]) min = image[i]; else if (max < image[i]) max = image[i]; } } if (min == Float.POSITIVE_INFINITY) { IJ.error("The image has no values"); return DONE; } imp.getProcessor().setThreshold(min, max, ImageProcessor.NO_LUT_UPDATE); } } return super.setup(arg, imp) + flags; } private BufferedWriter createOutput(String filename) { try { FileOutputStream fos = new FileOutputStream(filename); BufferedWriter out = new BufferedWriter(new OutputStreamWriter(fos, "UTF-8")); out.write("Histogram\tParticle Value\tPixel Value\tCount"); out.newLine(); return out; } catch (Exception e) { IJ.error("Failed to create histogram file: " + filename); return null; } } private BufferedWriter writeHistogram(BufferedWriter out, int id, double particleValue, int[] histogram) { if (out == null || histogram == null) return null; final String prefix = String.format("%d\t%s\t", id, Double.toString(particleValue)); for (int i = 0; i < histogram.length; i++) { if (histogram[i] == 0) continue; try { out.write(prefix); out.write(Integer.toString(i)); out.write('\t'); out.write(Integer.toString(histogram[i])); out.newLine(); } catch (Exception e) { close(out); return null; } } return out; } private void close(BufferedWriter out) { if (out != null) { try { out.close(); } catch (Exception e) { } } } public boolean isNoThreshold(ImagePlus imp) { boolean noThreshold = false; ImageProcessor ip = imp.getProcessor(); double t1 = ip.getMinThreshold(); int imageType; if (ip instanceof ShortProcessor) imageType = SHORT; else if (ip instanceof FloatProcessor) imageType = FLOAT; else imageType = BYTE; if (t1 == ImageProcessor.NO_THRESHOLD) { ImageStatistics stats = imp.getStatistics(); if (imageType != BYTE || (stats.histogram[0] + stats.histogram[255] != stats.pixelCount)) { noThreshold = true; } } return noThreshold; } private void cache(ImageProcessor ip) { // Cache a floating-point copy of the image final int w = ip.getWidth(); final int h = ip.getHeight(); for (int y = 0, i = 0; y < h; y++) for (int x = 0; x < w; x++, i++) { image[i] = (useGetPixelValue) ? ip.getPixelValue(x, y) : ip.getf(i); } } @Override public void run(ImageProcessor ip) { cache(ip); super.run(ip); } @Override protected void analyzeParticle(int x, int y, ImagePlus imp, ImageProcessor ip) { // x,y - the position the particle was first found // imp - the particle image // ip - the current processor from the particle image // We need to perform the same work as the super-class but instead of outlining using the // configured thresholds in the particle image we just use the position's current value. // Do this by zeroing all pixels that are not the same value and then calling the super-class method. ImageProcessor originalIp = ip.duplicate(); value = (useGetPixelValue) ? ip.getPixelValue(x, y) : ip.getf(x, y); //IJ.log(String.format("Analysing x=%d,y=%d value=%f", x, y, value)); for (int i = 0; i < image.length; i++) if (image[i] != value) ip.set(i, 0); ImageProcessor particleIp = ip.duplicate(); //System.out.printf("Particle = %f\n", value); //Utils.display("Particle", particleIp); super.analyzeParticle(x, y, imp, ip); // At the end of processing the analyser fills the image processor to prevent // re-processing this object's pixels. // We must copy back the filled pixel values. final int newValue = ip.get(x, y); //System.out.printf("Particle changed to = %d\n", newValue); for (int i = 0; i < image.length; i++) { // Check if different from the input particle if (ip.get(i) != particleIp.get(i)) { // Change to the reset value originalIp.set(i, newValue); } // Now copy back all the pixels from the original processor ip.set(i, originalIp.get(i)); } } /** * Saves statistics for one particle in a results table. This is * a method subclasses may want to override. */ @Override protected void saveResults(ImageStatistics stats, Roi roi) { analyzer.saveResults(stats, roi); if (recordStarts) { rt.addValue("XStart", stats.xstart); rt.addValue("YStart", stats.ystart); } //IJ.log(String.format("Saving x=%d,y=%d count=%d, value=%f", roi.getBounds().x, roi.getBounds().y, // stats.pixelCount, value)); rt.addValue("Particle Value", value); rt.addValue("Pixels", stats.pixelCount); // Optionally save histogram to file int[] hist = (stats.histogram16 != null) ? stats.histogram16 : stats.histogram; if (hist != null) { final double particleValue = value; out = writeHistogram(out, rt.getCounter(), particleValue, hist); if (particleSummary) { // Create and store a cumulative histogram if we are summarising the particles if (summaryHistogram.containsKey(particleValue)) { int[] hist2 = summaryHistogram.get(particleValue); if (hist.length < hist2.length) { int[] tmp = hist; hist = hist2; hist2 = tmp; } for (int i = 0; i < hist2.length; i++) hist[i] += hist2[i]; } summaryHistogram.put(particleValue, hist); } } // Copy the superclass methods using the super-class variables obtained from relfection if (addToManager) { if (roiManager == null) { if (Macro.getOptions() != null && Interpreter.isBatchMode()) roiManager = Interpreter.getBatchModeRoiManager(); if (roiManager == null) { Frame frame = WindowManager.getFrame("ROI Manager"); if (frame == null) IJ.run("ROI Manager..."); frame = WindowManager.getFrame("ROI Manager"); if (frame == null || !(frame instanceof RoiManager)) { addToManager = false; return; } roiManager = (RoiManager) frame; } if (resetCounter) roiManager.runCommand("reset"); } if (imp.getStackSize() > 1) roi.setPosition(imp.getCurrentSlice()); if (lineWidth != 1) roi.setStrokeWidth(lineWidth); roiManager.add(imp, roi, rt.getCounter()); } if (showResultsWindow && showResults) rt.addResults(); } private void createSummary() { int nRows = rt.getCounter(); String label = (nRows > 0) ? rt.getLabel(0) : null; // The second last column is the particle value // The last column is the number of pixels double[] particles = rt.getColumnAsDoubles(rt.getLastColumn() - 1); double[] nPixels = rt.getColumnAsDoubles(rt.getLastColumn()); // Summarise only certain columns: int[] toProcess = new int[] { ResultsTable.AREA, ResultsTable.MEAN, ResultsTable.MIN, ResultsTable.MAX, ResultsTable.X_CENTER_OF_MASS, ResultsTable.Y_CENTER_OF_MASS, ResultsTable.INTEGRATED_DENSITY, ResultsTable.RAW_INTEGRATED_DENSITY }; int next = 0; double[][] values = new double[toProcess.length][]; for (int i = 0; i < rt.getLastColumn(); i++) { if (toProcess[next] != i) continue; if (rt.columnExists(i)) { values[next] = rt.getColumnAsDoubles(i); } if (++next == toProcess.length) break; } // Map all particles to a single result HashMap<Double, double[]> map = new HashMap<Double, double[]>(); LinkedList<Double> order = new LinkedList<Double>(); // Now summarise for (int r = 0; r < nRows; r++) { double particle = particles[r]; double n = nPixels[r]; // Get the data to be summarised double[] data = new double[toProcess.length + 2]; // AREA => sum this if (values[0] != null) data[0] = values[0][r]; // MEAN => multiply by nPixels and sum, divide at end by nPixels if (values[1] != null) data[1] = values[1][r] * n; // MIN => Find min if (values[2] != null) data[2] = values[2][r]; // MAX => Find max if (values[3] != null) data[3] = values[3][r]; // X_CENTER_OF_MASS => multiply by nPixels and sum if (values[4] != null) data[4] = values[4][r] * n; // Y_CENTER_OF_MASS => multiply by nPixels and sum, divide at end by nPixels if (values[5] != null) data[5] = values[5][r] * n; // INTEGRATED_DENSITY == area*mean => Just compute at end // RAW_INTEGRATED_DENSITY == sum of pixels => sum if (values[7] != null) data[7] = values[7][r]; data[8] = n; data[9] = 1; // Find the record for the summary if (map.containsKey(particle)) { double[] record = map.get(particle); // AREA => sum this record[0] += data[0]; // MEAN => multiply by nPixels and sum, divide at end by nPixels record[1] += data[1]; // MIN => Find min record[2] = Math.min(data[2], record[2]); // MAX => Find max record[3] = Math.max(data[3], record[3]); // X_CENTER_OF_MASS => multiply by nPixels and sum, divide at end by nPixels record[4] += data[4]; // Y_CENTER_OF_MASS => multiply by nPixels and sum, divide at end by nPixels record[5] += data[5]; // INTEGRATED_DENSITY == area*mean => Just compute at end // RAW_INTEGRATED_DENSITY == sum of pixels => sum record[7] += data[7]; // nPixels record[8] += data[8]; // nParticles record[9] += data[9]; } else { map.put(particle, data); order.add(particle); } } // Produce summary ResultsTable summary = new ResultsTable(); if (summary.getColumnHeading(ResultsTable.LAST_HEADING) == null) summary.setDefaultHeadings(); for (Double particle : order) { summary.incrementCounter(); if (label != null) summary.addLabel(label); double[] data = map.get(particle); double n = data[8]; // AREA => sum this if (values[0] != null) summary.addValue(ResultsTable.AREA, data[0]); // MEAN => multiply by nPixels and sum, divide at end by nPixels if (values[1] != null) summary.addValue(ResultsTable.MEAN, data[1] /= n); // MIN => Find min if (values[2] != null) summary.addValue(ResultsTable.MIN, data[2]); // MAX => Find max if (values[3] != null) summary.addValue(ResultsTable.MAX, data[3]); // X_CENTER_OF_MASS => multiply by nPixels and sum, divide at end by nPixels if (values[4] != null) summary.addValue(ResultsTable.X_CENTER_OF_MASS, data[4] / n); // Y_CENTER_OF_MASS => multiply by nPixels and sum, divide at end by nPixels if (values[5] != null) summary.addValue(ResultsTable.Y_CENTER_OF_MASS, data[5] / n); // INTEGRATED_DENSITY == area*mean => Just compute at end if (values[6] != null) // Assumes that data[0] and data[1] were also present summary.addValue(ResultsTable.INTEGRATED_DENSITY, data[0] * data[1]); // RAW_INTEGRATED_DENSITY == sum of pixels => sum if (values[7] != null) summary.addValue(ResultsTable.RAW_INTEGRATED_DENSITY, data[7]); summary.addValue("Particle Value", particle.doubleValue()); summary.addValue("Pixels", data[8]); summary.addValue("Particles", data[9]); } String windowTitle = "Particle Summary"; // This method does not work on my JRE as closing a results window throws an exception // leaving the frame still in memory but not visible //summary.show(windowTitle); TextWindow win = null; String tableHeadings = summary.getColumnHeadings(); boolean newWindow = false; // This method does not check the frame is visible //Frame frame = WindowManager.getFrame(windowTitle); // Find the results table if visible for (Frame frame : WindowManager.getNonImageWindows()) { if (frame != null && frame instanceof TextWindow && frame.isVisible()) { if (windowTitle.equals(frame.getTitle())) { win = (TextWindow) frame; break; } } } if (win == null) { // Create a new window matching the size of the "Results" table int w = (int) Prefs.get(TextWindow.WIDTH_KEY, 800); int h = (int) Prefs.get(TextWindow.HEIGHT_KEY, 250); win = new TextWindow(windowTitle, tableHeadings, "", w, h); newWindow = true; } TextPanel tp = win.getTextPanel(); if (!newWindow) // Setting columns headings forces the table to be reset tp.setColumnHeadings(tableHeadings); tp.setResultsTable(summary); int n = summary.getCounter(); if (n > 0) { if (tp.getLineCount() > 0) tp.clear(); StringBuilder sb = new StringBuilder(n * tableHeadings.length()); for (int i = 0; i < n; i++) sb.append(summary.getRowAsString(i)).append("\n"); // Adding all the data in one go does not auto-adjust column width tp.append(sb.toString()); } // Forces auto column width calculation tp.scrollToTop(); // Optionally save summary histogram to file if (saveHistogram) saveSummaryHistogram(order); } private void saveSummaryHistogram(List<Double> order) { if (summaryHistogram.isEmpty()) return; String summaryFilename = createSummaryFilename(histogramFile); BufferedWriter out = createOutput(summaryFilename); int id = 1; for (Double value : order) { out = writeHistogram(out, id++, value, summaryHistogram.get(value)); } close(out); } private String createSummaryFilename(String filename) { // The histogramFile had a default .txt, so look for the extension and insert 'summary' int i = filename.lastIndexOf('.'); return filename.substring(0, i) + ".summary" + filename.substring(i); } }