package gdsc.smlm.ij.plugins;
import java.awt.Checkbox;
import java.awt.Choice;
/*-----------------------------------------------------------------------------
* GDSC SMLM Software
*
* Copyright (C) 2016 Alex Herbert
* Genome Damage and Stability Centre
* University of Sussex, UK
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*---------------------------------------------------------------------------*/
import java.awt.Color;
import java.awt.Rectangle;
import java.awt.TextField;
import java.awt.event.ItemEvent;
import java.awt.event.ItemListener;
import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.LinkedList;
import java.util.List;
import java.util.Vector;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.atomic.AtomicInteger;
import org.apache.commons.math3.stat.descriptive.rank.Percentile;
import org.apache.commons.math3.util.FastMath;
import gdsc.core.ij.IJLogger;
import gdsc.core.ij.Utils;
import gdsc.core.logging.Logger;
import gdsc.core.match.BasePoint;
import gdsc.core.match.Coordinate;
import gdsc.core.match.MatchCalculator;
import gdsc.core.match.PointPair;
import gdsc.core.utils.ImageExtractor;
import gdsc.core.utils.Maths;
import gdsc.core.utils.NoiseEstimator.Method;
import gdsc.core.utils.RampedScore;
import gdsc.core.utils.StoredDataStatistics;
import gdsc.smlm.engine.DataFilter;
import gdsc.smlm.engine.DataFilterType;
import gdsc.smlm.engine.FitEngineConfiguration;
import gdsc.smlm.engine.FitWorker;
import gdsc.smlm.engine.QuadrantAnalysis;
import gdsc.smlm.filters.MaximaSpotFilter;
import gdsc.smlm.filters.Spot;
import gdsc.smlm.fitting.FitConfiguration;
import gdsc.smlm.fitting.FitFunction;
import gdsc.smlm.fitting.FitResult;
import gdsc.smlm.fitting.FitSolver;
import gdsc.smlm.fitting.FitStatus;
import gdsc.smlm.fitting.FunctionSolver;
import gdsc.smlm.fitting.LSEFunctionSolver;
// TODO - add support for using the chi-squared distribution to generate a q-value for the fit
//import gdsc.smlm.fitting.WLSEFunctionSolver;
//import gdsc.smlm.fitting.MLEFunctionSolver;
import gdsc.smlm.fitting.FunctionSolverType;
import gdsc.smlm.fitting.Gaussian2DFitter;
import gdsc.smlm.function.gaussian.Gaussian2DFunction;
import gdsc.smlm.ij.plugins.ResultsMatchCalculator.PeakResultPoint;
import gdsc.smlm.ij.settings.GlobalSettings;
import gdsc.smlm.ij.settings.SettingsManager;
import gdsc.smlm.ij.utils.ImageConverter;
import gdsc.smlm.results.Calibration;
import gdsc.smlm.results.MemoryPeakResults;
import gnu.trove.map.hash.TIntObjectHashMap;
import gnu.trove.procedure.TIntProcedure;
import gnu.trove.procedure.TObjectProcedure;
import ij.IJ;
import ij.ImagePlus;
import ij.ImageStack;
import ij.Macro;
import ij.Prefs;
import ij.gui.GenericDialog;
import ij.gui.Overlay;
import ij.gui.Plot;
import ij.gui.Plot2;
import ij.gui.PlotWindow;
import ij.gui.PointRoi;
import ij.plugin.PlugIn;
import ij.plugin.WindowOrganiser;
import ij.text.TextWindow;
/**
* Fits spots created by CreateData plugin.
* <p>
* Assigns results to filter candidates to determine if spots are either single or doublets or larger clusters. Outputs
* a table of the single and double fit for each spot with metrics. This can be used to determine the best settings for
* optimum doublet fitting and filtering.
*/
public class DoubletAnalysis implements PlugIn, ItemListener
{
/*
* Note: 21-Oct-2016
*
* This plugin may be obsolete now that the BenchmarkSpotFit and BenchmarkFilterAnalysis plugins
* can handle singles, multiples and doublets together. This means that the residuals threshold can be
* optimised concurrently with the fail count and the filter. It is left within the codebase in case
* it is useful in the future.
*/
private static final String TITLE = "Doublet Analysis";
private static FitConfiguration fitConfig, filterFitConfig;
private static FitEngineConfiguration config;
private static Calibration cal;
private static int lastId = 0;
static
{
cal = new Calibration();
fitConfig = new FitConfiguration();
config = new FitEngineConfiguration(fitConfig);
// Set some default fit settings here ...
// Ensure all candidates are fitted
config.setFailuresLimit(-1);
fitConfig.setSmartFilter(false);
fitConfig.setDisableSimpleFilter(false);
fitConfig.setMinPhotons(1); // Do not allow negative photons
fitConfig.setCoordinateShiftFactor(0); // Disable
fitConfig.setPrecisionThreshold(0);
fitConfig.setMinWidthFactor(0);
fitConfig.setWidthFactor(0);
fitConfig.setMinIterations(0);
fitConfig.setNoise(0);
config.setNoiseMethod(Method.QUICK_RESIDUALS_LEAST_MEAN_OF_SQUARES);
fitConfig.setBackgroundFitting(true);
fitConfig.setNotSignalFitting(false);
fitConfig.setComputeDeviations(false);
fitConfig.setComputeResiduals(true);
//
filterFitConfig = new FitConfiguration();
filterFitConfig.setSmartFilter(false);
filterFitConfig.setDisableSimpleFilter(false);
filterFitConfig.setMinPhotons(0);
filterFitConfig.setCoordinateShiftFactor(0);
filterFitConfig.setPrecisionThreshold(0);
filterFitConfig.setMinWidthFactor(0);
filterFitConfig.setWidthFactor(0);
filterFitConfig.setPrecisionUsingBackground(false);
}
private static boolean useBenchmarkSettings = false;
private static double iterationIncrease = 1;
private static boolean ignoreWithNeighbours = false;
private static boolean showOverlay = false;
private static boolean showHistograms = false;
private static boolean showResults = false;
private static boolean showJaccardPlot = true;
private static boolean useMaxResiduals = true;
private static double lowerDistance = 1;
private static double matchDistance = 1;
private static double signalFactor = 2;
private static double lowerSignalFactor = 1;
private static String[] MATCHING = { "Simple", "By residuals", "By candidate" };
private static int matching = 0;
private static boolean analysisUseBenchmarkSettings = false;
private static double analysisDriftAngle = 45;
private static double minGap = 0;
private static boolean analysisShowResults = false;
private static boolean analysisLogging = false;
private static String analysisTitle = "";
private static boolean saveTemplate = false;
private static String templateFilename = "";
static
{
String currentUsersHomeDir = System.getProperty("user.home");
templateFilename = currentUsersHomeDir + File.separator + "gdsc.smlm" + File.separator + "template";
}
private static String[] SELECTION_CRITERIA = { "R2", "AIC", "BIC", "ML AIC", "ML BIC" };
private static int selectionCriteria = 4;
private static TextWindow summaryTable = null, resultsTable = null, analysisTable = null;
private static ArrayList<DoubletResult> doubletResults;
private ResidualsScore residualsScore;
private static ResidualsScore _residualsScoreMax;
private static ResidualsScore _residualsScoreAv;
private static int numberOfMolecules;
private static String analysisPrefix;
private ImagePlus imp;
private MemoryPeakResults results;
private CreateData.SimulationParameters simulationParameters;
private TextField textInitialPeakStdDev0;
private Choice textDataFilterType;
private Choice textDataFilter;
private TextField textSmooth;
private TextField textSearch;
private TextField textBorder;
private TextField textFitting;
private Choice textFitSolver;
private Choice textFitFunction;
private TextField textMatchDistance;
private TextField textLowerDistance;
private TextField textSignalFactor;
private TextField textLowerFactor;
private Checkbox cbSmartFilter;
private TextField textCoordinateShiftFactor;
private TextField textSignalStrength;
private TextField textMinPhotons;
private TextField textPrecisionThreshold;
private Checkbox cbLocalBackground;
private TextField textMinWidthFactor;
private TextField textWidthFactor;
private AtomicInteger ignored = new AtomicInteger(0);
private static final String[] NAMES = new String[] { "Candidate:N results in candidate",
"Assigned Result:N results in assigned spot", "Singles:Neighbours", "Doublets:Neighbours",
"Multiples:Neighbours", "Singles:Almost", "Doublets:Almost", "Multiples:Almost"
};
private static final String[] NAMES2 = { "Score n=1", "Score n=2", "Score n=N", "Iter n=1", "Eval n=1", "Iter n>1",
"Eval n>1" };
private static boolean[] displayHistograms = new boolean[NAMES.length + NAMES2.length];
static
{
for (int i = 0; i < displayHistograms.length; i++)
displayHistograms[i] = true;
}
private WindowOrganiser windowOrganiser = new WindowOrganiser();
private class ResidualsScore
{
final double[] residuals, jaccard, recall, precision;
final int maxJaccardIndex;
double[] bestResiduals = new double[3];
public ResidualsScore(double[] residuals, double[] jaccard, double[] recall, double[] precision,
int maxJaccardIndex)
{
this.residuals = residuals;
this.jaccard = jaccard;
this.recall = recall;
this.precision = precision;
this.maxJaccardIndex = maxJaccardIndex;
}
}
/**
* Allows plotting the bonus from fitting all spots at a given residuals threshold
*/
public class DoubletBonus implements Comparable<DoubletBonus>
{
final double rMax, rAv;
final double tp, fp;
public double residuals;
/**
* Instantiates a new doublet bonus.
*
* @param rMax
* the maximum residuals score
* @param rAv
* the average residuals score
* @param tp
* the additional true positives if this was accepted as a doublet
* @param fp
* the additional false positives if this was accepted as a doublet
*/
public DoubletBonus(double rMax, double rAv, double tp, double fp)
{
this.rMax = rMax;
this.rAv = rAv;
this.residuals = rMax;
this.tp = tp;
this.fp = fp;
}
/*
* (non-Javadoc)
*
* @see java.lang.Comparable#compareTo(java.lang.Object)
*/
public int compareTo(DoubletBonus that)
{
if (this.residuals < that.residuals)
return -1;
if (this.residuals > that.residuals)
return 1;
return 0;
}
public void setScore(boolean useMax)
{
this.residuals = (useMax) ? rMax : rAv;
}
}
private class ResultCoordinate extends BasePoint implements Comparable<ResultCoordinate>
{
final DoubletResult result;
final int id;
public ResultCoordinate(DoubletResult result, int id, double x, double y)
{
// Add the 0.5 pixel offset
super((float) (x + 0.5), (float) (y + 0.5));
this.result = result;
this.id = id;
}
/*
* (non-Javadoc)
*
* @see java.lang.Comparable#compareTo(java.lang.Object)
*/
public int compareTo(ResultCoordinate that)
{
//if (this.result.spot.intensity > that.result.spot.intensity)
// return -1;
//if (this.result.spot.intensity < that.result.spot.intensity)
// return 1;
//return 0;
return this.result.spotIndex - that.result.spotIndex;
}
}
/**
* Stores results from single and doublet fitting.
*/
public class DoubletResult implements Comparable<DoubletResult>
{
final int frame;
final float noise;
final Spot spot;
final int n, c, neighbours, almostNeighbours, spotIndex;
FitResult fitResult1 = null;
FitResult fitResult2 = null;
double sumOfSquares1, sumOfSquares2;
double r1, r2;
double value1, value2;
double score1, score2;
double aic1, aic2, bic1, bic2;
double maic1, maic2, mbic1, mbic2;
double[] xshift = new double[2];
double[] yshift = new double[2];
double[] a = new double[2];
double gap;
int iter1, iter2, eval1, eval2;
boolean good1, good2, valid, valid2;
double tp1, fp1, tp2a, fp2a, tp2b, fp2b;
public DoubletResult(int frame, float noise, Spot spot, int n, int neighbours, int almostNeighbours,
int spotIndex)
{
this.frame = frame;
this.noise = noise;
this.spot = spot;
this.n = n;
this.c = DoubletAnalysis.getClass(n);
this.neighbours = neighbours;
this.almostNeighbours = almostNeighbours;
this.spotIndex = spotIndex;
this.tp1 = 0;
this.fp1 = 1;
this.tp2a = 0;
this.fp2a = 1;
this.tp2b = 0;
this.fp2b = 1;
}
public void addTP1(double score)
{
//if (score > 1)
// System.out.printf("Bad score %f\n", score);
if (tp1 != 0)
System.out.printf("Double counting: %f\n", score);
tp1 += score;
fp1 -= score;
}
public void addTP2(double score, int id)
{
//if (score > 1)
// System.out.printf("Bad score %f\n", score);
if (id == 0)
{
tp2a += score;
fp2a -= score;
}
else
{
tp2b += score;
fp2b -= score;
}
}
public double getMaxScore()
{
return (score1 > score2) ? score1 : score2;
}
public double getAvScore()
{
return (score1 + score2) * 0.5;
}
/*
* (non-Javadoc)
*
* @see java.lang.Comparable#compareTo(java.lang.Object)
*/
public int compareTo(DoubletResult that)
{
// Makes the resutls easy to find in the table
int r = this.frame - that.frame;
if (r != 0)
return r;
r = this.spot.x - that.spot.x;
if (r != 0)
return r;
return this.spot.y - that.spot.y;
//if (this.spot.intensity > that.spot.intensity)
// return -1;
//if (this.spot.intensity < that.spot.intensity)
// return 1;
//return 0;
}
}
/**
* Used to allow multi-threading of the fitting method.
*/
private class Worker implements Runnable
{
volatile boolean finished = false;
final BlockingQueue<Integer> jobs;
final ImageStack stack;
final TIntObjectHashMap<ArrayList<Coordinate>> actualCoordinates;
final int fitting;
final FitConfiguration fitConfig;
final MaximaSpotFilter spotFilter;
final Gaussian2DFitter gf;
final boolean relativeIntensity;
final double limit;
final int[] spotHistogram, resultHistogram;
final int[][] neighbourHistogram;
final int[][] almostNeighbourHistogram;
final Overlay o;
double[] region = null;
float[] data = null;
ArrayList<DoubletResult> results = new ArrayList<DoubletResult>();
int daic = 0, dbic = 0, cic = 0;
RampedScore rampedScore, signalScore = null;
/**
* Instantiates a new worker.
*
* @param jobs
* the jobs
* @param stack
* the stack
* @param actualCoordinates
* the actual coordinates
* @param fitConfig
* the fit config
* @param maxCount
* the max count
* @param o
* the o
*/
public Worker(BlockingQueue<Integer> jobs, ImageStack stack,
TIntObjectHashMap<ArrayList<Coordinate>> actualCoordinates, FitConfiguration fitConfig, Overlay o)
{
this.jobs = jobs;
this.stack = stack;
this.actualCoordinates = actualCoordinates;
this.fitConfig = fitConfig.clone();
this.gf = new Gaussian2DFitter(this.fitConfig);
this.spotFilter = config.createSpotFilter(true);
this.relativeIntensity = !spotFilter.isAbsoluteIntensity();
fitting = config.getRelativeFitting();
// Fit window is 2*fitting+1. The distance limit is thus 0.5 pixel higher than fitting.
limit = fitting + 0.5;
spotHistogram = new int[20];
resultHistogram = new int[spotHistogram.length];
neighbourHistogram = new int[3][spotHistogram.length];
almostNeighbourHistogram = new int[3][spotHistogram.length];
this.o = o;
rampedScore = new RampedScore(lowerDistance, matchDistance);
if (signalFactor > 0)
signalScore = new RampedScore(lowerSignalFactor, signalFactor);
}
/*
* (non-Javadoc)
*
* @see java.lang.Runnable#run()
*/
public void run()
{
try
{
while (true)
{
Integer job = jobs.take();
if (job == null || job.intValue() < 0)
break;
if (!finished)
// Only run if not finished to allow queue to be emptied
run(job.intValue());
}
}
catch (InterruptedException e)
{
System.out.println(e.toString());
//throw new RuntimeException(e);
}
finally
{
finished = true;
}
}
private void run(int frame)
{
if (Utils.isInterrupted())
{
finished = true;
return;
}
showProgress();
Coordinate[] actual = ResultsMatchCalculator.getCoordinates(actualCoordinates, frame);
ArrayList<DoubletResult> frameResults = new ArrayList<DoubletResult>(actual.length);
// Extract the data
final int maxx = stack.getWidth();
final int maxy = stack.getHeight();
data = ImageConverter.getData(stack.getPixels(frame), maxx, maxy, null, data);
// Smooth the image and identify spots with a filter
Spot[] spots = spotFilter.rank(data, maxx, maxy);
// Match the each actual result to the closest filter candidate.
// The match must be within the fit window used during fitting, i.e. could the actual
// result be fit using this candidate.
int[] matches = new int[actual.length];
for (int i = 0; i < actual.length; i++)
{
double dmin = Double.POSITIVE_INFINITY;
int match = -1;
// Get the coordinates, offset to allow for 0.5 to be the centre of the pixel
double x = actual[i].getX() - 0.5;
double y = actual[i].getY() - 0.5;
for (int j = 0; j < spots.length; j++)
{
double dx = Math.abs(x - spots[j].x);
double dy = Math.abs(y - spots[j].y);
if (dx < limit && dy < limit)
{
final double d2 = dx * dx + dy * dy;
if (dmin > d2)
{
dmin = d2;
match = j;
}
}
}
matches[i] = match;
}
ImageExtractor ie = null;
float estimatedBackground = 0;
float noise = 0;
// Identify single and doublets (and other)
int singles = 0, doublets = 0, multiples = 0, total = 0, ignored = 0;
int[] spotMatchCount = new int[spots.length];
int[] neighbourIndices = new int[spots.length];
for (int i = 0; i < actual.length; i++)
{
if (matches[i] != -1)
{
// Count all matches
int n = 0;
final int j = matches[i];
for (int ii = i; ii < matches.length; ii++)
{
if (matches[ii] == j)
{
n++;
// Reset to avoid double counting
matches[ii] = -1;
}
}
switch (n)
{
//@formatter:off
case 1: singles++; break;
case 2: doublets++; break;
default: multiples++;
//@formatter:on
}
// Initialise for fitting on first match
if (ie == null)
{
ie = new ImageExtractor(data, maxx, maxy);
estimatedBackground = estimateBackground(maxx, maxy);
noise = FitWorker.estimateNoise(data, maxx, maxy, config.getNoiseMethod());
}
final Spot spot = spots[j];
final Rectangle regionBounds = ie.getBoxRegionBounds(spot.x, spot.y, fitting);
// Count the number of candidates within the fitting window
// that are potential neighbours,
// i.e. will fit neighbours be used?
// It does not matter if the neighbours have a match to a result
// or not, just that they are present for multiple peak fitting
final int xmin = regionBounds.x;
final int xmax = xmin + regionBounds.width - 1;
final int ymin = regionBounds.y;
final int ymax = ymin + regionBounds.height - 1;
final int xmin2 = xmin - fitting;
final int xmax2 = xmax + fitting;
final int ymin2 = ymin - fitting;
final int ymax2 = ymax + fitting;
final float heightThreshold;
float background = estimatedBackground;
if (spot.intensity < background)
heightThreshold = spot.intensity;
else
heightThreshold = (float) ((spot.intensity - background) *
config.getNeighbourHeightThreshold() + background);
int neighbourCount = 0;
int almostNeighbourCount = 0;
for (int jj = 0; jj < spots.length; jj++)
{
if (j == jj)
continue;
if (spots[jj].x < xmin2 || spots[jj].x > xmax2 || spots[jj].y < ymin2 || spots[jj].y > ymax2)
continue;
if (spots[jj].x < xmin || spots[jj].x > xmax || spots[jj].y < ymin || spots[jj].y > ymax ||
spots[jj].intensity < heightThreshold)
{
almostNeighbourCount++;
}
else
{
neighbourIndices[neighbourCount++] = jj;
}
}
// Optionally ignore results with neighbours
if (ignoreWithNeighbours && neighbourCount != 0)
{
// TODO - Should we now remove these actual results from the actual[] array.
// This removes them from any scoring of fit results.
// For now leave them in as all spot candidates around them are going to be ignored
// so there should not be any fit results close by.
ignored += n;
continue;
}
// Store the number of actual results that match to a spot
addToHistogram(spotHistogram, n, 1);
// Store the number of results that match to a spot with n results
addToHistogram(resultHistogram, n, n);
total += n;
spotMatchCount[j] = n;
final int c = DoubletAnalysis.getClass(spotMatchCount[j]);
addToHistogram(neighbourHistogram[c], neighbourCount, 1);
addToHistogram(almostNeighbourHistogram[c], almostNeighbourCount, 1);
// TODO - Fit singles with neighbours?
// Currently this will only explore how to benchmark the fitting of medium
// density data with singles and a few doublets with no neighbours. This
// is fine for low density PALM data but not for high density STORM data.
// Fit the candidates (as per the FitWorker logic)
// (Fit even multiple since this is what the FitWorker will do)
region = ie.crop(regionBounds, region);
boolean[] amplitudeEstimate = new boolean[1];
float signal = 0;
double sum = 0;
final int width = regionBounds.width;
final int height = regionBounds.height;
final int size = width * height;
for (int k = size; k-- > 0;)
sum += region[k];
signal = (float) (sum - background * size);
if (signal <= 0)
{
amplitudeEstimate[0] = true;
signal = spot.intensity - ((relativeIntensity) ? 0 : background);
if (signal < 0)
{
signal += background;
background = 0;
}
}
final double[] params = new double[] { background, signal, 0, spot.x - regionBounds.x,
spot.y - regionBounds.y, 0, 0 };
final DoubletResult result = new DoubletResult(frame, noise, spot, n, neighbourCount,
almostNeighbourCount, j);
result.fitResult1 = gf.fit(region, width, height, 1, params, amplitudeEstimate);
FunctionSolver f1 = gf.getFunctionSolver();
result.iter1 = f1.getIterations();
result.eval1 = f1.getEvaluations();
// For now only process downstream if the fit was reasonable. This allows a good attempt at doublet fitting.
result.good1 = goodFit(result.fitResult1, width, height) == 2;
if (result.good1)
{
result.sumOfSquares1 = (f1.getType() == FunctionSolverType.LSE)
? ((LSEFunctionSolver) f1).getTotalSumOfSquares() : 0;
result.value1 = gf.getValue();
// Compute residuals and fit as a doublet
final double[] fitParams = result.fitResult1.getParameters();
final int cx = (int) Math.round(fitParams[Gaussian2DFunction.X_POSITION]);
final int cy = (int) Math.round(fitParams[Gaussian2DFunction.Y_POSITION]);
final double[] residuals = gf.getResiduals();
QuadrantAnalysis qa = new QuadrantAnalysis();
// TODO - Also perform quadrant analysis on a new region centred around
// the fit centre...
if (qa.quadrantAnalysis(residuals, width, height, cx, cy) && qa.computeDoubletCentres(width,
height, cx, cy, fitParams[Gaussian2DFunction.X_SD], fitParams[Gaussian2DFunction.Y_SD]))
{
result.score1 = qa.score1;
result.score2 = qa.score2;
// -+-+-
// Estimate params using the single fitted peak
// -+-+-
final double[] doubletParams = new double[1 + 2 * 6];
doubletParams[Gaussian2DFunction.BACKGROUND] = fitParams[Gaussian2DFunction.BACKGROUND];
doubletParams[Gaussian2DFunction.SIGNAL] = fitParams[Gaussian2DFunction.SIGNAL] * 0.5;
doubletParams[Gaussian2DFunction.X_POSITION] = (float) (qa.x1 - 0.5);
doubletParams[Gaussian2DFunction.Y_POSITION] = (float) (qa.y1 - 0.5);
doubletParams[6 + Gaussian2DFunction.SIGNAL] = params[Gaussian2DFunction.SIGNAL] * 0.5;
doubletParams[6 + Gaussian2DFunction.X_POSITION] = (float) (qa.x2 - 0.5);
doubletParams[6 + Gaussian2DFunction.Y_POSITION] = (float) (qa.y2 - 0.5);
// -+-+-
// Increase the iterations level then reset afterwards.
final int maxIterations = fitConfig.getMaxIterations();
final int maxEvaluations = fitConfig.getMaxFunctionEvaluations();
fitConfig.setMaxIterations((int) (maxIterations *
FitWorker.ITERATION_INCREASE_FOR_DOUBLETS * iterationIncrease));
fitConfig.setMaxFunctionEvaluations((int) (maxEvaluations *
FitWorker.EVALUATION_INCREASE_FOR_DOUBLETS * iterationIncrease));
gf.setComputeResiduals(false);
result.fitResult2 = gf.fit(region, width, height, 2, doubletParams, new boolean[2]);
gf.setComputeResiduals(true);
fitConfig.setMaxIterations(maxIterations);
fitConfig.setMaxFunctionEvaluations(maxEvaluations);
FunctionSolver f2 = gf.getFunctionSolver();
result.iter2 = f2.getIterations();
result.eval2 = f2.getEvaluations();
int r2 = goodFit2(result.fitResult2, width, height);
// Store all results if we made a fit, even if the fit was not good
if (r2 != 0)
{
result.good2 = r2 == 2;
result.sumOfSquares2 = (f2.getType() == FunctionSolverType.LSE)
? ((LSEFunctionSolver) f2).getTotalSumOfSquares() : 0;
result.value2 = gf.getValue();
final int length = width * height;
result.aic1 = Maths.getAkaikeInformationCriterionFromResiduals(result.sumOfSquares1,
length, result.fitResult1.getNumberOfFittedParameters());
result.aic2 = Maths.getAkaikeInformationCriterionFromResiduals(result.sumOfSquares2,
length, result.fitResult2.getNumberOfFittedParameters());
result.bic1 = Maths.getBayesianInformationCriterionFromResiduals(result.sumOfSquares1,
length, result.fitResult1.getNumberOfFittedParameters());
result.bic2 = Maths.getBayesianInformationCriterionFromResiduals(result.sumOfSquares2,
length, result.fitResult2.getNumberOfFittedParameters());
if (fitConfig.getFitSolver() == FitSolver.MLE)
{
result.maic1 = Maths.getAkaikeInformationCriterion(result.value1, length,
result.fitResult1.getNumberOfFittedParameters());
result.maic2 = Maths.getAkaikeInformationCriterion(result.value2, length,
result.fitResult2.getNumberOfFittedParameters());
result.mbic1 = Maths.getBayesianInformationCriterion(result.value1, length,
result.fitResult1.getNumberOfFittedParameters());
result.mbic2 = Maths.getBayesianInformationCriterion(result.value2, length,
result.fitResult2.getNumberOfFittedParameters());
// XXX - Debugging: see if the IC computed from the residuals would make a different choice
// Disable by setting to 1
if (result.getMaxScore() > 1)
{
cic++;
if (Math.signum(result.aic1 - result.aic2) != Math
.signum(result.maic1 - result.maic2))
{
daic++;
System.out.printf(
"AIC difference with residuals [%d] %d,%d : %d %f vs %f (%.2f)\n",
frame, spot.x, spot.y, n, Math.signum(result.aic1 - result.aic2),
Math.signum(result.maic1 - result.maic2), result.getMaxScore());
}
if (Math.signum(result.bic1 - result.bic2) != Math
.signum(result.mbic1 - result.mbic2))
{
dbic++;
System.out.printf(
"BIC difference with residuals [%d] %d,%d : %d %f vs %f (%.2f)\n",
frame, spot.x, spot.y, n, Math.signum(result.bic1 - result.bic2),
Math.signum(result.mbic1 - result.mbic2), result.getMaxScore());
}
if (Double.isInfinite(result.value1) || Double.isInfinite(result.value2))
System.out.printf("oops\n", result.value1, result.value2);
}
}
else
{
result.maic1 = result.aic1;
result.maic2 = result.aic2;
result.mbic1 = result.bic1;
result.mbic2 = result.bic2;
}
if (f1.getType() == FunctionSolverType.LSE)
{
result.r1 = ((LSEFunctionSolver) f1).getAdjustedCoefficientOfDetermination();
result.r2 = ((LSEFunctionSolver) f2).getAdjustedCoefficientOfDetermination();
}
// Debugging: see if the AIC or BIC ever differ
//if (Math.signum(result.aic1 - result.aic2) != Math.signum(result.bic1 - result.bic2))
// System.out.printf("BIC difference [%d] %d,%d : %d %f vs %f (%.2f)\n", frame,
// spot.x, spot.y, n, Math.signum(result.aic1 - result.aic2),
// Math.signum(result.bic1 - result.bic2), result.getMaxScore());
final double[] newParams = result.fitResult2.getParameters();
for (int p = 0; p < 2; p++)
{
final double xShift = newParams[Gaussian2DFunction.X_POSITION + p * 6] -
params[Gaussian2DFunction.X_POSITION];
final double yShift = newParams[Gaussian2DFunction.Y_POSITION + p * 6] -
params[Gaussian2DFunction.Y_POSITION];
result.a[p] = 57.29577951 *
QuadrantAnalysis.getAngle(qa.vector, new double[] { xShift, yShift });
result.xshift[p] = xShift / limit;
result.yshift[p] = yShift / limit;
}
// Store the distance between the spots
final double dx = newParams[Gaussian2DFunction.X_POSITION] -
newParams[Gaussian2DFunction.X_POSITION + 6];
final double dy = newParams[Gaussian2DFunction.Y_POSITION] -
newParams[Gaussian2DFunction.Y_POSITION + 6];
result.gap = Math.sqrt(dx * dx + dy * dy);
}
}
}
// True results, i.e. where there was a choice between selecting fit results of single or doublet
if (result.good1 && result.good2)
{
if (result.neighbours == 0)
{
result.valid = true;
if (result.almostNeighbours == 0)
result.valid2 = true;
}
}
frameResults.add(result);
}
}
DoubletAnalysis.this.ignored.addAndGet(ignored);
//System.out.printf("Frame %d, singles=%d, doublets=%d, multi=%d\n", frame, singles, doublets, multiples);
resultHistogram[0] += actual.length - total - ignored;
addToOverlay(frame, spots, singles, doublets, multiples, spotMatchCount);
results.addAll(frameResults);
// At the end of all the fitting, assign results as true or false positive.
if (matching == 0)
{
// Simple matching based on closest distance.
// This is valid for comparing the score between residuals=1 (all single) and
// residuals=0 (all doublets), when all spot candidates are fit.
ArrayList<ResultCoordinate> f1 = new ArrayList<ResultCoordinate>();
ArrayList<ResultCoordinate> f2 = new ArrayList<ResultCoordinate>();
for (DoubletResult result : frameResults)
{
if (result.good1)
{
final Rectangle regionBounds = ie.getBoxRegionBounds(result.spot.x, result.spot.y, fitting);
double x = result.fitResult1.getParameters()[Gaussian2DFunction.X_POSITION] + regionBounds.x;
double y = result.fitResult1.getParameters()[Gaussian2DFunction.Y_POSITION] + regionBounds.y;
f1.add(new ResultCoordinate(result, -1, x, y));
if (result.good2)
{
double x2 = result.fitResult2.getParameters()[Gaussian2DFunction.X_POSITION] +
regionBounds.x;
double y2 = result.fitResult2.getParameters()[Gaussian2DFunction.Y_POSITION] +
regionBounds.y;
f2.add(new ResultCoordinate(result, 0, x2, y2));
x2 = result.fitResult2.getParameters()[Gaussian2DFunction.X_POSITION + 6] + regionBounds.x;
y2 = result.fitResult2.getParameters()[Gaussian2DFunction.Y_POSITION + 6] + regionBounds.y;
f2.add(new ResultCoordinate(result, 1, x2, y2));
}
}
}
if (f1.isEmpty())
return;
List<PointPair> pairs = new ArrayList<PointPair>();
MatchCalculator.analyseResults2D(actual, f1.toArray(new ResultCoordinate[f1.size()]), matchDistance,
null, null, null, pairs);
for (PointPair pair : pairs)
{
ResultCoordinate coord = (ResultCoordinate) pair.getPoint2();
coord.result.addTP1(getScore(pair.getXYDistance2(), coord, pair.getPoint1()));
}
if (f2.isEmpty())
return;
// Note: Computing the closest match to all doublets
// results in a simple analysis since we are comparing all doublets against all singles.
// There may be a case where a actual coordinate is matched by a bad doublet but also by a
// good doublet at a higher distance. However when we filter the results to remove bad doublets
// (low residuals, etc) this result is not scored even though it would also match the better doublet.
// This may not matter unless the density is high.
MatchCalculator.analyseResults2D(actual, f2.toArray(new Coordinate[f2.size()]), matchDistance, null,
null, null, pairs);
for (PointPair pair : pairs)
{
ResultCoordinate coord = (ResultCoordinate) pair.getPoint2();
coord.result.addTP2(getScore(pair.getXYDistance2(), coord, pair.getPoint1()), coord.id);
}
}
else if (matching == 1)
{
// Rank doublets by the residuals score.
// This is valid for comparing the score between residuals=1 (all singles)
// and the effect of altering the residuals threshold to allow more doublets.
// It is not a true effect as doublets with a higher residuals score may not be
// first spot candidates that are fit.
// Rank singles by the Candidate spot
Collections.sort(frameResults, new Comparator<DoubletResult>()
{
public int compare(DoubletResult o1, DoubletResult o2)
{
return o1.spotIndex - o2.spotIndex;
}
});
final double threshold = matchDistance * matchDistance;
final boolean[] assigned = new boolean[actual.length];
int count = 0;
int[] matched = new int[frameResults.size()];
OUTER: for (int j = 0; j < frameResults.size(); j++)
{
DoubletResult result = frameResults.get(j);
if (result.good1)
{
final Rectangle regionBounds = ie.getBoxRegionBounds(result.spot.x, result.spot.y, fitting);
float x = (float) (result.fitResult1.getParameters()[Gaussian2DFunction.X_POSITION] +
regionBounds.x);
float y = (float) (result.fitResult1.getParameters()[Gaussian2DFunction.Y_POSITION] +
regionBounds.y);
for (int i = 0; i < actual.length; i++)
{
if (assigned[i])
continue;
final double d2 = actual[i].distance2(x, y);
if (d2 <= threshold)
{
assigned[i] = true;
matched[j] = i + 1;
result.addTP1(getScore(d2, result, -1, actual[i]));
if (++count == actual.length)
break OUTER;
break;
}
}
}
}
// Rank the doublets by residuals threshold instead. 1 from the doublet
// must match the spot that it matched as a single (if still available).
// The other can match anything else...
Collections.sort(frameResults, new Comparator<DoubletResult>()
{
public int compare(DoubletResult o1, DoubletResult o2)
{
if (o1.getMaxScore() > o2.getMaxScore())
return -1;
if (o1.getMaxScore() < o2.getMaxScore())
return -1;
return 0;
}
});
count = 0;
Arrays.fill(assigned, false);
OUTER: for (int j = 0; j < frameResults.size(); j++)
{
DoubletResult result = frameResults.get(j);
if (result.good1)
{
final Rectangle regionBounds = ie.getBoxRegionBounds(result.spot.x, result.spot.y, fitting);
if (result.good2)
{
float x1 = (float) (result.fitResult2.getParameters()[Gaussian2DFunction.X_POSITION] +
regionBounds.x);
float y1 = (float) (result.fitResult2.getParameters()[Gaussian2DFunction.Y_POSITION] +
regionBounds.y);
float x2 = (float) (result.fitResult2.getParameters()[Gaussian2DFunction.X_POSITION + 6] +
regionBounds.x);
float y2 = (float) (result.fitResult2.getParameters()[Gaussian2DFunction.Y_POSITION + 6] +
regionBounds.y);
ResultCoordinate ra = new ResultCoordinate(result, 0, x1, y1);
ResultCoordinate rb = new ResultCoordinate(result, 1, x2, y2);
// Q. what did the single match?
int i = matched[j] - 1;
if (i != -1 && !assigned[i])
{
// One of the doublet pair must match this
final double d2a = ra.distanceXY2(actual[i]);
final double d2b = rb.distanceXY2(actual[i]);
if (d2a < d2b)
{
if (d2a <= threshold)
{
ra.result.addTP2(getScore(d2a, ra, actual[i]), ra.id);
if (++count == actual.length)
break OUTER;
assigned[i] = true;
ra = null;
}
}
else
{
if (d2b <= threshold)
{
rb.result.addTP2(getScore(d2b, rb, actual[i]), rb.id);
if (++count == actual.length)
break OUTER;
assigned[i] = true;
rb = null;
}
}
}
// Process the rest of the results
for (i = 0; i < actual.length; i++)
{
if (assigned[i])
continue;
if (ra == null)
{
final double d2 = rb.distanceXY2(actual[i]);
if (d2 <= threshold)
{
rb.result.addTP2(getScore(d2, rb, actual[i]), rb.id);
if (++count == actual.length)
break OUTER;
assigned[i] = true;
break;
}
}
else if (rb == null)
{
final double d2 = ra.distanceXY2(actual[i]);
if (d2 <= threshold)
{
ra.result.addTP2(getScore(d2, ra, actual[i]), ra.id);
if (++count == actual.length)
break OUTER;
assigned[i] = true;
break;
}
}
else
{
final double d2a = ra.distanceXY2(actual[i]);
final double d2b = rb.distanceXY2(actual[i]);
if (d2a < d2b)
{
if (d2a <= threshold)
{
ra.result.addTP2(getScore(d2a, ra, actual[i]), ra.id);
if (++count == actual.length)
break OUTER;
assigned[i] = true;
ra = null;
}
}
else
{
if (d2b <= threshold)
{
rb.result.addTP2(getScore(d2b, rb, actual[i]), rb.id);
if (++count == actual.length)
break OUTER;
assigned[i] = true;
rb = null;
}
}
}
}
}
}
}
}
else
{
// Matching based on spot ranking
ArrayList<ResultCoordinate> f1 = new ArrayList<ResultCoordinate>();
ArrayList<ResultCoordinate> f2 = new ArrayList<ResultCoordinate>();
for (DoubletResult result : frameResults)
{
if (result.good1)
{
final Rectangle regionBounds = ie.getBoxRegionBounds(result.spot.x, result.spot.y, fitting);
double x = result.fitResult1.getParameters()[Gaussian2DFunction.X_POSITION] + regionBounds.x;
double y = result.fitResult1.getParameters()[Gaussian2DFunction.Y_POSITION] + regionBounds.y;
f1.add(new ResultCoordinate(result, -1, x, y));
if (result.good2)
{
double x2 = result.fitResult2.getParameters()[Gaussian2DFunction.X_POSITION] +
regionBounds.x;
double y2 = result.fitResult2.getParameters()[Gaussian2DFunction.Y_POSITION] +
regionBounds.y;
f2.add(new ResultCoordinate(result, 0, x2, y2));
x2 = result.fitResult2.getParameters()[Gaussian2DFunction.X_POSITION + 6] + regionBounds.x;
y2 = result.fitResult2.getParameters()[Gaussian2DFunction.Y_POSITION + 6] + regionBounds.y;
f2.add(new ResultCoordinate(result, 1, x2, y2));
}
}
}
if (f1.isEmpty())
return;
Collections.sort(f1);
Collections.sort(f2);
// Match the singles to actual coords, rank by the Candidate spot (not distance)
final double threshold = matchDistance * matchDistance;
int count = 0;
boolean[] assigned = new boolean[actual.length];
OUTER: for (ResultCoordinate r : f1)
{
for (int i = 0; i < actual.length; i++)
{
if (assigned[i])
continue;
final double d2 = r.distanceXY2(actual[i]);
if (d2 <= threshold)
{
r.result.addTP1(getScore(d2, r, actual[i]));
if (++count == actual.length)
break OUTER;
assigned[i] = true;
break;
}
}
}
// Match the doublets to actual coords, rank by the Candidate spot (not distance)
// Process in pairs
count = 0;
Arrays.fill(assigned, false);
OUTER: for (int j = 0; j < f2.size(); j += 2)
{
ResultCoordinate ra = f2.get(j);
ResultCoordinate rb = f2.get(j + 1);
for (int i = 0; i < actual.length; i++)
{
if (assigned[i])
continue;
if (ra == null)
{
final double d2 = rb.distanceXY2(actual[i]);
if (d2 <= threshold)
{
rb.result.addTP2(getScore(d2, rb, actual[i]), rb.id);
if (++count == actual.length)
break OUTER;
assigned[i] = true;
break;
}
}
else if (rb == null)
{
final double d2 = ra.distanceXY2(actual[i]);
if (d2 <= threshold)
{
ra.result.addTP2(getScore(d2, ra, actual[i]), ra.id);
if (++count == actual.length)
break OUTER;
assigned[i] = true;
break;
}
}
else
{
final double d2a = ra.distanceXY2(actual[i]);
final double d2b = rb.distanceXY2(actual[i]);
if (d2a < d2b)
{
if (d2a <= threshold)
{
ra.result.addTP2(getScore(d2a, ra, actual[i]), ra.id);
if (++count == actual.length)
break OUTER;
assigned[i] = true;
ra = null;
}
}
else
{
if (d2b <= threshold)
{
rb.result.addTP2(getScore(d2b, rb, actual[i]), rb.id);
if (++count == actual.length)
break OUTER;
assigned[i] = true;
rb = null;
}
}
}
}
}
}
}
private void addToHistogram(int[] h, int i, int n)
{
if (h.length <= i)
{
final int newLength = (int) Math.ceil(i * 1.5);
h = Arrays.copyOf(h, newLength);
}
h[i] += n;
}
private double getScore(double d2, ResultCoordinate resultCoord, Coordinate coord)
{
return getScore(d2, resultCoord.result, resultCoord.id, coord);
}
private double getScore(double d2, DoubletResult result, int id, Coordinate coord)
{
double matchScore = rampedScore.scoreAndFlatten(d2, 256);
if (signalScore != null)
{
PeakResultPoint p = (PeakResultPoint) coord;
double s1 = p.peakResult.getSignal();
double s2;
switch (id)
{
case -1:
s2 = result.fitResult1.getParameters()[Gaussian2DFunction.SIGNAL];
break;
case 1:
s2 = result.fitResult2.getParameters()[Gaussian2DFunction.SIGNAL + 6];
break;
case 0:
default:
s2 = result.fitResult2.getParameters()[Gaussian2DFunction.SIGNAL];
}
double rsf = s1 / s2;
double sf = Math.abs((rsf < 1) ? 1 - 1 / rsf : rsf - 1);
final double fScore = signalScore.scoreAndFlatten(sf, 256);
matchScore = RampedScore.flatten(matchScore * fScore, 256);
}
return matchScore;
}
private int goodFit(FitResult fitResult, final int width, final int height)
{
if (fitResult == null)
return 0;
final double[] params = fitResult.getParameters();
if (params == null)
return 0;
switch (fitResult.getStatus())
{
case OK:
break;
// The following happen when we are doing validation
case INSUFFICIENT_SIGNAL:
return 1; // Failed validation
case WIDTH_DIVERGED:
case INSUFFICIENT_PRECISION:
case COORDINATES_MOVED:
break; // Ignore these and check again
default:
return 0;
}
// Do some simple validation
// Check if centre is within the region
final double border = FastMath.min(width, height) / 4.0;
if ((params[Gaussian2DFunction.X_POSITION] < border ||
params[Gaussian2DFunction.X_POSITION] > width - border) ||
params[Gaussian2DFunction.Y_POSITION] < border ||
params[Gaussian2DFunction.Y_POSITION] > height - border)
return 1;
// Check the width is reasonable
final double regionSize = FastMath.max(width, height) * 0.5;
if (params[Gaussian2DFunction.X_SD] < 0 || params[Gaussian2DFunction.X_SD] > regionSize ||
params[Gaussian2DFunction.Y_SD] < 0 || params[Gaussian2DFunction.Y_SD] > regionSize)
return 1;
return 2;
}
private int goodFit2(FitResult fitResult, final int width, final int height)
{
if (fitResult == null)
return 0;
final double[] params = fitResult.getParameters();
if (params == null)
return 0;
switch (fitResult.getStatus())
{
case OK:
break;
// The following happen when we are doing validation
case INSUFFICIENT_SIGNAL:
return 1; // Failed validation
case WIDTH_DIVERGED:
case INSUFFICIENT_PRECISION:
case COORDINATES_MOVED:
break; // Ignore these and check again
default:
return 0;
}
// Do some simple validation
final double regionSize = FastMath.max(width, height) * 0.5;
for (int n = 0; n < 2; n++)
{
// Check the width is reasonable
if (params[n * 6 + Gaussian2DFunction.X_SD] < 0 ||
params[n * 6 + Gaussian2DFunction.X_SD] > regionSize ||
params[n * 6 + Gaussian2DFunction.Y_SD] < 0 ||
params[n * 6 + Gaussian2DFunction.Y_SD] > regionSize)
return 1;
// Check if centre is within the region - Border allowing fit slightly outside
final double borderx = Gaussian2DFunction.SD_TO_HWHM_FACTOR * fitConfig.getInitialPeakStdDev0();
final double bordery = Gaussian2DFunction.SD_TO_HWHM_FACTOR * fitConfig.getInitialPeakStdDev1();
if ((params[n * 6 + Gaussian2DFunction.X_POSITION] < -borderx ||
params[n * 6 + Gaussian2DFunction.X_POSITION] > width + borderx) ||
params[n * 6 + Gaussian2DFunction.Y_POSITION] < -bordery ||
params[n * 6 + Gaussian2DFunction.Y_POSITION] > height + bordery)
{
// Perhaps do a check on the quadrant?
return 1;
}
}
return 2;
}
/**
* Get an estimate of the background level using the mean of image.
*
* @param width
* the width
* @param height
* the height
* @return the float
*/
private float estimateBackground(int width, int height)
{
// Compute average of the entire image
double sum = 0;
for (int i = width * height; i-- > 0;)
sum += data[i];
return (float) sum / (width * height);
}
/**
* Adds the to overlay.
*
* @param frame
* the frame
* @param spots
* the spots
* @param singles
* the singles
* @param doublets
* the doublets
* @param multiples
* the multiples
* @param spotMatchCount
* the spot match count
*/
private void addToOverlay(int frame, Spot[] spots, int singles, int doublets, int multiples,
int[] spotMatchCount)
{
if (o != null)
{
// Create an output stack with coloured ROI overlay for each n=1, n=2, n=other
// to check that the doublets are correctly identified.
final int[] sx = new int[singles];
final int[] sy = new int[singles];
final int[] dx = new int[doublets];
final int[] dy = new int[doublets];
final int[] mx = new int[multiples];
final int[] my = new int[multiples];
final int[] count = new int[3];
final int[][] coords = new int[][] { sx, dx, mx, sy, dy, my };
final Color[] color = new Color[] { Color.red, Color.green, Color.blue };
for (int j = 0; j < spotMatchCount.length; j++)
{
final int c = DoubletAnalysis.getClass(spotMatchCount[j]);
if (c < 0)
continue;
coords[c][count[c]] = spots[j].x;
coords[c + 3][count[c]] = spots[j].y;
count[c]++;
}
for (int c = 0; c < 3; c++)
{
final PointRoi roi = new PointRoi(coords[c], coords[c + 3], count[c]);
roi.setPosition(frame);
roi.setShowLabels(false);
roi.setFillColor(color[c]);
roi.setStrokeColor(color[c]);
// Overlay uses a vector which is synchronized already
o.add(roi);
}
}
}
}
/**
* Gets the class.
*
* @param n
* the number of results that match to the spot
* @return the class (none = -1, single = 0, double = 1, multiple = 2)
*/
private static int getClass(int n)
{
if (n == 0)
return -1;
if (n == 1)
return 0;
if (n == 2)
return 1;
return 2;
}
/*
* (non-Javadoc)
*
* @see ij.plugin.PlugIn#run(java.lang.String)
*/
public void run(String arg)
{
SMLMUsageTracker.recordPlugin(this.getClass(), arg);
if ("analysis".equals(arg))
{
runAnalysis();
}
else
{
simulationParameters = CreateData.simulationParameters;
if (simulationParameters == null)
{
IJ.error(TITLE, "No simulation parameters in memory");
return;
}
imp = CreateData.getImage();
if (imp == null)
{
IJ.error(TITLE, "No simulation image");
return;
}
results = CreateData.getResults();
if (results == null)
{
IJ.error(TITLE, "No simulation results in memory");
return;
}
if (!showDialog())
return;
run();
}
}
/**
* Show dialog.
*
* @return true, if successful
*/
@SuppressWarnings("unchecked")
private boolean showDialog()
{
GenericDialog gd = new GenericDialog(TITLE);
gd.addHelp(About.HELP_URL);
final double sa = getSa();
gd.addMessage(
String.format("Fits the benchmark image created by CreateData plugin.\nPSF width = %s, adjusted = %s",
Utils.rounded(simulationParameters.s / simulationParameters.a), Utils.rounded(sa)));
// For each new benchmark width, reset the PSF width to the square pixel adjustment
if (lastId != simulationParameters.id)
{
double w = sa;
matchDistance = w * Gaussian2DFunction.SD_TO_HWHM_FACTOR;
lowerDistance = 0.5 * matchDistance;
fitConfig.setInitialPeakStdDev(w);
cal.setNmPerPixel(simulationParameters.a);
cal.setGain(simulationParameters.gain);
cal.setAmplification(simulationParameters.amplification);
cal.setExposureTime(100);
cal.setReadNoise(simulationParameters.readNoise);
cal.setBias(simulationParameters.bias);
cal.setEmCCD(simulationParameters.emCCD);
fitConfig.setGain(cal.getGain());
fitConfig.setBias(cal.getBias());
fitConfig.setReadNoise(cal.getReadNoise());
fitConfig.setAmplification(cal.getAmplification());
}
// Support for using templates
String[] templates = ConfigurationTemplate.getTemplateNames(true);
gd.addChoice("Template", templates, templates[0]);
// Allow the settings from the benchmark analysis to be used
gd.addCheckbox("Benchmark_settings", useBenchmarkSettings);
// Collect options for fitting
gd.addNumericField("Initial_StdDev", fitConfig.getInitialPeakStdDev0(), 3);
String[] filterTypes = SettingsManager.getNames((Object[]) DataFilterType.values());
gd.addChoice("Spot_filter_type", filterTypes, filterTypes[config.getDataFilterType().ordinal()]);
String[] filterNames = SettingsManager.getNames((Object[]) DataFilter.values());
gd.addChoice("Spot_filter", filterNames, filterNames[config.getDataFilter(0).ordinal()]);
gd.addSlider("Smoothing", 0, 2.5, config.getSmooth(0));
gd.addSlider("Search_width", 0.5, 2.5, config.getSearch());
gd.addSlider("Border", 0.5, 2.5, config.getBorder());
gd.addSlider("Fitting_width", 2, 4.5, config.getFitting());
String[] solverNames = SettingsManager.getNames((Object[]) FitSolver.values());
gd.addChoice("Fit_solver", solverNames, solverNames[fitConfig.getFitSolver().ordinal()]);
String[] functionNames = SettingsManager.getNames((Object[]) FitFunction.values());
gd.addChoice("Fit_function", functionNames, functionNames[fitConfig.getFitFunction().ordinal()]);
gd.addSlider("Iteration_increase", 1, 4.5, iterationIncrease);
gd.addCheckbox("Ignore_with_neighbours", ignoreWithNeighbours);
gd.addCheckbox("Show_overlay", showOverlay);
gd.addCheckbox("Show_histograms", showHistograms);
gd.addCheckbox("Show_results", showResults);
gd.addCheckbox("Show_Jaccard_Plot", showJaccardPlot);
gd.addCheckbox("Use_max_residuals", useMaxResiduals);
gd.addNumericField("Match_distance", matchDistance, 2);
gd.addNumericField("Lower_distance", lowerDistance, 2);
gd.addNumericField("Signal_factor", signalFactor, 2);
gd.addNumericField("Lower_factor", lowerSignalFactor, 2);
gd.addChoice("Matching", MATCHING, MATCHING[matching]);
// Add a mouse listener to the config file field
if (Utils.isShowGenericDialog())
{
Vector<TextField> numerics = (Vector<TextField>) gd.getNumericFields();
Vector<Choice> choices = (Vector<Choice>) gd.getChoices();
int n = 0;
int ch = 0;
choices.get(ch++).addItemListener(this);
Checkbox b = (Checkbox) gd.getCheckboxes().get(0);
b.addItemListener(this);
textInitialPeakStdDev0 = numerics.get(n++);
textDataFilterType = choices.get(ch++);
textDataFilter = choices.get(ch++);
textSmooth = numerics.get(n++);
textSearch = numerics.get(n++);
textBorder = numerics.get(n++);
textFitting = numerics.get(n++);
textFitSolver = choices.get(ch++);
textFitFunction = choices.get(ch++);
n++; // Iteration increase
textMatchDistance = numerics.get(n++);
textLowerDistance = numerics.get(n++);
textSignalFactor = numerics.get(n++);
textLowerFactor = numerics.get(n++);
}
gd.showDialog();
if (gd.wasCanceled())
return false;
// Ignore the template
gd.getNextChoice();
useBenchmarkSettings = gd.getNextBoolean();
fitConfig.setInitialPeakStdDev(gd.getNextNumber());
config.setDataFilterType(gd.getNextChoiceIndex());
config.setDataFilter(gd.getNextChoiceIndex(), Math.abs(gd.getNextNumber()), 0);
config.setSearch(gd.getNextNumber());
config.setBorder(gd.getNextNumber());
config.setFitting(gd.getNextNumber());
fitConfig.setFitSolver(gd.getNextChoiceIndex());
fitConfig.setFitFunction(gd.getNextChoiceIndex());
// Avoid stupidness. Note: We are mostly ignoring the validation result and
// checking the results for the doublets manually.
fitConfig.setMinPhotons(15); // Realistically we cannot fit lower than this
// Set the width factors to help establish bounds for bounded fitters
fitConfig.setMinWidthFactor(1.0 / 10);
fitConfig.setWidthFactor(10);
iterationIncrease = gd.getNextNumber();
ignoreWithNeighbours = gd.getNextBoolean();
showOverlay = gd.getNextBoolean();
showHistograms = gd.getNextBoolean();
showResults = gd.getNextBoolean();
showJaccardPlot = gd.getNextBoolean();
useMaxResiduals = gd.getNextBoolean();
matchDistance = Math.abs(gd.getNextNumber());
lowerDistance = Math.abs(gd.getNextNumber());
signalFactor = Math.abs(gd.getNextNumber());
lowerSignalFactor = Math.abs(gd.getNextNumber());
matching = gd.getNextChoiceIndex();
if (gd.invalidNumber())
return false;
if (lowerDistance > matchDistance)
lowerDistance = matchDistance;
if (lowerSignalFactor > signalFactor)
lowerSignalFactor = signalFactor;
if (useBenchmarkSettings)
{
if (!updateFitConfiguration(config))
return false;
}
GlobalSettings settings = new GlobalSettings();
settings.setFitEngineConfiguration(config);
settings.setCalibration(cal);
boolean configure = true;
if (useBenchmarkSettings)
{
// Only configure the fit solver if not in a macro
configure = Macro.getOptions() == null;
}
if (configure && !PeakFit.configureFitSolver(settings, null, false))
return false;
lastId = simulationParameters.id;
if (showHistograms)
{
gd = new GenericDialog(TITLE);
gd.addMessage("Select the histograms to display");
for (int i = 0; i < NAMES.length; i++)
gd.addCheckbox(NAMES[i].replace(' ', '_'), displayHistograms[i]);
for (int i = 0; i < NAMES2.length; i++)
gd.addCheckbox(NAMES2[i].replace(' ', '_'), displayHistograms[i + NAMES.length]);
gd.showDialog();
if (gd.wasCanceled())
return false;
for (int i = 0; i < displayHistograms.length; i++)
displayHistograms[i] = gd.getNextBoolean();
}
return true;
}
private boolean updateFitConfiguration(FitEngineConfiguration config)
{
// Do this first as it sets the initial SD
if (!BenchmarkSpotFit.updateConfiguration(config))
{
IJ.error(TITLE, "Unable to use the benchmark spot fit configuration");
return false;
}
cal.setNmPerPixel(simulationParameters.a);
cal.setGain(simulationParameters.gain);
cal.setAmplification(simulationParameters.amplification);
cal.setExposureTime(100);
cal.setReadNoise(simulationParameters.readNoise);
cal.setBias(simulationParameters.bias);
cal.setEmCCD(simulationParameters.emCCD);
fitConfig.setGain(cal.getGain());
fitConfig.setBias(cal.getBias());
fitConfig.setReadNoise(cal.getReadNoise());
fitConfig.setAmplification(cal.getAmplification());
if (!BenchmarkSpotFilter.updateConfiguration(config))
{
IJ.error(TITLE, "Unable to use the benchmark spot filter configuration");
return false;
}
// Make sure all spots are fit
config.setFailuresLimit(-1);
// Get the distance from the filter analysis. This ensures that we compute scores the
// same as the filter analysis
if (BenchmarkFilterAnalysis.distanceInPixels > 0)
{
matchDistance = BenchmarkFilterAnalysis.distanceInPixels;
lowerDistance = BenchmarkFilterAnalysis.lowerDistanceInPixels;
signalFactor = BenchmarkFilterAnalysis.signalFactor;
lowerSignalFactor = BenchmarkFilterAnalysis.lowerSignalFactor;
}
else
{
// Use the fit analysis distance if no filter analysis has been run
matchDistance = BenchmarkSpotFit.distanceInPixels;
lowerDistance = BenchmarkSpotFit.lowerDistanceInPixels;
signalFactor = lowerSignalFactor = BenchmarkSpotFit.signalFactor;
}
return true;
}
/**
* Gets the sa.
*
* @return the sa
*/
private double getSa()
{
final double sa = PSFCalculator.squarePixelAdjustment(simulationParameters.s, simulationParameters.a) /
simulationParameters.a;
return sa;
}
/** The total progress. */
int progress, stepProgress, totalProgress;
/**
* Show progress.
*/
private synchronized void showProgress()
{
if (progress % stepProgress == 0)
{
if (Utils.showStatus("Frame: " + progress + " / " + totalProgress))
IJ.showProgress(progress, totalProgress);
}
progress++;
}
/**
* Run.
*/
private void run()
{
doubletResults = null;
final ImageStack stack = imp.getImageStack();
// Get the coordinates per frame
TIntObjectHashMap<ArrayList<Coordinate>> actualCoordinates = ResultsMatchCalculator
.getCoordinates(results.getResults(), false);
final long[] sumCount = new long[1];
actualCoordinates.forEachValue(new TObjectProcedure<ArrayList<Coordinate>>()
{
public boolean execute(ArrayList<Coordinate> list)
{
sumCount[0] += list.size();
return true;
}
});
final double density = 1e6 * sumCount[0] / (simulationParameters.a * simulationParameters.a *
results.getBounds().getWidth() * results.getBounds().getHeight() * actualCoordinates.size());
// Create a pool of workers
final int nThreads = Prefs.getThreads();
final BlockingQueue<Integer> jobs = new ArrayBlockingQueue<Integer>(nThreads * 2);
List<Worker> workers = new LinkedList<Worker>();
List<Thread> threads = new LinkedList<Thread>();
Overlay overlay = (showOverlay) ? new Overlay() : null;
for (int i = 0; i < nThreads; i++)
{
Worker worker = new Worker(jobs, stack, actualCoordinates, fitConfig, overlay);
Thread t = new Thread(worker);
workers.add(worker);
threads.add(t);
t.start();
}
// Fit the frames
long runTime = System.nanoTime();
totalProgress = actualCoordinates.size();
stepProgress = Utils.getProgressInterval(totalProgress);
progress = 0;
actualCoordinates.forEachKey(new TIntProcedure()
{
public boolean execute(int frame)
{
put(jobs, frame);
return true;
}
});
// Finish all the worker threads by passing in a null job
for (int i = 0; i < threads.size(); i++)
{
put(jobs, -1);
}
// Wait for all to finish
for (int i = 0; i < threads.size(); i++)
{
try
{
threads.get(i).join();
}
catch (InterruptedException e)
{
e.printStackTrace();
}
}
threads.clear();
threads = null;
IJ.showProgress(1);
IJ.showStatus("Collecting results ...");
runTime = System.nanoTime() - runTime;
// Collect the results
int cic = 0, daic = 0, dbic = 0;
ArrayList<DoubletResult> results = null;
int maxH = 0, maxH2 = 0, maxH3 = 0;
for (Worker worker : workers)
{
if (results == null)
results = worker.results;
else
results.addAll(worker.results);
cic += worker.cic;
daic += worker.daic;
dbic += worker.dbic;
maxH = Maths.max(maxH, worker.spotHistogram.length);
for (int k = 0; k < 3; k++)
{
maxH2 = Maths.max(maxH2, worker.neighbourHistogram[k].length);
maxH3 = Maths.max(maxH3, worker.almostNeighbourHistogram[k].length);
}
}
if (cic > 0)
System.out.printf("Difference AIC %d, BIC %d, Total %d\n", daic, dbic, cic);
if (showHistograms)
{
double[] spotHistogram = new double[maxH];
double[] resultHistogram = new double[maxH];
double[][] neighbourHistogram = new double[3][maxH2];
double[][] almostNeighbourHistogram = new double[3][maxH3];
for (Worker worker : workers)
{
final int[] h1a = worker.spotHistogram;
final int[] h1b = worker.resultHistogram;
for (int j = 0; j < h1a.length; j++)
{
spotHistogram[j] += h1a[j];
resultHistogram[j] += h1b[j];
}
final int[][] h2 = worker.neighbourHistogram;
final int[][] h3 = worker.almostNeighbourHistogram;
for (int k = 0; k < 3; k++)
{
for (int j = 0; j < h2[k].length; j++)
neighbourHistogram[k][j] += h2[k][j];
for (int j = 0; j < h3[k].length; j++)
almostNeighbourHistogram[k][j] += h3[k][j];
}
}
showHistogram(0, spotHistogram);
showHistogram(1, resultHistogram);
showHistogram(2, neighbourHistogram[0]);
showHistogram(3, neighbourHistogram[1]);
showHistogram(4, neighbourHistogram[2]);
showHistogram(5, almostNeighbourHistogram[0]);
showHistogram(6, almostNeighbourHistogram[1]);
showHistogram(7, almostNeighbourHistogram[2]);
}
workers.clear();
workers = null;
if (overlay != null)
imp.setOverlay(overlay);
MemoryPeakResults.freeMemory();
Collections.sort(results);
summariseResults(results, density, runTime);
windowOrganiser.tile();
IJ.showStatus("");
}
/**
* Show histogram.
*
* @param i
* the i
* @param histogram
* the spot histogram
*/
private void showHistogram(int i, double[] histogram)
{
if (!displayHistograms[i])
return;
// Truncate to correct size
for (int j = histogram.length; j-- > 0;)
if (histogram[j] != 0)
{
histogram = Arrays.copyOf(histogram, j + 1);
break;
}
String[] labels = NAMES[i].split(":");
Plot2 plot = new Plot2(labels[0], labels[1], "Count");
double max = Maths.max(histogram);
plot.setLimits(0, histogram.length, 0, max * 1.05);
plot.addPoints(Utils.newArray(histogram.length, 0, 1.0), histogram, Plot2.BAR);
PlotWindow pw = Utils.display(labels[0], plot);
if (Utils.isNewWindow())
windowOrganiser.add(pw.getImagePlus().getID());
}
/**
* Put.
*
* @param jobs
* the jobs
* @param i
* the i
*/
private void put(BlockingQueue<Integer> jobs, int i)
{
try
{
jobs.put(i);
}
catch (InterruptedException e)
{
throw new RuntimeException("Unexpected interruption", e);
}
}
/**
* Summarise results.
*
* @param results
* the results
* @param runTime
* @param density2
*/
private void summariseResults(ArrayList<DoubletResult> results, double density, long runTime)
{
// Store results in memory for later analysis
doubletResults = results;
// If we are only assessing results with no neighbour candidates
// TODO - Count the number of actual results that have no neighbours
numberOfMolecules = this.results.size() - ignored.get();
// Store details we want in the analysis table
StringBuilder sb = new StringBuilder();
sb.append(Utils.rounded(density)).append("\t");
sb.append(Utils.rounded(getSa())).append("\t");
sb.append(config.getRelativeFitting()).append("\t");
sb.append(fitConfig.getFitFunction().toString());
sb.append(":").append(PeakFit.getSolverName(fitConfig));
if (fitConfig.getFitSolver() == FitSolver.MLE && fitConfig.isModelCamera())
{
sb.append(":Camera\t");
// Add details of the noise model for the MLE
sb.append("EM=").append(fitConfig.isEmCCD());
sb.append(":A=").append(Utils.rounded(fitConfig.getAmplification()));
sb.append(":N=").append(Utils.rounded(fitConfig.getReadNoise()));
sb.append("\t");
}
else
sb.append("\t\t");
analysisPrefix = sb.toString();
// -=-=-=-=-
showResults(results, showResults);
createSummaryTable();
sb.setLength(0);
final int n = countN(results);
// Create the benchmark settings and the fitting settings
sb.append(numberOfMolecules).append("\t");
sb.append(n).append("\t");
sb.append(Utils.rounded(density)).append("\t");
sb.append(Utils.rounded(simulationParameters.minSignal)).append("\t");
sb.append(Utils.rounded(simulationParameters.maxSignal)).append("\t");
sb.append(Utils.rounded(simulationParameters.signalPerFrame)).append("\t");
sb.append(Utils.rounded(simulationParameters.s)).append("\t");
sb.append(Utils.rounded(simulationParameters.a)).append("\t");
sb.append(Utils.rounded(getSa() * simulationParameters.a)).append("\t");
sb.append(Utils.rounded(simulationParameters.gain)).append("\t");
sb.append(Utils.rounded(simulationParameters.readNoise)).append("\t");
sb.append(Utils.rounded(simulationParameters.b)).append("\t");
// Compute the noise
double noise = Math
.sqrt((simulationParameters.b * ((simulationParameters.emCCD) ? 2 : 1)) / simulationParameters.gain +
simulationParameters.readNoise * simulationParameters.readNoise);
sb.append(Utils.rounded(noise)).append("\t");
sb.append(Utils.rounded(simulationParameters.signalPerFrame / noise)).append("\t");
sb.append(config.getRelativeFitting()).append("\t");
sb.append(fitConfig.getFitFunction().toString());
sb.append(":").append(PeakFit.getSolverName(fitConfig));
if (fitConfig.getFitSolver() == FitSolver.MLE && fitConfig.isModelCamera())
{
sb.append(":Camera\t");
// Add details of the noise model for the MLE
sb.append("EM=").append(fitConfig.isEmCCD());
sb.append(":A=").append(Utils.rounded(fitConfig.getAmplification()));
sb.append(":N=").append(Utils.rounded(fitConfig.getReadNoise()));
sb.append("\t");
}
else
sb.append("\t\t");
// Now output the actual results ...
// Show histograms as cumulative to avoid problems with bin width
// Residuals scores
// Iterations and evaluations where fit was OK
StoredDataStatistics[] stats = new StoredDataStatistics[NAMES2.length];
for (int i = 0; i < stats.length; i++)
stats[i] = new StoredDataStatistics();
// For Jaccard scoring we need to count the score with no residuals threshold,
// i.e. Accumulate the score accepting all doublets that were fit
double tp = 0;
double fp = 0;
double bestTp = 0, bestFp = 0;
ArrayList<DoubletBonus> data = new ArrayList<DoubletBonus>(results.size());
for (DoubletResult result : results)
{
final double score = result.getMaxScore();
// Filter the singles that would be accepted
if (result.good1)
{
// Filter the doublets that would be accepted
if (result.good2)
{
final double tp2 = result.tp2a + result.tp2b;
final double fp2 = result.fp2a + result.fp2b;
tp += tp2;
fp += fp2;
if (result.tp2a > 0.5)
{
bestTp += result.tp2a;
bestFp += result.fp2a;
}
if (result.tp2b > 0.5)
{
bestTp += result.tp2b;
bestFp += result.fp2b;
}
// Store this as a doublet bonus
data.add(new DoubletBonus(score, result.getAvScore(), tp2 - result.tp1, fp2 - result.fp1));
}
else
{
// No doublet fit so this will always be the single fit result
tp += result.tp1;
fp += result.fp1;
if (result.tp1 > 0.5)
{
bestTp += result.tp1;
bestFp += result.fp1;
}
}
}
// Build statistics
final int c = result.c;
// True results, i.e. where there was a choice between single or doublet
if (result.valid)
{
stats[c].add(score);
}
// Of those where the fit was good, summarise the iterations and evaluations
if (result.good1)
{
stats[3].add(result.iter1);
stats[4].add(result.eval1);
// Summarise only those which are a valid doublet. We do not really care
// about the iteration increase for singles that are not doublets.
if (c != 0 && result.good2)
{
stats[5].add(result.iter2);
stats[6].add(result.eval2);
}
}
}
// Debug the counts
// double tpSingle = 0;
// double fpSingle = 0;
// double tpDoublet = 0;
// double fpDoublet = 0;
// int nSingle = 0, nDoublet = 0;
// for (DoubletResult result : results)
// {
// if (result.good1)
// {
// if (result.good2)
// {
// tpDoublet += result.tp2;
// fpDoublet += result.fp2;
// nDoublet++;
// }
// tpSingle += result.tp1;
// fpSingle += result.fp1;
// nSingle++;
// }
// }
// System.out.printf("Single %.1f,%.1f (%d) : Doublet %.1f,%.1f (%d)\n", tpSingle, fpSingle, nSingle, tpDoublet, fpDoublet, nDoublet*2);
// Summarise score for true results
Percentile p = new Percentile(99);
for (int c = 0; c < stats.length; c++)
{
double[] values = stats[c].getValues();
// Sorting is need for the percentile and the cumulative histogram so do it once
Arrays.sort(values);
sb.append(Utils.rounded(stats[c].getMean())).append("+/-")
.append(Utils.rounded(stats[c].getStandardDeviation())).append(" (").append(stats[c].getN())
.append(") ").append(Utils.rounded(p.evaluate(values))).append('\t');
if (showHistograms && displayHistograms[c + NAMES.length])
showHistogram(values, NAMES2[c]);
}
sb.append(MATCHING[matching]).append('\t');
// Plot a graph of the additional results we would fit at all score thresholds.
// This assumes we just pick the the doublet if we fit it (NO FILTERING at all!)
// Initialise the score for residuals 0
// Store this as it serves as a baseline for the filtering analysis
computeScores(data, tp, fp, numberOfMolecules, true);
_residualsScoreMax = this.residualsScore;
computeScores(data, tp, fp, numberOfMolecules, false);
_residualsScoreAv = this.residualsScore;
residualsScore = (useMaxResiduals) ? _residualsScoreMax : _residualsScoreAv;
if (showJaccardPlot)
plotJaccard(residualsScore, null);
String bestJaccard = Utils.rounded(bestTp / (bestFp + numberOfMolecules)) + '\t';
analysisPrefix += bestJaccard;
sb.append(bestJaccard);
addJaccardScores(sb);
sb.append("\t").append(Utils.timeToString(runTime / 1000000.0));
summaryTable.append(sb.toString());
}
/**
* Compute the total number of true results that all the candidates could have fit, including singles, doublets and
* multiples.
*
* @param results
* the doublet fitting results
* @return the total localisation results we could have fit
*/
private int countN(ArrayList<DoubletResult> results)
{
int n = 0;
for (DoubletResult r : results)
n += r.n;
return n;
}
/**
* Compute maximum jaccard for all the residuals thresholds.
*
* @param data
* the data
* @param tp
* the true positives at residuals = 0
* @param fp
* the false positives at residuals = 0
* @param n
* the number of true positives at residuals = 0
* @param useMax
* Use the max residuals
*/
private void computeScores(ArrayList<DoubletBonus> data, double tp, double fp, int n, boolean useMax)
{
// Add data at ends to complete the residuals scale from 0 to 1
data.add(new DoubletBonus(0, 0, 0, 0));
data.add(new DoubletBonus(1, 1, 0, 0));
for (DoubletBonus b : data)
b.setScore(useMax);
Collections.sort(data);
double[] residuals = new double[data.size() + 2];
double[] jaccard = new double[residuals.length];
double[] recall = new double[residuals.length];
double[] precision = new double[residuals.length];
int maxJaccardIndex = 0;
int count = 0;
double last = 0;
for (DoubletBonus b : data)
{
if (last != b.residuals)
{
residuals[count] = last;
jaccard[count] = tp / (n + fp);
if (tp + fp != 0)
precision[count] = tp / (tp + fp);
recall[count] = tp / n;
if (jaccard[maxJaccardIndex] < jaccard[count])
maxJaccardIndex = count;
count++;
}
tp -= b.tp;
fp -= b.fp;
last = b.residuals;
}
residuals[count] = last;
jaccard[count] = tp / (n + fp);
if (jaccard[maxJaccardIndex] < jaccard[count])
maxJaccardIndex = count;
if (tp + fp != 0)
precision[count] = tp / (tp + fp);
recall[count] = tp / n;
count++;
residuals = Arrays.copyOf(residuals, count);
jaccard = Arrays.copyOf(jaccard, count);
precision = Arrays.copyOf(precision, count);
recall = Arrays.copyOf(recall, count);
this.residualsScore = new ResidualsScore(residuals, jaccard, recall, precision, maxJaccardIndex);
}
private void plotJaccard(ResidualsScore residualsScore, ResidualsScore residualsScoreReference)
{
String title = TITLE + " Score";
Plot plot = new Plot(title, "Residuals", "Score");
double max = getMax(0.01, residualsScore);
if (residualsScoreReference != null)
{
max = getMax(max, residualsScore);
}
plot.setLimits(0, 1, 0, max * 1.05);
addLines(plot, residualsScore, 1);
if (residualsScoreReference != null)
{
addLines(plot, residualsScoreReference, 0.5);
}
plot.setColor(Color.black);
plot.addLabel(0, 0,
String.format("Residuals %s; Jaccard %s (Black); Precision %s (Blue); Recall %s (Red)",
Utils.rounded(residualsScore.residuals[residualsScore.maxJaccardIndex]),
Utils.rounded(residualsScore.jaccard[residualsScore.maxJaccardIndex]),
Utils.rounded(residualsScore.precision[residualsScore.maxJaccardIndex]),
Utils.rounded(residualsScore.recall[residualsScore.maxJaccardIndex])));
display(title, plot);
}
private double getMax(double max, ResidualsScore residualsScore)
{
max = Math.max(max, residualsScore.jaccard[residualsScore.maxJaccardIndex]);
max = Maths.maxDefault(max, residualsScore.precision);
max = Maths.maxDefault(max, residualsScore.recall);
return max;
}
private void addLines(Plot plot, ResidualsScore residualsScore, double saturation)
{
if (saturation == 1)
{
// Colours are the same as the BenchmarkSpotFilter
plot.setColor(Color.black);
plot.addPoints(residualsScore.residuals, residualsScore.jaccard, Plot.LINE);
plot.setColor(Color.blue);
plot.addPoints(residualsScore.residuals, residualsScore.precision, Plot.LINE);
plot.setColor(Color.red);
plot.addPoints(residualsScore.residuals, residualsScore.recall, Plot.LINE);
}
else
{
plot.setColor(getColor(Color.black, saturation));
plot.addPoints(residualsScore.residuals, residualsScore.jaccard, Plot.LINE);
plot.setColor(getColor(Color.blue, saturation));
plot.addPoints(residualsScore.residuals, residualsScore.precision, Plot.LINE);
plot.setColor(getColor(Color.red, saturation));
plot.addPoints(residualsScore.residuals, residualsScore.recall, Plot.LINE);
}
}
private Color getColor(Color color, double saturation)
{
int r = color.getRed();
int g = color.getGreen();
int b = color.getBlue();
if (r == 0 && g == 0 && b == 0)
{
// Black so make a grey
return Color.gray;
}
float[] hsbvals = Color.RGBtoHSB(r, g, b, null);
hsbvals[1] *= saturation;
return Color.getHSBColor(hsbvals[0], hsbvals[1], hsbvals[2]);
}
/**
* Show a cumulative histogram of the data.
*
* @param values
* the values
* @param xTitle
* The name of plotted statistic
*/
public void showHistogram(double[] values, String xTitle)
{
double[][] h = Maths.cumulativeHistogram(values, false);
String title = TITLE + " " + xTitle + " Cumulative";
Plot2 plot = new Plot2(title, xTitle, "Frequency");
double xMax = h[0][h[0].length - 1];
double yMax = h[1][h[1].length - 1];
plot.setLimits(0, xMax, 0, 1.05 * yMax);
plot.setColor(Color.blue);
plot.addPoints(h[0], h[1], Plot2.BAR);
display(title, plot);
}
private void display(String title, Plot plot)
{
PlotWindow pw = Utils.display(title, plot);
if (Utils.isNewWindow())
windowOrganiser.add(pw.getImagePlus().getID());
}
/**
* Creates the summary table.
*/
private void createSummaryTable()
{
if (summaryTable == null || !summaryTable.isVisible())
{
summaryTable = new TextWindow(TITLE + " Summary", createSummaryHeader(), "", 1000, 300);
summaryTable.setVisible(true);
}
}
/**
* Creates the summary header.
*
* @return the string
*/
private String createSummaryHeader()
{
StringBuilder sb = new StringBuilder();
sb.append(
"Molecules\tMatched\tDensity\tminN\tmaxN\tN\ts (nm)\ta (nm)\tsa (nm)\tGain\tReadNoise (ADUs)\tB (photons)\tnoise (ADUs)\tSNR\tWidth\tMethod\tOptions\t");
for (String name : NAMES2)
sb.append(name).append('\t');
sb.append(
"Matching\tBest J\tJ (r=1)\tMax J\tResiduals\tArea +/-15%\tArea 98%\tMin 98%\tMax 98%\tRange 98%\twMean 98%\tArea >90%\tMin >90%\tMax >90%\tRange >90%\twMean >90%\tRun time");
return sb.toString();
}
/**
* Show results.
*
* @param results
* the results
*/
private void showResults(ArrayList<DoubletResult> results, boolean show)
{
if (!show)
return;
createResultsTable();
ArrayList<String> list = new ArrayList<String>(results.size());
int flush = 9;
StringBuilder sb = new StringBuilder();
for (DoubletResult result : results)
{
sb.append(result.frame).append('\t');
sb.append(result.spot.x).append('\t');
sb.append(result.spot.y).append('\t');
sb.append(IJ.d2s(result.spot.intensity, 1)).append('\t');
sb.append(result.n).append('\t');
sb.append(result.neighbours).append('\t');
sb.append(result.almostNeighbours).append('\t');
sb.append(Utils.rounded(result.score1)).append('\t');
sb.append(Utils.rounded(result.score2)).append('\t');
add(sb, result.fitResult1);
add(sb, result.fitResult2);
sb.append(IJ.d2s(result.sumOfSquares1, 1)).append("\t");
sb.append(IJ.d2s(result.sumOfSquares2, 1)).append("\t");
sb.append(IJ.d2s(result.value1, 1)).append("\t");
sb.append(IJ.d2s(result.value2, 1)).append("\t");
sb.append(Utils.rounded(result.r1)).append("\t");
sb.append(Utils.rounded(result.r2)).append("\t");
sb.append(Utils.rounded(result.aic1)).append('\t');
sb.append(Utils.rounded(result.aic2)).append('\t');
sb.append(Utils.rounded(result.bic1)).append('\t');
sb.append(Utils.rounded(result.bic2)).append('\t');
sb.append(Utils.rounded(result.maic1)).append('\t');
sb.append(Utils.rounded(result.maic2)).append('\t');
sb.append(Utils.rounded(result.mbic1)).append('\t');
sb.append(Utils.rounded(result.mbic2)).append('\t');
sb.append(Utils.rounded(result.a[0])).append('\t');
sb.append(Utils.rounded(result.a[1])).append('\t');
sb.append(Utils.rounded(result.gap)).append('\t');
sb.append(Utils.rounded(result.xshift[0])).append('\t');
sb.append(Utils.rounded(result.yshift[0])).append('\t');
sb.append(Utils.rounded(result.xshift[1])).append('\t');
sb.append(Utils.rounded(result.yshift[1])).append('\t');
sb.append(result.iter1).append('\t');
sb.append(result.iter2).append('\t');
sb.append(result.eval1).append('\t');
sb.append(result.eval2).append('\t');
addParams(sb, result.fitResult1);
addParams(sb, result.fitResult2);
list.add(sb.toString());
sb.setLength(0);
// Flush below 10 lines so ImageJ will layout the columns
if (--flush == 0)
{
resultsTable.getTextPanel().append(list);
list.clear();
}
}
resultsTable.getTextPanel().append(list);
}
/**
* Adds the.
*
* @param sb
* the sb
* @param fitResult
* the fit result
*/
private void add(StringBuilder sb, FitResult fitResult)
{
if (fitResult != null)
{
sb.append(fitResult.getStatus()).append("\t");
}
else
{
sb.append("\t");
}
}
/**
* Adds the params.
*
* @param sb
* the sb
* @param fitResult
* the fit result
*/
private void addParams(StringBuilder sb, FitResult fitResult)
{
if (fitResult != null)
{
sb.append(Arrays.toString(fitResult.getParameters())).append("\t");
}
else
{
sb.append("\t");
}
}
/**
* Creates the results table.
*/
private void createResultsTable()
{
if (resultsTable == null || !resultsTable.isVisible())
{
resultsTable = new TextWindow(TITLE + " Results", createResultsHeader(), "", 1000, 300);
resultsTable.setVisible(true);
}
}
/**
* Creates the results header.
*
* @return the string
*/
private String createResultsHeader()
{
return "Frame\tx\ty\tI\tn\tneighbours\talmost\tscore1\tscore2\tR1\tR2\tss1\tss2\tv1\tv2\tr1\tr2\taic1\taic2\tbic1\tbic2\tmaic1\tmaic2\tmbic1\tmbic2\ta1\ta2\tgap\tx1\ty1\tx2\ty2\ti1\ti2\te1\te2\tparams1\tparams2";
}
/**
* Run analysis.
*/
private void runAnalysis()
{
if (doubletResults == null)
{
IJ.error(TITLE, "No doublet results in memory");
return;
}
// Ask the user to set filters
if (!showAnalysisDialog())
return;
showResults(doubletResults, analysisShowResults);
// Store the effect of fitting as a doublet
ArrayList<DoubletBonus> data = new ArrayList<DoubletBonus>(doubletResults.size());
// True positive and False positives at residuals = 0
double tp = 0;
double fp = 0;
Logger logger = (analysisLogging) ? new IJLogger() : null;
// Get filters for the single and double fits
// No coordinate shift for the doublet. We have already done simple checking of the
// coordinates to get the good=2 flag
FitConfiguration filterFitConfig2 = filterFitConfig.clone();
filterFitConfig2.setCoordinateShift(Integer.MAX_VALUE);
final int size = 2 * config.getRelativeFitting() + 1;
Rectangle regionBounds = new Rectangle(0, 0, size, size);
final double otherDriftAngle = 180 - analysisDriftAngle;
// Process all the results
for (DoubletResult result : doubletResults)
{
// Filter the singles that would be accepted
if (result.good1)
{
filterFitConfig.setNoise(result.noise);
FitStatus fitStatus0 = filterFitConfig.validatePeak(0, result.fitResult1.getInitialParameters(),
result.fitResult1.getParameters());
double tp1 = 0, fp1 = 0;
if (fitStatus0 == FitStatus.OK)
{
tp1 = result.tp1;
fp1 = result.fp1;
}
else if (analysisLogging)
logFailure(logger, 0, result, fitStatus0);
// Filter the doublets that would be accepted
// We have already done simple checking to get the good1 = true flag. So accept
// width diverged spots as OK for a doublet fit
if ((fitStatus0 == FitStatus.OK || fitStatus0 == FitStatus.WIDTH_DIVERGED) && selectFit(result) &&
result.good2)
{
double tp2 = 0, fp2 = 0;
// Basic spot criteria (SNR, Photons, width)
filterFitConfig2.setNoise(result.noise);
FitStatus fitStatus1 = filterFitConfig2.validatePeak(0, result.fitResult2.getInitialParameters(),
result.fitResult2.getParameters());
FitStatus fitStatus2 = filterFitConfig2.validatePeak(1, result.fitResult2.getInitialParameters(),
result.fitResult2.getParameters());
// Log basic failures
boolean[] accept = new boolean[2];
if (fitStatus1 == FitStatus.OK)
{
accept[0] = true;
}
else if (analysisLogging)
logFailure(logger, 1, result, fitStatus1);
if (fitStatus2 == FitStatus.OK)
{
accept[1] = true;
}
else if (analysisLogging)
logFailure(logger, 2, result, fitStatus2);
// If the basic filters are OK, do some analysis of the doublet.
// We can filter each spot with criteria such as shift and the angle to the quadrant.
if (accept[0] || accept[1])
{
if (result.gap < minGap)
{
accept[0] = accept[1] = false;
if (analysisLogging)
logger.info("Reject Doublet (%.2f): Fitted coordinates below min gap (%g<%g)\n",
result.getMaxScore(), result.gap, minGap);
}
}
if (accept[0] || accept[1])
{
// The logic in here will be copied to the FitWorker.quadrantAnalysis routine.
double[] params = result.fitResult1.getParameters();
double[] newParams = result.fitResult2.getParameters();
// Set up for shift filtering
double shift = filterFitConfig.getCoordinateShift();
if (shift == 0 || shift == Double.POSITIVE_INFINITY)
{
// Allow the shift to span half of the fitted window.
shift = 0.5 * FastMath.min(regionBounds.width, regionBounds.height);
}
// Set an upper limit on the shift that is not too far outside the fit window
final double maxShiftX, maxShiftY;
final double factor = Gaussian2DFunction.SD_TO_HWHM_FACTOR;
if (fitConfig.isWidth0Fitting())
{
// Add the fitted standard deviation to the allowed shift
maxShiftX = regionBounds.width * 0.5 + factor * params[Gaussian2DFunction.X_SD];
maxShiftY = regionBounds.height * 0.5 + factor * params[Gaussian2DFunction.Y_SD];
}
else
{
// Add the configured standard deviation to the allowed shift
maxShiftX = regionBounds.width * 0.5 + factor * fitConfig.getInitialPeakStdDev0();
maxShiftY = regionBounds.height * 0.5 + factor * fitConfig.getInitialPeakStdDev1();
}
for (int n = 0; n < 2; n++)
{
if (!accept[n])
continue;
accept[n] = false; // Reset
final double xShift = newParams[Gaussian2DFunction.X_POSITION + n * 6] -
params[Gaussian2DFunction.X_POSITION];
final double yShift = newParams[Gaussian2DFunction.Y_POSITION + n * 6] -
params[Gaussian2DFunction.Y_POSITION];
if (Math.abs(xShift) > maxShiftX || Math.abs(yShift) > maxShiftY)
{
if (analysisLogging)
logger.info(
"Reject P%d (%.2f): Fitted coordinates moved outside fit region (x=%g,y=%g)\n",
n + 1, result.getMaxScore(), xShift, yShift);
continue;
}
if (Math.abs(xShift) > shift || Math.abs(yShift) > shift)
{
// Check the domain is OK (the angle is in radians).
// Allow up to a 45 degree difference to show the shift is along the vector
if (result.a[n] > analysisDriftAngle && result.a[n] < otherDriftAngle)
{
if (analysisLogging)
logger.info(
"Reject P%d (%.2f): Fitted coordinates moved into wrong quadrant (x=%g,y=%g,a=%f)",
n + 1, result.getMaxScore(), xShift, yShift, result.a[n]);
continue;
}
// Note: The FitWorker also checks for drift to another candidate.
}
// This is OK
accept[n] = true;
}
}
if (accept[0])
{
tp2 += result.tp2a;
fp2 += result.fp2a;
}
if (accept[1])
{
tp2 += result.tp2b;
fp2 += result.fp2b;
}
if (accept[0] || accept[1])
{
tp += tp2;
fp += fp2;
// Store this as a doublet bonus
data.add(new DoubletBonus(result.getMaxScore(), result.getAvScore(), tp2 - tp1, fp2 - fp1));
}
else
{
// No doublet fit so this will always be the single fit result
tp += tp1;
fp += fp1;
}
}
else
{
// No doublet fit so this will always be the single fit result
tp += tp1;
fp += fp1;
}
}
}
// Compute the max Jaccard
computeScores(data, tp, fp, numberOfMolecules, useMaxResiduals);
if (showJaccardPlot)
plotJaccard(residualsScore, (useMaxResiduals) ? _residualsScoreMax : _residualsScoreAv);
createAnalysisTable();
StringBuilder sb = new StringBuilder(analysisPrefix);
sb.append(analysisTitle).append('\t');
sb.append((useMaxResiduals) ? "Max" : "Average").append('\t');
sb.append(SELECTION_CRITERIA[selectionCriteria]).append('\t');
if (filterFitConfig.isSmartFilter())
{
sb.append(filterFitConfig.getSmartFilterName()).append("\t\t\t\t\t\t\t\t");
}
else
{
sb.append('\t');
sb.append(filterFitConfig.getCoordinateShiftFactor()).append('\t');
sb.append(filterFitConfig.getSignalStrength()).append('\t');
sb.append(filterFitConfig.getMinPhotons()).append('\t');
sb.append(filterFitConfig.getMinWidthFactor()).append('\t');
sb.append(filterFitConfig.getWidthFactor()).append('\t');
sb.append(filterFitConfig.getPrecisionThreshold()).append('\t');
sb.append(filterFitConfig.isPrecisionUsingBackground()).append('\t');
}
sb.append(analysisDriftAngle).append('\t');
sb.append(minGap).append('\t');
addJaccardScores(sb);
analysisTable.append(sb.toString());
saveTemplate(sb.toString());
}
/**
* Save PeakFit configuration template using the current benchmark settings.
*
* @param summary
*/
private void saveTemplate(String summary)
{
if (!saveTemplate)
return;
// Start with a clone of the filter settings
FitConfiguration fitConfig = filterFitConfig.clone();
FitEngineConfiguration config = new FitEngineConfiguration(fitConfig);
// Copy settings used during fitting
updateConfiguration(config);
// Remove the PSF width to make the template generic
fitConfig.setInitialPeakStdDev(0);
fitConfig.setNmPerPixel(0);
fitConfig.setGain(0);
fitConfig.setNoise(0);
// This was done fitting all the results
config.setFailuresLimit(-1);
if (useBenchmarkSettings)
{
FitEngineConfiguration pConfig = new FitEngineConfiguration(new FitConfiguration());
// TODO - add option to use latest or the best
if (BenchmarkFilterAnalysis.updateConfiguration(pConfig, false))
config.setFailuresLimit(pConfig.getFailuresLimit());
}
// Set the residuals
fitConfig.setComputeResiduals(true);
// TODO - make the choice of the best residuals configurable
config.setResidualsThreshold(residualsScore.bestResiduals[2]);
String filename = BenchmarkFilterAnalysis.getFilename("Template_File", templateFilename);
if (filename != null)
{
templateFilename = filename;
GlobalSettings settings = new GlobalSettings();
settings.setNotes(getNotes(summary));
settings.setFitEngineConfiguration(config);
if (!SettingsManager.saveSettings(settings, filename, true))
IJ.log("Unable to save the template configuration");
}
}
/**
* Updates the given configuration using the latest fitting settings used in benchmarking.
*
* @param pConfig
* the configuration
* @return true, if successful
*/
public static boolean updateConfiguration(FitEngineConfiguration pConfig)
{
final FitConfiguration pFitConfig = pConfig.getFitConfiguration();
pFitConfig.setInitialPeakStdDev(fitConfig.getInitialPeakStdDev0());
pConfig.copyDataFilter(config);
pConfig.setSearch(config.getSearch());
pConfig.setBorder(config.getBorder());
pConfig.setFitting(config.getFitting());
pFitConfig.setFitSolver(fitConfig.getFitSolver());
pFitConfig.setFitFunction(fitConfig.getFitFunction());
pConfig.setIncludeNeighbours(config.isIncludeNeighbours());
pConfig.setNeighbourHeightThreshold(config.getNeighbourHeightThreshold());
pFitConfig.setDuplicateDistance(fitConfig.getDuplicateDistance());
pFitConfig.setMaxIterations(fitConfig.getMaxIterations());
pFitConfig.setMaxFunctionEvaluations(fitConfig.getMaxFunctionEvaluations());
// MLE settings
pFitConfig.setModelCamera(fitConfig.isModelCamera());
pFitConfig.setBias(0);
pFitConfig.setReadNoise(0);
pFitConfig.setAmplification(0);
pFitConfig.setEmCCD(fitConfig.isEmCCD());
pFitConfig.setSearchMethod(fitConfig.getSearchMethod());
pFitConfig.setRelativeThreshold(fitConfig.getRelativeThreshold());
pFitConfig.setAbsoluteThreshold(fitConfig.getAbsoluteThreshold());
pFitConfig.setGradientLineMinimisation(fitConfig.isGradientLineMinimisation());
// LSE settings
pFitConfig.setFitCriteria(fitConfig.getFitCriteria());
pFitConfig.setSignificantDigits(fitConfig.getSignificantDigits());
pFitConfig.setDelta(fitConfig.getDelta());
pFitConfig.setLambda(fitConfig.getLambda());
return true;
}
private String getNotes(String summary)
{
StringBuilder sb = new StringBuilder();
sb.append("Benchmark template\n");
if (!Utils.isNullOrEmpty(analysisTitle))
BenchmarkFilterAnalysis.addField(sb, "Doublet Analysis Title", analysisTitle);
// Add create data settings.
// Just add the columns and the data from the summary window
final String header = createAnalysisHeader();
BenchmarkFilterAnalysis.addField(sb, "Doublet Analysis Summary Fields", header);
BenchmarkFilterAnalysis.addField(sb, "Doublet Analysis Summary Values", summary);
// Now pick out key values...
BenchmarkFilterAnalysis.addKeyFields(sb, header, summary, new String[] { "Density", "s", "Selection", "Max J",
"Residuals", "Area >90", "Range >90", "wMean >90" });
// Add any other settings that may be useful in the template
BenchmarkFilterAnalysis.addField(sb, "Created", BenchmarkFilterAnalysis.getCurrentTimeStamp());
return sb.toString();
}
private void logFailure(Logger logger, int i, DoubletResult result, FitStatus fitStatus)
{
logger.info("Reject P%d (%.2f): %s\n", i, result.getMaxScore(), fitStatus.toString());
}
private boolean selectFit(DoubletResult result)
{
switch (selectionCriteria)
{
//@formatter:off
case 0: return result.r2 > result.r1;
case 1: return result.aic2 < result.aic1;
case 2: return result.bic2 < result.bic1;
case 3: return result.maic2 < result.maic1;
case 4: return result.mbic2 < result.mbic1;
//@formatter:on
}
return false;
}
private void addJaccardScores(StringBuilder sb)
{
double[] residuals = residualsScore.residuals;
double[] jaccard = residualsScore.jaccard;
int maxJaccardIndex = residualsScore.maxJaccardIndex;
sb.append(Utils.rounded(jaccard[jaccard.length - 1])).append('\t');
sb.append(Utils.rounded(jaccard[maxJaccardIndex])).append('\t')
.append(Utils.rounded(residuals[maxJaccardIndex]));
sb.append('\t').append(Utils.rounded(getArea(residuals, jaccard, maxJaccardIndex, 0.15)));
//sb.append('\t').append(Utils.rounded(getArea(residuals, jaccard, maxJaccardIndex, 0.3)));
//sb.append('\t').append(Utils.rounded(getArea(residuals, jaccard, maxJaccardIndex, 1)));
residualsScore.bestResiduals[0] = residuals[maxJaccardIndex];
// Find the range that has a Jaccard within a % of the max
residualsScore.bestResiduals[1] = addRange(sb, residuals, jaccard, maxJaccardIndex,
jaccard[maxJaccardIndex] * 0.98);
// Do the same within a fraction of the performance improvement over no residuals
residualsScore.bestResiduals[2] = addRange(sb, residuals, jaccard, maxJaccardIndex,
jaccard[jaccard.length - 1] + (jaccard[maxJaccardIndex] - jaccard[jaccard.length - 1]) * 0.9);
}
private double addRange(StringBuilder sb, double[] residuals, double[] jaccard, int maxJaccardIndex, double limit)
{
double sum = 0;
double lower = residuals[maxJaccardIndex];
double upper = residuals[maxJaccardIndex];
int j = maxJaccardIndex;
// For weighted mean
double sumR = 0;
for (int i = j; i-- > 0;)
{
if (jaccard[i] < limit)
{
break;
}
final double height = (jaccard[j] + jaccard[i]) * 0.5;
final double width = (residuals[j] - residuals[i]);
final double area = height * width;
sum += area;
j = i;
lower = residuals[j];
final double avResiduals = (residuals[j] + residuals[i]) * 0.5;
sumR += area * avResiduals;
}
j = maxJaccardIndex;
for (int i = j; ++i < jaccard.length;)
{
if (jaccard[i] < limit)
{
break;
}
final double height = (jaccard[j] + jaccard[i]) * 0.5;
final double width = (residuals[i] - residuals[j]);
final double area = height * width;
sum += area;
j = i;
upper = residuals[j];
final double avResiduals = (residuals[j] + residuals[i]) * 0.5;
sumR += area * avResiduals;
}
final double weightedMean;
if (sum != 0)
{
weightedMean = sumR / sum;
sum /= (upper - lower);
}
else
{
weightedMean = sum = jaccard[maxJaccardIndex];
}
sb.append('\t').append(Utils.rounded(sum)).append('\t').append(Utils.rounded(lower)).append('\t')
.append(Utils.rounded(upper)).append('\t').append(Utils.rounded(upper - lower)).append('\t')
.append(Utils.rounded(weightedMean));
return weightedMean;
}
private double getArea(double[] residuals, double[] jaccard, int maxJaccardIndex, double window)
{
double sum = 0;
double lower = Math.max(0, residuals[maxJaccardIndex] - window);
double upper = Math.min(1, residuals[maxJaccardIndex] + window);
// LinearInterpolator li = new LinearInterpolator();
// PolynomialSplineFunction fun = li.interpolate(residuals, jaccard);
// //TrapezoidIntegrator in = new TrapezoidIntegrator();
// SimpsonIntegrator in = new SimpsonIntegrator();
//
// try
// {
// sum = in.integrate(1000, fun, lower, upper);
// }
// catch (TooManyEvaluationsException e)
// {
// System.out.println("Failed to calculate the area: " + e.getMessage());
int j = maxJaccardIndex;
for (int i = j; i-- > 0;)
{
if (residuals[i] < lower)
{
lower = residuals[j];
break;
}
sum += (jaccard[j] + jaccard[i]) * 0.5 * (residuals[j] - residuals[i]);
j = i;
}
j = maxJaccardIndex;
for (int i = j; ++i < jaccard.length;)
{
if (residuals[i] > upper)
{
upper = residuals[j];
break;
}
sum += (jaccard[j] + jaccard[i]) * 0.5 * (residuals[i] - residuals[j]);
j = i;
}
// }
return sum / (upper - lower);
}
/**
* Show dialog.
*
* @return true, if successful
*/
@SuppressWarnings("unchecked")
private boolean showAnalysisDialog()
{
GenericDialog gd = new GenericDialog(TITLE);
gd.addHelp(About.HELP_URL);
StringBuilder sb = new StringBuilder("Filters the doublet fits and reports the performance increase\n");
// Show the fitting settings that will effect filters, i.e. fit standard deviation, fit width
sb.append("SD0 = ").append(Utils.rounded(fitConfig.getInitialPeakStdDev0())).append("\n");
sb.append("SD1 = ").append(Utils.rounded(fitConfig.getInitialPeakStdDev1())).append("\n");
sb.append("Fit Width = ").append(config.getRelativeFitting()).append("\n");
gd.addMessage(sb.toString());
// Collect options for filtering
gd.addChoice("Selection_Criteria", SELECTION_CRITERIA, SELECTION_CRITERIA[selectionCriteria]);
// Copy the settings used when fitting
filterFitConfig.setInitialPeakStdDev0(fitConfig.getInitialPeakStdDev0());
filterFitConfig.setInitialPeakStdDev1(fitConfig.getInitialPeakStdDev1());
filterFitConfig.setModelCamera(fitConfig.isModelCamera());
filterFitConfig.setNmPerPixel(cal.getNmPerPixel());
filterFitConfig.setGain(cal.getGain());
filterFitConfig.setBias(cal.getBias());
filterFitConfig.setReadNoise(cal.getReadNoise());
filterFitConfig.setAmplification(cal.getAmplification());
filterFitConfig.setEmCCD(cal.isEmCCD());
filterFitConfig.setFitSolver(fitConfig.getFitSolver());
String[] templates = ConfigurationTemplate.getTemplateNames(true);
gd.addChoice("Template", templates, templates[0]);
// Allow the settings from the benchmark analysis to be used
gd.addCheckbox("Benchmark_settings", analysisUseBenchmarkSettings);
gd.addCheckbox("Smart_filter", fitConfig.isSmartFilter());
gd.addSlider("Shift_factor", 0.01, 2, filterFitConfig.getCoordinateShiftFactor());
gd.addNumericField("Signal_strength", filterFitConfig.getSignalStrength(), 2);
gd.addNumericField("Min_photons", filterFitConfig.getMinPhotons(), 0);
gd.addSlider("Min_width_factor", 0, 0.99, filterFitConfig.getMinWidthFactor());
gd.addSlider("Max_width_factor", 1.01, 5, filterFitConfig.getWidthFactor());
gd.addNumericField("Precision", filterFitConfig.getPrecisionThreshold(), 2);
gd.addCheckbox("Local_background", filterFitConfig.isPrecisionUsingBackground());
gd.addNumericField("Drift_angle", analysisDriftAngle, 2);
gd.addNumericField("Min_gap", minGap, 2);
// Collect display options
gd.addCheckbox("Show_results", analysisShowResults);
gd.addCheckbox("Show_Jaccard_Plot", showJaccardPlot);
gd.addCheckbox("Use_max_residuals", useMaxResiduals);
gd.addCheckbox("Logging", analysisLogging);
gd.addStringField("Title", analysisTitle);
gd.addCheckbox("Save_template", saveTemplate);
// TODO - Add support for updating a template with a residuals threshold, e.g. from the BenchmarkFilterAnalysis plugin
// Add a mouse listener to the config file field
if (Utils.isShowGenericDialog())
{
Vector<TextField> numerics = (Vector<TextField>) gd.getNumericFields();
Vector<Checkbox> checkboxes = (Vector<Checkbox>) gd.getCheckboxes();
Vector<Choice> choices = (Vector<Choice>) gd.getChoices();
int n = 0;
choices.get(1).addItemListener(this);
checkboxes.get(0).addItemListener(this);
cbSmartFilter = checkboxes.get(1);
textCoordinateShiftFactor = numerics.get(n++);
textSignalStrength = numerics.get(n++);
textMinPhotons = numerics.get(n++);
textMinWidthFactor = numerics.get(n++);
textWidthFactor = numerics.get(n++);
textPrecisionThreshold = numerics.get(n++);
cbLocalBackground = checkboxes.get(2);
}
gd.showDialog();
if (gd.wasCanceled())
return false;
if (gd.invalidNumber())
return false;
selectionCriteria = gd.getNextChoiceIndex();
// Ignore the template
gd.getNextChoice();
analysisUseBenchmarkSettings = gd.getNextBoolean();
fitConfig.setSmartFilter(gd.getNextBoolean());
filterFitConfig.setCoordinateShiftFactor(gd.getNextNumber());
filterFitConfig.setSignalStrength(gd.getNextNumber());
filterFitConfig.setMinPhotons(gd.getNextNumber());
filterFitConfig.setMinWidthFactor(gd.getNextNumber());
filterFitConfig.setWidthFactor(gd.getNextNumber());
filterFitConfig.setPrecisionThreshold(gd.getNextNumber());
filterFitConfig.setPrecisionUsingBackground(gd.getNextBoolean());
analysisDriftAngle = gd.getNextNumber();
minGap = gd.getNextNumber();
analysisShowResults = gd.getNextBoolean();
showJaccardPlot = gd.getNextBoolean();
useMaxResiduals = gd.getNextBoolean();
analysisLogging = gd.getNextBoolean();
analysisTitle = gd.getNextString();
saveTemplate = gd.getNextBoolean();
if (gd.invalidNumber())
return false;
if (analysisUseBenchmarkSettings)
{
if (!updateFilterConfiguration(filterFitConfig))
return false;
}
return true;
}
private boolean updateFilterConfiguration(FitConfiguration filterFitConfig)
{
FitEngineConfiguration c = new FitEngineConfiguration(filterFitConfig);
// TODO - add option to use latest or the best
if (!BenchmarkFilterAnalysis.updateConfiguration(c, false))
{
IJ.error(TITLE, "Unable to use the benchmark filter analysis configuration");
return false;
}
return true;
}
/**
* Creates the analysis table.
*/
private void createAnalysisTable()
{
if (analysisTable == null || !analysisTable.isVisible())
{
analysisTable = new TextWindow(TITLE + " Analysis", createAnalysisHeader(), "", 1200, 300);
analysisTable.setVisible(true);
}
}
/**
* Creates the analysis header.
*
* @return the string
*/
private String createAnalysisHeader()
{
return "Density\ts\tWidth\tMethod\tOptions\tBest J\tTitle\tUse residuals\tSelection\tFilter\tShift\tSNR\tPhotons\tMin Width\tWidth\tPrecision\tLocal B\tAngle\tGap\tJ (r=1)\tMax J\tResiduals\tArea +/-15%\tArea 98%\tMin 98%\tMax 98%\tRange 98%\twMean 98%\tArea >90%\tMin >90%\tMax >90%\tRange >90%\twMean >90%";
}
/*
* (non-Javadoc)
*
* @see java.awt.event.ItemListener#itemStateChanged(java.awt.event.ItemEvent)
*/
public void itemStateChanged(ItemEvent e)
{
if (e.getSource() instanceof Choice)
{
// Update the settings from the template
Choice choice = (Choice) e.getSource();
String templateName = choice.getSelectedItem();
// Get the configuration template
GlobalSettings template = ConfigurationTemplate.getTemplate(templateName);
if (textCoordinateShiftFactor != null)
{
if (template != null)
{
if (template.isFitEngineConfiguration())
{
FitConfiguration fitConfig = template.getFitEngineConfiguration().getFitConfiguration();
cbSmartFilter.setState(fitConfig.isSmartFilter());
textCoordinateShiftFactor.setText("" + fitConfig.getCoordinateShiftFactor());
textSignalStrength.setText("" + fitConfig.getSignalStrength());
textMinPhotons.setText("" + fitConfig.getMinPhotons());
textMinWidthFactor.setText("" + fitConfig.getMinWidthFactor());
textWidthFactor.setText("" + fitConfig.getWidthFactor());
textPrecisionThreshold.setText("" + fitConfig.getPrecisionThreshold());
cbLocalBackground.setState(fitConfig.isPrecisionUsingBackground());
}
}
else
{
// Reset
cbSmartFilter.setState(false);
textCoordinateShiftFactor.setText("0");
textSignalStrength.setText("0");
textMinPhotons.setText("0");
textMinWidthFactor.setText("0");
textWidthFactor.setText("0");
textPrecisionThreshold.setText("0");
cbLocalBackground.setState(false);
}
}
else
{
if (template != null)
{
if (template.isFitEngineConfiguration())
{
boolean custom = ConfigurationTemplate.isCustomTemplate(templateName);
FitEngineConfiguration config2 = template.getFitEngineConfiguration();
FitConfiguration fitConfig2 = config2.getFitConfiguration();
if (custom && fitConfig2.getInitialPeakStdDev0() > 0)
textInitialPeakStdDev0.setText("" + fitConfig2.getInitialPeakStdDev0());
textDataFilterType.select(config2.getDataFilterType().ordinal());
textDataFilter.select(config2.getDataFilter(0).ordinal());
textSmooth.setText("" + config2.getSmooth(0));
textSearch.setText("" + config2.getSearch());
textBorder.setText("" + config2.getBorder());
textFitting.setText("" + config2.getFitting());
textFitSolver.select(fitConfig2.getFitSolver().ordinal());
textFitFunction.select(fitConfig2.getFitFunction().ordinal());
// Copy settings not in the dialog for the fit solver
fitConfig.setMaxIterations(fitConfig2.getMaxIterations());
fitConfig.setMaxFunctionEvaluations(fitConfig2.getMaxFunctionEvaluations());
// MLE settings
fitConfig.setModelCamera(fitConfig2.isModelCamera());
fitConfig.setSearchMethod(fitConfig2.getSearchMethod());
fitConfig.setRelativeThreshold(fitConfig2.getRelativeThreshold());
fitConfig.setAbsoluteThreshold(fitConfig2.getAbsoluteThreshold());
fitConfig.setGradientLineMinimisation(fitConfig2.isGradientLineMinimisation());
// LSE settings
fitConfig.setFitCriteria(fitConfig2.getFitCriteria());
fitConfig.setSignificantDigits(fitConfig2.getSignificantDigits());
fitConfig.setDelta(fitConfig2.getDelta());
fitConfig.setLambda(fitConfig2.getLambda());
}
}
else
{
// Ignore
}
}
}
else if (e.getSource() instanceof Checkbox)
{
Checkbox checkbox = (Checkbox) e.getSource();
if (!checkbox.getState())
return;
if (textCoordinateShiftFactor != null)
{
if (!updateFilterConfiguration(filterFitConfig))
return;
cbSmartFilter.setState(filterFitConfig.isSmartFilter());
textCoordinateShiftFactor.setText("" + filterFitConfig.getCoordinateShiftFactor());
textSignalStrength.setText("" + filterFitConfig.getSignalStrength());
textMinPhotons.setText("" + filterFitConfig.getMinPhotons());
textMinWidthFactor.setText("" + filterFitConfig.getMinWidthFactor());
textWidthFactor.setText("" + filterFitConfig.getWidthFactor());
textPrecisionThreshold.setText("" + filterFitConfig.getPrecisionThreshold());
cbLocalBackground.setState(filterFitConfig.isPrecisionUsingBackground());
}
else
{
if (!updateFitConfiguration(config))
return;
textInitialPeakStdDev0.setText("" + fitConfig.getInitialPeakStdDev0());
textDataFilterType.select(config.getDataFilterType().ordinal());
textDataFilter.select(config.getDataFilter(0).ordinal());
textSmooth.setText("" + config.getSmooth(0));
textSearch.setText("" + config.getSearch());
textBorder.setText("" + config.getBorder());
textFitting.setText("" + config.getFitting());
textFitSolver.select(fitConfig.getFitSolver().ordinal());
textFitFunction.select(fitConfig.getFitFunction().ordinal());
textMatchDistance.setText("" + matchDistance);
textLowerDistance.setText("" + lowerDistance);
textSignalFactor.setText("" + signalFactor);
textLowerFactor.setText("" + lowerSignalFactor);
}
}
}
}