package gdsc.smlm.ij.plugins;
import java.awt.AWTEvent;
import java.awt.Checkbox;
import java.awt.Color;
import java.awt.Component;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Label;
import java.awt.Rectangle;
import java.awt.Scrollbar;
import java.awt.TextField;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.geom.Rectangle2D;
import java.util.Arrays;
import java.util.Collection;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import org.apache.commons.math3.analysis.MultivariateFunction;
import org.apache.commons.math3.analysis.ParametricUnivariateFunction;
import org.apache.commons.math3.analysis.UnivariateFunction;
import org.apache.commons.math3.analysis.function.Gaussian;
import org.apache.commons.math3.analysis.interpolation.LoessInterpolator;
import org.apache.commons.math3.exception.TooManyEvaluationsException;
import org.apache.commons.math3.fitting.GaussianCurveFitter;
import org.apache.commons.math3.fitting.SimpleCurveFitter;
import org.apache.commons.math3.fitting.WeightedObservedPoint;
import org.apache.commons.math3.fitting.WeightedObservedPoints;
import org.apache.commons.math3.optim.InitialGuess;
import org.apache.commons.math3.optim.MaxEval;
import org.apache.commons.math3.optim.PointValuePair;
import org.apache.commons.math3.optim.nonlinear.scalar.GoalType;
import org.apache.commons.math3.optim.nonlinear.scalar.ObjectiveFunction;
import org.apache.commons.math3.optim.nonlinear.scalar.noderiv.NelderMeadSimplex;
import org.apache.commons.math3.optim.nonlinear.scalar.noderiv.SimplexOptimizer;
import org.apache.commons.math3.optim.univariate.BracketFinder;
import org.apache.commons.math3.optim.univariate.BrentOptimizer;
import org.apache.commons.math3.optim.univariate.SearchInterval;
import org.apache.commons.math3.optim.univariate.UnivariateObjectiveFunction;
import org.apache.commons.math3.optim.univariate.UnivariateOptimizer;
import org.apache.commons.math3.optim.univariate.UnivariatePointValuePair;
import org.apache.commons.math3.random.Well19937c;
import org.apache.commons.math3.special.Erf;
import org.apache.commons.math3.stat.descriptive.DescriptiveStatistics;
import org.apache.commons.math3.util.FastMath;
import org.apache.commons.math3.util.MathArrays;
import gdsc.core.ij.IJTrackProgress;
import gdsc.core.ij.Utils;
import gdsc.core.logging.NullTrackProgress;
import gdsc.core.logging.TrackProgress;
import gdsc.core.utils.Maths;
import gdsc.core.utils.MedianWindow;
import gdsc.core.utils.Random;
import gdsc.core.utils.Statistics;
import gdsc.core.utils.StoredDataStatistics;
import gdsc.core.utils.TextUtils;
import gdsc.core.utils.TurboList;
/*-----------------------------------------------------------------------------
* GDSC SMLM Software
*
* Copyright (C) 2017 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 gdsc.smlm.function.gaussian.Gaussian2DFunction;
import gdsc.smlm.ij.frc.FRC;
import gdsc.smlm.ij.frc.FRC.FIREResult;
import gdsc.smlm.ij.frc.FRC.FRCCurve;
import gdsc.smlm.ij.frc.FRC.FRCCurveResult;
import gdsc.smlm.ij.frc.FRC.FourierMethod;
import gdsc.smlm.ij.frc.FRC.SamplingMethod;
import gdsc.smlm.ij.frc.FRC.ThresholdMethod;
import gdsc.smlm.ij.plugins.ResultsManager.InputSource;
import gdsc.smlm.ij.results.IJImagePeakResults;
import gdsc.smlm.ij.results.ImagePeakResultsFactory;
import gdsc.smlm.ij.results.ResultsImage;
import gdsc.smlm.ij.results.ResultsMode;
import gdsc.smlm.ij.settings.SettingsManager;
import gdsc.smlm.results.Calibration;
import gdsc.smlm.results.MemoryPeakResults;
import gdsc.smlm.results.PeakResult;
import gnu.trove.list.array.TDoubleArrayList;
import ij.IJ;
import ij.ImagePlus;
import ij.Macro;
import ij.Prefs;
import ij.WindowManager;
import ij.gui.DialogListener;
import ij.gui.GenericDialog;
import ij.gui.NonBlockingGenericDialog;
import ij.gui.Plot;
import ij.gui.Plot2;
import ij.gui.PlotWindow;
import ij.plugin.PlugIn;
import ij.plugin.WindowOrganiser;
import ij.plugin.frame.Recorder;
import ij.process.ImageProcessor;
import ij.process.LUT;
import ij.process.LUTHelper;
import ij.process.LUTHelper.LutColour;
/**
* Computes the Fourier Image Resolution of an image
* <p>
* Implements the FIRE (Fourier Image REsolution) method described in:<br>
* Niewenhuizen, et al (2013). Measuring image resolution in optical nanoscopy. Nature Methods, 10, 557<br>
* http://www.nature.com/nmeth/journal/v10/n6/full/nmeth.2448.html
* <p>
* A second plugin allows estimation of the spurious correlation component contributed by the same molecule being
* present in both super-resolution images due to splitting of repeat localisations. The correction Q factor is the
* number of times a molecule is repeat localised (i.e. average blinks per molecule). This code was developed using the
* Matlab examples provided by Bernd Reiger.
*/
public class FIRE implements PlugIn
{
private String TITLE = "Fourier Image REsolution (FIRE)";
private static String inputOption = "";
private static String inputOption2 = "";
private static int repeats = 1;
private static boolean useSignal = false;
private boolean myUseSignal = false;
private static int maxPerBin = 0; // 5 in the Niewenhuizen paper
private static boolean randomSplit = true;
private static int blockSize = 50;
private static String[] SCALE_ITEMS;
private static int[] SCALE_VALUES = new int[] { 0, 1, 2, 4, 8, 16, 32, 64, 128 };
private static String[] IMAGE_SIZE_ITEMS;
private static int[] IMAGE_SIZE_VALUES;
private static int imageScaleIndex = 0;
private static int imageSizeIndex;
// The Q value and the mean and sigma for spurious correlation correction
private static boolean spuriousCorrelationCorrection = false;
private static double qValue, mean, sigma;
static
{
SCALE_ITEMS = new String[SCALE_VALUES.length];
SCALE_ITEMS[0] = "Auto";
for (int i = 1; i < SCALE_VALUES.length; i++)
SCALE_ITEMS[i] = Integer.toString(SCALE_VALUES[i]);
// Create size for Fourier transforms. Must be power of 2.
IMAGE_SIZE_VALUES = new int[32];
IMAGE_SIZE_ITEMS = new String[IMAGE_SIZE_VALUES.length];
int size = 512; // Start at a reasonable size. Too small does not work.
int count = 0;
while (size <= 16384)
{
if (size == 2048)
imageSizeIndex = count;
// Image sizes are 1 smaller so that rounding error when scaling does not create an image too large for the power of 2
IMAGE_SIZE_VALUES[count] = size - 1;
IMAGE_SIZE_ITEMS[count] = Integer.toString(size);
size *= 2;
count++;
}
IMAGE_SIZE_VALUES = Arrays.copyOf(IMAGE_SIZE_VALUES, count);
IMAGE_SIZE_ITEMS = Arrays.copyOf(IMAGE_SIZE_ITEMS, count);
}
/**
* Specify the method to use to determine the parameters for the distribution of the localisation precision (assumed
* to be Gaussian)
*/
private enum PrecisionMethod
{
//@formatter:off
/**
* Use a fixed value for the precision distribution mean and standard deviation
*/
FIXED{ public String getName() { return "Fixed"; }},
/**
* Use the precision value that is stored in the results, e.g. from loaded data.
* The values can then be used to fit the entire distribution using a Gaussian or
* sampled to construct a decay curve from which the parameters are estimated.
*/
STORED{ public String getName() { return "Stored"; }},
/**
* Calculate the precision of each localisation using the formula of Mortensen.
* The values can then be used to fit the entire distribution using a Gaussian or
* sampled to construct a decay curve from which the parameters are estimated.
*/
CALCULATE{ public String getName() { return "Calculate"; }};
//@formatter:on
@Override
public String toString()
{
return getName();
}
/**
* Gets the name.
*
* @return the name
*/
abstract public String getName();
}
private static double perimeterSamplingFactor = 1;
private static int fourierMethodIndex = FourierMethod.JTRANSFORMS.ordinal();
private FourierMethod fourierMethod;
private static int samplingMethodIndex = SamplingMethod.RADIAL_SUM.ordinal();
private SamplingMethod samplingMethod;
private static int thresholdMethodIndex = ThresholdMethod.FIXED_1_OVER_7.ordinal();
private ThresholdMethod thresholdMethod;
private static boolean showFRCCurve = true;
private static boolean showFRCCurveRepeats = false;
private static boolean showFRCTimeEvolution = false;
private static int precisionMethodIndex = PrecisionMethod.CALCULATE.ordinal();
private PrecisionMethod precisionMethod;
private static boolean sampleDecay = false;
private static boolean loessSmoothing = false;
private static boolean fitPrecision = false;
private static double minQ = 0.2;
private static double maxQ = 0.45;
private static boolean chooseRoi = false;
private static String roiImage = "";
private boolean extraOptions;
private Rectangle roiBounds;
private int roiImageWidth, roiImageHeight;
// Stored in initialisation
MemoryPeakResults results, results2;
Rectangle2D dataBounds;
String units;
double nmPerPixel = 1;
// Stored in setCorrectionParameters
private double correctionQValue, correctionMean, correctionSigma;
public class FireImages
{
/** The first super-resolution image. */
final ImageProcessor ip1;
/** The second super-resolution image. */
final ImageProcessor ip2;
/** The nm per pixel in the super-resolution images */
final double nmPerPixel;
FireImages(ImageProcessor ip1, ImageProcessor ip2, double nmPerPixel)
{
this.ip1 = ip1;
this.ip2 = ip2;
this.nmPerPixel = nmPerPixel;
}
}
/**
* Contains the Fourier Image REsolution (FIRE) result.
*/
public class FireResult
{
/** The fire number (in nm). */
final double fireNumber;
/** The correlation at the given resolution. */
final double correlation;
/** The FRC curve used to compute the resolution. */
final FRCCurve frcCurve;
/** The original correlation curve, i.e. the raw curve before smoothing. */
final double[] originalCorrelationCurve;
FireResult(double fireNumber, double correlation, FRCCurve frcCurve, double[] originalCorrelationCurve)
{
this.fireNumber = fireNumber;
this.correlation = correlation;
this.frcCurve = frcCurve;
this.originalCorrelationCurve = originalCorrelationCurve;
}
/**
* Gets the nm per pixel for the super-resolution images used to construct the FRC curve.
*
* @return the nm per pixel
*/
double getNmPerPixel()
{
return frcCurve.nmPerPixel;
}
}
private class FIREWorker implements Runnable
{
final double fourierImageScale;
final int imageSize;
String name;
FireResult result;
Plot2 plot;
/**
* Flag to denote that an out-of-memory error occurred. This is probably due to using too many threads to
* compute large Fourier transforms.
*/
boolean oom = false;
public FIREWorker(int id, double fourierImageScale, int imageSize)
{
this.fourierImageScale = fourierImageScale;
this.imageSize = imageSize;
name = results.getName() + " [" + id + "]";
}
public void run()
{
try
{
result = calculateFireNumber(fourierMethod, samplingMethod, thresholdMethod, fourierImageScale,
imageSize);
if (showFRCCurve)
{
plot = createFrcCurve(name, result, thresholdMethod);
if (showFRCCurveRepeats)
// Do this on the thread
plot.draw();
}
}
catch (OutOfMemoryError e)
{
oom = true;
}
}
}
/*
* (non-Javadoc)
*
* @see ij.plugin.PlugIn#run(java.lang.String)
*/
public void run(String arg)
{
extraOptions = Utils.isExtraOptions();
SMLMUsageTracker.recordPlugin(this.getClass(), arg);
// Require some fit results and selected regions
int size = MemoryPeakResults.countMemorySize();
if (size == 0)
{
IJ.error(TITLE, "There are no fitting results in memory");
return;
}
if ("q".equals(arg))
{
TITLE += " Q estimation";
runQEstimation();
return;
}
IJ.showStatus(TITLE + " ...");
if (!showInputDialog())
return;
MemoryPeakResults results = ResultsManager.loadInputResults(inputOption, false);
if (results == null || results.size() == 0)
{
IJ.error(TITLE, "No results could be loaded");
return;
}
MemoryPeakResults results2 = ResultsManager.loadInputResults(inputOption2, false);
results = cropToRoi(results);
if (results.size() < 2)
{
IJ.error(TITLE, "No results within the crop region");
return;
}
if (results2 != null)
{
results2 = cropToRoi(results2);
if (results2.size() < 2)
{
IJ.error(TITLE, "No results2 within the crop region");
return;
}
}
initialise(results, results2);
if (!showDialog())
return;
long start = System.currentTimeMillis();
// Compute FIRE
String name = results.getName();
double fourierImageScale = SCALE_VALUES[imageScaleIndex];
int imageSize = IMAGE_SIZE_VALUES[imageSizeIndex];
if (this.results2 != null)
{
name += " vs " + results2.getName();
FireResult result = calculateFireNumber(fourierMethod, samplingMethod, thresholdMethod, fourierImageScale,
imageSize);
if (result != null)
{
logResult(name, result);
if (showFRCCurve)
showFrcCurve(name, result, thresholdMethod);
}
}
else
{
FireResult result = null;
int repeats = (randomSplit) ? Math.max(1, FIRE.repeats) : 1;
if (repeats == 1)
{
result = calculateFireNumber(fourierMethod, samplingMethod, thresholdMethod, fourierImageScale,
imageSize);
if (result != null)
{
logResult(name, result);
if (showFRCCurve)
showFrcCurve(name, result, thresholdMethod);
}
}
else
{
// Multi-thread this ...
int nThreads = Maths.min(repeats, getThreads());
ExecutorService executor = Executors.newFixedThreadPool(nThreads);
TurboList<Future<?>> futures = new TurboList<Future<?>>(repeats);
TurboList<FIREWorker> workers = new TurboList<FIREWorker>(repeats);
setProgress(repeats);
IJ.showProgress(0);
IJ.showStatus(TITLE + " computing ...");
for (int i = 1; i <= repeats; i++)
{
FIREWorker w = new FIREWorker(i, fourierImageScale, imageSize);
workers.add(w);
futures.add(executor.submit(w));
}
// Wait for all to finish
for (int t = futures.size(); t-- > 0;)
{
try
{
// The future .get() method will block until completed
futures.get(t).get();
}
catch (Exception e)
{
// This should not happen.
// Ignore it and allow processing to continue (the number of neighbour samples will just be smaller).
e.printStackTrace();
}
}
IJ.showProgress(1);
executor.shutdown();
// Show a combined FRC curve plot of all the smoothed curves if we have multiples.
LUT valuesLUT = LUTHelper.createLUT(LutColour.FIRE_GLOW);
@SuppressWarnings("unused")
LUT noSmoothLUT = LUTHelper.createLUT(LutColour.GRAYS).createInvertedLut(); // Black at max value
LUTHelper.DefaultLUTMapper mapper = new LUTHelper.DefaultLUTMapper(0, repeats);
FrcCurve curve = new FrcCurve();
Statistics stats = new Statistics();
WindowOrganiser wo = new WindowOrganiser();
boolean oom = false;
for (int i = 0; i < repeats; i++)
{
FIREWorker w = workers.get(i);
if (w.oom)
oom = true;
if (w.result == null)
continue;
result = w.result;
if (!Double.isNaN(result.fireNumber))
stats.add(result.fireNumber);
if (showFRCCurveRepeats)
{
// Output each FRC curve using a suffix.
logResult(w.name, result);
wo.add(Utils.display(w.plot.getTitle(), w.plot));
}
if (showFRCCurve)
{
int index = mapper.map(i + 1);
//@formatter:off
curve.add(name, result, thresholdMethod,
LUTHelper.getColour(valuesLUT, index),
Color.blue,
null //LUTHelper.getColour(noSmoothLUT, index)
);
//@formatter:on
}
}
if (result != null)
{
wo.cascade();
double mean = stats.getMean();
logResult(name, result, mean, stats);
if (showFRCCurve)
{
curve.addResolution(mean);
Plot2 plot = curve.getPlot();
Utils.display(plot.getTitle(), plot);
}
}
if (oom)
{
//@formatter:off
IJ.error(TITLE,
"ERROR - Parallel computation out-of-memory.\n \n" +
TextUtils.wrap("The number of results will be reduced. " +
"Please reduce the size of the Fourier image " +
"or change the number of threads " +
"using the extra options (hold down the 'Shift' " +
"key when running the plugin).",
80));
//@formatter:on
}
}
// Only do this once
if (showFRCTimeEvolution && result != null && !Double.isNaN(result.fireNumber))
showFrcTimeEvolution(name, result.fireNumber, thresholdMethod, nmPerPixel / result.getNmPerPixel(),
imageSize);
}
IJ.showStatus(TITLE + " complete : " + Utils.timeToString(System.currentTimeMillis() - start));
}
private void logResult(String name, FireResult result)
{
IJ.log(String.format("%s : FIRE number = %s %s (Fourier scale = %s)", name, Utils.rounded(result.fireNumber, 4),
units, Utils.rounded(nmPerPixel / result.getNmPerPixel(), 3)));
if (Double.isNaN(result.fireNumber))
{
Utils.log(
"%s Warning: NaN result possible if the resolution is below the pixel size of the input Fourier image (%s %s).",
TITLE, Utils.rounded(result.getNmPerPixel()), units);
}
}
private void logResult(String name, FireResult result, double mean, Statistics stats)
{
IJ.log(String.format("%s : FIRE number = %s +/- %s %s [95%% CI, n=%d] (Fourier scale = %s)", name,
Utils.rounded(mean, 4), Utils.rounded(stats.getConfidenceInterval(0.95), 4), units, stats.getN(),
Utils.rounded(nmPerPixel / result.getNmPerPixel(), 3)));
}
private MemoryPeakResults cropToRoi(MemoryPeakResults results)
{
if (roiBounds == null)
return results;
// Adjust bounds relative to input results image
//Rectangle2D.Float bounds = results.getDataBounds();
Rectangle bounds = results.getBounds(true);
double xscale = roiImageWidth / bounds.width;
double yscale = roiImageHeight / bounds.height;
float minX = (float) (bounds.x + roiBounds.x / xscale);
float maxX = (float) (minX + roiBounds.width / xscale);
float minY = (float) (bounds.y + (roiBounds.y / yscale));
float maxY = (float) (minY + roiBounds.height / yscale);
// Create a new set of results within the bounds
MemoryPeakResults newResults = new MemoryPeakResults();
newResults.begin();
for (PeakResult peakResult : results.getResults())
{
float x = peakResult.params[Gaussian2DFunction.X_POSITION];
float y = peakResult.params[Gaussian2DFunction.Y_POSITION];
if (x < minX || x > maxX || y < minY || y > maxY)
continue;
newResults.add(peakResult);
}
newResults.end();
newResults.copySettings(results);
newResults.setBounds(new Rectangle((int) minX, (int) minY, (int) (maxX - minX), (int) (maxY - minY)));
return newResults;
}
private boolean showInputDialog()
{
GenericDialog gd = new GenericDialog(TITLE);
gd.addMessage("Compute the resolution using Fourier Ring Correlation");
gd.addHelp(About.HELP_URL);
// Build a list of all images with a region ROI
List<String> titles = new LinkedList<String>();
if (WindowManager.getWindowCount() > 0)
{
for (int imageID : WindowManager.getIDList())
{
ImagePlus imp = WindowManager.getImage(imageID);
if (imp != null && imp.getRoi() != null && imp.getRoi().isArea())
titles.add(imp.getTitle());
}
}
ResultsManager.addInput(gd, inputOption, InputSource.MEMORY);
ResultsManager.addInput(gd, "Input2", inputOption2, InputSource.NONE, InputSource.MEMORY);
if (!titles.isEmpty())
gd.addCheckbox((titles.size() == 1) ? "Use_ROI" : "Choose_ROI", chooseRoi);
gd.showDialog();
if (gd.wasCanceled())
return false;
inputOption = ResultsManager.getInputSource(gd);
inputOption2 = ResultsManager.getInputSource(gd);
if (!titles.isEmpty())
chooseRoi = gd.getNextBoolean();
if (!titles.isEmpty() && chooseRoi)
{
if (titles.size() == 1)
{
roiImage = titles.get(0);
Recorder.recordOption("Image", roiImage);
}
else
{
String[] items = titles.toArray(new String[titles.size()]);
gd = new GenericDialog(TITLE);
gd.addMessage("Select the source image for the ROI");
gd.addChoice("Image", items, roiImage);
gd.showDialog();
if (gd.wasCanceled())
return false;
roiImage = gd.getNextChoice();
}
ImagePlus imp = WindowManager.getImage(roiImage);
roiBounds = imp.getRoi().getBounds();
roiImageWidth = imp.getWidth();
roiImageHeight = imp.getHeight();
}
else
{
roiBounds = null;
}
return true;
}
private boolean showDialog()
{
GenericDialog gd = new GenericDialog(TITLE);
gd.addMessage("Compute the resolution using Fourier Ring Correlation");
gd.addHelp(About.HELP_URL);
boolean single = results2 == null;
gd.addMessage("Image construction options:");
gd.addChoice("Image_scale", SCALE_ITEMS, SCALE_ITEMS[imageScaleIndex]);
gd.addChoice("Auto_image_size", IMAGE_SIZE_ITEMS, IMAGE_SIZE_ITEMS[imageSizeIndex]);
if (extraOptions)
gd.addCheckbox("Use_signal (if present)", useSignal);
gd.addNumericField("Max_per_bin", maxPerBin, 0);
gd.addMessage("Fourier options:");
String[] fourierMethodNames = SettingsManager.getNames((Object[]) FRC.FourierMethod.values());
gd.addChoice("Fourier_method", fourierMethodNames, fourierMethodNames[fourierMethodIndex]);
String[] samplingMethodNames = SettingsManager.getNames((Object[]) FRC.SamplingMethod.values());
gd.addChoice("Sampling_method", samplingMethodNames, samplingMethodNames[samplingMethodIndex]);
gd.addSlider("Sampling_factor", 0.2, 4, perimeterSamplingFactor);
gd.addMessage("FIRE options:");
String[] thresholdMethodNames = SettingsManager.getNames((Object[]) FRC.ThresholdMethod.values());
gd.addChoice("Threshold_method", thresholdMethodNames, thresholdMethodNames[thresholdMethodIndex]);
gd.addCheckbox("Show_FRC_curve", showFRCCurve);
if (single)
{
gd.addMessage("For single datasets:");
Label l = (Label) gd.getMessage();
gd.addNumericField("Block_size", blockSize, 0);
gd.addCheckbox("Random_split", randomSplit);
gd.addNumericField("Repeats", repeats, 0);
gd.addCheckbox("Show_FRC_curve_repeats", showFRCCurveRepeats);
gd.addCheckbox("Show_FRC_time_evolution", showFRCTimeEvolution);
gd.addCheckbox("Spurious correlation correction", spuriousCorrelationCorrection);
gd.addNumericField("Q-value", qValue, 3);
gd.addNumericField("Precision_Mean", mean, 2, 6, "nm");
gd.addNumericField("Precision_Sigma", sigma, 2, 6, "nm");
if (extraOptions)
gd.addNumericField("Threads", getLastNThreads(), 0);
// Rearrange the dialog
if (gd.getLayout() != null)
{
GridBagLayout grid = (GridBagLayout) gd.getLayout();
int xOffset = 0, yOffset = 0;
int lastY = -1, rowCount = 0;
for (Component comp : gd.getComponents())
{
// Check if this should be the second major column
if (comp == l)
{
xOffset += 2;
yOffset = yOffset - rowCount + 1; // Skip title row
}
// Reposition the field
GridBagConstraints c = grid.getConstraints(comp);
if (lastY != c.gridy)
rowCount++;
lastY = c.gridy;
c.gridx = c.gridx + xOffset;
c.gridy = c.gridy + yOffset;
c.insets.left = c.insets.left + 10 * xOffset;
c.insets.top = 0;
c.insets.bottom = 0;
grid.setConstraints(comp, c);
}
if (IJ.isLinux())
gd.setBackground(new Color(238, 238, 238));
}
}
gd.showDialog();
if (gd.wasCanceled())
return false;
imageScaleIndex = gd.getNextChoiceIndex();
imageSizeIndex = gd.getNextChoiceIndex();
if (extraOptions)
myUseSignal = useSignal = gd.getNextBoolean();
maxPerBin = Math.abs((int) gd.getNextNumber());
fourierMethodIndex = gd.getNextChoiceIndex();
fourierMethod = FourierMethod.values()[fourierMethodIndex];
samplingMethodIndex = gd.getNextChoiceIndex();
samplingMethod = SamplingMethod.values()[samplingMethodIndex];
perimeterSamplingFactor = gd.getNextNumber();
thresholdMethodIndex = gd.getNextChoiceIndex();
thresholdMethod = FRC.ThresholdMethod.values()[thresholdMethodIndex];
showFRCCurve = gd.getNextBoolean();
if (single)
{
blockSize = Math.max(1, (int) gd.getNextNumber());
randomSplit = gd.getNextBoolean();
repeats = Math.max(1, (int) gd.getNextNumber());
showFRCCurveRepeats = gd.getNextBoolean();
showFRCTimeEvolution = gd.getNextBoolean();
spuriousCorrelationCorrection = gd.getNextBoolean();
qValue = Math.abs(gd.getNextNumber());
mean = Math.abs(gd.getNextNumber());
sigma = Math.abs(gd.getNextNumber());
if (extraOptions)
{
setThreads((int) gd.getNextNumber());
lastNThreads = this.nThreads;
}
}
// Check arguments
try
{
Parameters.isAboveZero("Perimeter sampling factor", perimeterSamplingFactor);
if (single && spuriousCorrelationCorrection)
{
Parameters.isAboveZero("Q-value", qValue);
Parameters.isAboveZero("Precision Mean", mean);
Parameters.isAboveZero("Precision Sigma", sigma);
// Set these for use in FIRE computation
setCorrectionParameters(qValue, mean, sigma);
}
}
catch (IllegalArgumentException e)
{
IJ.error(TITLE, e.getMessage());
return false;
}
return true;
}
/**
* Initialise this instance with localisation results for the FIRE computation.
*
* @param results
* the results
* @param results2
* the second set of results (can be null)
*/
public void initialise(MemoryPeakResults results, MemoryPeakResults results2)
{
this.results = verify(results);
this.results2 = verify(results2);
nmPerPixel = 1;
units = "px";
if (this.results == null)
return;
// Use the float data bounds. This prevents problems if the data is far from the origin.
dataBounds = results.getDataBounds();
if (this.results2 != null)
{
Rectangle2D dataBounds2 = results.getDataBounds();
dataBounds = dataBounds.createUnion(dataBounds2);
}
if (results.getCalibration() != null)
{
// Calibration must match between datasets
if (this.results2 != null)
{
if (results2.getNmPerPixel() != results.getNmPerPixel())
{
IJ.log(TITLE +
" Error: Calibration between the two input datasets does not match, defaulting to pixels");
return;
}
}
nmPerPixel = results.getNmPerPixel();
units = "nm";
}
}
/**
* Sets the correction parameters for spurious correlation correction. Only relevant for single images.
*
* @param qValue
* the q value
* @param mean
* the mean of the localisation precision
* @param sigma
* the standard deviation of the localisation precision
*/
public void setCorrectionParameters(double qValue, double mean, double sigma)
{
if (qValue > 0 && mean > 0 && sigma > 0)
{
correctionQValue = qValue;
correctionMean = mean;
correctionSigma = sigma;
}
else
{
correctionQValue = correctionMean = correctionSigma = 0;
}
}
/**
* Shallow copy this instance so skipping initialisation. Only variables required for the FIRE calculation are
* copied.
*
* @return the new FIRE instance
*/
private FIRE copy()
{
FIRE f = new FIRE();
f.results = results;
f.results2 = results2;
f.nmPerPixel = nmPerPixel;
f.units = units;
f.dataBounds = dataBounds;
f.correctionQValue = correctionQValue;
f.correctionMean = correctionMean;
f.correctionSigma = correctionSigma;
return f;
}
/**
* Verify the results can be used for FIRE. Results are sorted in time order if the block size is above 1.
*
* @param results
* the results
* @return the memory peak results
*/
private MemoryPeakResults verify(MemoryPeakResults results)
{
if (results == null || results.size() < 2)
return null;
if (blockSize > 1)
// Results must be in time order when processing blocks
results.sort();
return results;
}
/**
* Creates the images to use for the FIRE calculation. This must be called after
* {@link #initialise(MemoryPeakResults, MemoryPeakResults)}.
*
* @param fourierImageScale
* the fourier image scale (set to zero to auto compute)
* @param imageSize
* the image size
* @return the fire images
*/
public FireImages createImages(double fourierImageScale, int imageSize)
{
return createImages(fourierImageScale, imageSize, myUseSignal);
}
private interface SignalProvider
{
float getSignal(PeakResult p);
}
private static class FixedSignalProvider implements SignalProvider
{
public float getSignal(PeakResult p)
{
return 1f;
}
}
private static class PeakSignalProvider implements SignalProvider
{
public float getSignal(PeakResult p)
{
return p.getSignal();
}
}
/**
* Creates the images to use for the FIRE calculation. This must be called after
* {@link #initialise(MemoryPeakResults, MemoryPeakResults)}.
*
* @param fourierImageScale
* the fourier image scale (set to zero to auto compute)
* @param imageSize
* the image size
* @param useSignal
* Use the localisation signal to weight the intensity. The default uses a value of 1 per localisation.
* @return the fire images
*/
public FireImages createImages(double fourierImageScale, int imageSize, boolean useSignal)
{
if (results == null)
return null;
final SignalProvider signalProvider = (useSignal && (results.getHead().getSignal() > 0))
? new PeakSignalProvider() : new FixedSignalProvider();
// Draw images using the existing IJ routines.
Rectangle bounds = new Rectangle(0, 0, (int) Math.ceil(dataBounds.getWidth()),
(int) Math.ceil(dataBounds.getHeight()));
boolean weighted = true;
boolean equalised = false;
double imageScale;
if (fourierImageScale <= 0)
{
double size = FastMath.max(bounds.width, bounds.height);
if (size <= 0)
size = 1;
imageScale = imageSize / size;
}
else
imageScale = fourierImageScale;
IJImagePeakResults image1 = ImagePeakResultsFactory.createPeakResultsImage(ResultsImage.NONE, weighted,
equalised, "IP1", bounds, 1, 1, imageScale, 0, ResultsMode.ADD);
image1.setDisplayImage(false);
image1.begin();
IJImagePeakResults image2 = ImagePeakResultsFactory.createPeakResultsImage(ResultsImage.NONE, weighted,
equalised, "IP2", bounds, 1, 1, imageScale, 0, ResultsMode.ADD);
image2.setDisplayImage(false);
image2.begin();
float minx = (float) dataBounds.getX();
float miny = (float) dataBounds.getY();
if (this.results2 != null)
{
// Two image comparison
for (PeakResult p : results)
{
float x = p.getXPosition() - minx;
float y = p.getYPosition() - miny;
image1.add(x, y, signalProvider.getSignal(p));
}
for (PeakResult p : results2)
{
float x = p.getXPosition() - minx;
float y = p.getYPosition() - miny;
image2.add(x, y, signalProvider.getSignal(p));
}
}
else
{
// Block sampling.
// Ensure we have at least 2 even sized blocks.
int blockSize = Math.min(results.size() / 2, Math.max(1, FIRE.blockSize));
int nBlocks = (int) Math.ceil((double) results.size() / blockSize);
while (nBlocks <= 1 && blockSize > 1)
{
blockSize /= 2;
nBlocks = (int) Math.ceil((double) results.size() / blockSize);
}
if (nBlocks <= 1)
// This should not happen since the results should contain at least 2 localisations
return null;
if (blockSize != FIRE.blockSize)
IJ.log(TITLE + " Warning: Changed block size to " + blockSize);
int i = 0;
int block = 0;
PeakResult[][] blocks = new PeakResult[nBlocks][blockSize];
for (PeakResult p : results)
{
if (i == blockSize)
{
block++;
i = 0;
}
blocks[block][i++] = p;
}
// Truncate last block
blocks[block] = Arrays.copyOf(blocks[block], i);
final int[] indices = Utils.newArray(nBlocks, 0, 1);
if (randomSplit)
MathArrays.shuffle(indices);
for (int index : indices)
{
// Split alternating so just rotate
IJImagePeakResults image = image1;
image1 = image2;
image2 = image;
for (PeakResult p : blocks[index])
{
float x = p.getXPosition() - minx;
float y = p.getYPosition() - miny;
image.add(x, y, signalProvider.getSignal(p));
}
}
}
image1.end();
ImageProcessor ip1 = image1.getImagePlus().getProcessor();
image2.end();
ImageProcessor ip2 = image2.getImagePlus().getProcessor();
if (maxPerBin > 0 && signalProvider instanceof FixedSignalProvider)
{
// We can eliminate over-sampled pixels
for (int i = ip1.getPixelCount(); i-- > 0;)
{
if (ip1.getf(i) > maxPerBin)
ip1.setf(i, maxPerBin);
if (ip2.getf(i) > maxPerBin)
ip2.setf(i, maxPerBin);
}
}
return new FireImages(ip1, ip2, nmPerPixel / imageScale);
}
/**
* Encapsulate plotting the FRC curve to allow multiple curves to be plotted together
*/
private class FrcCurve
{
double[] xValues = null;
double[] threshold = null;
Plot2 plot = null;
void add(String name, FireResult result, ThresholdMethod thresholdMethod, Color colorValues,
Color colorThreshold, Color colorNoSmooth)
{
FRCCurve frcCurve = result.frcCurve;
double[] yValues = new double[frcCurve.getSize()];
if (plot == null)
{
String title = name + " FRC Curve";
plot = new Plot2(title, String.format("Spatial Frequency (%s^-1)", units), "FRC");
xValues = new double[frcCurve.getSize()];
final double L = frcCurve.fieldOfView;
final double conversion = 1.0 / (L * result.getNmPerPixel());
for (int i = 0; i < xValues.length; i++)
{
xValues[i] = frcCurve.get(i).getRadius() * conversion;
}
// The threshold curve is the same
threshold = FRC.calculateThresholdCurve(frcCurve, thresholdMethod);
add(colorThreshold, threshold);
}
for (int i = 0; i < xValues.length; i++)
{
yValues[i] = frcCurve.get(i).getCorrelation();
}
add(colorValues, yValues);
add(colorNoSmooth, result.originalCorrelationCurve);
}
public void addResolution(double resolution)
{
// Convert back to nm^-1
double x = 1 / resolution;
// Find the intersection with the threshold line
for (int i = 1; i < xValues.length; i++)
{
if (x < xValues[i])
{
double correlation;
// Interpolate
double upper = xValues[i], lower = xValues[i - 1];
double xx = (x - lower) / (upper - lower);
correlation = threshold[i - 1] + xx * (threshold[i] - threshold[i - 1]);
addResolution(resolution, correlation);
return;
}
}
}
public void addResolution(double resolution, double correlation)
{
addResolution(resolution, Double.NaN, correlation);
}
public void addResolution(double resolution, double originalResolution, double correlation)
{
// Convert back to nm^-1
double x = 1 / resolution;
plot.setColor(Color.MAGENTA);
plot.drawLine(x, 0, x, correlation);
plot.setColor(Color.BLACK);
if (Double.isNaN(originalResolution))
plot.addLabel(0, 0, String.format("Resolution = %s %s", Utils.rounded(resolution), units));
else
plot.addLabel(0, 0, String.format("Resolution = %s %s (Original = %s %s)", Utils.rounded(resolution),
units, Utils.rounded(originalResolution), units));
}
private void add(Color color, double[] y)
{
if (color == null)
return;
plot.setColor(color);
plot.addPoints(xValues, y, Plot2.LINE);
}
Plot2 getPlot()
{
plot.setLimitsToFit(false);
// Q. For some reason the limits calculated are ignored,
// so set them as the defaults.
double[] limits = plot.getCurrentMinAndMax();
// The FRC should not go above 1 so limit Y
plot.setLimits(limits[0], limits[1], Math.min(0, limits[2]), 1.05);
return plot;
}
}
private Plot2 createFrcCurve(String name, FireResult result, ThresholdMethod thresholdMethod)
{
FrcCurve curve = new FrcCurve();
curve.add(name, result, thresholdMethod, Color.red, Color.blue, Color.black);
curve.addResolution(result.fireNumber, result.correlation);
return curve.getPlot();
}
private PlotWindow showFrcCurve(String name, FireResult result, ThresholdMethod thresholdMethod)
{
return showFrcCurve(name, result, thresholdMethod, 0);
}
private PlotWindow showFrcCurve(String name, FireResult result, ThresholdMethod thresholdMethod, int flags)
{
Plot2 plot = createFrcCurve(name, result, thresholdMethod);
return Utils.display(plot.getTitle(), plot, flags);
}
private void showFrcTimeEvolution(String name, double fireNumber, ThresholdMethod thresholdMethod,
double fourierImageScale, int imageSize)
{
IJ.showStatus("Calculating FRC time evolution curve...");
List<PeakResult> list = results.getResults();
int nSteps = 10;
int maxT = list.get(list.size() - 1).getFrame();
if (maxT == 0)
maxT = list.size();
int step = maxT / nSteps;
TDoubleArrayList x = new TDoubleArrayList();
TDoubleArrayList y = new TDoubleArrayList();
double yMin = fireNumber;
double yMax = fireNumber;
MemoryPeakResults newResults = new MemoryPeakResults();
newResults.copySettings(results);
int i = 0;
for (int t = step; t <= maxT - step; t += step)
{
while (i < list.size())
{
if (list.get(i).getFrame() <= t)
{
newResults.add(list.get(i));
i++;
}
else
break;
}
x.add((double) t);
FIRE f = this.copy();
FireResult result = f.calculateFireNumber(fourierMethod, samplingMethod, thresholdMethod, fourierImageScale,
imageSize);
double fire = (result == null) ? 0 : result.fireNumber;
y.add(fire);
yMin = FastMath.min(yMin, fire);
yMax = FastMath.max(yMax, fire);
}
// Add the final fire number
x.add((double) maxT);
y.add(fireNumber);
double[] xValues = x.toArray();
double[] yValues = y.toArray();
String units = "px";
if (results.getCalibration() != null)
{
nmPerPixel = results.getNmPerPixel();
units = "nm";
}
String title = name + " FRC Time Evolution";
Plot2 plot = new Plot2(title, "Frames", "Resolution (" + units + ")", (float[]) null, (float[]) null);
double range = Math.max(1, yMax - yMin) * 0.05;
plot.setLimits(xValues[0], xValues[xValues.length - 1], yMin - range, yMax + range);
plot.setColor(Color.red);
plot.addPoints(xValues, yValues, Plot.CONNECTED_CIRCLES);
Utils.display(title, plot);
}
/**
* Calculate the Fourier Image REsolution (FIRE) number using the chosen threshold method. Should be called after
* {@link #initialise(MemoryPeakResults)}
*
* @param fourierMethod
* the fourier method
* @param samplingMethod
* the sampling method
* @param thresholdMethod
* the threshold method
* @param fourierImageScale
* The scale to use when reconstructing the super-resolution images (0 for auto)
* @param imageSize
* The width of the super resolution images when using auto scale (should be a power of two minus 1 for
* optimum memory usage)
* @return The FIRE number
*/
public FireResult calculateFireNumber(FourierMethod fourierMethod, SamplingMethod samplingMethod,
ThresholdMethod thresholdMethod, double fourierImageScale, int imageSize)
{
FireImages images = createImages(fourierImageScale, imageSize);
return calculateFireNumber(fourierMethod, samplingMethod, thresholdMethod, images);
}
private TrackProgress progress = new IJTrackProgress();
private void setProgress(int repeats)
{
if (repeats > 1)
progress = new ParallelTrackProgress(repeats);
else
progress = new IJTrackProgress();
}
/**
* Dumb implementation of the track progress interface for parallel threads. Used simple synchronisation to
* increment total progress.
*/
private static class ParallelTrackProgress extends NullTrackProgress
{
double done = 0;
final int total;
ParallelTrackProgress(int repeats)
{
total = repeats;
}
@Override
public void incrementProgress(double fraction)
{
// Avoid synchronisation for nothing
if (fraction == 0)
return;
double done = add(fraction);
IJ.showProgress(done / this.total);
}
synchronized double add(double d)
{
done += d;
return done;
}
}
/**
* Calculate the Fourier Image REsolution (FIRE) number using the chosen threshold method. Should be called after
* {@link #initialise(MemoryPeakResults)}
*
* @param fourierMethod
* the fourier method
* @param samplingMethod
* the sampling method
* @param thresholdMethod
* the threshold method
* @param images
* the images
* @return The FIRE number
*/
public FireResult calculateFireNumber(FourierMethod fourierMethod, SamplingMethod samplingMethod,
ThresholdMethod thresholdMethod, FireImages images)
{
if (images == null)
return null;
FRC frc = new FRC();
// Allow a progress tracker to be input.
// This should be setup for the total number of repeats.
// If parallelised then do not output the text status messages as they conflict.
frc.progress = progress;
frc.setFourierMethod(fourierMethod);
frc.setSamplingMethod(samplingMethod);
frc.setPerimeterSamplingFactor(perimeterSamplingFactor);
FRCCurve frcCurve = frc.calculateFrcCurve(images.ip1, images.ip2, images.nmPerPixel);
if (frcCurve == null)
return null;
if (correctionQValue > 0)
FRC.applyQCorrection(frcCurve, correctionQValue, correctionMean, correctionSigma);
double[] originalCorrelationCurve = frcCurve.getCorrelationValues();
FRC.getSmoothedCurve(frcCurve, true);
// Resolution in pixels
FIREResult result = FRC.calculateFire(frcCurve, thresholdMethod);
if (result == null)
return new FireResult(Double.NaN, Double.NaN, frcCurve, originalCorrelationCurve);
double fireNumber = result.fireNumber;
// The FRC paper states that the super-resolution pixel size should be smaller
// than 1/4 of R (the resolution).
if (fireNumber > 0 && (images.nmPerPixel > fireNumber / 4))
{
// Q. Should this be output somewhere else?
Utils.log(
"%s Warning: The super-resolution pixel size (%s) should be smaller than 1/4 of R (the resolution %s)",
TITLE, Utils.rounded(images.nmPerPixel), Utils.rounded(fireNumber));
}
return new FireResult(fireNumber, result.correlation, frcCurve, originalCorrelationCurve);
}
private void runQEstimation()
{
IJ.showStatus(TITLE + " ...");
if (!showQEstimationInputDialog())
return;
MemoryPeakResults results = ResultsManager.loadInputResults(inputOption, false);
if (results == null || results.size() == 0)
{
IJ.error(TITLE, "No results could be loaded");
return;
}
if (results.getCalibration() == null)
{
IJ.error(TITLE, "The results are not calibrated");
return;
}
results = cropToRoi(results);
if (results.size() < 2)
{
IJ.error(TITLE, "No results within the crop region");
return;
}
initialise(results, null);
// We need localisation precision.
// Build a histogram of the localisation precision.
// Get the initial mean and SD and plot as a Gaussian.
PrecisionHistogram histogram = calculatePrecisionHistogram();
if (histogram == null)
{
IJ.error(TITLE, "No localisation precision available.\n \nPlease choose " + PrecisionMethod.FIXED +
" and enter a precision mean and SD.");
return;
}
StoredDataStatistics precision = histogram.precision;
//String name = results.getName();
double fourierImageScale = SCALE_VALUES[imageScaleIndex];
int imageSize = IMAGE_SIZE_VALUES[imageSizeIndex];
// Create the image and compute the numerator of FRC.
// Do not use the signal so results.size() is the number of localisations.
IJ.showStatus("Computing FRC curve ...");
FireImages images = createImages(fourierImageScale, imageSize, false);
// DEBUGGING - Save the two images to disk. Load the images into the Matlab
// code that calculates the Q-estimation and make this plugin match the functionality.
//IJ.save(new ImagePlus("i1", images.ip1), "/scratch/i1.tif");
//IJ.save(new ImagePlus("i2", images.ip2), "/scratch/i2.tif");
FRC frc = new FRC();
frc.progress = progress;
frc.setFourierMethod(fourierMethod);
frc.setSamplingMethod(samplingMethod);
frc.setPerimeterSamplingFactor(perimeterSamplingFactor);
FRCCurve frcCurve = frc.calculateFrcCurve(images.ip1, images.ip2, images.nmPerPixel);
if (frcCurve == null)
{
IJ.error(TITLE, "Failed to compute FRC curve");
return;
}
IJ.showStatus("Running Q-estimation ...");
// Note:
// The method implemented here is based on Matlab code provided by Bernd Rieger.
// The idea is to compute the spurious correlation component of the FRC Numerator
// using an initial estimate of distribution of the localisation precision (assumed
// to be Gaussian). This component is the contribution of repeat localisations of
// the same molecule to the numerator and is modelled as an exponential decay
// (exp_decay). The component is scaled by the Q-value which
// is the average number of times a molecule is seen in addition to the first time.
// At large spatial frequencies the scaled component should match the numerator,
// i.e. at high resolution (low FIRE number) the numerator is made up of repeat
// localisations of the same molecule and not actual structure in the image.
// The best fit is where the numerator equals the scaled component, i.e. num / (q*exp_decay) == 1.
// The FRC Numerator is plotted and Q can be determined by
// adjusting Q and the precision mean and SD to maximise the cost function.
// This can be done interactively by the user with the effect on the FRC curve
// dynamically updated and displayed.
// Compute the scaled FRC numerator
double qNorm = (1 / frcCurve.mean1 + 1 / frcCurve.mean2);
double[] frcnum = new double[frcCurve.getSize()];
for (int i = 0; i < frcnum.length; i++)
{
FRCCurveResult r = frcCurve.get(i);
frcnum[i] = qNorm * r.getNumerator() / r.getNumberOfSamples();
}
// Compute the spatial frequency and the region for curve fitting
double[] q = FRC.computeQ(frcCurve, false);
int low = 0, high = q.length;
while (high > 0 && q[high - 1] > maxQ)
high--;
while (low < q.length && q[low] < minQ)
low++;
// Require we fit at least 10% of the curve
if (high - low < q.length * 0.1)
{
IJ.error(TITLE, "Not enough points for Q estimation");
return;
}
// Obtain initial estimate of Q plateau height and decay.
// This can be done by fitting the precision histogram and then fixing the mean and sigma.
// Or it can be done by allowing the precision to be sampled and the mean and sigma
// become parameters for fitting.
// Check if we can sample precision values
boolean sampleDecay = precision != null && FIRE.sampleDecay;
double[] exp_decay;
if (sampleDecay)
{
// Random sample of precision values from the distribution is used to
// construct the decay curve
int[] sample = Random.sample(10000, precision.getN(), new Well19937c());
final double four_pi2 = 4 * Math.PI * Math.PI;
double[] pre = new double[q.length];
for (int i = 1; i < q.length; i++)
pre[i] = -four_pi2 * q[i] * q[i];
// Sample
final int n = sample.length;
double[] hq = new double[n];
for (int j = 0; j < n; j++)
{
// Scale to SR pixels
double s2 = precision.getValue(sample[j]) / images.nmPerPixel;
s2 *= s2;
for (int i = 1; i < q.length; i++)
hq[i] += FastMath.exp(pre[i] * s2);
}
for (int i = 1; i < q.length; i++)
hq[i] /= n;
exp_decay = new double[q.length];
exp_decay[0] = 1;
for (int i = 1; i < q.length; i++)
{
double sinc_q = sinc(Math.PI * q[i]);
exp_decay[i] = sinc_q * sinc_q * hq[i];
}
}
else
{
// Note: The sigma mean and std should be in the units of super-resolution
// pixels so scale to SR pixels
exp_decay = computeExpDecay(histogram.mean / images.nmPerPixel, histogram.sigma / images.nmPerPixel, q);
}
// Smoothing
double[] smooth;
if (loessSmoothing)
{
// Note: This computes the log then smooths it
double bandwidth = 0.1;
int robustness = 0;
double[] l = new double[exp_decay.length];
for (int i = 0; i < l.length; i++)
{
// Original Matlab code computes the log for each array.
// This is equivalent to a single log on the fraction of the two.
// Perhaps the two log method is more numerically stable.
//l[i] = Math.log(Math.abs(frcnum[i])) - Math.log(exp_decay[i]);
l[i] = Math.log(Math.abs(frcnum[i] / exp_decay[i]));
}
try
{
LoessInterpolator loess = new LoessInterpolator(bandwidth, robustness);
smooth = loess.smooth(q, l);
}
catch (Exception e)
{
IJ.error(TITLE, "LOESS smoothing failed");
return;
}
}
else
{
// Note: This smooths the curve before computing the log
double[] norm = new double[exp_decay.length];
for (int i = 0; i < norm.length; i++)
{
norm[i] = frcnum[i] / exp_decay[i];
}
// Median window of 5 == radius of 2
MedianWindow mw = new MedianWindow(norm, 2);
smooth = new double[exp_decay.length];
for (int i = 0; i < norm.length; i++)
{
smooth[i] = Math.log(Math.abs(mw.getMedian()));
mw.increment();
}
}
// Fit with quadratic to find the initial guess.
// Note: example Matlab code frc_Qcorrection7.m identifies regions of the
// smoothed log curve with low derivative and only fits those. The fit is
// used for the final estimate. Fitting a subset with low derivative is not
// implemented here since the initial estimate is subsequently optimised
// to maximise a cost function.
Quadratic curve = new Quadratic();
SimpleCurveFitter fit = SimpleCurveFitter.create(curve, new double[2]);
WeightedObservedPoints points = new WeightedObservedPoints();
for (int i = low; i < high; i++)
points.add(q[i], smooth[i]);
double[] estimate = fit.fit(points.toList());
double qValue = FastMath.exp(estimate[0]);
//System.out.printf("Initial q-estimate = %s => %.3f\n", Arrays.toString(estimate), qValue);
// This could be made an option. Just use for debugging
boolean debug = false;
if (debug)
{
// Plot the initial fit and the fit curve
double[] qScaled = FRC.computeQ(frcCurve, true);
double[] line = new double[q.length];
for (int i = 0; i < q.length; i++)
line[i] = curve.value(q[i], estimate);
String title = TITLE + " Initial fit";
Plot2 plot = new Plot2(title, "Spatial Frequency (nm^-1)", "FRC Numerator");
String label = String.format("Q = %.3f", qValue);
plot.addPoints(qScaled, smooth, Plot.LINE);
plot.setColor(Color.red);
plot.addPoints(qScaled, line, Plot.LINE);
plot.setColor(Color.black);
plot.addLabel(0, 0, label);
Utils.display(title, plot, Utils.NO_TO_FRONT);
}
if (fitPrecision)
{
// Q - Should this be optional?
if (sampleDecay)
{
// If a sample of the precision was used to construct the data for the initial fit
// then update the estimate using the fit result since it will be a better start point.
histogram.sigma = precision.getStandardDeviation();
// Normalise sum-of-squares to the SR pixel size
double meanSumOfSquares = (precision.getSumOfSquares() / (images.nmPerPixel * images.nmPerPixel)) /
precision.getN();
histogram.mean = images.nmPerPixel *
Math.sqrt(meanSumOfSquares - estimate[1] / (4 * Math.PI * Math.PI));
}
// Do a multivariate fit ...
SimplexOptimizer opt = new SimplexOptimizer(1e-6, 1e-10);
PointValuePair p = null;
MultiPlateauness f = new MultiPlateauness(frcnum, q, low, high);
double[] initial = new double[] { histogram.mean / images.nmPerPixel, histogram.sigma / images.nmPerPixel,
qValue };
p = findMin(p, opt, f, scale(initial, 0.1));
p = findMin(p, opt, f, scale(initial, 0.5));
p = findMin(p, opt, f, initial);
p = findMin(p, opt, f, scale(initial, 2));
p = findMin(p, opt, f, scale(initial, 10));
if (p != null)
{
double[] point = p.getPointRef();
histogram.mean = point[0] * images.nmPerPixel;
histogram.sigma = point[1] * images.nmPerPixel;
qValue = point[2];
}
}
else
{
// Reset to theoretical curve. This is what will be used to compute the final correction.
// TODO - check if the Matlab code uses a sampled curve to compute the correction.
// If so then this should be optional.
if (sampleDecay)
{
// If a sample of the precision was used to construct the data for the initial fit
// then update the estimate using the fit result since it will be a better start point.
if (precisionMethod != PrecisionMethod.FIXED)
{
histogram.sigma = precision.getStandardDeviation();
// Normalise sum-of-squares to the SR pixel size
double meanSumOfSquares = (precision.getSumOfSquares() / (images.nmPerPixel * images.nmPerPixel)) /
precision.getN();
histogram.mean = images.nmPerPixel *
Math.sqrt(meanSumOfSquares - estimate[1] / (4 * Math.PI * Math.PI));
}
exp_decay = computeExpDecay(histogram.mean / images.nmPerPixel, histogram.sigma / images.nmPerPixel, q);
}
// Estimate spurious component by promoting plateauness.
// The Matlab code used random initial points for a Simplex optimiser.
// A Brent line search should be pretty deterministic so do simple repeats.
// However it will proceed downhill so if the initial point is wrong then
// it will find a sub-optimal result.
UnivariateOptimizer o = new BrentOptimizer(1e-3, 1e-6);
Plateauness f = new Plateauness(frcnum, exp_decay, low, high);
UnivariatePointValuePair p = null;
p = findMin(p, o, f, qValue, 0.1);
p = findMin(p, o, f, qValue, 0.2);
p = findMin(p, o, f, qValue, 0.333);
p = findMin(p, o, f, qValue, 0.5);
// Do some Simplex repeats as well
SimplexOptimizer opt = new SimplexOptimizer(1e-6, 1e-10);
p = findMin(p, opt, f, qValue * 0.1);
p = findMin(p, opt, f, qValue * 0.5);
p = findMin(p, opt, f, qValue);
p = findMin(p, opt, f, qValue * 2);
p = findMin(p, opt, f, qValue * 10);
if (p != null)
qValue = p.getPoint();
}
QPlot qplot = new QPlot(frcCurve, qValue, low, high);
// Interactive dialog to estimate Q (blinking events per flourophore) using
// sliders for the mean and standard deviation of the localisation precision.
showQEstimationDialog(histogram, qplot, frcCurve, images.nmPerPixel);
IJ.showStatus(TITLE + " complete");
}
private double[] scale(double[] a, double f)
{
a = a.clone();
for (int i = 0; i < a.length; i++)
a[i] *= f;
return a;
}
private double[] computeExpDecay(double mean, double sigma, double[] q)
{
double[] hq = FRC.computeHq(q, mean, sigma);
double[] exp_decay = new double[q.length];
exp_decay[0] = 1;
for (int i = 1; i < q.length; i++)
{
double sinc_q = sinc(Math.PI * q[i]);
exp_decay[i] = sinc_q * sinc_q * hq[i];
}
return exp_decay;
}
/**
* Compute the Sinc function.
*
* @param d
* the d
* @return the sinc value
*/
private static double sinc(double d)
{
return Math.sin(d) / d;
}
private class Quadratic implements ParametricUnivariateFunction
{
public double value(double x, double... parameters)
{
return parameters[0] + parameters[1] * x * x;
}
public double[] gradient(double x, double... parameters)
{
return new double[] { 1, x * x };
}
}
private UnivariatePointValuePair findMin(UnivariatePointValuePair current, UnivariateOptimizer o,
UnivariateFunction f, double qValue, double factor)
{
try
{
BracketFinder bracket = new BracketFinder();
bracket.search(f, GoalType.MINIMIZE, qValue * factor, qValue / factor);
UnivariatePointValuePair next = o.optimize(GoalType.MINIMIZE, new MaxEval(3000),
new SearchInterval(bracket.getLo(), bracket.getHi(), bracket.getMid()),
new UnivariateObjectiveFunction(f));
if (next == null)
return current;
//System.out.printf("LineMin [%.1f] %f = %f\n", factor, next.getPoint(), next.getValue());
if (current != null)
return (next.getValue() < current.getValue()) ? next : current;
return next;
}
catch (Exception e)
{
return current;
}
}
private UnivariatePointValuePair findMin(UnivariatePointValuePair current, SimplexOptimizer o,
MultivariateFunction f, double qValue)
{
try
{
NelderMeadSimplex simplex = new NelderMeadSimplex(1);
double[] initialSolution = { qValue };
PointValuePair solution = o.optimize(new MaxEval(1000), new InitialGuess(initialSolution), simplex,
new ObjectiveFunction(f), GoalType.MINIMIZE);
UnivariatePointValuePair next = (solution == null) ? null
: new UnivariatePointValuePair(solution.getPointRef()[0], solution.getValue());
if (next == null)
return current;
//System.out.printf("Simplex [%f] %f = %f\n", qValue, next.getPoint(), next.getValue());
if (current != null)
return (next.getValue() < current.getValue()) ? next : current;
return next;
}
catch (Exception e)
{
return current;
}
}
private PointValuePair findMin(PointValuePair current, SimplexOptimizer o, MultivariateFunction f,
double[] initialSolution)
{
try
{
NelderMeadSimplex simplex = new NelderMeadSimplex(initialSolution.length);
PointValuePair next = o.optimize(new MaxEval(1000), new InitialGuess(initialSolution), simplex,
new ObjectiveFunction(f), GoalType.MINIMIZE);
if (next == null)
return current;
//System.out.printf("MultiSimplex [%s] %s = %f\n", Arrays.toString(initialSolution),
// Arrays.toString(next.getPointRef()), next.getValue());
if (current != null)
return (next.getValue() < current.getValue()) ? next : current;
return next;
}
catch (Exception e)
{
return current;
}
}
private class Plateauness implements UnivariateFunction, MultivariateFunction
{
final double frcnum_noisevar = 0.1;
final double[] pre;
final double n2;
/**
* Instantiates a new plateauness.
*
* @param frcnum
* the scaled FRC numerator
* @param exp_decay
* the precomputed exponential decay (hq)
* @param low
* the lower bound of the array for optimisation
* @param high
* the higher bound of the array for optimisation
*/
Plateauness(double[] frcnum, double[] exp_decay, int low, int high)
{
// Precompute
pre = new double[high - low];
for (int i = 0; i < pre.length; i++)
{
int index = i + low;
pre[i] = frcnum[index] / exp_decay[index];
}
n2 = frcnum_noisevar * frcnum_noisevar;
}
public double value(double qValue)
{
if (qValue < 1e-16)
qValue = 1e-16;
double v = 0;
for (int i = 0; i < pre.length; i++)
{
// Original cost function. Note that each observation has a
// contribution of 0 to 1.
double diff = (pre[i] / qValue) - 1;
v += 1 - FastMath.exp(-diff * diff / n2);
// Modified cost function so that the magnitude of difference over or
// under 1 is penalised the same. This has a problem if FRC numerator
// is negative. Also the range is unchecked so observation can have
// unequal contributions.
//double diff = Math.abs(pre[i]) / qValue;
//v += Math.abs(Math.log(diff));
}
return v;
}
public double value(double[] point) throws IllegalArgumentException
{
return value(point[0]);
}
}
private class MultiPlateauness implements MultivariateFunction
{
final double frcnum_noisevar = 0.1;
final double[] pre, q2;
final double n2;
final double four_pi2 = 4 * Math.PI * Math.PI;
@SuppressWarnings("unused")
final double[] q;
@SuppressWarnings("unused")
final int low;
/**
* Instantiates a new plateauness.
*
* @param frcnum
* the scaled FRC numerator
* @param exp_decay
* the precomputed exponential decay (hq)
* @param low
* the lower bound of the array for optimisation
* @param high
* the higher bound of the array for optimisation
*/
MultiPlateauness(double[] frcnum, double[] q, int low, int high)
{
this.q = q;
this.low = low;
q2 = new double[q.length];
// Precompute
pre = new double[high - low];
for (int i = 0; i < pre.length; i++)
{
int index = i + low;
double sinc_q = (index == 0) ? 1 : sinc(Math.PI * q[index]);
pre[i] = frcnum[index] / (sinc_q * sinc_q);
q2[i] = q[index] * q[index];
}
n2 = frcnum_noisevar * frcnum_noisevar;
}
public double value(double[] point) throws IllegalArgumentException
{
double mean = point[0];
double sigma = point[1];
double qValue = point[2];
if (qValue < 1e-16)
qValue = 1e-16;
// Fast computation of a subset of hq
double eight_pi2_s2 = 2 * four_pi2 * sigma * sigma;
double factor = -four_pi2 * mean * mean;
// Check
//double[] hq2 = FRC.computeHq(q, mean, sigma);
double v = 0;
for (int i = 0; i < pre.length; i++)
{
double d = 1 + eight_pi2_s2 * q2[i];
double hq = FastMath.exp((factor * q2[i]) / d) / Math.sqrt(d);
// Check
//if (hq != hq2[i + low])
// System.out.printf("hq error: %f != %f\n", hq, hq2[i + low]);
// Original cost function. Note that each observation has a
// contribution of 0 to 1.
double diff = (pre[i] / (qValue * hq)) - 1;
v += 1 - FastMath.exp(-diff * diff / n2);
}
return v;
}
}
private boolean showQEstimationInputDialog()
{
GenericDialog gd = new GenericDialog(TITLE);
gd.addHelp(About.HELP_URL);
// Build a list of all images with a region ROI
List<String> titles = new LinkedList<String>();
if (WindowManager.getWindowCount() > 0)
{
for (int imageID : WindowManager.getIDList())
{
ImagePlus imp = WindowManager.getImage(imageID);
if (imp != null && imp.getRoi() != null && imp.getRoi().isArea())
titles.add(imp.getTitle());
}
}
gd.addMessage("Estimate the blinking correction parameter Q for Fourier Ring Correlation");
ResultsManager.addInput(gd, inputOption, InputSource.MEMORY);
if (!titles.isEmpty())
gd.addCheckbox((titles.size() == 1) ? "Use_ROI" : "Choose_ROI", chooseRoi);
gd.addMessage("Image construction options:");
//gd.addCheckbox("Use_signal (if present)", useSignal);
gd.addChoice("Image_scale", SCALE_ITEMS, SCALE_ITEMS[imageScaleIndex]);
gd.addChoice("Auto_image_size", IMAGE_SIZE_ITEMS, IMAGE_SIZE_ITEMS[imageSizeIndex]);
gd.addNumericField("Block_size", blockSize, 0);
gd.addCheckbox("Random_split", randomSplit);
gd.addNumericField("Max_per_bin", maxPerBin, 0);
gd.addMessage("Fourier options:");
String[] fourierMethodNames = SettingsManager.getNames((Object[]) FRC.FourierMethod.values());
gd.addChoice("Fourier_method", fourierMethodNames, fourierMethodNames[fourierMethodIndex]);
String[] samplingMethodNames = SettingsManager.getNames((Object[]) FRC.SamplingMethod.values());
gd.addChoice("Sampling_method", samplingMethodNames, samplingMethodNames[samplingMethodIndex]);
gd.addSlider("Sampling_factor", 0.2, 4, perimeterSamplingFactor);
gd.addMessage("Estimation options:");
String[] thresholdMethodNames = SettingsManager.getNames((Object[]) FRC.ThresholdMethod.values());
gd.addChoice("Threshold_method", thresholdMethodNames, thresholdMethodNames[thresholdMethodIndex]);
String[] precisionMethodNames = SettingsManager.getNames((Object[]) PrecisionMethod.values());
gd.addChoice("Precision_method", precisionMethodNames, precisionMethodNames[precisionMethodIndex]);
gd.addNumericField("Precision_Mean", mean, 2, 6, "nm");
gd.addNumericField("Precision_Sigma", sigma, 2, 6, "nm");
gd.addCheckbox("Sample_decay", sampleDecay);
gd.addCheckbox("LOESS_smoothing", loessSmoothing);
gd.addCheckbox("Fit_precision", fitPrecision);
gd.addSlider("MinQ", 0, 0.4, minQ);
gd.addSlider("MaxQ", 0.1, 0.5, maxQ);
gd.showDialog();
if (gd.wasCanceled())
return false;
inputOption = ResultsManager.getInputSource(gd);
if (!titles.isEmpty())
chooseRoi = gd.getNextBoolean();
//useSignal = gd.getNextBoolean();
imageScaleIndex = gd.getNextChoiceIndex();
imageSizeIndex = gd.getNextChoiceIndex();
blockSize = Math.max(1, (int) gd.getNextNumber());
randomSplit = gd.getNextBoolean();
maxPerBin = Math.abs((int) gd.getNextNumber());
fourierMethodIndex = gd.getNextChoiceIndex();
fourierMethod = FourierMethod.values()[fourierMethodIndex];
samplingMethodIndex = gd.getNextChoiceIndex();
samplingMethod = SamplingMethod.values()[samplingMethodIndex];
perimeterSamplingFactor = gd.getNextNumber();
thresholdMethodIndex = gd.getNextChoiceIndex();
thresholdMethod = FRC.ThresholdMethod.values()[thresholdMethodIndex];
precisionMethodIndex = gd.getNextChoiceIndex();
precisionMethod = PrecisionMethod.values()[precisionMethodIndex];
mean = Math.abs(gd.getNextNumber());
sigma = Math.abs(gd.getNextNumber());
sampleDecay = gd.getNextBoolean();
loessSmoothing = gd.getNextBoolean();
fitPrecision = gd.getNextBoolean();
minQ = Maths.clip(0, 0.5, gd.getNextNumber());
maxQ = Maths.clip(0, 0.5, gd.getNextNumber());
// Check arguments
try
{
Parameters.isAboveZero("Perimeter sampling factor", perimeterSamplingFactor);
if (precisionMethod == PrecisionMethod.FIXED)
{
Parameters.isAboveZero("Precision Mean", mean);
Parameters.isAboveZero("Precision Sigma", sigma);
}
Parameters.isAbove("MaxQ", maxQ, minQ);
}
catch (IllegalArgumentException e)
{
IJ.error(TITLE, e.getMessage());
return false;
}
if (!titles.isEmpty() && chooseRoi)
{
if (titles.size() == 1)
{
roiImage = titles.get(0);
Recorder.recordOption("Image", roiImage);
}
else
{
String[] items = titles.toArray(new String[titles.size()]);
gd = new GenericDialog(TITLE);
gd.addMessage("Select the source image for the ROI");
gd.addChoice("Image", items, roiImage);
gd.showDialog();
if (gd.wasCanceled())
return false;
roiImage = gd.getNextChoice();
}
ImagePlus imp = WindowManager.getImage(roiImage);
roiBounds = imp.getRoi().getBounds();
roiImageWidth = imp.getWidth();
roiImageHeight = imp.getHeight();
}
else
{
roiBounds = null;
}
return true;
}
public class QPlot
{
final FRCCurve frcCurve;
final double nmPerPixel, qNorm;
final double[] vq, sinc2, q, qScaled;
final int low, high;
String title, title2;
FireResult originalFireResult;
double originalFireNumber = Double.NaN;
// Store the last plotted value
double mean, sigma, qValue;
QPlot(FRCCurve frcCurve, double qValue, int low, int high)
{
this.nmPerPixel = frcCurve.nmPerPixel;
this.frcCurve = frcCurve;
this.qValue = qValue;
this.low = low;
this.high = high;
// For normalisation
qNorm = (1 / frcCurve.mean1 + 1 / frcCurve.mean2);
// Compute v(q) - The numerator of the FRC divided by the number of pixels
// in the Fourier circle (2*pi*q*L)
vq = new double[frcCurve.getSize()];
for (int i = 0; i < vq.length; i++)
{
vq[i] = qNorm * frcCurve.get(i).getNumerator() / frcCurve.get(i).getNumberOfSamples();
}
q = FRC.computeQ(frcCurve, false);
// For the plot
qScaled = FRC.computeQ(frcCurve, true);
// Compute sinc factor
sinc2 = new double[frcCurve.getSize()];
sinc2[0] = 1; // By definition
for (int i = 1; i < sinc2.length; i++)
{
// Note the original equation given in the paper: sinc(pi*q*L)^2 is a typo.
// Matlab code provided by Bernd Rieger removes L to compute: sinc(pi*q)^2
// with q == 1/L, 2/L, ... (i.e. no unit conversion to nm). This means that
// the function will start at 1 and drop off to zero at q=L.
// sinc(pi*q)^2
sinc2[i] = sinc(Math.PI * q[i]);
sinc2[i] *= sinc2[i];
}
// For the plot
title = results.getName() + " FRC Numerator Curve";
title2 = results.getName() + " FRC Numerator/Correction Ratio";
// Reset
FRC.applyQCorrection(frcCurve, 0, 0, 0);
FRCCurve smoothedFrcCurve = FRC.getSmoothedCurve(frcCurve, false);
originalFireResult = new FireResult(0, 0, smoothedFrcCurve, frcCurve.getCorrelationValues());
FIREResult result = FRC.calculateFire(smoothedFrcCurve, thresholdMethod);
if (result != null)
{
originalFireNumber = result.fireNumber;
}
}
private double sinc(double x)
{
return Math.sin(x) / x;
}
PlotWindow[] plot(double mean, double sigma, double qValue)
{
this.mean = mean;
this.sigma = sigma;
this.qValue = qValue;
double mu = mean / nmPerPixel;
double sd = sigma / nmPerPixel;
double[] hq = FRC.computeHq(q, mu, sd);
double[] correction = new double[hq.length];
double[] vq_corr = new double[vq.length];
double[] ratio = new double[vq.length];
for (int i = 0; i < hq.length; i++)
{
// Note: vq already has the qNorm factor applied so we do not
// divide qValue by qNorm.
correction[i] = qValue * sinc2[i] * hq[i];
// This is not actually the corrected numerator since it is made absolute
//vq_corr[i] = Math.abs(vq[i] - correction[i]);
vq_corr[i] = vq[i] - correction[i];
ratio[i] = vq[i] / correction[i];
}
// Add this to aid is manual adjustment
double plateauness = computePlateauness(qValue, mu, sd);
Plot2 plot = new Plot2(title, "Spatial Frequency (nm^-1)", "FRC Numerator");
String label = String.format("Q = %.3f (Precision = %.3f +/- %.3f)", qValue, mean, sigma);
plot.setColor(Color.red);
double[] vq = makeStrictlyPositive(this.vq, Double.POSITIVE_INFINITY);
plot.addPoints(qScaled, vq, Plot.LINE);
double min = Maths.min(vq);
if (qValue > 0)
{
label += String.format(". Cost = %.3f", plateauness);
plot.setColor(Color.darkGray);
plot.addPoints(qScaled, correction, Plot.DOT);
plot.setColor(Color.blue);
plot.addPoints(qScaled, makeStrictlyPositive(vq_corr, min), Plot.LINE);
plot.setColor(Color.black);
plot.addLegend("Numerator\nCorrection\nCorrected Numerator", "top-right");
}
plot.setColor(Color.magenta);
plot.drawLine(qScaled[low], min, qScaled[low], vq[0]);
plot.drawLine(qScaled[high], min, qScaled[high], vq[0]);
plot.setColor(Color.black);
plot.addLabel(0, 0, label);
plot.setAxisYLog(true);
PlotWindow pw1 = Utils.display(title, plot, Utils.NO_TO_FRONT);
plot.setLimitsToFit(true); // For the log scale this seems to only work after drawing
// Show how the resolution changes
FRC.applyQCorrection(frcCurve, qValue, mean, sigma);
FRCCurve smoothedFrcCurve = FRC.getSmoothedCurve(frcCurve, false);
// Resolution in pixels
FIREResult result = FRC.calculateFire(smoothedFrcCurve, thresholdMethod);
PlotWindow pw2 = null;
if (result != null)
{
double fireNumber = result.fireNumber;
FrcCurve curve = new FrcCurve();
FireResult fireResult = new FireResult(fireNumber, result.correlation, smoothedFrcCurve,
frcCurve.getCorrelationValues());
double orig = Double.NaN;
if (qValue > 0)
{
curve.add(results.getName(), originalFireResult, thresholdMethod, Color.orange, Color.blue,
Color.lightGray);
orig = originalFireNumber;
}
curve.add(results.getName(), fireResult, thresholdMethod, Color.red, Color.blue, Color.black);
curve.addResolution(fireNumber, orig, result.correlation);
plot = curve.getPlot();
pw2 = Utils.display(plot.getTitle(), plot, Utils.NO_TO_FRONT);
}
// Produce a ratio plot. Plateauness is designed to achieve a value of 1 for this ratio.
plot = new Plot2(title2, "Spatial Frequency (nm^-1)", "FRC Numerator / Spurious component");
double xMax = qScaled[qScaled.length - 1];
if (qValue > 0)
{
plot.addLabel(0, 0, String.format("Cost = %.3f", plateauness));
plot.setColor(Color.blue);
plot.addPoints(qScaled, ratio, Plot.LINE);
}
plot.setColor(Color.black);
plot.drawLine(0, 1, xMax, 1);
plot.setColor(Color.magenta);
plot.drawLine(qScaled[low], 0, qScaled[low], 2);
plot.drawLine(qScaled[high], 0, qScaled[high], 2);
plot.setLimits(0, xMax, 0, 2);
PlotWindow pw3 = Utils.display(title2, plot, Utils.NO_TO_FRONT);
return new PlotWindow[] { pw1, pw2, pw3 };
}
private double computePlateauness(double qValue, double mu, double sd)
{
double[] exp_decay = computeExpDecay(mu, sd, q);
Plateauness p = new Plateauness(vq, exp_decay, low, high);
double plateauness = p.value(qValue);
return plateauness;
}
private double[] makeStrictlyPositive(double[] data, double min)
{
data = data.clone();
if (min == Double.POSITIVE_INFINITY)
{
// Find min positive value
for (int i = 0; i < data.length; i++)
if (data[i] > 0 && data[i] < min)
min = data[i];
}
for (int i = 0; i < data.length; i++)
if (data[i] < min)
data[i] = min;
return data;
}
}
public class PrecisionHistogram
{
final float[] x, y;
final String title;
final double standardAmplitude;
final float[] x2;
final StoredDataStatistics precision;
/**
* The mean of the localisation precision distribution (in nm). This value can be updated by the
* {@link #plot(double, double)} method.
*/
double mean;
/**
* The standard deviation of the localisation precision distribution (in nm). This value can be updated by the
* {@link #plot(double, double)} method.
*/
double sigma;
PrecisionHistogram(float[][] hist, StoredDataStatistics precision, String title)
{
this.title = title;
x = Utils.createHistogramAxis(hist[0]);
y = Utils.createHistogramValues(hist[1]);
this.precision = precision;
// Sum the area under the histogram to use for normalisation.
// Amplitude = volume / (sigma * sqrt(2*pi))
// Precompute the correct amplitude for a standard width Gaussian
double dx = (hist[0][1] - hist[0][0]);
standardAmplitude = precision.getN() * dx / Math.sqrt(2 * Math.PI);
// Set up for drawing the Gaussian curve
double min = x[0];
double max = x[x.length - 1];
int n = 100;
dx = (max - min) / n;
x2 = new float[n + 1];
for (int i = 0; i <= n; i++)
x2[i] = (float) (min + i * dx);
}
public PrecisionHistogram(String title)
{
this.title = title;
// Set some defaults
this.mean = 20;
this.sigma = 2;
x = y = x2 = null;
precision = null;
standardAmplitude = 0;
}
PlotWindow plot(double mean, double sigma)
{
this.mean = mean;
this.sigma = sigma;
return plot();
}
PlotWindow plot()
{
Plot2 plot = new Plot2(title, "Precision (nm)", "Frequency");
if (x != null)
{
plot.setColor(Color.black);
plot.addPoints(x, y, Plot.LINE);
plot.addLabel(0, 0, String.format("Precision = %.3f +/- %.3f", mean, sigma));
// Add the Gaussian line
// Compute the intergal of the standard gaussian between the min and max
final double denom0 = 1.0 / (Math.sqrt(2.0) * sigma);
double integral = 0.5 * Erf.erf((x2[0] - mean) * denom0, (x2[x2.length - 1] - mean) * denom0);
// Normalise so the integral has the same volume as the histogram
Gaussian g = new Gaussian(this.standardAmplitude / (sigma * integral), mean, sigma);
float[] y2 = new float[x2.length];
for (int i = 0; i < y2.length; i++)
{
y2[i] = (float) g.value(x2[i]);
}
// Normalise
plot.setColor(Color.red);
plot.addPoints(x2, y2, Plot.LINE);
float max = Maths.max(y2);
max = Maths.maxDefault(max, y);
double rangex = 0; //(x2[x2.length - 1] - x2[0]) * 0.025;
plot.setLimits(x2[0] - rangex, x2[x2.length - 1] + rangex, 0, max * 1.05);
}
else
{
// There is no base histogram.
// Just plot a Gaussian +/- 4 SD.
plot.addLabel(0, 0, String.format("Precision = %.3f +/- %.3f", mean, sigma));
double min = Math.max(0, mean - 4 * sigma);
double max = mean + 4 * sigma;
int n = 100;
double dx = (max - min) / n;
float[] x2 = new float[n + 1];
Gaussian g = new Gaussian(1, mean, sigma);
float[] y2 = new float[x2.length];
for (int i = 0; i <= n; i++)
{
x2[i] = (float) (min + i * dx);
y2[i] = (float) g.value(x2[i]);
}
plot.setColor(Color.red);
plot.addPoints(x2, y2, Plot.LINE);
// Always put min = 0 otherwise the plot does not change.
plot.setLimits(0, max, 0, 1.05);
}
return Utils.display(title, plot, Utils.NO_TO_FRONT);
}
}
/**
* Calculate a histogram of the precision. The precision can be either stored in the results or calculated using the
* Mortensen formula. If the precision method for Q estimation is not fixed then the histogram is fitted with a
* Gaussian to create an initial estimate.
*
* @param precisionMethod
* the precision method
* @return The precision histogram
*/
private PrecisionHistogram calculatePrecisionHistogram()
{
boolean logFitParameters = false;
String title = results.getName() + " Precision Histogram";
// Check if the results has the precision already or if it can be computed.
boolean canUseStored = canUseStoredPrecision(results);
boolean canCalculatePrecision = canCalculatePrecision(results);
// Set the method to compute a histogram. Default to the user selected option.
PrecisionMethod m = null;
if (canUseStored && precisionMethod == PrecisionMethod.STORED)
m = precisionMethod;
else if (canCalculatePrecision && precisionMethod == PrecisionMethod.CALCULATE)
m = precisionMethod;
if (m == null)
{
// We get here if the choice of the user is not available.
// We only have two choices so if one is available then select it.
if (canUseStored)
m = PrecisionMethod.STORED;
else if (canCalculatePrecision)
m = PrecisionMethod.CALCULATE;
// If the user selected a method not available then log a warning
if (m != null && precisionMethod != PrecisionMethod.FIXED)
{
IJ.log(String.format("%s : Selected precision method '%s' not available, switching to '%s'", TITLE,
precisionMethod, m.getName()));
}
if (m == null)
{
// We cannot compute a precision histogram.
// This does not matter if the user has provide a fixed input.
if (precisionMethod == PrecisionMethod.FIXED)
{
PrecisionHistogram histogram = new PrecisionHistogram(title);
histogram.mean = mean;
histogram.sigma = sigma;
return histogram;
}
// No precision
return null;
}
}
// We get here if we can compute precision.
// Build the histogram
StoredDataStatistics precision = new StoredDataStatistics(results.size());
if (m == PrecisionMethod.STORED)
{
for (PeakResult r : results.getResults())
{
precision.add(r.getPrecision());
}
}
else
{
final double nmPerPixel = results.getNmPerPixel();
final double gain = results.getGain();
final boolean emCCD = results.isEMCCD();
for (PeakResult r : results.getResults())
{
precision.add(r.getPrecision(nmPerPixel, gain, emCCD));
}
}
//System.out.printf("Raw p = %f\n", precision.getMean());
double yMin = Double.NEGATIVE_INFINITY, yMax = 0;
// Set the min and max y-values using 1.5 x IQR
DescriptiveStatistics stats = precision.getStatistics();
double lower = stats.getPercentile(25);
double upper = stats.getPercentile(75);
if (Double.isNaN(lower) || Double.isNaN(upper))
{
if (logFitParameters)
Utils.log("Error computing IQR: %f - %f", lower, upper);
}
else
{
double iqr = upper - lower;
yMin = FastMath.max(lower - iqr, stats.getMin());
yMax = FastMath.min(upper + iqr, stats.getMax());
if (logFitParameters)
Utils.log(" Data range: %f - %f. Plotting 1.5x IQR: %f - %f", stats.getMin(), stats.getMax(), yMin,
yMax);
}
if (yMin == Double.NEGATIVE_INFINITY)
{
int n = 5;
yMin = Math.max(stats.getMin(), stats.getMean() - n * stats.getStandardDeviation());
yMax = Math.min(stats.getMax(), stats.getMean() + n * stats.getStandardDeviation());
if (logFitParameters)
Utils.log(" Data range: %f - %f. Plotting mean +/- %dxSD: %f - %f", stats.getMin(), stats.getMax(), n,
yMin, yMax);
}
// Get the data within the range
double[] data = precision.getValues();
precision = new StoredDataStatistics(data.length);
for (double d : data)
{
if (d < yMin || d > yMax)
continue;
precision.add(d);
}
int histogramBins = Utils.getBins(precision, Utils.BinMethod.SCOTT);
float[][] hist = Utils.calcHistogram(precision.getFloatValues(), yMin, yMax, histogramBins);
PrecisionHistogram histogram = new PrecisionHistogram(hist, precision, title);
if (precisionMethod == PrecisionMethod.FIXED)
{
histogram.mean = mean;
histogram.sigma = sigma;
return histogram;
}
// Fitting of the histogram to produce the initial estimate
// Extract non-zero data
float[] x = Arrays.copyOf(hist[0], hist[0].length);
float[] y = hist[1];
int count = 0;
float dx = (x[1] - x[0]) * 0.5f;
for (int i = 0; i < y.length; i++)
if (y[i] > 0)
{
x[count] = x[i] + dx;
y[count] = y[i];
count++;
}
x = Arrays.copyOf(x, count);
y = Arrays.copyOf(y, count);
// Sense check to fitted data. Get mean and SD of histogram
double[] stats2 = Utils.getHistogramStatistics(x, y);
if (logFitParameters)
Utils.log(" Initial Statistics: %f +/- %f", stats2[0], stats2[1]);
histogram.mean = stats2[0];
histogram.sigma = stats2[1];
// Standard Gaussian fit
double[] parameters = fitGaussian(x, y);
if (parameters == null)
{
Utils.log(" Failed to fit initial Gaussian");
return histogram;
}
double newMean = parameters[1];
double error = Math.abs(stats2[0] - newMean) / stats2[1];
if (error > 3)
{
Utils.log(" Failed to fit Gaussian: %f standard deviations from histogram mean", error);
return histogram;
}
if (newMean < yMin || newMean > yMax)
{
Utils.log(" Failed to fit Gaussian: %f outside data range %f - %f", newMean, yMin, yMax);
return histogram;
}
if (logFitParameters)
Utils.log(" Initial Gaussian: %f @ %f +/- %f", parameters[0], parameters[1], parameters[2]);
histogram.mean = parameters[1];
histogram.sigma = parameters[2];
return histogram;
}
private boolean canCalculatePrecision(MemoryPeakResults results)
{
// Calibration is required to compute the precision
Calibration cal = results.getCalibration();
if (cal == null)
return false;
if (!cal.hasNmPerPixel() || !cal.hasGain() || !cal.hasEMCCD())
return false;
// Check all have a width and signal
PeakResult[] data = results.toArray();
for (int i = 0; i < data.length; i++)
{
PeakResult p = data[i];
if (p.getSD() <= 0 || p.getSignal() <= 0)
return true;
}
// Check for variable width that is not 1 and a variable signal
for (int i = 0; i < data.length; i++)
{
PeakResult p = data[i];
// Check this is valid
if (p.getSD() != 1)
{
// Check the rest for a different value
float w1 = p.getSD();
float s1 = p.getSignal();
for (int j = i + 1; j < data.length; j++)
{
PeakResult p2 = data[j];
if (p2.getSD() != 1 && p2.getSD() != w1 && p2.getSignal() != s1)
return true;
}
// All the results are the same, this is not valid
break;
}
}
return false;
}
private boolean canUseStoredPrecision(MemoryPeakResults results)
{
for (PeakResult p : results)
if (!p.hasPrecision())
return false;
return true;
}
/**
* Fit gaussian.
*
* @param x
* the x
* @param y
* the y
* @return new double[] { norm, mean, sigma }
*/
private double[] fitGaussian(float[] x, float[] y)
{
WeightedObservedPoints obs = new WeightedObservedPoints();
for (int i = 0; i < x.length; i++)
obs.add(x[i], y[i]);
Collection<WeightedObservedPoint> observations = obs.toList();
GaussianCurveFitter fitter = GaussianCurveFitter.create().withMaxIterations(2000);
GaussianCurveFitter.ParameterGuesser guess = new GaussianCurveFitter.ParameterGuesser(observations);
double[] initialGuess = null;
try
{
initialGuess = guess.guess();
return fitter.withStartPoint(initialGuess).fit(observations);
}
catch (TooManyEvaluationsException e)
{
// Use the initial estimate
return initialGuess;
}
catch (Exception e)
{
// Just in case there is another exception type, or the initial estimate failed
return null;
}
}
/**
* Used to tile the windows from the worker threads on the first plot
*/
private class MyWindowOrganiser
{
final WindowOrganiser wo = new WindowOrganiser();
int expected;
int size = 0;
boolean ignore = false;
public void add(PlotWindow plot)
{
if (ignore)
return;
// This is not perfect since multiple threads may reset the same new-window flag
if (Utils.isNewWindow())
wo.add(plot);
// Layout the windows if we reached the expected size.
if (++size == expected)
{
wo.tile();
ignore = true; // No further need to track the windows
}
}
}
private class WorkSettings implements Cloneable
{
double mean, sigma, qValue = 0;
WorkSettings(double mean, double sigma, double qValue)
{
this.mean = mean;
this.sigma = sigma;
this.qValue = qValue;
}
@Override
public WorkSettings clone()
{
try
{
return (WorkSettings) super.clone();
}
catch (CloneNotSupportedException e)
{
return null; // Shouldn't happen
}
}
}
private class BaseWorker extends WorkflowWorker<WorkSettings, Object>
{
final MyWindowOrganiser wo;
BaseWorker(MyWindowOrganiser wo)
{
this.wo = wo;
}
@Override
public boolean equalSettings(WorkSettings current, WorkSettings previous)
{
if (current.mean != previous.mean)
return false;
if (current.sigma != previous.sigma)
return false;
return true;
}
@Override
public boolean equalResults(Object current, Object previous)
{
// We never create any results so ignore this
return true;
}
@Override
public Object createResults(WorkSettings settings, Object results)
{
return null;
}
}
private class HistogramWorker extends BaseWorker
{
final PrecisionHistogram histogram;
HistogramWorker(MyWindowOrganiser wo, PrecisionHistogram histogram)
{
super(wo);
this.histogram = histogram;
}
@Override
public Object createResults(WorkSettings settings, Object results)
{
// Plot the histogram
wo.add(histogram.plot(settings.mean, settings.sigma));
return null;
}
}
private class QPlotWorker extends BaseWorker
{
final QPlot qplot;
QPlotWorker(MyWindowOrganiser wo, QPlot qplot)
{
super(wo);
this.qplot = qplot;
}
@Override
public boolean equalSettings(WorkSettings current, WorkSettings previous)
{
if (current.qValue != previous.qValue)
return false;
return super.equalSettings(current, previous);
}
@Override
public Object createResults(WorkSettings settings, Object results)
{
// Compute Q and then plot the scaled FRC numerator
for (PlotWindow pw : qplot.plot(settings.mean, settings.sigma, settings.qValue))
wo.add(pw);
return null;
}
}
private boolean showQEstimationDialog(final PrecisionHistogram histogram, final QPlot qplot,
final FRCCurve frcCurve, final double nmPerPixel)
{
// This is used for the initial layout of windows
final MyWindowOrganiser wo = new MyWindowOrganiser();
// Use a simple workflow
Workflow<WorkSettings, Object> workflow = new Workflow<WorkSettings, Object>();
// Split the work to two children with a dummy initial worker
int previous = workflow.add(new BaseWorker(wo));
workflow.add(new HistogramWorker(wo, histogram), previous);
workflow.add(new QPlotWorker(wo, qplot), previous);
workflow.start();
// The number of plots
wo.expected = 4;
String KEY_MEAN = "mean_estimate";
String KEY_SIGMA = "sigma_estimate";
String KEY_Q = "q_estimate";
String macroOptions = Macro.getOptions();
if (macroOptions != null)
{
// If inside a macro then just get the options and run the work
double mean = Double.parseDouble(Macro.getValue(macroOptions, KEY_MEAN, Double.toString(histogram.mean)));
double sigma = Double
.parseDouble(Macro.getValue(macroOptions, KEY_SIGMA, Double.toString(histogram.sigma)));
double qValue = Double.parseDouble(Macro.getValue(macroOptions, KEY_Q, Double.toString(qplot.qValue)));
workflow.run(new WorkSettings(mean, sigma, qValue));
workflow.shutdown(false);
}
else
{
// Draw the plots with the first set of work
workflow.run(new WorkSettings(histogram.mean, histogram.sigma, qplot.qValue));
// Build the dialog
NonBlockingGenericDialog gd = new NonBlockingGenericDialog(TITLE);
gd.addHelp(About.HELP_URL);
double mu = histogram.mean / nmPerPixel;
double sd = histogram.sigma / nmPerPixel;
double plateauness = qplot.computePlateauness(qplot.qValue, mu, sd);
gd.addMessage("Estimate the blinking correction parameter Q for Fourier Ring Correlation\n \n" +
String.format("Initial estimate:\nPrecision = %.3f +/- %.3f\n", histogram.mean, histogram.sigma) +
String.format("Q = %s\nCost = %.3f", Utils.rounded(qplot.qValue), plateauness));
double mean10 = histogram.mean * 10;
double sd10 = histogram.sigma * 10;
double q10 = qplot.qValue * 10;
gd.addSlider("Mean (x10)", Math.max(0, mean10 - sd10 * 2), mean10 + sd10 * 2, mean10);
gd.addSlider("Sigma (x10)", Math.max(0, sd10 / 2), sd10 * 2, sd10);
gd.addSlider("Q (x10)", 0, Math.max(50, q10 * 2), q10);
gd.addCheckbox("Reset_all", false);
gd.addMessage("Double-click a slider to reset");
gd.addDialogListener(new FIREDialogListener(gd, histogram, qplot, workflow));
// Show this when the workers have finished drawing the plots so it is on top
try
{
long timeout = System.currentTimeMillis() + 5000;
while (wo.size < wo.expected)
{
Thread.sleep(50);
if (System.currentTimeMillis() > timeout)
break;
}
}
catch (InterruptedException e)
{
// Ignore
}
gd.showDialog();
// Finish the worker threads
boolean cancelled = gd.wasCanceled();
workflow.shutdown(cancelled);
if (cancelled)
return false;
}
// Store the Q value and the mean and sigma
qValue = qplot.qValue;
mean = qplot.mean;
sigma = qplot.sigma;
// Record the values for Macros since the NonBlockingDialog doesn't
if (Recorder.record)
{
Recorder.recordOption(KEY_MEAN, Double.toString(mean));
Recorder.recordOption(KEY_SIGMA, Double.toString(sigma));
Recorder.recordOption(KEY_Q, Double.toString(qValue));
}
return true;
}
private class FIREDialogListener implements DialogListener, MouseListener
{
/**
* Delay (in milliseconds) used when entering new values in the dialog before the preview is processed
*/
@SuppressWarnings("unused")
static final long DELAY = 500;
long time;
boolean notActive = true;
volatile int ignore = 0;
Workflow<WorkSettings, Object> workflow;
double defaultMean, defaultSigma, defaultQValue;
String m, s, q;
TextField tf1, tf2, tf3;
Scrollbar sl1, sl2, sl3;
Checkbox cb;
final boolean isMacro;
FIREDialogListener(GenericDialog gd, PrecisionHistogram histogram, QPlot qplot,
Workflow<WorkSettings, Object> workflow)
{
time = System.currentTimeMillis() + 1000;
this.workflow = workflow;
this.defaultMean = histogram.mean;
this.defaultSigma = histogram.sigma;
this.defaultQValue = qplot.qValue;
isMacro = Utils.isMacro();
// For the reset
tf1 = (TextField) gd.getNumericFields().get(0);
tf2 = (TextField) gd.getNumericFields().get(1);
tf3 = (TextField) gd.getNumericFields().get(2);
cb = (Checkbox) (gd.getCheckboxes().get(0));
// Sliders
sl1 = (Scrollbar) gd.getSliders().get(0);
sl2 = (Scrollbar) gd.getSliders().get(1);
sl3 = (Scrollbar) gd.getSliders().get(2);
sl1.addMouseListener(this);
sl2.addMouseListener(this);
sl3.addMouseListener(this);
m = tf1.getText();
s = tf2.getText();
q = tf3.getText();
// Implement a delay to allow typing.
// This is also applied to the sliders which we do not want.
// Ideally we would have no delay for sliders (since they are in the correct place already
// but a delay for typing in the text field). Unfortunately the AWTEvent raised by ImageJ
// for the slider is actually from the TextField so we cannot tell the difference.
// For now just have no delay.
//if (!isMacro)
// workflow.startPreview();
}
public boolean dialogItemChanged(GenericDialog gd, AWTEvent e)
{
// Delay reading the dialog when in interactive mode. This is a workaround for a bug
// where the dialog has not yet been drawn.
if (notActive && !isMacro && System.currentTimeMillis() < time)
return true;
if (ignore-- > 0)
{
//System.out.println("ignored");
return true;
}
notActive = false;
double mean = Math.abs(gd.getNextNumber()) / 10;
double sigma = Math.abs(gd.getNextNumber()) / 10;
double qValue = Math.abs(gd.getNextNumber()) / 10;
boolean reset = gd.getNextBoolean();
// Even events from the slider come through as TextEvent from the TextField
// since ImageJ captures the slider event as just updates the TextField.
//System.out.printf("Event: %s, %f, %f\n", e, mean, sigma);
// Allow reset to default
if (reset)
{
// This does not trigger the event
cb.setState(false);
mean = this.defaultMean;
sigma = this.defaultSigma;
qValue = this.defaultQValue;
}
WorkSettings work = new WorkSettings(mean, sigma, qValue);
// Offload this work onto a thread that just picks up the most recent dialog input.
workflow.run(work);
if (reset)
{
// These trigger dialogItemChanged(...) so do them after we added
// work to the queue and ignore the events
ignore = 3;
tf1.setText(m);
tf2.setText(s);
tf3.setText(q);
}
return true;
}
public void mouseClicked(MouseEvent e)
{
// Reset the slider on double-click
if (e.getClickCount() < 2)
return;
if (e.getSource() == null || !(e.getSource() instanceof Scrollbar))
return;
Scrollbar sl = (Scrollbar) e.getSource();
if (sl == sl1)
tf1.setText(m);
if (sl == sl2)
tf2.setText(s);
if (sl == sl3)
tf3.setText(q);
}
public void mousePressed(MouseEvent e)
{
// Ignore
}
public void mouseReleased(MouseEvent e)
{
// Ignore
}
public void mouseEntered(MouseEvent e)
{
// Ignore
}
public void mouseExited(MouseEvent e)
{
// Ignore
}
}
private static int imagejNThreads = Prefs.getThreads();
private static int lastNThreads = imagejNThreads;
private int nThreads = 0;
/**
* Gets the last N threads used in the input dialog.
*
* @return the last N threads
*/
private static int getLastNThreads()
{
// See if ImageJ preference were updated
if (imagejNThreads != Prefs.getThreads())
{
lastNThreads = imagejNThreads = Prefs.getThreads();
}
// Otherwise use the last user input
return lastNThreads;
}
/**
* Gets the threads to use for multi-threaded computation.
*
* @return the threads
*/
private int getThreads()
{
if (nThreads == 0)
{
nThreads = Prefs.getThreads();
}
return nThreads;
}
/**
* Sets the threads to use for multi-threaded computation.
*
* @param nThreads
* the new threads
*/
public void setThreads(int nThreads)
{
this.nThreads = Math.max(1, nThreads);
}
}