/*________________________________________________________________________________________________ Title: Hierarchical clustering using JNDs '''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''' Code Author: Jolyon Troscianko Date Originally Implemented: 28/7/2015 Source for first implementation: Russell A. Ligon, Christopher D. Diaz, Janelle L. Morano, Jolyon Troscianko, Martin Stevens, Annalyse Moskeland, Timothy G. Laman, Edwin Scholes III 2018. Evolution of correlated complexity in the radically different courtship signals of birds-of-paradise PLOS BIOLOGY For detailed implementation with examples and test cases, please see: van den Berg CP, Cheney KL, Endler JA, Marshall JN, Troscianko J. (In prep). Image Smoothing and Segmentation using Colour and Luminance Discrimination Thresholds: JND Clustering and Weber Smart Blur. ............................................................................................................ This code uses agglomerative hierarchical clustering to reduce a colour image down to a 'usable' number of clusters. The code starts with each pixel being its own cluster. Each cluster is compared to its neighbouring clusters in the image XY plane, and distances calculated from V&O JNDs. Following distance measurements each cluster is combined with its nearest neighbouring cluster. Nodes can have multiple clustering events at each pass, e.g. if cluster A is closest to B, but B is closest to C then alll three will be clustered, meaning whole strings or neighbouring regions can be clustered. In practice each cluster tends to be combined with two or three clusters on each pass. The distance radius increases with each pass, so as clusters get larger they can also combine with neighbours further away. This also keeps the processing for each pass fairly constant (i.e. there are fewer clusters on each pass, but each one must be compared to a larger number of neighbours). Clustering therefore takes place across n + 2 dimensions (n colours plus x & y space). -------------------------------------------------------------------------------------------------------------------------------- Requirements: Input images should be cone-catch images (see Troscianko & Stevens 2015). The Weber fraction order must match the slice order. Troscianko J, Stevens M. 2015. Image calibration and analysis toolbox – a free software suite for objectively measuring reflectance, colour and pattern. Methods in Ecology & Evolution. https://doi.org/10.1111/2041-210X.12439 _________________________________________________________________________________________________ */ import ij.*; import ij.plugin.filter.PlugInFilter; import ij.process.*; import ij.gui.*; import ij.measure.ResultsTable; import ij.text.TextWindow; public class S1_Code implements PlugInFilter { ImageStack stack; public int setup(String arg, ImagePlus imp) { stack = imp.getStack(); return DOES_32 + STACK_REQUIRED; } public void run(ImageProcessor ip) { IJ.showStatus("Clustering"); int w = stack.getWidth(); int h = stack.getHeight(); int nPoints = w*h; int nDims = stack.getSize(); int nLoops = 12; int radiusMultiplier = 2; int haltClusters = 1; int recordClusters = 8; String imTitle = "ImageID"; double[] weber = new double[4]; // weber[0] = 0.05; // LW HUMAN scaled to 0.05 for LWS from Hofer et al 2005, assuming a 2:1 L:M and 5.72% S cones weber[1] = 0.0707106781; // MW weber[2] = 0.1657433633; // SW weber[3] = 0.1; // double lumWeber = 0.05; double JNDthreshold = 3.0; double lumJNDthreshold = 3.0; String[] sliceNames = new String[nDims]; sliceNames = stack.getSliceLabels(); GenericDialog gd = new GenericDialog("Hierarchical Clustering"); gd.addNumericField("Colour_JND_Threshold", JNDthreshold, 3); gd.addNumericField("Luminance_JND_Threshold", lumJNDthreshold, 3); gd.addMessage("Number of clustering loops"); gd.addNumericField("Loops", nLoops, 0); gd.addMessage("The search radius is increased with each loop by this variable"); gd.addNumericField("Radius Multiplier:", radiusMultiplier, 0); gd.addMessage("Clustering will stop early if this value >1"); gd.addNumericField("Stop Number:", haltClusters, 0); gd.addNumericField("Record from pass:", recordClusters, 0); gd.addCheckbox("Output Image", true); gd.addStringField("Image ID:", imTitle, 20); gd.addMessage(" "); gd.addMessage("Weber Fractions"); //gd.addNumericField("Weber_" + (i+1), weber[i], 3); for(int i=0; i 0) // don't import negative numbers means[(i*w*h)+k] = (double) (pixels[k]); else means[k] = 0; // flag negative numbers in the first channel } for(int i=0; i=0 && x=0 && y cbB[newLoc]) // BOTTOM cbB[newLoc] = cbB[oldLoc]; if(cbR[oldLoc] > cbR[newLoc]) // RIGHT cbR[newLoc] = cbR[oldLoc]; if(cbL[oldLoc] < cbL[newLoc]) // LEFT cbL[newLoc] = cbL[oldLoc]; // Update the nearest-neighbour of the oldLoc // if the oldLoc was due to be linked to a 3rd group then re-loop this iteration, adding 3rd loc (this can then start a whole chain) if(nearestID[oldLoc] != newLoc){ // oldLoc's nearest neighbour was a 3rd value, so add this as newLoc's nearest neighbour & re-loop nearestID[i] = nearestID[oldLoc]; i = i-1; } } // i if(m>recordClusters-2){ row = 0; for(int i=0; i