package gdsc.foci;
import java.awt.AWTEvent;
import java.awt.Label;
import org.apache.commons.math3.exception.NumberIsTooSmallException;
import org.apache.commons.math3.stat.correlation.PearsonsCorrelation;
import org.apache.commons.math3.stat.descriptive.DescriptiveStatistics;
import org.apache.commons.math3.stat.inference.TestUtils;
import gdsc.UsageTracker;
import gdsc.core.threshold.AutoThreshold;
import ij.IJ;
import ij.ImagePlus;
import ij.ImageStack;
import ij.WindowManager;
import ij.gui.DialogListener;
import ij.gui.GenericDialog;
import ij.gui.Roi;
import ij.measure.ResultsTable;
import ij.plugin.filter.ExtendedPlugInFilter;
import ij.plugin.filter.GaussianBlur;
import ij.plugin.filter.ParticleAnalyzer;
import ij.plugin.filter.PlugInFilterRunner;
import ij.process.Blitter;
import ij.process.ByteProcessor;
import ij.process.ImageProcessor;
import ij.text.TextWindow;
/**
* Analyses the intensity of each channel within the brightest spot of the
* selected channel.
* <P>
* Identifies cells using thresholding. For each cell the region of the brightest spot is identified. The intensity of
* each channel inside and outside the spot are then compared.
*/
public class SpotAnalyser implements ExtendedPlugInFilter, DialogListener
{
public static final String TITLE = "Spot Analyser";
private static String[] maskOptions = new String[] { "Threshold", "Use ROI" };
private static final int INSIDE = 0;
private static final int OUTSIDE = 1;
private int flags = DOES_16 + DOES_8G + SNAPSHOT + FINAL_PROCESSING;
private ImagePlus imp;
private ImageProcessor maskIp;
private Label label;
private double min = 0, max = 0;
private int spotChannel = 0, thresholdChannel = 0;
private int noOfParticles = 0;
private boolean containsRoiMask = false;
private static TextWindow results = null;
private static boolean writeHeader = true;
private static int maskOption = 0;
private static double blur = 3;
private static String thresholdMethod = AutoThreshold.Method.OTSU.name;
private static double minSize = 50;
private static boolean showParticles = false;
private static int maxPeaks = 1;
private static double fraction = 0.9;
private static boolean showSpots = false;
/*
* (non-Javadoc)
*
* @see ij.plugin.filter.PlugInFilter#setup(java.lang.String, ij.ImagePlus)
*/
public int setup(String arg, ImagePlus imp)
{
UsageTracker.recordPlugin(this.getClass(), arg);
if (imp == null)
{
IJ.noImage();
return DONE;
}
this.imp = imp;
ImageProcessor ip = imp.getProcessor();
if (arg.equals("final"))
{
analyseImage();
return DONE;
}
min = ip.getMin();
max = ip.getMax();
return flags;
}
private void resetImage()
{
ImageProcessor ip = imp.getProcessor();
ip.reset();
ip.setMinAndMax(min, max);
imp.updateAndDraw();
}
/*
* (non-Javadoc)
*
* @see ij.plugin.filter.ExtendedPlugInFilter#showDialog(ij.ImagePlus,
* java.lang.String, ij.plugin.filter.PlugInFilterRunner)
*/
public int showDialog(ImagePlus imp, String command, PlugInFilterRunner pfr)
{
GenericDialog gd = new GenericDialog(TITLE);
spotChannel = imp.getChannel();
thresholdChannel = spotChannel;
Roi roi = imp.getRoi();
if (roi != null && roi.isArea())
{
containsRoiMask = true;
gd.addChoice("Threshold_Channel", maskOptions, maskOptions[maskOption]);
// Set up the mask using the ROI
maskIp = new ByteProcessor(imp.getWidth(), imp.getHeight());
maskIp.setColor(255);
maskIp.fill(roi);
maskIp.setThreshold(0, 254, ImageProcessor.NO_LUT_UPDATE);
}
String[] channels = new String[imp.getNChannels()];
for (int i = 0; i < channels.length; i++)
channels[i] = Integer.toString(i + 1);
gd.addChoice("Threshold_Channel", channels, channels[thresholdChannel - 1]);
gd.addSlider("Blur", 0.01, 5, blur);
gd.addChoice("Threshold_method", AutoThreshold.getMethods(), thresholdMethod);
gd.addChoice("Spot_Channel", channels, channels[spotChannel - 1]);
gd.addSlider("Min_size", 50, 10000, minSize);
gd.addCheckbox("Show_particles", showParticles);
gd.addSlider("Max_peaks", 1, 10, maxPeaks);
gd.addSlider("Fraction", 0.01, 1, fraction);
gd.addCheckbox("Show_spots", showSpots);
gd.addMessage("");
label = (Label) gd.getMessage();
gd.addHelp(gdsc.help.URL.UTILITY);
gd.addPreviewCheckbox(pfr);
gd.addDialogListener(this);
gd.showDialog();
if (gd.wasCanceled() || !dialogItemChanged(gd, null))
return DONE;
return flags;
}
/*
* (non-Javadoc)
*
* @see ij.gui.DialogListener#dialogItemChanged(ij.gui.GenericDialog,
* java.awt.AWTEvent)
*/
public boolean dialogItemChanged(GenericDialog gd, AWTEvent e)
{
if (containsRoiMask)
maskOption = gd.getNextChoiceIndex();
thresholdChannel = gd.getNextChoiceIndex() + 1;
blur = gd.getNextNumber();
if (gd.invalidNumber())
return false;
thresholdMethod = gd.getNextChoice();
spotChannel = gd.getNextChoiceIndex() + 1;
minSize = gd.getNextNumber();
if (gd.invalidNumber())
minSize = 50;
showParticles = gd.getNextBoolean();
maxPeaks = (int) gd.getNextNumber();
if (gd.invalidNumber() || maxPeaks < 1)
maxPeaks = 1;
fraction = gd.getNextNumber();
if (gd.invalidNumber())
fraction = 0.9;
showSpots = gd.getNextBoolean();
// Preview checkbox will be null if running headless
if (gd.getPreviewCheckbox() != null && !gd.getPreviewCheckbox().getState())
{
// Reset preview
label.setText("");
resetImage();
}
return true;
}
/*
* (non-Javadoc)
*
* @see ij.plugin.filter.ExtendedPlugInFilter#setNPasses(int)
*/
public void setNPasses(int nPasses)
{
// Do nothing
}
/*
* (non-Javadoc)
*
* @see ij.plugin.filter.PlugInFilter#run(ij.process.ImageProcessor)
*/
public void run(ImageProcessor inputIp)
{
// Just run the particle analyser
noOfParticles = 0;
// Avoid destructively modifying the image. Work on the selected channel for thresholding
int index = imp.getStackIndex(thresholdChannel, imp.getSlice(), imp.getFrame());
ImageProcessor ip = imp.getImageStack().getProcessor(index).duplicate();
if (!checkData(ip))
{
IJ.error(TITLE, "Channel has no data range");
resetImage();
return;
}
if (containsRoiMask && maskOption == 1)
{
ip = maskIp.duplicate();
ip.setThreshold(254, 255, ImageProcessor.NO_LUT_UPDATE);
}
else
{
// Blur the image
if (blur > 0)
{
GaussianBlur gb = new GaussianBlur();
gb.blurGaussian(ip, blur, blur, 0.002);
}
// Threshold
int t = AutoThreshold.getThreshold(thresholdMethod, ip.getHistogram());
ip.setThreshold(t, 65536, ImageProcessor.NO_LUT_UPDATE);
}
// Analyse particles
ImagePlus particlesImp = new ImagePlus(imp.getTitle() + " Particles", ip);
int analyserOptions = ParticleAnalyzer.SHOW_ROI_MASKS + ParticleAnalyzer.IN_SITU_SHOW +
ParticleAnalyzer.EXCLUDE_EDGE_PARTICLES;
int measurements = 0;
ResultsTable rt = new ResultsTable();
double maxSize = Double.POSITIVE_INFINITY;
ParticleAnalyzer pa = new ParticleAnalyzer(analyserOptions, measurements, rt, minSize, maxSize);
pa.analyze(particlesImp, ip);
ImageProcessor particlesIp = particlesImp.getProcessor();
noOfParticles = rt.getCounter();
if (label != null)
label.setText(String.format("%d particle%s", noOfParticles, (noOfParticles == 1) ? "" : "s"));
// Show the particles
inputIp.copyBits(particlesIp, 0, 0, Blitter.COPY);
inputIp.setMinAndMax(0, noOfParticles);
imp.updateAndDraw();
}
private boolean checkData(ImageProcessor ip)
{
final int firstValue = ip.get(0);
for (int i = 1; i < ip.getPixelCount(); i++)
{
if (ip.get(i) != firstValue)
return true;
}
return false;
}
private void analyseImage()
{
if (noOfParticles == 0)
{
resetImage(); // Restore the original image
return;
}
// The particles have already been identified in the run() method.
// Copy the pixels.
ImageProcessor particlesIp = imp.getProcessor().duplicate();
resetImage(); // Restore the original image
// Get the channel for spot analysis
ImagePlus tmpImp;
{
int index = imp.getStackIndex(spotChannel, imp.getSlice(), imp.getFrame());
ImageProcessor ip = imp.getImageStack().getProcessor(index);
tmpImp = new ImagePlus("Dummy", ip);
}
if (showParticles)
{
ImageProcessor tmpIp = particlesIp.duplicate();
tmpIp.setMinAndMax(0, noOfParticles);
showImage(this.imp.getTitle() + " Particles", tmpIp);
}
ImageProcessor spotsIp = particlesIp.createProcessor(particlesIp.getWidth(), particlesIp.getHeight());
// For each particle:
for (int particle = 1; particle <= noOfParticles; particle++)
{
// Run FindFoci to find the single highest spot
FindFoci ff = new FindFoci();
ImagePlus mask = createMask(particlesIp, particle);
int backgroundMethod = FindFoci.BACKGROUND_MEAN;
double backgroundParameter = 0;
String autoThresholdMethod = "";
int searchMethod = FindFoci.SEARCH_ABOVE_BACKGROUND;
double searchParameter = 0;
int minSize = 5;
int peakMethod = FindFoci.PEAK_RELATIVE_ABOVE_BACKGROUND;
double peakParameter = 0.5;
int outputType = FindFoci.OUTPUT_MASK | FindFoci.OUTPUT_MASK_FRACTION_OF_HEIGHT |
FindFoci.OUTPUT_MASK_NO_PEAK_DOTS;
int sortIndex = FindFoci.SORT_MAX_VALUE;
int options = FindFoci.OPTION_STATS_INSIDE;
int centreMethod = FindFoci.CENTRE_MAX_VALUE_ORIGINAL;
double centreParameter = 0;
double fractionParameter = fraction;
FindFociResults result = ff.findMaxima(tmpImp, mask, backgroundMethod, backgroundParameter, autoThresholdMethod,
searchMethod, searchParameter, maxPeaks, minSize, peakMethod, peakParameter, outputType, sortIndex,
options, blur, centreMethod, centreParameter, fractionParameter);
if (result == null)
continue;
ImagePlus maximaImp = result.mask;
ImageProcessor spotIp = maximaImp.getProcessor();
// Renumber to the correct particle value
for (int j = 0; j < spotIp.getPixelCount(); j++)
if (spotIp.get(j) != 0)
spotIp.set(j, particle);
spotsIp.copyBits(spotIp, 0, 0, Blitter.ADD);
}
if (showSpots)
{
spotsIp.setMinAndMax(0, noOfParticles);
showImage(this.imp.getTitle() + " Spots", spotsIp);
}
// Now we have:
// particlesIp => Particles
// spotsIp => The largest spot within each particle
// Create a mask of the particles
byte[] mask = new byte[particlesIp.getPixelCount()];
for (int i = 0; i < particlesIp.getPixelCount(); i++)
{
if (particlesIp.get(i) != 0)
mask[i] = 1;
}
// Subtract the spots from the particles
particlesIp.copyBits(spotsIp, 0, 0, Blitter.SUBTRACT);
createResultsWindow();
// Create a statistical summary for [channel][inside/outside][particle]
DescriptiveStatistics[][][] stats = new DescriptiveStatistics[imp.getNChannels() + 1][2][noOfParticles + 1];
ImageStack stack = imp.getImageStack();
for (int channel = 1; channel <= imp.getNChannels(); channel++)
{
int index = imp.getStackIndex(channel, imp.getSlice(), imp.getFrame());
ImageProcessor channelIp = stack.getProcessor(index);
for (int particle = 0; particle <= noOfParticles; particle++)
{
stats[channel][INSIDE][particle] = new DescriptiveStatistics();
stats[channel][OUTSIDE][particle] = new DescriptiveStatistics();
}
for (int i = 0; i < mask.length; i++)
{
if (mask[i] != 0)
{
int v = channelIp.get(i);
stats[channel][INSIDE][spotsIp.get(i)].addValue(v);
stats[channel][OUTSIDE][particlesIp.get(i)].addValue(v);
}
}
}
// Add the counts inside and outside
for (int particle = 1; particle <= noOfParticles; particle++)
{
// Just choose the first channel (all are the same)
addResult(particle, stats[1][INSIDE][particle].getN(), stats[1][OUTSIDE][particle].getN());
}
// Add the statistics inside and outside for each channel
for (int channel = 1; channel <= imp.getNChannels(); channel++)
{
for (int particle = 1; particle <= noOfParticles; particle++)
{
addResult(channel, particle, stats);
}
}
}
private ImagePlus showImage(String title, ImageProcessor ip)
{
ImagePlus imp = WindowManager.getImage(title);
if (imp == null)
{
imp = new ImagePlus(title, ip);
imp.show();
}
else
{
imp.setProcessor(ip);
imp.updateAndDraw();
}
return imp;
}
/**
* Zero all pixels that are not the given value
*
* @param ip
* @param value
* @return
*/
private ImagePlus createMask(ImageProcessor ip, int value)
{
ip = ip.duplicate();
for (int i = 0; i < ip.getPixelCount(); i++)
{
if (ip.get(i) != value)
ip.set(i, 0);
}
return new ImagePlus("Mask", ip);
}
private void createResultsWindow()
{
if (!java.awt.GraphicsEnvironment.isHeadless())
{
if (results == null || !results.isShowing())
{
results = new TextWindow(TITLE + " Results",
"Image\tChannel\tParticle\tInside Sum\tAv\tSD\tR\tOutside Sum\tAv\tSD\tR\tIncrease\tp-value",
"", 800, 600);
results.setVisible(true);
}
}
else
{
if (writeHeader)
{
writeHeader = false;
IJ.log("Image\tChannel\tParticle\tInside Sum\tAv\tSD\tR\tOutside Sum\tAv\tSD\tR\tIncrease\tp-value");
}
}
}
private void addResult(int particle, long countInside, long countOutside)
{
StringBuilder sb = new StringBuilder();
sb.append(imp.getTitle()).append("\t\t");
sb.append(particle).append("\t");
sb.append(countInside).append("\t\t\t\t");
sb.append(countOutside).append("\t\t\t\t");
if (java.awt.GraphicsEnvironment.isHeadless())
{
IJ.log(sb.toString());
}
else
{
results.append(sb.toString());
}
}
private void addResult(int channel, int particle, DescriptiveStatistics[][][] stats)
{
StringBuilder sb = new StringBuilder();
sb.append(imp.getTitle()).append("\t");
sb.append(channel).append("\t");
sb.append(particle).append("\t");
double sx = stats[channel][INSIDE][particle].getSum();
double sd = stats[channel][INSIDE][particle].getStandardDeviation();
long n = stats[channel][INSIDE][particle].getN();
double av = sx / n;
double sx2 = stats[channel][OUTSIDE][particle].getSum();
double sd2 = stats[channel][OUTSIDE][particle].getStandardDeviation();
long n2 = stats[channel][OUTSIDE][particle].getN();
double av2 = sx2 / n2;
double p = 0;
try
{
p = TestUtils.tTest(stats[channel][INSIDE][particle], stats[channel][OUTSIDE][particle]);
}
catch (NumberIsTooSmallException e)
{
// Ignore
}
// Correlate inside & outside spot with the principle channel
double r = 1;
double r2 = 1;
if (channel != spotChannel) // Principle channel => No test required
{
r = new PearsonsCorrelation().correlation(stats[channel][INSIDE][particle].getValues(),
stats[spotChannel][INSIDE][particle].getValues());
r2 = new PearsonsCorrelation().correlation(stats[channel][OUTSIDE][particle].getValues(),
stats[spotChannel][OUTSIDE][particle].getValues());
}
sb.append(IJ.d2s(sx, 0)).append("\t").append(IJ.d2s(av, 2)).append("\t").append(IJ.d2s(sd, 2)).append("\t")
.append(IJ.d2s(r, 3)).append("\t");
sb.append(IJ.d2s(sx2, 0)).append("\t").append(IJ.d2s(av2, 2)).append("\t").append(IJ.d2s(sd2, 2)).append("\t")
.append(IJ.d2s(r2, 3)).append("\t");
sb.append(IJ.d2s(av / av2, 2)).append("\t");
sb.append(String.format("%.3g", p));
if (java.awt.GraphicsEnvironment.isHeadless())
{
IJ.log(sb.toString());
}
else
{
results.append(sb.toString());
}
}
}