package ij.plugin.filter; import java.awt.*; import java.awt.image.IndexColorModel; import java.util.Properties; import gdsc.core.ij.Utils; import ij.*; import ij.gui.*; import ij.process.*; import ij.measure.*; import ij.text.*; import ij.plugin.filter.Analyzer; import ij.plugin.frame.RoiManager; import ij.plugin.Colors; import ij.macro.Interpreter; import ij.util.Tools; /** * Implements ImageJ's Analyze Particles command. * <p> * * <pre> * for each line do * for each pixel in this line do * if the pixel value is "inside" the threshold range then * trace the edge to mark the object * do the measurement * fill the object with a color outside the threshold range * else * continue the scan * </pre> */ @SuppressWarnings("unused") public class ParticleAnalyzerCopy implements PlugInFilter, Measurements { /** Display results in the ImageJ console. */ public static final int SHOW_RESULTS = 1; /** Obsolete, replaced by DISPLAY_SUMMARY */ public static final int SHOW_SUMMARY = 2; /** Display image containing outlines of measured particles. */ public static final int SHOW_OUTLINES = 4; /** Do not measure particles touching edge of image. */ public static final int EXCLUDE_EDGE_PARTICLES = 8; /** Display image containing grayscales masks that identify measured particles. */ public static final int SHOW_ROI_MASKS = 16; /** Display a progress bar. */ public static final int SHOW_PROGRESS = 32; /** Clear ImageJ console before starting. */ public static final int CLEAR_WORKSHEET = 64; /** Record starting coordinates so outline can be recreated later using doWand(x,y). */ public static final int RECORD_STARTS = 128; /** Display a summary. */ public static final int DISPLAY_SUMMARY = 256; /** Do not display particle outline image. */ public static final int SHOW_NONE = 512; /** Flood fill to ignore interior holes. */ public static final int INCLUDE_HOLES = 1024; /** Add particles to ROI Manager. */ public static final int ADD_TO_MANAGER = 2048; /** Display image containing binary masks of measured particles. */ public static final int SHOW_MASKS = 4096; /** Use 4-connected particle tracing. */ public static final int FOUR_CONNECTED = 8192; /** Replace original image with masks. */ public static final int IN_SITU_SHOW = 16384; /** Display particle outlines as an overlay. */ public static final int SHOW_OVERLAY_OUTLINES = 32768; /** Display filled particle as an overlay. */ public static final int SHOW_OVERLAY_MASKS = 65536; static final String OPTIONS = "ap.options"; static final int BYTE = 0, SHORT = 1, FLOAT = 2, RGB = 3; static final double DEFAULT_MIN_SIZE = 0.0; static final double DEFAULT_MAX_SIZE = Double.POSITIVE_INFINITY; private static double staticMinSize = 0.0; private static double staticMaxSize = DEFAULT_MAX_SIZE; private static boolean pixelUnits; private static int staticOptions = Prefs.getInt(OPTIONS, CLEAR_WORKSHEET); private static String[] showStrings = { "Nothing", "Outlines", "Bare Outlines", "Ellipses", "Masks", "Count Masks", "Overlay Outlines", "Overlay Masks" }; private static double staticMinCircularity = 0.0, staticMaxCircularity = 1.0; private static String prevHdr; protected static final int NOTHING = 0, OUTLINES = 1, BARE_OUTLINES = 2, ELLIPSES = 3, MASKS = 4, ROI_MASKS = 5, OVERLAY_OUTLINES = 6, OVERLAY_MASKS = 7; protected static int staticShowChoice; protected ImagePlus imp; protected ResultsTable rt; protected Analyzer analyzer; protected int slice; protected boolean processStack; protected boolean showResults, excludeEdgeParticles, showSizeDistribution, resetCounter, showProgress, recordStarts, displaySummary, floodFill, addToManager, inSituShow; protected boolean showResultsWindow = true; private String summaryHdr = "Slice\tCount\tTotal Area\tAverage Size\t%Area"; private double level1, level2; private double minSize, maxSize; private double minCircularity, maxCircularity; private int showChoice; private int options; private int measurements; private Calibration calibration; private String arg; private double fillColor; private boolean thresholdingLUT; private ImageProcessor drawIP; private int width, height; private boolean canceled; private ImageStack outlines; private IndexColorModel customLut; private int particleCount; private int maxParticleCount = 0; private int totalCount; private TextWindow tw; private Wand wand; private int imageType, imageType2; private boolean roiNeedsImage; private int minX, maxX, minY, maxY; private ImagePlus redirectImp; private ImageProcessor redirectIP; private PolygonFiller pf; private Roi saveRoi; private int beginningCount; private Rectangle r; private ImageProcessor mask; private double totalArea; private FloodFiller ff; private Polygon polygon; protected RoiManager roiManager; private static RoiManager staticRoiManager; private static ResultsTable staticResultsTable; private ImagePlus outputImage; private boolean hideOutputImage; private int roiType; private int wandMode = Wand.LEGACY_MODE; private Overlay overlay; boolean blackBackground; private static int defaultFontSize = 9; private static int nextFontSize = defaultFontSize; private static Color defaultFontColor = Color.red; private static Color nextFontColor = defaultFontColor; private static int nextLineWidth = 1; private int fontSize = nextFontSize; private Color fontColor = nextFontColor; protected int lineWidth = nextLineWidth; /** * Constructs a ParticleAnalyzerCopy. * * @param options * a flag word created by Oring SHOW_RESULTS, EXCLUDE_EDGE_PARTICLES, etc. * @param measurements * a flag word created by ORing constants defined in the Measurements interface * @param rt * a ResultsTable where the measurements will be stored * @param minSize * the smallest particle size in pixels * @param maxSize * the largest particle size in pixels * @param minCirc * minimum circularity * @param maxCirc * maximum circularity */ public ParticleAnalyzerCopy(int options, int measurements, ResultsTable rt, double minSize, double maxSize, double minCirc, double maxCirc) { this.options = options; this.measurements = measurements; this.rt = rt; if (this.rt == null) this.rt = new ResultsTable(); this.minSize = minSize; this.maxSize = maxSize; this.minCircularity = minCirc; this.maxCircularity = maxCirc; slice = 1; if ((options & SHOW_ROI_MASKS) != 0) showChoice = ROI_MASKS; if ((options & SHOW_OVERLAY_OUTLINES) != 0) showChoice = OVERLAY_OUTLINES; if ((options & SHOW_OVERLAY_MASKS) != 0) showChoice = OVERLAY_MASKS; if ((options & SHOW_OUTLINES) != 0) showChoice = OUTLINES; if ((options & SHOW_MASKS) != 0) showChoice = MASKS; if ((options & SHOW_NONE) != 0) showChoice = NOTHING; if ((options & FOUR_CONNECTED) != 0) { wandMode = Wand.FOUR_CONNECTED; options |= INCLUDE_HOLES; } nextFontSize = defaultFontSize; nextFontColor = defaultFontColor; nextLineWidth = 1; } /** Constructs a ParticleAnalyzerCopy using the default min and max circularity values (0 and 1). */ public ParticleAnalyzerCopy(int options, int measurements, ResultsTable rt, double minSize, double maxSize) { this(options, measurements, rt, minSize, maxSize, 0.0, 1.0); } /** Default constructor */ public ParticleAnalyzerCopy() { slice = 1; } public int setup(String arg, ImagePlus imp) { this.arg = arg; this.imp = imp; IJ.register(ParticleAnalyzerCopy.class); if (imp == null) { IJ.noImage(); return DONE; } if (imp.getBitDepth() == 24 && !isThresholdedRGB(imp)) { IJ.error("Particle Analyzer", "RGB images must be thresholded using\n" + "Image>Adjust>Color Threshold."); return DONE; } if (!showDialog()) return DONE; int baseFlags = DOES_ALL + NO_CHANGES + NO_UNDO; int flags = IJ.setupDialog(imp, baseFlags); processStack = (flags & DOES_STACKS) != 0; slice = 0; saveRoi = imp.getRoi(); if (saveRoi != null && saveRoi.getType() != Roi.RECTANGLE && saveRoi.isArea()) polygon = saveRoi.getPolygon(); imp.startTiming(); nextFontSize = defaultFontSize; nextFontColor = defaultFontColor; nextLineWidth = 1; return flags; } public void run(ImageProcessor ip) { if (canceled) return; slice++; if (imp.getStackSize() > 1 && processStack) imp.setSlice(slice); if (imp.getType() == ImagePlus.COLOR_RGB) { ip = (ImageProcessor) imp.getProperty("Mask"); ip.setThreshold(255, 255, ImageProcessor.NO_LUT_UPDATE); } if (!analyze(imp, ip)) canceled = true; if (slice == imp.getStackSize()) { imp.updateAndDraw(); if (saveRoi != null) imp.setRoi(saveRoi); } } /** Displays a modal options dialog. */ public boolean showDialog() { Calibration cal = imp != null ? imp.getCalibration() : (new Calibration()); double unitSquared = cal.pixelWidth * cal.pixelHeight; if (pixelUnits) unitSquared = 1.0; if (Macro.getOptions() != null) { boolean oldMacro = updateMacroOptions(); if (oldMacro) unitSquared = 1.0; staticMinSize = 0.0; staticMaxSize = DEFAULT_MAX_SIZE; staticMinCircularity = 0.0; staticMaxCircularity = 1.0; staticShowChoice = NOTHING; } GenericDialog gd = new GenericDialog("Analyze Particles"); minSize = staticMinSize; maxSize = staticMaxSize; minCircularity = staticMinCircularity; maxCircularity = staticMaxCircularity; showChoice = staticShowChoice; if (maxSize == 999999) maxSize = DEFAULT_MAX_SIZE; options = staticOptions; String unit = cal.getUnit(); boolean scaled = cal.scaled(); String units = unit + "^2"; int places = 0; double cmin = minSize * unitSquared; if ((int) cmin != cmin) places = 2; double cmax = maxSize * unitSquared; if ((int) cmax != cmax && cmax != DEFAULT_MAX_SIZE) places = 2; String minStr = ResultsTable.d2s(cmin, places); if (minStr.indexOf("-") != -1) { for (int i = places; i <= 6; i++) { minStr = ResultsTable.d2s(cmin, i); if (minStr.indexOf("-") == -1) break; } } String maxStr = ResultsTable.d2s(cmax, places); if (maxStr.indexOf("-") != -1) { for (int i = places; i <= 6; i++) { maxStr = ResultsTable.d2s(cmax, i); if (maxStr.indexOf("-") == -1) break; } } if (scaled) gd.setInsets(5, 0, 0); gd.addStringField("Size (" + units + "):", minStr + "-" + maxStr, 12); if (scaled) { gd.setInsets(0, 40, 5); gd.addCheckbox("Pixel units", pixelUnits); } gd.addStringField("Circularity:", IJ.d2s(minCircularity) + "-" + IJ.d2s(maxCircularity), 12); gd.addChoice("Show:", showStrings, showStrings[showChoice]); String[] labels = new String[8]; boolean[] states = new boolean[8]; labels[0] = "Display results"; states[0] = (options & SHOW_RESULTS) != 0; labels[1] = "Exclude on edges"; states[1] = (options & EXCLUDE_EDGE_PARTICLES) != 0; labels[2] = "Clear results"; states[2] = (options & CLEAR_WORKSHEET) != 0; labels[3] = "Include holes"; states[3] = (options & INCLUDE_HOLES) != 0; labels[4] = "Summarize"; states[4] = (options & DISPLAY_SUMMARY) != 0; labels[5] = "Record starts"; states[5] = false; labels[6] = "Add to Manager"; states[6] = (options & ADD_TO_MANAGER) != 0; labels[7] = "In_situ Show"; states[7] = (options & IN_SITU_SHOW) != 0; gd.addCheckboxGroup(4, 2, labels, states); gd.addHelp(IJ.URL + "/docs/menus/analyze.html#ap"); gd.showDialog(); if (gd.wasCanceled()) return false; String size = gd.getNextString(); // min-max size if (scaled) pixelUnits = gd.getNextBoolean(); if (pixelUnits) unitSquared = 1.0; else unitSquared = cal.pixelWidth * cal.pixelHeight; String[] minAndMax = Tools.split(size, " -"); double mins = gd.parseDouble(minAndMax[0]); double maxs = minAndMax.length == 2 ? gd.parseDouble(minAndMax[1]) : Double.NaN; minSize = Double.isNaN(mins) ? DEFAULT_MIN_SIZE : mins / unitSquared; maxSize = Double.isNaN(maxs) ? DEFAULT_MAX_SIZE : maxs / unitSquared; if (minSize < DEFAULT_MIN_SIZE) minSize = DEFAULT_MIN_SIZE; if (maxSize < minSize) maxSize = DEFAULT_MAX_SIZE; staticMinSize = minSize; staticMaxSize = maxSize; minAndMax = Tools.split(gd.getNextString(), " -"); // min-max circularity double minc = gd.parseDouble(minAndMax[0]); double maxc = minAndMax.length == 2 ? gd.parseDouble(minAndMax[1]) : Double.NaN; minCircularity = Double.isNaN(minc) ? 0.0 : minc; maxCircularity = Double.isNaN(maxc) ? 1.0 : maxc; if (minCircularity < 0.0 || minCircularity > 1.0) minCircularity = 0.0; if (maxCircularity < minCircularity || maxCircularity > 1.0) maxCircularity = 1.0; if (minCircularity == 1.0 && maxCircularity == 1.0) minCircularity = 0.0; staticMinCircularity = minCircularity; staticMaxCircularity = maxCircularity; if (gd.invalidNumber()) { IJ.error("Bins invalid."); canceled = true; return false; } showChoice = gd.getNextChoiceIndex(); staticShowChoice = showChoice; if (gd.getNextBoolean()) options |= SHOW_RESULTS; else options &= ~SHOW_RESULTS; if (gd.getNextBoolean()) options |= EXCLUDE_EDGE_PARTICLES; else options &= ~EXCLUDE_EDGE_PARTICLES; if (gd.getNextBoolean()) options |= CLEAR_WORKSHEET; else options &= ~CLEAR_WORKSHEET; if (gd.getNextBoolean()) options |= INCLUDE_HOLES; else options &= ~INCLUDE_HOLES; if (gd.getNextBoolean()) options |= DISPLAY_SUMMARY; else options &= ~DISPLAY_SUMMARY; if (gd.getNextBoolean()) options |= RECORD_STARTS; else options &= ~RECORD_STARTS; if (gd.getNextBoolean()) options |= ADD_TO_MANAGER; else options &= ~ADD_TO_MANAGER; if (gd.getNextBoolean()) options |= IN_SITU_SHOW; else options &= ~IN_SITU_SHOW; staticOptions = options; options |= SHOW_PROGRESS; if ((options & DISPLAY_SUMMARY) != 0) Analyzer.setMeasurements(Analyzer.getMeasurements() | AREA); return true; } private boolean isThresholdedRGB(ImagePlus imp) { Object obj = imp.getProperty("Mask"); if (obj == null || !(obj instanceof ImageProcessor)) return false; ImageProcessor mask = (ImageProcessor) obj; return mask.getWidth() == imp.getWidth() && mask.getHeight() == imp.getHeight(); } boolean updateMacroOptions() { String options = Macro.getOptions(); int index = options.indexOf("maximum="); if (index == -1) return false; index += 8; int len = options.length(); while (index < len - 1 && options.charAt(index) != ' ') index++; if (index == len - 1) return false; int min = (int) Tools.parseDouble(Macro.getValue(options, "minimum", "1")); int max = (int) Tools.parseDouble(Macro.getValue(options, "maximum", "999999")); options = "size=" + min + "-" + max + options.substring(index, len); Macro.setOptions(options); return true; } /** * Performs particle analysis on the specified image. Returns * false if there is an error. */ public boolean analyze(ImagePlus imp) { return analyze(imp, imp.getProcessor()); } /** * Performs particle analysis on the specified ImagePlus and * ImageProcessor. Returns false if there is an error. */ public boolean analyze(ImagePlus imp, ImageProcessor ip) { if (this.imp == null) this.imp = imp; showResults = (options & SHOW_RESULTS) != 0; excludeEdgeParticles = (options & EXCLUDE_EDGE_PARTICLES) != 0; resetCounter = (options & CLEAR_WORKSHEET) != 0; showProgress = (options & SHOW_PROGRESS) != 0; floodFill = (options & INCLUDE_HOLES) == 0; recordStarts = (options & RECORD_STARTS) != 0; addToManager = (options & ADD_TO_MANAGER) != 0; if (staticRoiManager != null) { addToManager = true; roiManager = staticRoiManager; staticRoiManager = null; } if (staticResultsTable != null) { rt = staticResultsTable; staticResultsTable = null; showResultsWindow = false; } displaySummary = (options & DISPLAY_SUMMARY) != 0 || (options & SHOW_SUMMARY) != 0; inSituShow = (options & IN_SITU_SHOW) != 0; outputImage = null; ip.snapshot(); ip.setProgressBar(null); if (Analyzer.isRedirectImage()) { redirectImp = Analyzer.getRedirectImage(imp); if (redirectImp == null) return false; int depth = redirectImp.getStackSize(); if (depth > 1 && depth == imp.getStackSize()) { ImageStack redirectStack = redirectImp.getStack(); redirectIP = redirectStack.getProcessor(imp.getCurrentSlice()); } else redirectIP = redirectImp.getProcessor(); } else if (imp.getType() == ImagePlus.COLOR_RGB) { ImagePlus original = (ImagePlus) imp.getProperty("OriginalImage"); if (original != null && original.getWidth() == imp.getWidth() && original.getHeight() == imp.getHeight()) { redirectImp = original; redirectIP = original.getProcessor(); } } if (!setThresholdLevels(imp, ip)) return false; width = ip.getWidth(); height = ip.getHeight(); if (!(showChoice == NOTHING || showChoice == OVERLAY_OUTLINES || showChoice == OVERLAY_MASKS)) { blackBackground = Prefs.blackBackground && inSituShow; if (slice == 1) outlines = new ImageStack(width, height); if (showChoice == ROI_MASKS) drawIP = new ShortProcessor(width, height); else drawIP = new ByteProcessor(width, height); drawIP.setLineWidth(lineWidth); if (showChoice == ROI_MASKS) { } // Place holder for now... else if (showChoice == MASKS && !blackBackground) drawIP.invertLut(); else if (showChoice == OUTLINES) { if (!inSituShow) { if (customLut == null) makeCustomLut(); drawIP.setColorModel(customLut); } drawIP.setFont(new Font("SansSerif", Font.PLAIN, fontSize)); if (fontSize > 12 && inSituShow) drawIP.setAntialiasedText(true); } outlines.addSlice(null, drawIP); if (showChoice == ROI_MASKS || blackBackground) { drawIP.setColor(Color.black); drawIP.fill(); drawIP.setColor(Color.white); } else { drawIP.setColor(Color.white); drawIP.fill(); drawIP.setColor(Color.black); } } calibration = redirectImp != null ? redirectImp.getCalibration() : imp.getCalibration(); if (rt == null) { rt = Analyzer.getResultsTable(); analyzer = new Analyzer(imp); } else { if (measurements == 0) measurements = Analyzer.getMeasurements(); analyzer = new Analyzer(imp, measurements, rt); } if (resetCounter && slice == 1) { if (!Analyzer.resetCounter()) return false; } beginningCount = Analyzer.getCounter(); byte[] pixels = null; if (ip instanceof ByteProcessor) pixels = (byte[]) ip.getPixels(); if (r == null) { r = ip.getRoi(); mask = ip.getMask(); if (displaySummary) { if (mask != null) totalArea = ImageStatistics.getStatistics(ip, AREA, calibration).area; else totalArea = r.width * calibration.pixelWidth * r.height * calibration.pixelHeight; } } minX = r.x; maxX = r.x + r.width; minY = r.y; maxY = r.y + r.height; if (r.width < width || r.height < height || mask != null) { if (!eraseOutsideRoi(ip, r, mask)) return false; } int offset; double value; int inc = Math.max(r.height / 25, 1); int mi = 0; ImageWindow win = imp.getWindow(); if (win != null) win.running = true; if (measurements == 0) measurements = Analyzer.getMeasurements(); if (showChoice == ELLIPSES) measurements |= ELLIPSE; measurements &= ~LIMIT; // ignore "Limit to Threshold" roiNeedsImage = (measurements & PERIMETER) != 0 || (measurements & SHAPE_DESCRIPTORS) != 0 || (measurements & FERET) != 0; particleCount = 0; wand = new Wand(ip); pf = new PolygonFiller(); if (floodFill) { ImageProcessor ipf = ip.duplicate(); ipf.setValue(fillColor); ff = new FloodFiller(ipf); } roiType = Wand.allPoints() ? Roi.FREEROI : Roi.TRACED_ROI; for (int y = r.y; y < (r.y + r.height); y++) { offset = y * width; for (int x = r.x; x < (r.x + r.width); x++) { if (pixels != null) value = pixels[offset + x] & 255; else if (imageType == SHORT) value = ip.getPixel(x, y); else value = ip.getPixelValue(x, y); if (value >= level1 && value <= level2) analyzeParticle(x, y, imp, ip); } if (showProgress && ((y % inc) == 0)) IJ.showProgress((double) (y - r.y) / r.height); if (win != null) canceled = !win.running; if (canceled) { Macro.abort(); break; } } if (showProgress) IJ.showProgress(1.0); if (showResults && showResultsWindow) rt.updateResults(); imp.deleteRoi(); ip.resetRoi(); ip.reset(); if (displaySummary && IJ.getInstance() != null) updateSliceSummary(); if (addToManager && roiManager != null) roiManager.setEditMode(imp, true); maxParticleCount = (particleCount > maxParticleCount) ? particleCount : maxParticleCount; totalCount += particleCount; if (!canceled) showResults(); return true; } void updateSliceSummary() { int slices = imp.getStackSize(); float[] areas = rt.getColumn(ResultsTable.AREA); if (areas == null) areas = new float[0]; String label = imp.getTitle(); if (slices > 1) { if (processStack) label = imp.getStack().getShortSliceLabel(slice); else label = imp.getStack().getShortSliceLabel(imp.getCurrentSlice()); label = label != null && !label.equals("") ? label : "" + slice; } String aLine = null; double sum = 0.0; int start = areas.length - particleCount; if (start < 0) return; for (int i = start; i < areas.length; i++) sum += areas[i]; int places = Analyzer.getPrecision(); Calibration cal = imp.getCalibration(); String total = "\t" + ResultsTable.d2s(sum, places); String average = "\t" + ResultsTable.d2s(sum / particleCount, places); String fraction = "\t" + ResultsTable.d2s(sum * 100.0 / totalArea, places); aLine = label + "\t" + particleCount + total + average + fraction; aLine = addMeans(aLine, areas.length > 0 ? start : -1); if (slices == 1) { Frame frame = WindowManager.getFrame("Summary"); if (frame != null && (frame instanceof TextWindow) && summaryHdr.equals(prevHdr)) tw = (TextWindow) frame; } else { Frame frame = WindowManager.getFrame("Summary of " + imp.getTitle()); if (frame != null && (frame instanceof TextWindow) && summaryHdr.equals(prevHdr)) tw = (TextWindow) frame; } if (tw == null) { String title = slices == 1 ? "Summary" : "Summary of " + imp.getTitle(); tw = new TextWindow(title, summaryHdr, aLine, 450, 300); prevHdr = summaryHdr; } else tw.append(aLine); } String addMeans(String line, int start) { if ((measurements & MEAN) != 0) line = addMean(ResultsTable.MEAN, line, start); if ((measurements & MODE) != 0) line = addMean(ResultsTable.MODE, line, start); if ((measurements & PERIMETER) != 0) line = addMean(ResultsTable.PERIMETER, line, start); if ((measurements & ELLIPSE) != 0) { line = addMean(ResultsTable.MAJOR, line, start); line = addMean(ResultsTable.MINOR, line, start); line = addMean(ResultsTable.ANGLE, line, start); } if ((measurements & SHAPE_DESCRIPTORS) != 0) { line = addMean(ResultsTable.CIRCULARITY, line, start); line = addMean(ResultsTable.SOLIDITY, line, start); } if ((measurements & FERET) != 0) { line = addMean(ResultsTable.FERET, line, start); line = addMean(ResultsTable.FERET_X, line, start); line = addMean(ResultsTable.FERET_Y, line, start); line = addMean(ResultsTable.FERET_ANGLE, line, start); line = addMean(ResultsTable.MIN_FERET, line, start); } if ((measurements & INTEGRATED_DENSITY) != 0) line = addMean(ResultsTable.INTEGRATED_DENSITY, line, start); if ((measurements & MEDIAN) != 0) line = addMean(ResultsTable.MEDIAN, line, start); if ((measurements & SKEWNESS) != 0) line = addMean(ResultsTable.SKEWNESS, line, start); if ((measurements & KURTOSIS) != 0) line = addMean(ResultsTable.KURTOSIS, line, start); return line; } private String addMean(int column, String line, int start) { if (start == -1) { line += "\tNaN"; summaryHdr += "\t" + ResultsTable.getDefaultHeading(column); } else { float[] c = column >= 0 ? rt.getColumn(column) : null; if (c != null) { ImageProcessor ip = new FloatProcessor(c.length, 1, c, null); if (ip == null) return line; ip.setRoi(start, 0, ip.getWidth() - start, 1); ip = ip.crop(); ImageStatistics stats = new FloatStatistics(ip); if (stats == null) return line; line += n(stats.mean); } else line += "\tNaN"; summaryHdr += "\t" + rt.getColumnHeading(column); } return line; } String n(double n) { String s; if (Math.round(n) == n) s = ResultsTable.d2s(n, 0); else s = ResultsTable.d2s(n, Analyzer.getPrecision()); return "\t" + s; } boolean eraseOutsideRoi(ImageProcessor ip, Rectangle r, ImageProcessor mask) { int width = ip.getWidth(); int height = ip.getHeight(); ip.setRoi(r); if (excludeEdgeParticles && polygon != null) { ImageStatistics stats = ImageStatistics.getStatistics(ip, MIN_MAX, null); if (fillColor >= stats.min && fillColor <= stats.max) { double replaceColor = level1 - 1.0; if (replaceColor < 0.0 || replaceColor == fillColor) { replaceColor = level2 + 1.0; int maxColor = imageType == BYTE ? 255 : 65535; if (replaceColor > maxColor || replaceColor == fillColor) { IJ.error("Particle Analyzer", "Unable to remove edge particles"); return false; } } for (int y = minY; y < maxY; y++) { for (int x = minX; x < maxX; x++) { int v = ip.getPixel(x, y); if (v == fillColor) ip.putPixel(x, y, (int) replaceColor); } } } } ip.setValue(fillColor); if (mask != null) { mask = mask.duplicate(); mask.invert(); ip.fill(mask); } ip.setRoi(0, 0, r.x, height); ip.fill(); ip.setRoi(r.x, 0, r.width, r.y); ip.fill(); ip.setRoi(r.x, r.y + r.height, r.width, height - (r.y + r.height)); ip.fill(); ip.setRoi(r.x + r.width, 0, width - (r.x + r.width), height); ip.fill(); ip.resetRoi(); //IJ.log("erase: "+fillColor+" "+level1+" "+level2+" "+excludeEdgeParticles); //(new ImagePlus("ip2", ip.duplicate())).show(); return true; } boolean setThresholdLevels(ImagePlus imp, ImageProcessor ip) { double t1 = ip.getMinThreshold(); double t2 = ip.getMaxThreshold(); boolean invertedLut = imp.isInvertedLut(); boolean byteImage = ip instanceof ByteProcessor; 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)) { IJ.error("Particle Analyzer", "A thresholded image or 8-bit binary image is\n" + "required. Threshold levels can be set using\n" + "the Image->Adjust->Threshold tool."); canceled = true; return false; } boolean threshold255 = invertedLut; if (Prefs.blackBackground) threshold255 = !threshold255; if (threshold255) { level1 = 255; level2 = 255; fillColor = 64; } else { level1 = 0; level2 = 0; fillColor = 192; } } else { level1 = t1; level2 = t2; if (imageType == BYTE) { if (level1 > 0) fillColor = 0; else if (level2 < 255) fillColor = 255; } else if (imageType == SHORT) { if (level1 > 0) fillColor = 0; else if (level2 < 65535) fillColor = 65535; } else if (imageType == FLOAT) fillColor = -Float.MAX_VALUE; else return false; } imageType2 = imageType; if (redirectIP != null) { if (redirectIP instanceof ShortProcessor) imageType2 = SHORT; else if (redirectIP instanceof FloatProcessor) imageType2 = FLOAT; else if (redirectIP instanceof ColorProcessor) imageType2 = RGB; else imageType2 = BYTE; } return true; } int counter = 0; protected void analyzeParticle(int x, int y, ImagePlus imp, ImageProcessor ip) { //Wand wand = new Wand(ip); ImageProcessor ip2 = redirectIP != null ? redirectIP : ip; wand.autoOutline(x, y, level1, level2, wandMode); if (wand.npoints == 0) { IJ.log("wand error: " + x + " " + y); return; } Roi roi = new PolygonRoi(wand.xpoints, wand.ypoints, wand.npoints, roiType); Rectangle r = roi.getBounds(); if (r.width > 1 && r.height > 1) { PolygonRoi proi = (PolygonRoi) roi; pf.setPolygon(proi.getXCoordinates(), proi.getYCoordinates(), proi.getNCoordinates()); ip2.setMask(pf.getMask(r.width, r.height)); if (floodFill) { // For the MaskParticleAnalyzer flood filling does not work using the original image. // This is because it is filling between level1 & level2. If two objects touch so that // the bounding box of 1 enters that of the other then the analysis will be incorrect. // This can be fixed by duplicating the IP again so only the current object is present // or flood filling at the current IP value (faster) // Duplicate so only the current object is present //ImageProcessor ipf = ip.duplicate(); //ipf.setValue(fillColor); //ff = new FloodFiller(ipf); //ff.particleAnalyzerFill(x, y, level1, level2, ip2.getMask(), r); // Flood filling can use only the current pixel value final double value = (ip instanceof FloatProcessor) ? ip.getPixelValue(x, y) : ip.getPixel(x, y); ff.particleAnalyzerFill(x, y, value, value, ip2.getMask(), r); } } ip2.setRoi(r); ip.setValue(fillColor); ImageStatistics stats = getStatistics(ip2, measurements, calibration); boolean include = true; if (excludeEdgeParticles) { if (r.x == minX || r.y == minY || r.x + r.width == maxX || r.y + r.height == maxY) include = false; if (polygon != null) { Rectangle bounds = roi.getBounds(); int x1 = bounds.x + wand.xpoints[wand.npoints - 1]; int y1 = bounds.y + wand.ypoints[wand.npoints - 1]; int x2, y2; for (int i = 0; i < wand.npoints; i++) { x2 = bounds.x + wand.xpoints[i]; y2 = bounds.y + wand.ypoints[i]; if (!polygon.contains(x2, y2)) { include = false; break; } if ((x1 == x2 && ip.getPixel(x1, y1 - 1) == fillColor) || (y1 == y2 && ip.getPixel(x1 - 1, y1) == fillColor)) { include = false; break; } x1 = x2; y1 = y2; } } } ImageProcessor mask = ip2.getMask(); if (minCircularity > 0.0 || maxCircularity < 1.0) { double perimeter = roi.getLength(); double circularity = perimeter == 0.0 ? 0.0 : 4.0 * Math.PI * (stats.pixelCount / (perimeter * perimeter)); if (circularity > 1.0) circularity = 1.0; //IJ.log(circularity+" "+perimeter+" "+stats.area); if (circularity < minCircularity || circularity > maxCircularity) include = false; } if (stats.pixelCount >= minSize && stats.pixelCount <= maxSize && include) { particleCount++; if (roiNeedsImage) roi.setImage(imp); stats.xstart = x; stats.ystart = y; saveResults(stats, roi); if (showChoice != NOTHING) drawParticle(drawIP, roi, stats, mask); } if (redirectIP != null) ip.setRoi(r); ip.fill(mask); } ImageStatistics getStatistics(ImageProcessor ip, int mOptions, Calibration cal) { switch (imageType2) { case BYTE: return new ByteStatistics(ip, mOptions, cal); case SHORT: return new ShortStatistics(ip, mOptions, cal); case FLOAT: return new FloatStatistics(ip, mOptions, cal); case RGB: return new ColorStatistics(ip, mOptions, cal); default: return null; } } /** * Saves statistics for one particle in a results table. This is * a method subclasses may want to override. */ protected void saveResults(ImageStatistics stats, Roi roi) { analyzer.saveResults(stats, roi); if (recordStarts) { rt.addValue("XStart", stats.xstart); rt.addValue("YStart", stats.ystart); } 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(); } /** * Draws a selected particle in a separate image. This is * another method subclasses may want to override. */ protected void drawParticle(ImageProcessor drawIP, Roi roi, ImageStatistics stats, ImageProcessor mask) { switch (showChoice) { case MASKS: drawFilledParticle(drawIP, roi, mask); break; case OUTLINES: case BARE_OUTLINES: case OVERLAY_OUTLINES: case OVERLAY_MASKS: drawOutline(drawIP, roi, rt.getCounter()); break; case ELLIPSES: drawEllipse(drawIP, stats, rt.getCounter()); break; case ROI_MASKS: drawRoiFilledParticle(drawIP, roi, mask, rt.getCounter()); break; default: } } void drawFilledParticle(ImageProcessor ip, Roi roi, ImageProcessor mask) { //IJ.write(roi.getBounds()+" "+mask.length); ip.setRoi(roi.getBounds()); ip.fill(mask); } void drawOutline(ImageProcessor ip, Roi roi, int count) { if (showChoice == OVERLAY_OUTLINES || showChoice == OVERLAY_MASKS) { if (overlay == null) { overlay = new Overlay(); overlay.drawLabels(true); overlay.setLabelFont(new Font("SansSerif", Font.PLAIN, fontSize)); } Roi roi2 = (Roi) roi.clone(); roi2.setStrokeColor(Color.cyan); if (lineWidth != 1) roi2.setStrokeWidth(lineWidth); if (showChoice == OVERLAY_MASKS) roi2.setFillColor(Color.cyan); overlay.add(roi2); } else { Rectangle r = roi.getBounds(); int nPoints = ((PolygonRoi) roi).getNCoordinates(); int[] xp = ((PolygonRoi) roi).getXCoordinates(); int[] yp = ((PolygonRoi) roi).getYCoordinates(); int x = r.x, y = r.y; if (!inSituShow) ip.setValue(0.0); ip.moveTo(x + xp[0], y + yp[0]); for (int i = 1; i < nPoints; i++) ip.lineTo(x + xp[i], y + yp[i]); ip.lineTo(x + xp[0], y + yp[0]); if (showChoice != BARE_OUTLINES) { String s = ResultsTable.d2s(count, 0); ip.moveTo(r.x + r.width / 2 - ip.getStringWidth(s) / 2, r.y + r.height / 2 + fontSize / 2); if (!inSituShow) ip.setValue(1.0); ip.drawString(s); } } } void drawEllipse(ImageProcessor ip, ImageStatistics stats, int count) { stats.drawEllipse(ip); } void drawRoiFilledParticle(ImageProcessor ip, Roi roi, ImageProcessor mask, int count) { int grayLevel = (count < 65535) ? count : 65535; ip.setValue((double) grayLevel); ip.setRoi(roi.getBounds()); ip.fill(mask); } void showResults() { int count = rt.getCounter(); // if (count==0) return; boolean lastSlice = !processStack || slice == imp.getStackSize(); if ((showChoice == OVERLAY_OUTLINES || showChoice == OVERLAY_MASKS) && slice == 1 && count > 0) imp.setOverlay(overlay); else if (outlines != null && lastSlice) { String title = imp != null ? imp.getTitle() : "Outlines"; String prefix; if (showChoice == MASKS) prefix = "Mask of "; else if (showChoice == ROI_MASKS) prefix = "Count Masks of "; else prefix = "Drawing of "; outlines.update(drawIP); outputImage = new ImagePlus(prefix + title, outlines); outputImage.setCalibration(imp.getCalibration()); if (inSituShow) { if (imp.getStackSize() == 1) Undo.setup(Undo.TRANSFORM, imp); imp.setStack(null, outputImage.getStack()); } else if (!hideOutputImage) outputImage.show(); } if (showResults && !processStack) { if (showResultsWindow) { TextPanel tp = IJ.getTextPanel(); if (beginningCount > 0 && tp != null && tp.getLineCount() != count) rt.show("Results"); } // XXX Set these using reflection //Analyzer.firstParticle = beginningCount; //Analyzer.lastParticle = Analyzer.getCounter()-1; MaskParticleAnalyzer.setAnalyzerFirstParticle(beginningCount); MaskParticleAnalyzer.setAnalyzerLastParticle(Analyzer.getCounter() - 1); } else { //Analyzer.firstParticle = Analyzer.lastParticle = 0; MaskParticleAnalyzer.setAnalyzerFirstParticle(0); MaskParticleAnalyzer.setAnalyzerLastParticle(0); } } /** * Returns the "Outlines", "Masks", "Elipses" or "Count Masks" image, * or null if "Nothing" is selected in the "Show:" menu. */ public ImagePlus getOutputImage() { return outputImage; } /** Set 'hideOutputImage' true to not display the "Show:" image. */ public void setHideOutputImage(boolean hideOutputImage) { this.hideOutputImage = hideOutputImage; } /** Sets the size of the font used to label outlines in the next particle analyzer instance. */ public static void setFontSize(int size) { nextFontSize = size; } /** * Sets the color ("blue", "black", etc.) of the font used to label outlines in the next particle analyzer instance. */ public static void setFontColor(String color) { nextFontColor = Colors.decode(color, defaultFontColor); } /** Sets the outline line width for the next ParticleAnalyzerCopy instance. */ public static void setLineWidth(int width) { nextLineWidth = width; } /** * Sets the RoiManager to be used by the next ParticleAnalyzerCopy * instance. There is a JavaScript example at * http://imagej.nih.gov/ij/macros/js/HiddenRoiManager.js */ public static void setRoiManager(RoiManager manager) { staticRoiManager = manager; } /** * Sets the ResultsTable to be used by the next * ParticleAnalyzerCopy instance. */ public static void setResultsTable(ResultsTable rt) { staticResultsTable = rt; } int getColumnID(String name) { int id = rt.getFreeColumn(name); if (id == ResultsTable.COLUMN_IN_USE) id = rt.getColumnIndex(name); return id; } void makeCustomLut() { IndexColorModel cm = (IndexColorModel) LookUpTable.createGrayscaleColorModel(false); byte[] reds = new byte[256]; byte[] greens = new byte[256]; byte[] blues = new byte[256]; cm.getReds(reds); cm.getGreens(greens); cm.getBlues(blues); reds[1] = (byte) fontColor.getRed(); greens[1] = (byte) fontColor.getGreen(); ; blues[1] = (byte) fontColor.getBlue(); ; customLut = new IndexColorModel(8, 256, reds, greens, blues); } /** Called once when ImageJ quits. */ public static void savePreferences(Properties prefs) { prefs.put(OPTIONS, Integer.toString(staticOptions)); } }