package gdsc.smlm.ij.plugins; /*----------------------------------------------------------------------------- * GDSC SMLM Software * * Copyright (C) 2016 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 3 of the License, or * (at your option) any later version. *---------------------------------------------------------------------------*/ import java.awt.AWTEvent; import java.awt.Color; import java.awt.Rectangle; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Comparator; import java.util.Iterator; import java.util.LinkedList; import java.util.Queue; import java.util.TreeSet; import org.apache.commons.math3.random.Well19937c; import org.apache.commons.math3.stat.descriptive.rank.Percentile; import gdsc.core.clustering.optics.ClusteringResult; import gdsc.core.clustering.optics.DBSCANResult; import gdsc.core.clustering.optics.OPTICSCluster; import gdsc.core.clustering.optics.OPTICSManager; import gdsc.core.clustering.optics.OPTICSManager.Option; import gdsc.core.clustering.optics.OPTICSResult; import gdsc.core.clustering.optics.SampleMode; import gdsc.core.ij.IJTrackProgress; import gdsc.core.ij.Utils; import gdsc.core.match.RandIndex; import gdsc.core.utils.ConvexHull; import gdsc.core.utils.Maths; import gdsc.core.utils.NotImplementedException; import gdsc.core.utils.Settings; import gdsc.core.utils.Sort; import gdsc.core.utils.TextUtils; import gdsc.smlm.ij.plugins.ResultsManager.InputSource; import gdsc.smlm.ij.results.IJImagePeakResults; import gdsc.smlm.ij.settings.GlobalSettings; import gdsc.smlm.ij.settings.OPTICSSettings; import gdsc.smlm.ij.settings.OPTICSSettings.ClusteringMode; import gdsc.smlm.ij.settings.OPTICSSettings.ImageMode; import gdsc.smlm.ij.settings.OPTICSSettings.OPTICSMode; import gdsc.smlm.ij.settings.OPTICSSettings.OutlineMode; import gdsc.smlm.ij.settings.OPTICSSettings.PlotMode; import gdsc.smlm.ij.settings.OPTICSSettings.SpanningTreeMode; import gdsc.smlm.ij.settings.SettingsManager; import gdsc.smlm.results.MemoryPeakResults; import gdsc.smlm.results.PeakResult; import gdsc.smlm.results.Trace; import ij.IJ; import ij.ImagePlus; import ij.Prefs; import ij.gui.DialogListener; import ij.gui.GenericDialog; import ij.gui.Line; import ij.gui.NonBlockingGenericDialog; import ij.gui.Overlay; import ij.gui.Plot; import ij.gui.Plot2; import ij.gui.PolygonRoi; import ij.gui.Roi; import ij.plugin.PlugIn; import ij.plugin.frame.Recorder; import ij.process.LUT; import ij.process.LUTHelper; import ij.process.LUTHelper.LUTMapper; import ij.process.LUTHelper.LutColour; /** * Run the OPTICS algorithm on the peak results. * <p> * This is an implementation of the OPTICS method. Mihael Ankerst, Markus M Breunig, Hans-Peter Kriegel, and Jorg * Sander. Optics: ordering points to identify the clustering structure. In ACM Sigmod Record, volume 28, pages * 49–60. ACM, 1999. */ public class OPTICS implements PlugIn { private static final String TITLE_OPTICS = "OPTICS"; private static final String TITLE_DBSCAN = "DBSCAN"; private static LUT clusterLut; private static LUT valueLut; private static LUT clusterDepthLut; private static LUT clusterOrderLut; private static LUT loopLut; static { valueLut = LUTHelper.createLUT(LutColour.FIRE); // Need to be able to see all colours against white (plot) or black (image) background LUT fireGlow = LUTHelper.createLUT(LutColour.FIRE_GLOW, true); // Clusters are scrambled so use a LUT with colours that are visible easily visible on black/white. // Note: The colours returned by LUTHelper.getColorModel() can be close to black. clusterLut = LUTHelper.createLUT(LutColour.PIMP_LIGHT, true); clusterDepthLut = fireGlow; clusterOrderLut = fireGlow; // This is only used on the image so can include white loopLut = LUTHelper.createLUT(LutColour.FIRE_LIGHT, true); } private String TITLE; private GlobalSettings globalSettings; private OPTICSSettings inputSettings; private boolean extraOptions, preview, debug; // Stack to which the work is first added private Workflow<OPTICSSettings, Settings> workflow = new Workflow<OPTICSSettings, Settings>(); private abstract class BaseWorker extends WorkflowWorker<OPTICSSettings, Settings> { @Override public boolean equalResults(Settings current, Settings previous) { if (current == null) return previous == null; return current.equals(previous); } } private class InputWorker extends BaseWorker { @Override public boolean equalSettings(OPTICSSettings current, OPTICSSettings previous) { // Nothing in the settings effects if we have to create a new OPTICS manager return true; } @Override public Settings createResults(OPTICSSettings settings, Settings resultList) { // The first item should be the memory peak results MemoryPeakResults results = (MemoryPeakResults) resultList.get(0); // Convert results to coordinates float[] x, y; int size = results.size(); x = new float[size]; y = new float[size]; ArrayList<PeakResult> list = (ArrayList<PeakResult>) results.getResults(); for (int i = 0; i < size; i++) { PeakResult p = list.get(i); x[i] = p.getXPosition(); y[i] = p.getYPosition(); } Rectangle bounds = results.getBounds(true); OPTICSManager opticsManager = new OPTICSManager(x, y, bounds); opticsManager.setTracker(new IJTrackProgress()); opticsManager.setOptions(Option.CACHE); return new Settings(results, opticsManager); } } private class OpticsWorker extends BaseWorker { @Override public boolean equalSettings(OPTICSSettings current, OPTICSSettings previous) { if (current.minPoints != previous.minPoints) return false; if (current.getOPTICSMode() != previous.getOPTICSMode()) return false; if (current.getOPTICSMode() == OPTICSMode.OPTICS) { if (current.generatingDistance != previous.generatingDistance) return false; } else { if (current.numberOfSplitSets != previous.numberOfSplitSets) return false; if (extraOptions) { if (current.useRandomVectors != previous.useRandomVectors) return false; if (current.saveApproximateSets != previous.saveApproximateSets) return false; if (current.getSampleMode() != previous.getSampleMode()) return false; } } return true; } @Override public Settings createResults(OPTICSSettings settings, Settings resultList) { // The first item should be the memory peak results MemoryPeakResults results = (MemoryPeakResults) resultList.get(0); // The second item should be the OPTICS manager OPTICSManager opticsManager = (OPTICSManager) resultList.get(1); int minPts = settings.minPoints; OPTICSResult opticsResult; if (settings.getOPTICSMode() == OPTICSMode.FAST_OPTICS) { int n = settings.numberOfSplitSets; // Q. Should these be options boolean useRandomVectors = false; boolean saveApproximateSets = false; SampleMode sampleMode = SampleMode.RANDOM; if (extraOptions) { useRandomVectors = settings.useRandomVectors; saveApproximateSets = settings.saveApproximateSets; sampleMode = settings.getSampleMode(); } synchronized (opticsManager) { opticsManager.setNumberOfThreads(Prefs.getThreads()); opticsResult = opticsManager.fastOptics(minPts, n, n, useRandomVectors, saveApproximateSets, sampleMode); } } else { double distance = settings.generatingDistance; if (distance > 0) { // Convert generating distance to pixels double nmPerPixel = getNmPerPixel(results); if (nmPerPixel != 1) { double newDistance = distance / nmPerPixel; Utils.log(TITLE + ": Converting generating distance %s nm to %s pixels", Utils.rounded(distance), Utils.rounded(newDistance)); distance = newDistance; } } else { double nmPerPixel = getNmPerPixel(results); if (nmPerPixel != 1) { Utils.log(TITLE + ": Default generating distance %s nm", Utils.rounded(opticsManager.computeGeneratingDistance(minPts) * nmPerPixel)); } } synchronized (opticsManager) { opticsResult = opticsManager.optics((float) distance, minPts); } } // It may be null if cancelled. However return null Work will close down the next thread return new Settings(results, opticsManager, opticsResult); } } private class OpticsClusterWorker extends BaseWorker { int clusterCount = 0; @Override public boolean equalSettings(OPTICSSettings current, OPTICSSettings previous) { if (current.getClusteringMode() != previous.getClusteringMode()) return false; if (current.getClusteringMode() == ClusteringMode.XI) { if (current.xi != previous.xi) return false; if (current.topLevel != previous.topLevel) return false; if (current.upperLimit != previous.upperLimit) return false; if (current.lowerLimit != previous.lowerLimit) return false; } else { if (current.clusteringDistance != previous.clusteringDistance) return false; if (current.core != previous.core) return false; } return true; } @Override public Settings createResults(OPTICSSettings settings, Settings resultList) { MemoryPeakResults results = (MemoryPeakResults) resultList.get(0); OPTICSManager opticsManager = (OPTICSManager) resultList.get(1); OPTICSResult opticsResult = (OPTICSResult) resultList.get(2); // It may be null if cancelled. if (opticsResult != null) { int nClusters = 0; synchronized (opticsResult) { double nmPerPixel = getNmPerPixel(results); if (settings.getClusteringMode() == ClusteringMode.XI) { int options = (settings.topLevel) ? OPTICSResult.XI_OPTION_TOP_LEVEL : 0; // Always include these as they are ignored if invalid options |= OPTICSResult.XI_OPTION_UPPER_LIMIT | OPTICSResult.XI_OPTION_LOWER_LIMIT; opticsResult.setUpperLimit(settings.upperLimit / nmPerPixel); opticsResult.setLowerLimit(settings.lowerLimit / nmPerPixel); opticsResult.extractClusters(settings.xi, options); } else { double distance; if (settings.getOPTICSMode() == OPTICSMode.FAST_OPTICS) { if (settings.clusteringDistance > 0) distance = settings.clusteringDistance; else { distance = opticsManager.computeGeneratingDistance(settings.minPoints) * nmPerPixel; if (nmPerPixel != 1) { Utils.log(TITLE + ": Default clustering distance %s nm", Utils.rounded(distance)); } } } else { // Ensure that the distance is valid distance = opticsResult.generatingDistance * nmPerPixel; if (settings.clusteringDistance > 0) distance = Math.min(settings.clusteringDistance, distance); } if (nmPerPixel != 1) { double newDistance = distance / nmPerPixel; Utils.log(TITLE + ": Converting clustering distance %s nm to %s pixels", Utils.rounded(distance), Utils.rounded(newDistance)); distance = newDistance; } opticsResult.extractDBSCANClustering((float) distance, settings.core); } nClusters = opticsResult.getNumberOfClusters(); // We must scramble after extracting the clusters since the cluster Ids have been rewritten scrambleClusters(opticsResult); } // We created a new clustering clusterCount++; Utils.log("Clustering mode: %s = %s", settings.getClusteringMode(), Utils.pleural(nClusters, "Cluster")); } return new Settings(results, opticsManager, opticsResult, clusterCount); } } private class ResultsWorker extends BaseWorker { @Override public boolean equalSettings(OPTICSSettings current, OPTICSSettings previous) { // Only depends on if the clustering results are new. This is triggered // in the default comparison of the Settings object. return true; } @Override public Settings createResults(OPTICSSettings settings, Settings resultList) { // The result is in position 2. // It may be null if cancelled. if (resultList.get(2) == null) { // Only log here so it happens once IJ.log(TITLE + ": No results to display"); } return resultList; } } private class ClusterResult { final int n1, n2; final int[] c1, c2; ClusterResult(int[] clusters, int[] topClusters) { // The original set of clusters does not need to be compacted n1 = Maths.max(clusters) + 1; this.c1 = clusters; if (topClusters != null) { // The top clusters from OPTICS may contain non-sequential integers n2 = RandIndex.compact(topClusters); this.c2 = topClusters; } else { n2 = 0; c2 = null; } } } private class RandIndexWorker extends BaseWorker { Queue<ClusterResult> queue = new LinkedList<ClusterResult>(); @Override public boolean equalSettings(OPTICSSettings current, OPTICSSettings previous) { if (!current.inputOption.equals(previous.inputOption)) { // We only cache results for the same set of raw results, i.e. the input coordinates. queue.clear(); return false; } return true; } @Override public Settings createResults(OPTICSSettings settings, Settings resultList) { ClusteringResult clusteringResult = (ClusteringResult) resultList.get(2); if (clusteringResult == null) return resultList; int[] clusters, topClusters = null; synchronized (clusteringResult) { clusters = clusteringResult.getClusters(); if (clusteringResult instanceof OPTICSResult) { topClusters = ((OPTICSResult) clusteringResult).getTopLevelClusters(false); } } ClusterResult current = new ClusterResult(clusters, topClusters); // Compare to previous results if (!queue.isEmpty()) { int i = -queue.size(); StringBuilder sb = new StringBuilder(); sb.append("Cluster comparison: RandIndex (AdjustedRandIndex)\n"); for (Iterator<ClusterResult> it = queue.iterator(); it.hasNext();) { ClusterResult previous = it.next(); sb.append("[").append(i++).append("] "); compare(sb, "Clusters", current.c1, current.n1, previous.c1, previous.n1); if (current.c2 != null) { sb.append(" : "); compare(sb, "Top-level clusters", current.c2, current.n2, previous.c2, previous.n2); } sb.append('\n'); } IJ.log(sb.toString()); } queue.add(current); // Limit size if (queue.size() > 2) queue.poll(); return resultList; } private void compare(StringBuilder sb, String title, int[] set1, int n1, int[] set2, int n2) { RandIndex ri = new RandIndex(); ri.compute(set1, n1, set2, n2); double r = ri.getRandIndex(); double ari = ri.getAdjustedRandIndex(); sb.append(title); sb.append(" "); sb.append(Utils.rounded(r)); sb.append(" ("); sb.append(Utils.rounded(ari)); sb.append(")"); } } private class MemoryResultsWorker extends BaseWorker { @Override public boolean equalSettings(OPTICSSettings current, OPTICSSettings previous) { // Only depends on if the clustering results are new. This is triggered // in the default comparison of the Settings object. return true; } @Override public Settings createResults(OPTICSSettings settings, Settings resultList) { MemoryPeakResults results = (MemoryPeakResults) resultList.get(0); //OPTICSManager opticsManager = (OPTICSManager) resultList.get(1); ClusteringResult clusteringResult = (ClusteringResult) resultList.get(2); // It may be null if cancelled. if (clusteringResult != null) { int[] clusters; synchronized (clusteringResult) { clusters = clusteringResult.getClusters(); } int max = Maths.max(clusters); // Save the clusters to memory Trace[] traces = new Trace[max + 1]; for (int i = 0; i <= max; i++) { traces[i] = new Trace(); traces[i].setId(i); } ArrayList<PeakResult> list = (ArrayList<PeakResult>) results.getResults(); for (int i = 0, size = results.size(); i < size; i++) { PeakResult r = list.get(i); traces[clusters[i++]].add(r); } TraceMolecules.saveResults(results, traces, TITLE); } // We have not created anything new so return the current object return resultList; } } private class ReachabilityResultsWorker extends BaseWorker { @Override public boolean equalSettings(OPTICSSettings current, OPTICSSettings previous) { if (current.getPlotMode() != previous.getPlotMode()) return false; return true; } @Override public Settings createResults(OPTICSSettings settings, Settings resultList) { OPTICSResult opticsResult = (OPTICSResult) resultList.get(2); // It may be null if cancelled. if (opticsResult == null) { return resultList; } MemoryPeakResults results = (MemoryPeakResults) resultList.get(0); double nmPerPixel = getNmPerPixel(results); // Draw the reachability profile PlotMode mode = settings.getPlotMode(); if (mode != PlotMode.OFF) { double[] profile; synchronized (opticsResult) { profile = opticsResult.getReachabilityDistanceProfile(true); } String units = " (px)"; if (nmPerPixel != 1) { units = " (nm)"; for (int i = 0; i < profile.length; i++) profile[i] *= nmPerPixel; } double[] order = Utils.newArray(profile.length, 1.0, 1.0); String title = TITLE + " Reachability Distance"; Plot2 plot = new Plot2(title, "Order", "Reachability" + units); double[] limits = Maths.limits(profile); // plot to zero limits[0] = 0; ArrayList<OPTICSCluster> clusters = null; LUT lut = clusterLut; int maxClusterId = 0; int maxLevel = 0; if (mode.requiresClusters()) { synchronized (opticsResult) { clusters = opticsResult.getAllClusters(); } for (OPTICSCluster cluster : clusters) { if (maxLevel < cluster.getLevel()) maxLevel = cluster.getLevel(); if (maxClusterId < cluster.getClusterId()) maxClusterId = cluster.getClusterId(); } } if (settings.getOPTICSMode() == OPTICSMode.FAST_OPTICS) { // The profile may be very high. Compute the outliers and remove. Percentile p = new Percentile(); p.setData(profile); double max; boolean useIQR = true; if (useIQR) { double lq = p.evaluate(25); double uq = p.evaluate(75); max = (uq - lq) * 2 + uq; } else { // Remove top 2% max = p.evaluate(98); } if (limits[1] > max) limits[1] = max; } else { // Show extra at the top limits[1] *= 1.05; } // Draw the clusters using lines underneath if (mode.isDrawClusters() && maxClusterId > 0) { // Get a good distance to start the lines, and the separation // Make the clusters fill 1/3 of the plot. double range = 0.5 * limits[1]; double separation = range / (maxLevel + 2); double start = -separation; LUTMapper mapper = new LUTHelper.NonZeroLUTMapper(1, maxClusterId); for (OPTICSCluster cluster : clusters) { int level = cluster.getLevel(); float y = (float) (start - (maxLevel - level) * separation); Color c = mapper.getColour(lut, cluster.getClusterId()); plot.setColor(c); //plot.drawLine(cluster.start, y, cluster.end, y); // Create as a line. This allows the plot to reset the range to the full data set float[] xx = new float[] { cluster.start, cluster.end }; float[] yy = new float[] { y, y }; plot.addPoints(xx, yy, Plot.LINE); } // Update the limits if we are plotting lines underneath for the clusters limits[0] = -range; //start - (maxLevel + 1) * separation; } plot.setLimits(1, order.length, limits[0], limits[1]); // Create the colour for each point on the line: // We draw lines between from and to of the same colour int[] profileColourFrom = new int[profile.length]; int[] profileColourTo = new int[profile.length]; //plot.setColor(Color.black); //plot.addPoints(order, profile, Plot.LINE); // Create a colour to match the LUT of the image LUTMapper mapper = null; // Colour the reachability plot line if it is in a cluster. Use a default colour if (mode.isColourProfile()) { if (mode.isColourProfileByOrder()) { lut = clusterOrderLut; mapper = new LUTHelper.NonZeroLUTMapper(1, profileColourTo.length - 1); for (int i = 1; i < profileColourTo.length; i++) { profileColourFrom[i - 1] = profileColourTo[i] = mapper.map(i); } // Ensure we correctly get colours for each value mapper = new LUTHelper.DefaultLUTMapper(0, 255); } else { // Do all clusters so rank by level Collections.sort(clusters, new Comparator<OPTICSCluster>() { public int compare(OPTICSCluster o1, OPTICSCluster o2) { return o1.getLevel() - o2.getLevel(); } }); final boolean useLevel = mode.isColourProfileByDepth(); if (useLevel) lut = clusterDepthLut; for (OPTICSCluster cluster : clusters) { int value = (useLevel) ? cluster.getLevel() + 1 : cluster.getClusterId(); Arrays.fill(profileColourFrom, cluster.start, cluster.end, value); Arrays.fill(profileColourTo, cluster.start + 1, cluster.end + 1, value); } } } else if (mode.isHighlightProfile()) { for (OPTICSCluster cluster : clusters) { // Only do top level clusters if (cluster.getLevel() != 0) continue; int value = 1; Arrays.fill(profileColourFrom, cluster.start, cluster.end, value); Arrays.fill(profileColourTo, cluster.start + 1, cluster.end + 1, value); } } // Now draw the line int maxColour = Maths.max(profileColourTo); if (mapper == null) // Make zero black mapper = new LUTHelper.NonZeroLUTMapper(1, maxColour); // Cache all the colours Color[] colors = new Color[maxColour + 1]; if (mode.isColourProfile()) { for (int c = mapper.getMin(); c <= maxColour; c++) colors[c] = mapper.getColour(lut, c); } else if (maxColour == 1) { colors[1] = Color.BLUE; } if (colors[0] == null) colors[0] = Color.BLACK; // We draw lines between from and to of the same colour int from = 0; int to = 1; int limit = profileColourTo.length - 1; while (to < profileColourTo.length) { while (to < limit && profileColourFrom[from] == profileColourTo[to + 1]) to++; // Draw the line on the plot double[] order1 = Arrays.copyOfRange(order, from, to + 1); double[] profile1 = Arrays.copyOfRange(profile, from, to + 1); plot.setColor(colors[profileColourFrom[from]]); plot.addPoints(order1, profile1, Plot.LINE); from = to++; } // Draw the final line if (from != limit) { to = limit; double[] order1 = Arrays.copyOfRange(order, from, to); double[] profile1 = Arrays.copyOfRange(profile, from, to); plot.setColor(colors[profileColourFrom[from]]); plot.addPoints(order1, profile1, Plot.LINE); } // Add the clustering distance limits double distance = -1, distance2 = -1; if (inputSettings.getClusteringMode() == ClusteringMode.DBSCAN) { if (settings.getOPTICSMode() == OPTICSMode.FAST_OPTICS) { if (settings.clusteringDistance > 0) distance = settings.clusteringDistance; else { OPTICSManager opticsManager = (OPTICSManager) resultList.get(1); distance = opticsManager.computeGeneratingDistance(settings.minPoints) * nmPerPixel; } } else { // Ensure that the distance is valid distance = opticsResult.generatingDistance * nmPerPixel; if (settings.clusteringDistance > 0) distance = Math.min(settings.clusteringDistance, distance); } if (distance > limits[1]) limits[1] = distance * 1.05; } else // Assume Optics Xi { if (settings.upperLimit > 0) distance = settings.upperLimit; if (settings.lowerLimit > 0) distance2 = settings.lowerLimit; } if (distance > -1) { plot.setColor(Color.red); plot.drawLine(1, distance, order.length, distance); } if (distance2 > -1) { plot.setColor(Color.magenta); plot.drawLine(1, distance2, order.length, distance2); } // Preserve current limits (but not y-min so the clusters profile can be redrawn) Utils.display(title, plot, Utils.PRESERVE_X_MIN | Utils.PRESERVE_X_MAX | Utils.PRESERVE_Y_MAX); } else { // We could close an existing plot here. // However we leave it as the user may wish to keep it for something. } // We have not created anything new so return the current object return resultList; } } private class OrderProvider { int getOrder(int i) { return i; } } private class RealOrderProvider extends OrderProvider { final int[] order; RealOrderProvider(int[] order) { this.order = order; } @Override int getOrder(int i) { return order[i]; } } /** * Map the input value as an index to an output value */ public class ValueLUTMapper extends LUTHelper.NullLUTMapper { float[] values; public ValueLUTMapper(float[] values) { this.values = values; } public float mapf(float value) { return values[(int) value]; } } private class ImageResultsWorker extends BaseWorker { IJImagePeakResults image = null; OutlineMode lastOutlineMode = null; Overlay outline = null; SpanningTreeMode lastSpanningTreeMode = null; Overlay spanningTree = null; double lastLambda = 0; int lastMinPoints; float[] loop = null; @Override public boolean equalSettings(OPTICSSettings current, OPTICSSettings previous) { if (current.imageScale != previous.imageScale) { // Clear all the cached results clearCache(true); return false; } boolean result = true; if (current.getOutlineMode() != previous.getOutlineMode()) { if (current.getOutlineMode().isOutline() && current.getOutlineMode() != lastOutlineMode) outline = null; result = false; } if (current.getSpanningTreeMode() != previous.getSpanningTreeMode()) { if (current.getSpanningTreeMode().isSpanningTree() && current.getSpanningTreeMode() != lastSpanningTreeMode) spanningTree = null; result = false; } if (current.getImageMode() != previous.getImageMode() || getDisplayFlags(current) != getDisplayFlags(previous)) { // We can only cache the image if the display mode is the same image = null; result = false; } if (requiresLoop(current) && (current.minPoints != lastMinPoints || (extraOptions && current.lambda != lastLambda))) { // We can only cache the loop values if the minPts is the same loop = null; if (current.getImageMode() == ImageMode.LOOP) // We must rebuild the image image = null; if (current.getSpanningTreeMode() == SpanningTreeMode.COLOURED_BY_LOOP) // We must rebuild the outline outline = null; result = false; } return result; } private boolean requiresLoop(OPTICSSettings settings) { return settings.getImageMode() == ImageMode.LOOP || settings.getSpanningTreeMode() == SpanningTreeMode.COLOURED_BY_LOOP; } @Override protected void newResults() { // We can keep the image but should clear the overlays clearCache(false); } private void clearCache(boolean clearImage) { // Clear cache if (clearImage) image = null; lastOutlineMode = null; outline = null; lastSpanningTreeMode = null; spanningTree = null; } @Override public Settings createResults(OPTICSSettings settings, Settings resultList) { MemoryPeakResults results = (MemoryPeakResults) resultList.get(0); OPTICSManager opticsManager = (OPTICSManager) resultList.get(1); ClusteringResult clusteringResult = (ClusteringResult) resultList.get(2); int clusterCount = (Integer) resultList.get(3); // It may be null if cancelled. if (clusteringResult == null) { clearCache(true); return new Settings(results, opticsManager, clusteringResult, clusterCount, image); } int[] clusters = null, order = null; int max = 0; // max cluster value float[] x = null, y = null; ImageMode mode = settings.getImageMode(); if (settings.imageScale > 0) { // Check if the image should be redrawn based on the clusters if (mode.isRequiresClusters()) image = null; if (image == null) { // Display the results ... Rectangle bounds = results.getBounds(); image = new IJImagePeakResults(results.getName() + " " + TITLE, bounds, (float) settings.imageScale); // Options to control rendering image.copySettings(results); image.setDisplayFlags(getDisplayFlags(settings)); image.setLiveImage(false); image.begin(); ImagePlus imp = image.getImagePlus(); imp.setOverlay(null); if (mode != ImageMode.NONE) { float[] map = null; // Used to map clusters to a display value synchronized (clusteringResult) { clusters = clusteringResult.getClusters(); max = Maths.max(clusters); if (clusteringResult instanceof OPTICSResult) { OPTICSResult opticsResult = (OPTICSResult) clusteringResult; if (mode == ImageMode.CLUSTER_ORDER) { order = opticsResult.getOrder(); } else if (mode == ImageMode.CLUSTER_DEPTH) { ArrayList<OPTICSCluster> allClusters = opticsResult.getAllClusters(); map = new float[max + 1]; for (OPTICSCluster c : allClusters) map[c.getClusterId()] = c.getLevel() + 1; } } } if (requiresLoop(settings) && loop == null) { synchronized (opticsManager) { lastLambda = (extraOptions) ? settings.lambda : 3; lastMinPoints = settings.minPoints; loop = opticsManager.loop(lastMinPoints, lastLambda, true); } float[] limits = Maths.limits(loop); Utils.log("LoOP range: %s - %s", Utils.rounded(limits[0]), Utils.rounded(limits[1])); } // Draw each cluster in a new colour LUT lut = valueLut; LUTMapper mapper = new LUTHelper.NullLUTMapper(); if (mode.isMapped()) { switch (mode) { //@formatter:off case CLUSTER_ORDER: lut = clusterOrderLut; break; case CLUSTER_ID: lut = clusterLut; break; case CLUSTER_DEPTH: lut = clusterDepthLut; mapper = new ValueLUTMapper(map); break; case LOOP: lut = loopLut; mapper = new ValueLUTMapper(loop); break; default: throw new NotImplementedException(); //@formatter:on } } image.getImagePlus().getProcessor().setColorModel(lut); // Add in a single batch float[] v; x = new float[results.size()]; y = new float[x.length]; v = new float[x.length]; ArrayList<PeakResult> list = (ArrayList<PeakResult>) results.getResults(); OrderProvider op = (order == null) ? new OrderProvider() : new RealOrderProvider(order); for (int i = 0, size = results.size(); i < size; i++) { PeakResult r = list.get(i); x[i] = r.getXPosition(); y[i] = r.getYPosition(); v[i] = mapper.mapf(mode.getValue(r.getSignal(), clusters[i], op.getOrder(i))); } image.add(x, y, v); } image.end(); if (mode.isMapped()) { // Convert already mapped image to 8-bit (so the values are fixed) //imp.setProcessor(imp.getProcessor().convertToByteProcessor(false)); } imp.getWindow().toFront(); } } else { // We could close an image here. // However we leave it as the user may wish to keep it for something. } // Note: If the image scale is set to zero then the image cache will be cleared and the image will be null. // This will prevent computing an overlay even if the 'outline' setting is enabled. if (image != null) { ImagePlus imp = image.getImagePlus(); Overlay overlay = null; int[] map = null; // Used to map clusters to a display value int max2 = 0; // max mapped cluster value if (settings.getOutlineMode().isOutline()) { if (outline == null) { lastOutlineMode = settings.getOutlineMode(); if (clusters == null) { synchronized (clusteringResult) { clusters = clusteringResult.getClusters(); max = Maths.max(clusters); } } max2 = max; map = Utils.newArray(max + 1, 0, 1); LUT lut = clusterLut; if (clusteringResult instanceof OPTICSResult) { if (settings.getOutlineMode().isColourByDepth()) { lut = clusterDepthLut; synchronized (clusteringResult) { OPTICSResult opticsResult = (OPTICSResult) clusteringResult; ArrayList<OPTICSCluster> allClusters = opticsResult.getAllClusters(); Arrays.fill(map, 0); for (OPTICSCluster c : allClusters) map[c.getClusterId()] = c.getLevel() + 1; max2 = Maths.max(map); } } } outline = new Overlay(); ConvexHull[] hulls = new ConvexHull[max + 1]; synchronized (clusteringResult) { clusteringResult.computeConvexHulls(); for (int c = 1; c <= max; c++) { hulls[c] = clusteringResult.getConvexHull(c); } } // Create a colour to match the LUT of the image LUTMapper mapper = new LUTHelper.NonZeroLUTMapper(1, max2); // Cache all the colours Color[] colors = new Color[max2 + 1]; for (int c = 1; c <= max2; c++) colors[c] = mapper.getColour(lut, c); // Extract the ConvexHull of each cluster for (int c = 1; c <= max; c++) { ConvexHull hull = hulls[c]; if (hull != null) { // Convert the Hull to the correct image scale. float[] x2 = hull.x.clone(); float[] y2 = hull.y.clone(); for (int i = 0; i < x2.length; i++) { x2[i] = image.mapX(x2[i]); y2[i] = image.mapY(y2[i]); } PolygonRoi roi = new PolygonRoi(x2, y2, Roi.POLYGON); roi.setStrokeColor(colors[map[c]]); // TODO: Options to set a fill colour? outline.add(roi); } } } overlay = outline; } if (settings.getSpanningTreeMode().isSpanningTree() && clusteringResult instanceof OPTICSResult) { if (spanningTree == null) { OPTICSResult opticsResult = (OPTICSResult) clusteringResult; lastSpanningTreeMode = settings.getSpanningTreeMode(); int[] predecessor, topLevelClusters; synchronized (opticsResult) { predecessor = opticsResult.getPredecessor(); if (order == null) order = opticsResult.getOrder(); topLevelClusters = opticsResult.getTopLevelClusters(false); if (clusters == null) { clusters = opticsResult.getClusters(); max = Maths.max(clusters); } } if (map == null) { max2 = max; map = Utils.newArray(max + 1, 0, 1); } LUT lut = clusterLut; if (lastSpanningTreeMode == SpanningTreeMode.COLOURED_BY_ORDER) { lut = clusterOrderLut; } else if (lastSpanningTreeMode == SpanningTreeMode.COLOURED_BY_DEPTH && max == max2) { lut = clusterDepthLut; synchronized (clusteringResult) { ArrayList<OPTICSCluster> allClusters = opticsResult.getAllClusters(); Arrays.fill(map, 0); for (OPTICSCluster c : allClusters) map[c.getClusterId()] = c.getLevel() + 1; max2 = Maths.max(map); } } else if (lastSpanningTreeMode == SpanningTreeMode.COLOURED_BY_LOOP) { lut = loopLut; } // Get the coordinates if (x == null) { int size = results.size(); x = new float[size]; y = new float[x.length]; ArrayList<PeakResult> list = (ArrayList<PeakResult>) results.getResults(); for (int i = 0; i < size; i++) { PeakResult r = list.get(i); x[i] = r.getXPosition(); y[i] = r.getYPosition(); } } spanningTree = new Overlay(); // Cache all the colours Color[] colors; // Create a colour to match the LUT of the image LUTMapper mapper; boolean useMap = false, useLoop = false; if (lastSpanningTreeMode == SpanningTreeMode.COLOURED_BY_ORDER) { // We will use the order for the colour mapper = new LUTHelper.DefaultLUTMapper(0, 255); colors = new Color[256]; for (int c = 1; c < colors.length; c++) colors[c] = mapper.getColour(lut, c); mapper = new LUTHelper.NonZeroLUTMapper(1, clusters.length); } else if (lastSpanningTreeMode == SpanningTreeMode.COLOURED_BY_LOOP) { // We will use the LoOP for the colour useLoop = true; mapper = new LUTHelper.DefaultLUTMapper(0, 255); colors = new Color[256]; for (int c = 1; c < colors.length; c++) colors[c] = mapper.getColour(lut, c); mapper = new LUTHelper.NonZeroLUTMapper(0, 1); } else { // Alternative is to colour by cluster Id/Depth using a map useMap = true; mapper = new LUTHelper.NonZeroLUTMapper(1, max2); colors = new Color[max2 + 1]; for (int c = 1; c <= max2; c++) colors[c] = mapper.getColour(lut, c); } for (int i = 1; i < predecessor.length; i++) { if (clusters[i] == 0 || predecessor[i] < 0) continue; int j = predecessor[i]; // The spanning tree can jump across hierachical clusters. // Prevent jumps across top-level clusters if (topLevelClusters[i] != topLevelClusters[i]) continue; float xi = image.mapX(x[i]); float yi = image.mapY(y[i]); float xj = image.mapX(x[j]); float yj = image.mapY(y[j]); Line roi = new Line(xi, yi, xj, yj); if (useMap) roi.setStrokeColor(colors[map[clusters[i]]]); else if (useLoop) roi.setStrokeColor(colors[mapper.map(loop[i])]); else roi.setStrokeColor(colors[mapper.map(order[i])]); spanningTree.add(roi); } } if (overlay == null) { overlay = spanningTree; } else { // Merge the two overlay = new Overlay(); for (int i = outline.size(); i-- > 0;) overlay.add(outline.get(i)); for (int i = spanningTree.size(); i-- > 0;) overlay.add(spanningTree.get(i)); } } imp.setOverlay(overlay); } return new Settings(results, opticsManager, clusteringResult, clusterCount, image); } private int getDisplayFlags(OPTICSSettings inputSettings) { int displayFlags = 0; ImageMode imageMode = inputSettings.getImageMode(); if (imageMode.canBeWeighted()) { if (inputSettings.weighted) displayFlags |= IJImagePeakResults.DISPLAY_WEIGHTED; if (inputSettings.equalised) displayFlags |= IJImagePeakResults.DISPLAY_EQUALIZED; } if (imageMode == ImageMode.CLUSTER_ID || imageMode == ImageMode.CLUSTER_DEPTH || imageMode == ImageMode.CLUSTER_ORDER || imageMode == ImageMode.LOOP) { displayFlags = IJImagePeakResults.DISPLAY_MAX; } if (imageMode.isMapped()) { displayFlags |= IJImagePeakResults.DISPLAY_MAPPED; if (imageMode == ImageMode.LOOP) displayFlags |= IJImagePeakResults.DISPLAY_MAP_ZERO; } return displayFlags; } } private class KNNWorker extends BaseWorker { double[] profile = null; @Override public boolean equalSettings(OPTICSSettings current, OPTICSSettings previous) { if (current.minPoints != previous.minPoints) { newResults(); return false; } if (current.samples != previous.samples || current.sampleFraction != previous.sampleFraction) { newResults(); return false; } if (current.fractionNoise != previous.fractionNoise) return false; if (clusteringDistanceChange(current.clusteringDistance, previous.clusteringDistance)) return false; return true; } @Override protected void newResults() { // Clear cache profile = null; } @Override public Settings createResults(OPTICSSettings settings, Settings resultList) { // The first item should be the memory peak results MemoryPeakResults results = (MemoryPeakResults) resultList.get(0); // The second item should be the OPTICS manager OPTICSManager opticsManager = (OPTICSManager) resultList.get(1); int minPts = settings.minPoints; int k = minPts - 1; // Since min points includes the actual point double fractionNoise = settings.fractionNoise; double nmPerPixel = getNmPerPixel(results); // Flag indicating that the scale can be kept on a new plot int preserve = Utils.PRESERVE_ALL; // Create a profile of the K-Nearest Neighbour distances if (profile == null) { preserve = 0; synchronized (opticsManager) { int samples = settings.samples; if (samples > 1 || settings.sampleFraction > 0) { // Ensure we take a reasonable amount of samples (min=100) samples = Maths.max(100, samples, (int) Math.ceil(opticsManager.getSize() * settings.sampleFraction)); } float[] d = opticsManager.nearestNeighbourDistance(k, samples, true); profile = new double[d.length]; for (int i = d.length; i-- > 0;) profile[i] = d[i]; } Arrays.sort(profile); Sort.reverse(profile); if (nmPerPixel != 1) { for (int i = 0; i < profile.length; i++) profile[i] *= nmPerPixel; } } String units = (nmPerPixel != 1) ? " (nm)" : " (px)"; double[] order = Utils.newArray(profile.length, 1.0, 1.0); String title = TITLE + " KNN Distance"; Plot2 plot = new Plot2(title, "Sample", k + "-NN Distance" + units); double[] limits = new double[] { profile[profile.length - 1], profile[0] }; plot.setLimits(1, order.length, limits[0], limits[1] * 1.05); plot.setColor(Color.black); plot.addPoints(order, profile, Plot.LINE); // Add the DBSCAN clustering distance double distance = settings.clusteringDistance; if (distance > 0) { plot.setColor(Color.red); plot.drawLine(1, distance, order.length, distance); } // Find the clustering distance using a % noise in the KNN distance samples distance = findClusteringDistance(profile, fractionNoise); plot.setColor(Color.blue); plot.drawDottedLine(1, distance, order.length, distance, 2); Utils.display(title, plot, preserve); if (settings.clusteringDistance == 0) { // Set this distance into the settings if there is no clustering distance // Use a negative value to show it is an auto-distance settings.clusteringDistance = -distance; } // We have not created anything new so return the current object return resultList; } } private boolean clusteringDistanceChange(double newD, double oldD) { // The input distance can never be below zero due to the use of abs. // If the auto-distance changes then we want to rerun DBSCAN so remove this check. //if (newD <= 0 && oldD <= 0) // // Auto-distance // return false; return newD != oldD; } private void scrambleClusters(ClusteringResult result) { // Scramble to ensure adjacent clusters have different Ids. // Same seed for consistency (e.g. in macros on the same data). result.scrambleClusters(new Well19937c(1999)); } /** * Find the clustering distance using a sorted profile of the KNN distance. * * @param profile * the profile (sorted high to low) * @param fractionNoise * the fraction noise * @return the clustering distance */ public static double findClusteringDistance(double[] profile, double fractionNoise) { // Return the next distance after the fraction has been achieved int n = Maths.clip(0, profile.length - 1, (int) Math.ceil(profile.length * fractionNoise)); return profile[n]; } private class DBSCANWorker extends BaseWorker { @Override public boolean equalSettings(OPTICSSettings current, OPTICSSettings previous) { if (current.minPoints != previous.minPoints) return false; if (clusteringDistanceChange(current.clusteringDistance, previous.clusteringDistance)) return false; return true; } @Override public Settings createResults(OPTICSSettings settings, Settings resultList) { // The first item should be the memory peak results MemoryPeakResults results = (MemoryPeakResults) resultList.get(0); // The second item should be the OPTICS manager OPTICSManager opticsManager = (OPTICSManager) resultList.get(1); double clusteringDistance = Math.abs(settings.clusteringDistance); int minPts = settings.minPoints; if (clusteringDistance > 0) { // Convert clustering distance to pixels double nmPerPixel = getNmPerPixel(results); if (nmPerPixel != 1) { double newGeneratingDistance = clusteringDistance / nmPerPixel; Utils.log(TITLE + ": Converting clustering distance %s nm to %s pixels", Utils.rounded(clusteringDistance), Utils.rounded(newGeneratingDistance)); clusteringDistance = newGeneratingDistance; } } else { // Note: This should not happen since the clustering distance is set using the KNN distance samples double nmPerPixel = getNmPerPixel(results); if (nmPerPixel != 1) { Utils.log(TITLE + ": Default clustering distance %s nm", Utils.rounded(opticsManager.computeGeneratingDistance(minPts) * nmPerPixel)); } } DBSCANResult dbscanResult; synchronized (opticsManager) { dbscanResult = opticsManager.dbscan((float) clusteringDistance, minPts); // Scramble only needs to be done once as the cluster Ids are not re-allocated when extracting the clusters scrambleClusters(dbscanResult); } // It may be null if cancelled. However return null Work will close down the next thread return new Settings(results, opticsManager, dbscanResult); } } private class DBSCANClusterWorker extends BaseWorker { int clusterCount = 0; @Override public boolean equalSettings(OPTICSSettings current, OPTICSSettings previous) { if (current.core != previous.core) return false; return true; } @Override public Settings createResults(OPTICSSettings settings, Settings resultList) { MemoryPeakResults results = (MemoryPeakResults) resultList.get(0); OPTICSManager opticsManager = (OPTICSManager) resultList.get(1); DBSCANResult dbscanResult = (DBSCANResult) resultList.get(2); // It may be null if cancelled. if (dbscanResult != null) { synchronized (dbscanResult) { dbscanResult.extractClusters(settings.core); } // We created a new clustering clusterCount++; } return new Settings(results, opticsManager, dbscanResult, clusterCount); } } /* * (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; } extraOptions = Utils.isExtraOptions(); globalSettings = SettingsManager.loadSettings(); inputSettings = globalSettings.getOPTICSSettings(); IJ.showStatus(""); if ("dbscan".equals(arg)) { runDBSCAN(); } else { runOPTICS(); } IJ.showStatus(TITLE + " finished"); // Update the settings SettingsManager.saveSettings(globalSettings); } private void runDBSCAN() { TITLE = TITLE_DBSCAN; // Create the workflow workflow.add(new InputWorker()); workflow.add(new KNNWorker()); workflow.add(new DBSCANWorker()); int previous = workflow.add(new DBSCANClusterWorker()); // The following can operate in parallel workflow.add(new ResultsWorker(), previous); workflow.add(new RandIndexWorker(), previous); workflow.add(new MemoryResultsWorker(), previous); workflow.add(new ImageResultsWorker(), previous); workflow.start(); boolean cancelled = !showDialog(true); workflow.shutdown(cancelled); } private void runOPTICS() { TITLE = TITLE_OPTICS; // Create the workflow workflow.add(new InputWorker()); workflow.add(new OpticsWorker()); int previous = workflow.add(new OpticsClusterWorker()); // The following can operate in parallel workflow.add(new ResultsWorker(), previous); workflow.add(new RandIndexWorker(), previous); workflow.add(new MemoryResultsWorker(), previous); workflow.add(new ReachabilityResultsWorker(), previous); workflow.add(new ImageResultsWorker(), previous); workflow.start(); boolean cancelled = !showDialog(false); workflow.shutdown(cancelled); } /** * Gets the nm per pixel. * * @param results * the results * @return the nm per pixel */ public double getNmPerPixel(MemoryPeakResults results) { if (results.getCalibration() != null && results.getCalibration().getNmPerPixel() > 0) return results.getCalibration().getNmPerPixel(); return 1; } private Object[] imageModeArray; private Object[] outlineModeArray; private boolean showDialog(boolean isDBSCAN) { logReferences(isDBSCAN); NonBlockingGenericDialog gd = new NonBlockingGenericDialog(TITLE); gd.addHelp(About.HELP_URL); ResultsManager.addInput(gd, inputSettings.inputOption, InputSource.MEMORY); //globalSettings = SettingsManager.loadSettings(); //settings = globalSettings.getClusteringSettings(); gd.addMessage("--- " + TITLE + " ---"); gd.addNumericField("Min_points", inputSettings.minPoints, 0); if (isDBSCAN) { // Add fields to auto-compute the clustering distance from the K-nearest neighbour distance profile gd.addSlider("Noise (%)", 0, 50, inputSettings.fractionNoise * 100); gd.addNumericField("Samples", inputSettings.samples, 0); gd.addSlider("Sample_fraction (%)", 0, 15, inputSettings.sampleFraction * 100); gd.addNumericField("Clustering_distance", inputSettings.clusteringDistance, 2, 6, "nm"); } else { String[] opticsModes = SettingsManager.getNames((Object[]) OPTICSMode.values()); gd.addChoice("OPTICS_mode", opticsModes, inputSettings.getOPTICSMode().toString()); gd.addNumericField("Number_of_splits", inputSettings.numberOfSplitSets, 0); if (extraOptions) { gd.addCheckbox("Random_vectors", inputSettings.useRandomVectors); gd.addCheckbox("Approx_sets", inputSettings.saveApproximateSets); String[] sampleModes = SettingsManager.getNames((Object[]) SampleMode.values()); gd.addChoice("Sample_mode", sampleModes, inputSettings.getSampleMode().toString()); } gd.addNumericField("Generating_distance", inputSettings.generatingDistance, 2, 6, "nm"); } gd.addMessage("--- Clustering ---"); if (isDBSCAN) { gd.addCheckbox("Core_points", inputSettings.core); } else { String[] clusteringModes = SettingsManager.getNames((Object[]) ClusteringMode.values()); gd.addChoice("Clustering_mode", clusteringModes, inputSettings.getClusteringMode().toString()); gd.addMessage(ClusteringMode.XI.toString() + " options:\n" + ClusteringMode.XI.toString() + " controls the change in reachability (profile steepness) to define a cluster"); gd.addNumericField("Xi", inputSettings.xi, 4); gd.addCheckbox("Top_clusters", inputSettings.topLevel); gd.addNumericField("Upper_limit", inputSettings.upperLimit, 4); gd.addNumericField("Lower_limit", inputSettings.lowerLimit, 4); gd.addMessage(ClusteringMode.DBSCAN.toString() + " options:"); gd.addNumericField("Clustering_distance", inputSettings.clusteringDistance, 4); gd.addCheckbox("Core_points", inputSettings.core); } gd.addMessage("--- Image ---"); gd.addSlider("Image_scale", 0, 15, inputSettings.imageScale); TreeSet<ImageMode> imageModeSet = new TreeSet<ImageMode>(); imageModeSet.addAll(Arrays.asList(ImageMode.values())); if (isDBSCAN) { imageModeSet.remove(ImageMode.CLUSTER_DEPTH); imageModeSet.remove(ImageMode.CLUSTER_ORDER); } imageModeArray = imageModeSet.toArray(); String[] imageModes = SettingsManager.getNames(imageModeArray); gd.addChoice("Image_mode", imageModes, inputSettings.getImageMode().toString()); gd.addCheckboxGroup(1, 2, new String[] { "Weighted", "Equalised" }, new boolean[] { inputSettings.weighted, inputSettings.equalised }, new String[] { "Image" }); if (extraOptions) { gd.addNumericField("LoOP_lambda", inputSettings.lambda, 4); } TreeSet<OutlineMode> outlineModeSet = new TreeSet<OutlineMode>(); outlineModeSet.addAll(Arrays.asList(OutlineMode.values())); if (isDBSCAN) { outlineModeSet.remove(OutlineMode.COLOURED_BY_DEPTH); } outlineModeArray = outlineModeSet.toArray(); String[] outlineModes = SettingsManager.getNames(outlineModeArray); gd.addChoice("Outline", outlineModes, inputSettings.getOutlineMode().toString()); if (!isDBSCAN) { String[] spanningTreeModes = SettingsManager.getNames((Object[]) SpanningTreeMode.values()); gd.addChoice("Spanning_tree", spanningTreeModes, spanningTreeModes[inputSettings.getSpanningTreeModeOridinal()]); gd.addMessage("--- Reachability Plot ---"); String[] plotModes = SettingsManager.getNames((Object[]) PlotMode.values()); gd.addChoice("Plot_mode", plotModes, plotModes[inputSettings.getPlotModeOridinal()]); } // Start disabled so the user can choose settings to update gd.addCheckbox("Preview", false); if (extraOptions) gd.addCheckbox("Debug", false); // Everything is done within the dialog listener if (isDBSCAN) gd.addDialogListener(new DBSCANDialogListener()); else gd.addDialogListener(new OPTICSDialogListener()); gd.showDialog(); if (gd.wasCanceled()) return false; // The dialog was OK'd so run if work was staged in the workflow. if (workflow.isStaged()) workflow.runStaged(); // Record the options for macros since the NonBlocking dialog does not if (Recorder.record) { Recorder.recordOption("Min_points", Integer.toString(inputSettings.minPoints)); if (isDBSCAN) { // Add fields to auto-compute the clustering distance from the K-nearest neighbour distance profile Recorder.recordOption("Noise", Double.toString(inputSettings.fractionNoise * 100)); Recorder.recordOption("Samples", Double.toString(inputSettings.samples)); Recorder.recordOption("Sample_fraction", Double.toString(inputSettings.sampleFraction * 100)); Recorder.recordOption("Clustering_distance", Double.toString(inputSettings.clusteringDistance)); } else { Recorder.recordOption("OPTICS_mode", inputSettings.getOPTICSMode().toString()); Recorder.recordOption("Number_of_splits", Integer.toString(inputSettings.numberOfSplitSets)); if (extraOptions) { if (inputSettings.useRandomVectors) Recorder.recordOption("Random_vectors"); if (inputSettings.saveApproximateSets) Recorder.recordOption("Approx_sets"); Recorder.recordOption("Sample_mode", inputSettings.getSampleMode().toString()); } Recorder.recordOption("Generating_distance", Double.toString(inputSettings.generatingDistance)); } if (isDBSCAN) { if (inputSettings.core) Recorder.recordOption("Core_points"); } else { Recorder.recordOption("Clustering_mode", inputSettings.getClusteringMode().toString()); Recorder.recordOption("Xi", Double.toString(inputSettings.xi)); if (inputSettings.topLevel) Recorder.recordOption("Top_clusters"); Recorder.recordOption("Upper_limit", Double.toString(inputSettings.upperLimit)); Recorder.recordOption("Lower_limit", Double.toString(inputSettings.lowerLimit)); Recorder.recordOption("Clustering_distance", Double.toString(inputSettings.clusteringDistance)); if (inputSettings.core) Recorder.recordOption("Core_points"); } gd.addMessage("--- Image ---"); Recorder.recordOption("Image_scale", Double.toString(inputSettings.imageScale)); Recorder.recordOption("Image_mode", inputSettings.getImageMode().toString()); if (inputSettings.weighted) Recorder.recordOption("Weighted"); if (inputSettings.equalised) Recorder.recordOption("Equalised"); if (extraOptions) { Recorder.recordOption("LoOP_lambda", Double.toString(inputSettings.lambda)); } Recorder.recordOption("Outline", inputSettings.getOutlineMode().toString()); if (!isDBSCAN) { Recorder.recordOption("Spanning_tree", inputSettings.getSpanningTreeMode().toString()); Recorder.recordOption("Plot_mode", inputSettings.getPlotMode().toString()); } if (debug) Recorder.recordOption("Debug"); } return true; } private static byte logged = 0; private static final byte LOG_DBSCAN = 0x01; private static final byte LOG_OPTICS = 0x02; private static final byte LOG_LOOP = 0x04; private static void logReferences(boolean isDBSCAN) { int width = 80; StringBuilder sb = new StringBuilder(); if (isDBSCAN && (logged & LOG_DBSCAN) != LOG_DBSCAN) { logged |= LOG_DBSCAN; sb.append("DBSCAN: "); sb.append(TextUtils.wrap( "Ester, et al (1996). 'A density-based algorithm for discovering clusters in large spatial databases with noise'. Proceedings of the Second International Conference on Knowledge Discovery and Data Mining (KDD-96). AAAI Press. pp. 226–231.", width)).append('\n'); } else if ((logged & LOG_OPTICS) != LOG_OPTICS) { logged |= LOG_OPTICS; sb.append("OPTICS: "); sb.append(TextUtils.wrap( "Kriegel, et al (2011). 'Density-based clustering'. Wiley Interdisciplinary Reviews: Data Mining and Knowledge Discovery. 1 (3): 231–240.", width)).append('\n'); sb.append("FastOPTICS: "); sb.append(TextUtils.wrap( "Schneider, et al (2013). 'Fast parameterless density-based clustering via random projections'. 22nd ACM International Conference on Information and Knowledge Management(CIKM). ACM. pp. 861-866.", width)).append('\n'); } if ((logged & LOG_LOOP) != LOG_LOOP) { logged |= LOG_LOOP; sb.append("LoOP: "); sb.append(TextUtils.wrap( "Kriegel, et al (2009). 'LoOP: Local Outlier Probabilities'. 18th ACM International Conference on Information and knowledge management(CIKM). ACM. pp. 1649-1652.", width)).append('\n'); } if (sb.length() > 0) IJ.log(sb.toString()); } private abstract class BaseDialogListener implements DialogListener { public boolean dialogItemChanged(GenericDialog gd, AWTEvent e) { // if (e == null) // { // // This happens when the dialog is first shown and can be ignored. // // It also happens when called from within a macro. In this case we should run. // if (!Utils.isMacro()) // return true; // } if (debug) System.out.println("dialogItemChanged: " + e); // A previous run may have been cancelled so we have to handle this. if (Utils.isInterrupted()) { if (Utils.isMacro()) return true; // Q. Should we ask if the user wants to restart? IJ.resetEscape(); } inputSettings.inputOption = ResultsManager.getInputSource(gd); // Load the results MemoryPeakResults results = ResultsManager.loadInputResults(inputSettings.inputOption, true); if (results == null || results.size() == 0) { IJ.error(TITLE, "No results could be loaded"); return false; } if (!readSettings(gd)) return false; // Clone so that the workflow has it's own unique reference OPTICSSettings settings = inputSettings.clone(); Settings baseResults = new Settings(results); if (preview) { // Run the settings if (debug) System.out.println("Adding work"); workflow.run(settings, baseResults); workflow.startPreview(); } else { workflow.stopPreview(); // Stage the work but do not run workflow.stage(settings, baseResults); } return true; } abstract boolean readSettings(GenericDialog gd); } private class OPTICSDialogListener extends BaseDialogListener { boolean readSettings(GenericDialog gd) { inputSettings.minPoints = (int) Math.abs(gd.getNextNumber()); inputSettings.setOPTICSMode(gd.getNextChoiceIndex()); inputSettings.numberOfSplitSets = (int) Math.abs(gd.getNextNumber()); if (extraOptions) { inputSettings.useRandomVectors = gd.getNextBoolean(); inputSettings.saveApproximateSets = gd.getNextBoolean(); inputSettings.setSampleMode(gd.getNextChoiceIndex()); } inputSettings.generatingDistance = Math.abs(gd.getNextNumber()); inputSettings.setClusteringMode(gd.getNextChoiceIndex()); inputSettings.xi = Math.abs(gd.getNextNumber()); inputSettings.topLevel = gd.getNextBoolean(); inputSettings.upperLimit = Math.abs(gd.getNextNumber()); inputSettings.lowerLimit = Math.abs(gd.getNextNumber()); inputSettings.clusteringDistance = Math.abs(gd.getNextNumber()); inputSettings.core = gd.getNextBoolean(); inputSettings.imageScale = Math.abs(gd.getNextNumber()); inputSettings.setImageMode((ImageMode) imageModeArray[gd.getNextChoiceIndex()]); inputSettings.weighted = gd.getNextBoolean(); inputSettings.equalised = gd.getNextBoolean(); if (extraOptions) { inputSettings.lambda = Math.abs(gd.getNextNumber()); } inputSettings.setOutlineMode((OutlineMode) outlineModeArray[gd.getNextChoiceIndex()]); inputSettings.setSpanningTreeMode(gd.getNextChoiceIndex()); inputSettings.setPlotMode(gd.getNextChoiceIndex()); preview = gd.getNextBoolean(); if (extraOptions) debug = gd.getNextBoolean(); if (gd.invalidNumber()) return false; // Check arguments try { Parameters.isAboveZero("Xi", inputSettings.xi); Parameters.isBelow("Xi", inputSettings.xi, 1); if (inputSettings.upperLimit > 0) Parameters.isAbove("Upper limit", inputSettings.upperLimit, inputSettings.lowerLimit); } catch (IllegalArgumentException ex) { Utils.log(TITLE + ": " + ex.getMessage()); return false; } return true; } } private class DBSCANDialogListener extends BaseDialogListener { boolean readSettings(GenericDialog gd) { inputSettings.minPoints = (int) Math.abs(gd.getNextNumber()); inputSettings.fractionNoise = Math.abs(gd.getNextNumber() / 100); inputSettings.samples = (int) Math.abs(gd.getNextNumber()); inputSettings.sampleFraction = Math.abs(gd.getNextNumber() / 100); inputSettings.clusteringDistance = Math.abs(gd.getNextNumber()); inputSettings.core = gd.getNextBoolean(); inputSettings.imageScale = Math.abs(gd.getNextNumber()); inputSettings.setImageMode((ImageMode) imageModeArray[gd.getNextChoiceIndex()]); inputSettings.weighted = gd.getNextBoolean(); inputSettings.equalised = gd.getNextBoolean(); if (extraOptions) { inputSettings.lambda = Math.abs(gd.getNextNumber()); } inputSettings.setOutlineMode((OutlineMode) outlineModeArray[gd.getNextChoiceIndex()]); preview = gd.getNextBoolean(); if (extraOptions) debug = gd.getNextBoolean(); if (gd.invalidNumber()) return false; return true; } } }