
uk.ac.sussex.gdsc.smlm.engine.FitWorker Maven / Gradle / Ivy
/*-
* #%L
* Genome Damage and Stability Centre SMLM Package
*
* Software for single molecule localisation microscopy (SMLM)
* %%
* Copyright (C) 2011 - 2023 Alex Herbert
* %%
* 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.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public
* License along with this program. If not, see
* .
* #L%
*/
package uk.ac.sussex.gdsc.smlm.engine;
import com.google.protobuf.util.JsonFormat;
import java.awt.Rectangle;
import java.io.BufferedWriter;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.concurrent.ConcurrentRuntimeException;
import uk.ac.sussex.gdsc.core.filters.AreaStatistics;
import uk.ac.sussex.gdsc.core.filters.FloatAreaSum;
import uk.ac.sussex.gdsc.core.logging.LoggerUtils;
import uk.ac.sussex.gdsc.core.utils.FileUtils;
import uk.ac.sussex.gdsc.core.utils.ImageExtractor;
import uk.ac.sussex.gdsc.core.utils.LocalList;
import uk.ac.sussex.gdsc.core.utils.MathUtils;
import uk.ac.sussex.gdsc.core.utils.NoiseEstimator;
import uk.ac.sussex.gdsc.core.utils.SimpleArrayUtils;
import uk.ac.sussex.gdsc.core.utils.Statistics;
import uk.ac.sussex.gdsc.core.utils.TextUtils;
import uk.ac.sussex.gdsc.smlm.data.config.CalibrationReader;
import uk.ac.sussex.gdsc.smlm.data.config.ConfigurationException;
import uk.ac.sussex.gdsc.smlm.data.config.FitProtos.NoiseEstimatorMethod;
import uk.ac.sussex.gdsc.smlm.data.config.FitProtos.PrecisionMethod;
import uk.ac.sussex.gdsc.smlm.data.config.FitProtosHelper;
import uk.ac.sussex.gdsc.smlm.data.config.PSFProtos.PSF;
import uk.ac.sussex.gdsc.smlm.data.config.PSFProtos.PSFType;
import uk.ac.sussex.gdsc.smlm.data.config.PsfHelper;
import uk.ac.sussex.gdsc.smlm.engine.FitConfiguration.PeakResultValidationData;
import uk.ac.sussex.gdsc.smlm.engine.FitParameters.FitTask;
import uk.ac.sussex.gdsc.smlm.filters.BlockAverageDataProcessor;
import uk.ac.sussex.gdsc.smlm.filters.MaximaSpotFilter;
import uk.ac.sussex.gdsc.smlm.filters.Spot;
import uk.ac.sussex.gdsc.smlm.filters.SpotScoreComparator;
import uk.ac.sussex.gdsc.smlm.fitting.FastGaussian2DFitter;
import uk.ac.sussex.gdsc.smlm.fitting.FitResult;
import uk.ac.sussex.gdsc.smlm.fitting.FitStatus;
import uk.ac.sussex.gdsc.smlm.fitting.FunctionSolver;
import uk.ac.sussex.gdsc.smlm.fitting.FunctionSolverType;
import uk.ac.sussex.gdsc.smlm.fitting.Gaussian2DFitter;
import uk.ac.sussex.gdsc.smlm.fitting.LseFunctionSolver;
import uk.ac.sussex.gdsc.smlm.fitting.MleFunctionSolver;
import uk.ac.sussex.gdsc.smlm.fitting.WLseFunctionSolver;
import uk.ac.sussex.gdsc.smlm.function.StandardValueProcedure;
import uk.ac.sussex.gdsc.smlm.function.gaussian.FastGaussianOverlapAnalysis;
import uk.ac.sussex.gdsc.smlm.function.gaussian.Gaussian2DFunction;
import uk.ac.sussex.gdsc.smlm.model.camera.CameraModel;
import uk.ac.sussex.gdsc.smlm.results.AttributePeakResult;
import uk.ac.sussex.gdsc.smlm.results.ExtendedPeakResult;
import uk.ac.sussex.gdsc.smlm.results.Gaussian2DPeakResultHelper;
import uk.ac.sussex.gdsc.smlm.results.IdPeakResult;
import uk.ac.sussex.gdsc.smlm.results.PeakResult;
import uk.ac.sussex.gdsc.smlm.results.PeakResultHelper;
import uk.ac.sussex.gdsc.smlm.results.PeakResults;
import uk.ac.sussex.gdsc.smlm.results.count.FailCounter;
import uk.ac.sussex.gdsc.smlm.results.filter.BasePreprocessedPeakResult.ResultType;
import uk.ac.sussex.gdsc.smlm.results.filter.CoordinateStore;
import uk.ac.sussex.gdsc.smlm.results.filter.CoordinateStoreFactory;
import uk.ac.sussex.gdsc.smlm.results.filter.IDirectFilter;
import uk.ac.sussex.gdsc.smlm.results.filter.IMultiPathFitResults;
import uk.ac.sussex.gdsc.smlm.results.filter.MultiFilter;
import uk.ac.sussex.gdsc.smlm.results.filter.MultiFilter2;
import uk.ac.sussex.gdsc.smlm.results.filter.MultiFilterCrlb;
import uk.ac.sussex.gdsc.smlm.results.filter.MultiPathFilter;
import uk.ac.sussex.gdsc.smlm.results.filter.MultiPathFilter.SelectedResult;
import uk.ac.sussex.gdsc.smlm.results.filter.MultiPathFilter.SelectedResultStore;
import uk.ac.sussex.gdsc.smlm.results.filter.MultiPathFitResult;
import uk.ac.sussex.gdsc.smlm.results.filter.PreprocessedPeakResult;
/**
* Fits local maxima using a 2D Gaussian.
*
* Note: The {@link FitConfiguration} is used to configure the fitting and filtering. The results
* are filtered using only the implementation of
* {@link IDirectFilter#validate(PreprocessedPeakResult)}. This will use a configured DirectFilter
* or else use the current filter configuration.
*
*
The implementation of
* {@link uk.ac.sussex.gdsc.smlm.fitting.Gaussian2DFitConfiguration#validateFit(int, double[], double[], double[])
* Gaussian2DFitConfiguration.validateFit} has the usage of the direct filter and the filter
* configuration disabled. This method is only used in a preliminary filtering of results that are
* outside the region bounds.
*/
public class FitWorker implements Runnable, IMultiPathFitResults, SelectedResultStore {
/**
* The number of additional iterations to use for multiple peaks.
*
*
Testings on a idealised dataset of simulated data show that multiple peaks increases the
* iterations but the increase asymptotes. Initial rate is 2-fold for 7x7 region, decreasing to
* 1.5-fold for 17x17 region. Best solution is to add a set of iterations for each additional
* peak.
*/
public static final int ITERATION_INCREASE_FOR_MULTIPLE_PEAKS = 1; // 0 for no effect
/**
* The number of additional iterations to use for doublets.
*
*
Testings on a idealised dataset of simulated data show that fitting the doublets increases
* the iterations by approx 3.5-fold for 7x7 region, decreasing to 2.5-fold for 17x17 region. An
* additional doubling of iterations were used when the fit of the doublet resulted in one peak
* being eliminated for moving.
*/
public static final int ITERATION_INCREASE_FOR_DOUBLETS = 4; // 1 for no effect
/** The number of additional evaluations to use for multiple peaks. */
public static final int EVALUATION_INCREASE_FOR_MULTIPLE_PEAKS = 1; // 0 for no effect
/** The number of additional evaluations to use for doublets. */
public static final int EVALUATION_INCREASE_FOR_DOUBLETS = 4; // 1 for no effect
/** The logger. */
final Logger logger;
/** The debug logger. */
Logger debugLogger;
private FitTypeCounter counter;
private long time;
private MaximaSpotFilter spotFilter;
private Rectangle lastBounds;
/** The fitting box region size (2n+1). */
int fitting = 1;
// Used for fitting
/** The fit engine config. */
final FitEngineConfiguration config;
/** The fit config. */
final FitConfiguration fitConfig;
private MultiPathFilter filter;
private final PeakResults results;
private final PSFType psfType;
private final BlockingQueue jobs;
/** The Gaussian fitter. */
final Gaussian2DFitter gf;
/** Cached value of the initial X standard deviation. */
final double xsd;
/** Cached value of the initial Y standard deviation. */
final double ysd;
// Used for fitting methods
private LocalList sliceResults;
private boolean useFittedBackground;
private Statistics fittedBackground;
/** The current slice (frame). */
int slice;
private int endT;
/** The coordinate converter. */
CoordinateConverter cc;
private boolean newBounds;
private final BlockAverageDataProcessor backgroundSmoothing = new BlockAverageDataProcessor(0, 1);
// private Rectangle regionBounds;
private int border;
private int borderLimitX;
private int borderLimitY;
private FitJob job;
private boolean benchmarking;
/** Flag if the local background is required. */
boolean localBackground;
/** The data for the current image frame. */
float[] data;
private DataEstimator dataEstimator;
// private float[] filteredData;
/** Flag if the spot filter is not absolute intensity. */
boolean relativeIntensity;
private float noise;
private final boolean calculateNoise;
/**
* Flag to indicate that the fit window covers enough of the initial peak width to allow the
* signal to be estimated.
*/
boolean estimateSignal;
private CandidateGridManager gridManager;
/** Contains the index in the list of maxima for any neighbours. */
int candidateNeighbourCount;
/** The candidate neighbours. */
Candidate[] candidateNeighbours;
/** Contains the index in the list of fitted results for any neighbours. */
int fittedNeighbourCount;
/**
* The fitted neighbours use the same parameters and result as output from fitting the function.
* They should be converted to PeakResults at the end of fitting. This allows using different
* representations of the PSF.
*/
Candidate[] fittedNeighbours;
private CoordinateStore coordinateStore;
private volatile boolean finished;
private static AtomicInteger nextWorkerId = new AtomicInteger();
private final int workerId;
private static final byte FILTER_RANK_MINIMAL = (byte) 0;
private static final byte FILTER_RANK_PRIMARY = (byte) 1;
/** Flag to indicate that the data is in raw count units (not photon-eletcrons). */
final boolean isFitCameraCounts;
/**
* The total gain of the system. This is only used if fitting in camera counts to accurately model
* the local noise.
*/
final float totalGain;
/** The camera model. */
final CameraModel cameraModel;
/** Flag if this is an EM-CCD camera. This is used during local noise estimation. */
final boolean isEmCcd;
/**
* Count the number of successful fits.
*/
private int success;
private DynamicMultiPathFitResult dynamicMultiPathFitResult;
/** The estimate x offset, relative to the data bounds. */
double estimateOffsetx;
/** The estimate y offset, relative to the data bounds. */
double estimateOffsety;
// Used to add fitted results to the grid for the current fit position.
// This prevents filtering duplicates within the current fit results,
// only with all that has been fit before.
private Candidate[] queue = new Candidate[5];
private int queueSize;
/** The estimates that are within 1 pixel of the candidate. */
Estimate[] estimates = new Estimate[0];
/** The estimates that are further than 1 pixel from the candidate. */
Estimate[] estimates2;
private boolean[] isValid;
/** The candidates for the current image frame. */
CandidateList candidates;
private CandidateList allNeighbours;
private CandidateList allFittedNeighbours;
/**
* Encapsulate all conversion of coordinates between the frame of data (data bounds) and the
* sub-section currently used in fitting (region bounds) and the global coordinate system.
*/
private static class CoordinateConverter {
/** The data bounds. */
final Rectangle dataBounds;
/** The region bounds. */
Rectangle regionBounds;
CoordinateConverter(Rectangle dataBounds) {
this.dataBounds = dataBounds;
}
void setRegionBounds(Rectangle regionBounds) {
this.regionBounds = regionBounds;
}
/**
* Convert from the data bounds to the global bounds.
*
* @param x the x coordinate
* @return the x coordinate
*/
int fromDataToGlobalX(int x) {
return x + dataBounds.x;
}
/**
* Convert from the data bounds to the global bounds.
*
* @param y the y coordinate
* @return the y coordinate
*/
int fromDataToGlobalY(int y) {
return y + dataBounds.y;
}
/**
* Convert from the region bounds to the global bounds.
*
* @param x the x coordinate
* @return the x coordinate
*/
int fromRegionToGlobalX(int x) {
return x + dataBounds.x + regionBounds.x;
}
/**
* Convert from the region bounds to the global bounds.
*
* @param y the y coordinate
* @return the y coordinate
*/
int fromRegionToGlobalY(int y) {
return y + dataBounds.y + regionBounds.y;
}
/**
* Conversion from the raw fit coordinates to the global bounds.
*
* @return the x offset
*/
double fromFitRegionToGlobalX() {
return 0.5 + dataBounds.x + regionBounds.x;
}
/**
* Conversion from the raw fit coordinates to the global bounds.
*
* @return the y offset
*/
double fromFitRegionToGlobalY() {
return 0.5 + dataBounds.y + regionBounds.y;
}
/**
* Conversion from the raw fit coordinates to the data bounds.
*
* @return the x offset
*/
@SuppressWarnings("unused")
public double fromFitRegionToDataX() {
return 0.5 + regionBounds.x;
}
/**
* Conversion from the raw fit coordinates to the data bounds.
*
* @return the y offset
*/
@SuppressWarnings("unused")
public double fromFitRegionToDataY() {
return 0.5 + regionBounds.y;
}
}
/**
* Store an estimate for a spot candidate. This may be aquired during multi fitting of neighbours.
*/
private static class Estimate {
final double[] params;
final byte filterRank;
final double d2;
final double precision;
Estimate(double[] params, byte filterRank, double d2, double precision) {
this.params = params;
this.filterRank = filterRank;
this.d2 = d2;
this.precision = precision;
}
boolean isWeaker(byte filterRank, double d2, double precision) {
if (this.filterRank < filterRank) {
return true;
}
// The spot must be close to the estimate.
// Note that if fitting uses a bounded fitter the estimates
// should always be within 2 (1 pixel max in each dimension).
// This check ensure that unbounded fitters store close estimates.
if (d2 > 2) {
// This is not very close so make sure the closest estimate is stored
return (this.d2 > d2);
}
// If it is close enough then we use to fit precision.
return this.precision > precision;
}
}
/**
* Allow recording the pass/fail events sent to the FailCounter from the MultiPathFilter.
*/
private class RecordingFailCounter implements FailCounter {
final boolean[] pass;
final FailCounter failCounter;
RecordingFailCounter(boolean[] pass, FailCounter failCounter) {
this.pass = pass;
this.failCounter = failCounter;
}
@Override
public String getDescription() {
return failCounter.getDescription();
}
@Override
public void pass() {
// We record that this candidate generated new fit results
pass[dynamicMultiPathFitResult.getCandidateId()] = true;
failCounter.pass();
}
@Override
public void pass(int n) {
throw new IllegalStateException("Cannot record multiple passes");
}
@Override
public void fail() {
failCounter.fail();
}
@Override
public void fail(int n) {
throw new IllegalStateException("Cannot record multiple fails");
}
@Override
public boolean isOk() {
return failCounter.isOk();
}
@Override
public FailCounter newCounter() {
throw new IllegalStateException("Cannot record to a new instance");
}
@Override
public void reset() {
failCounter.reset();
}
}
/**
* Instantiates a new fit worker.
*
* Note that if the fit configuration has fit validation enabled then the initial fit results
* will be validated using only the basic filtering setting of the fit configuration. The use of
* the smart filter will be disabled. Once all results have passed the basic validation the
* results are then filtered again using the IDirectFilter implementation of the fit
* configuration. This will use a configured smart filter if present.
*
* @param config the configuration
* @param results the results
* @param jobs the jobs
* @throws ConfigurationException if the configuration is invalid
*/
public FitWorker(FitEngineConfiguration config, PeakResults results, BlockingQueue jobs) {
this.config = config;
this.fitConfig = config.getFitConfiguration();
// The fitting method is current tied to a Gaussian 2D function
final PSF psf = fitConfig.getPsf();
if (!PsfHelper.isGaussian2D(psf)) {
throw new ConfigurationException("Gaussian 2D PSF required");
}
psfType = psf.getPsfType();
this.results = results;
this.jobs = jobs;
this.logger = fitConfig.getLog();
gf = new FastGaussian2DFitter(fitConfig);
// Cache for convenience
xsd = fitConfig.getInitialXSd();
ysd = fitConfig.getInitialYSd();
// Used for duplicate checking
coordinateStore = CoordinateStoreFactory.create(0, 0, 0, 0,
config.convertUsingHwhMax(config.getDuplicateDistanceParameter()));
calculateNoise = fitConfig.getNoise() <= 0;
if (!calculateNoise) {
noise = (float) fitConfig.getNoise();
}
// Disable the use of the direct filter and simple filtering within the FitConfiguration
// validate method. This allows validate() to be used for basic filtering of all fit results
// (e.g. using the fit region bounds).
// The validation of each result will be performed by the FitConfiguration implementation
// of the IDirectFilter interface. This may involve the DirectFilter object or else it defers
// to the simple filtering.
// TODO - Verify if simple filter can be set as a direct filter using
// if (fitConfig.getSmartFilterName().isEmpty()) {
// fitConfig.setDirectFilter(fitConfig.getDefaultSmartFilter());
// }
fitConfig.setSmartFilter(false);
fitConfig.setDisableSimpleFilter(true);
workerId = nextWorkerId.getAndIncrement();
// Store this flag so we know how to process the data
isFitCameraCounts = fitConfig.isFitCameraCounts();
cameraModel = fitConfig.getCameraModel();
// When fitting counts distinguish if the camera model is valid or unknown
if (isFitCameraCounts && fitConfig.hasValidCameraModelType()
&& !cameraModel.isPerPixelModel()) {
// In this case the model has a global gain to convert to photons.
// This can be used for local noise estimation.
totalGain = cameraModel.getGain(0, 0);
} else {
totalGain = 0;
}
isEmCcd = fitConfig.getCalibrationReader().isEmCcd();
}
/**
* Set the parameters for smoothing the image, searching for maxima and fitting maxima. This
* should be called before the {@link #run()} method to configure the fitting.
*
* @param spotFilter The spot filter for identifying fitting candidates
* @param fitting The block size to be used for fitting
*/
public void setSearchParameters(MaximaSpotFilter spotFilter, int fitting) {
this.spotFilter = spotFilter;
lastBounds = null;
this.border = spotFilter.getBorder();
this.fitting = fitting;
this.relativeIntensity = !spotFilter.isAbsoluteIntensity();
// We can estimate the signal for a single peak when the fitting window covers enough of the
// Gaussian
estimateSignal = 2.5 * config.getHwhmMax() / Gaussian2DFunction.SD_TO_HWHM_FACTOR < fitting;
}
@Override
public void run() {
try {
while (!finished) {
final FitJob fitjob = jobs.take();
if (fitjob == null || fitjob.data == null || finished) {
break;
}
run(fitjob);
}
} catch (final InterruptedException ex) {
if (!finished) {
Logger.getLogger(FitWorker.class.getName()).log(Level.WARNING,
() -> "Interrupted: " + ex.toString());
Thread.currentThread().interrupt();
throw new ConcurrentRuntimeException(ex);
}
} finally {
finished = true;
}
}
/**
* Locate all the peaks in the image specified by the fit job.
*
* WARNING: The FitWorker fits a sub-region of the data for each maxima. It then updates the
* FitResult parameters with an offset reflecting the position. The initialParameters are not
* updated with this offset unless configured.
*
* @param job The fit job
*/
public void run(FitJob job) {
final long start = System.nanoTime();
job.start();
this.job = job;
benchmarking = false;
this.slice = job.slice;
// Used for debugging
// if (logger == null) logger = new gdsc.fitting.logging.ConsoleLogger();
// Crop to the ROI
cc = new CoordinateConverter(job.bounds);
// Note if the bounds change for efficient caching.
newBounds = !cc.dataBounds.equals(lastBounds);
if (newBounds) {
lastBounds = cc.dataBounds;
}
final int width = cc.dataBounds.width;
final int height = cc.dataBounds.height;
borderLimitX = width - border;
borderLimitY = height - border;
data = job.data;
dataEstimator = null; // This is tied to the input data
// 06-Jun-2017
// The data model was changed to store the signal in photons.
// This allows support for per-pixel bias and gain (sCMOS cameras).
// Remove the bias and gain. This is done for all solvers except:
// - the legacy MLE solvers which model camera amplification
// - the basic LVM solver without a camera calibration
// Note: Assume that the camera model has been correctly initialised to be
// relative to the global origin.
if (isFitCameraCounts) {
cameraModel.removeBias(cc.dataBounds, data);
} else {
cameraModel.removeBiasAndGain(cc.dataBounds, data);
}
final FitParameters params = job.getFitParameters();
this.endT = (params != null) ? params.endT : -1;
candidates = indentifySpots(job, width, height, params);
if (candidates.getSize() == 0) {
finishJob(job, start);
return;
}
fittedBackground = new Statistics();
// TODO - Better estimate of the background and the noise. Using all the image pixels
// results in an estimate that is too high when there are many spots in the image.
// Create a method that thresholds the image and finds the mean/sd of the thresholded image.
// Note: Other code calls the static estimateNoise method.
// So add a private instance method to estimate the noise and background using a static helper
// class. This can also be called from the static estimateNoise method.
// Always get the noise and store it with the results.
if (params != null && !Float.isNaN(params.noise)) {
noise = params.noise;
fitConfig.setNoise(noise);
} else if (calculateNoise) {
noise = estimateNoise();
fitConfig.setNoise(noise);
}
// System.out.printf("Slice %d : Noise = %g\n", slice, noise);
if (logger != null) {
LoggerUtils.log(logger, Level.INFO, "Slice %d: Noise = %f", slice, noise);
}
final ImageExtractor ie = ImageExtractor.wrap(data, width, height);
double[] region = null;
final float offsetx = cc.dataBounds.x;
final float offsety = cc.dataBounds.y;
if (params != null && params.fitTask == FitTask.MAXIMA_IDENITIFICATION) {
final float sd0 = (float) xsd;
final float sd1 = (float) ysd;
for (int n = 0; n < candidates.getSize(); n++) {
// Find the background using the perimeter of the data.
// TODO - Perhaps the Gaussian Fitter should be used to produce the initial estimates but no
// actual fit done.
// This would produce coords using the centre-of-mass.
final Candidate candidate = candidates.get(n);
int x = candidate.x;
int y = candidate.y;
final Rectangle regionBounds = ie.getBoxRegionBounds(x, y, fitting);
region = ie.crop(regionBounds, region);
final float b = (float) Gaussian2DFitter.getBackground(region, regionBounds.width,
regionBounds.height, 1);
// Offset the coords to the centre of the pixel. Note the bounds will be added later.
// Subtract the background to get the amplitude estimate then convert to signal.
final float amplitude = candidate.intensity - ((relativeIntensity) ? 0 : b);
final float signal = (float) (amplitude * 2.0 * Math.PI * sd0 * sd1);
final int index = y * width + x;
x += offsetx;
y += offsety;
final float[] peakParams = new float[1 + Gaussian2DFunction.PARAMETERS_PER_PEAK];
peakParams[Gaussian2DFunction.BACKGROUND] = b;
peakParams[Gaussian2DFunction.SIGNAL] = signal;
peakParams[Gaussian2DFunction.X_POSITION] = x + 0.5f;
peakParams[Gaussian2DFunction.Y_POSITION] = y + 0.5f;
// peakParams[Gaussian2DFunction.Z_POSITION] = 0;
peakParams[Gaussian2DFunction.X_SD] = sd0;
peakParams[Gaussian2DFunction.Y_SD] = sd1;
// peakParams[Gaussian2DFunction.ANGLE] = 0;
final float u = (float) Gaussian2DPeakResultHelper.getMeanSignalUsingP05(signal, sd0, sd1);
sliceResults.add(createResult(x, y, data[index], 0, noise, u, peakParams, null, n, 0));
}
} else {
initialiseFitting();
// Smooth the data to provide initial background estimates
final float[] smoothedData = backgroundSmoothing.process(data, width, height);
final ImageExtractor ie2 = ImageExtractor.wrap(smoothedData, width, height);
// Perform the Gaussian fit
// The SpotFitter is used to create a dynamic MultiPathFitResult object.
// This is then passed to a multi-path filter. Thus the same fitting decision process
// is used when benchmarking and when running on actual data.
// Note: The SpotFitter labels each PreprocessedFitResult using the offset in the FitResult
// object.
// The initial params and deviations can then be extracted for the results that pass the
// filter.
MultiPathFilter filter;
final IMultiPathFitResults multiPathResults = this;
final SelectedResultStore store = this;
coordinateStore = coordinateStore.resize(cc.dataBounds.x, cc.dataBounds.y, width, height);
// TODO - Test if duplicate distance is now obsolete ...
if (params != null && params.fitTask == FitTask.BENCHMARKING) {
// Run filtering as normal. However in the event that a candidate is missed or some
// results are not generated we must generate them. This is done in the complete(int)
// method if we set the benchmarking flag.
benchmarking = true;
// Filter using the benchmark filter
filter = params.benchmarkFilter;
if (filter == null) {
// Create a default filter using the standard FitConfiguration to ensure sensible fits
// are stored as the current slice results.
// Note the current fit configuration for benchmarking may have minimal filtering settings
// so we do not use that object.
final FitConfiguration tmp = FitConfiguration.create();
final double residualsThreshold = 0.4;
filter = new MultiPathFilter(tmp, createMinimalFilter(PrecisionMethod.POISSON_CRLB),
residualsThreshold);
}
} else {
// Filter using the configuration.
if (this.filter == null) {
// This can be cached. Q. Clone the config?
this.filter = new MultiPathFilter(fitConfig,
createMinimalFilter(fitConfig.getPrecisionMethod()), config.getResidualsThreshold());
}
filter = this.filter;
}
// If we are benchmarking then do not generate results dynamically since we will store all
// results in the fit job.
dynamicMultiPathFitResult = new DynamicMultiPathFitResult(ie, ie2, !benchmarking);
// dynamicMultiPathFitResult = new DynamicMultiPathFitResult(ie, false);
// The local background computation is only required for the precision method.
// Also compute it when benchmarking.
localBackground = benchmarking || fitConfig
.getPrecisionMethodValue() == PrecisionMethod.MORTENSEN_LOCAL_BACKGROUND_VALUE;
// Debug where the fit config may be different between benchmarking and fitting
if (slice == -1) {
fitConfig.initialise(1, 1, 1);
final String newLine = System.lineSeparator();
final String tmpdir = System.getProperty("java.io.tmpdir");
try (BufferedWriter writer =
Files.newBufferedWriter(Paths.get(tmpdir, String.format("config.%d.txt", slice)))) {
JsonFormat.printer().appendTo(config.getFitEngineSettings(), writer);
} catch (final IOException ex) {
logger.log(Level.SEVERE, "Unable to write message", ex);
}
FileUtils.save(Paths.get(tmpdir, String.format("filter.%d.xml", slice)).toString(),
filter.toXml());
// filter.setDebugFile(String.format("/tmp/fitWorker.%b.txt", benchmarking));
final StringBuilder sb = new StringBuilder(512);
sb.append((benchmarking)
? ((uk.ac.sussex.gdsc.smlm.results.filter.Filter) filter.getFilter()).toXml()
: fitConfig.getSmartFilterString()).append(newLine)
//@formatter:off
.append(
((uk.ac.sussex.gdsc.smlm.results.filter.Filter) filter.getMinimalFilter()).toXml())
.append(newLine)
.append(filter.residualsThreshold).append(newLine)
.append(config.getFailuresLimit()).append(newLine)
.append(config.getDuplicateDistance()).append(':')
.append(config.getDuplicateDistanceAbsolute()).append(newLine);
//@formatter:on
if (spotFilter != null) {
sb.append(spotFilter.getDescription()).append(newLine);
}
sb.append("MaxCandidate = ").append(candidates.getSize()).append(newLine);
for (int i = 0, len = candidates.getLength(); i < len; i++) {
TextUtils.formatTo(sb, "Fit %d [%d,%d = %.1f]%n", i, candidates.get(i).x,
candidates.get(i).y, candidates.get(i).intensity);
}
FileUtils.save(Paths.get(tmpdir, String.format("candidates.%d.xml", slice)).toString(),
sb.toString());
}
FailCounter failCounter = config.getFailCounter();
if (!benchmarking && params != null && params.pass != null) {
// We want to store the pass/fail for consecutive candidates
params.pass = new boolean[candidates.getLength()];
failCounter = new RecordingFailCounter(params.pass, failCounter);
filter.select(multiPathResults, failCounter, true, store, coordinateStore);
} else {
filter.select(multiPathResults, failCounter, true, store, coordinateStore);
}
// Note: We go deeper into the candidate list than max candidate
// for any candidate where we have a good fit result as an estimate.
// Q. Should this only be for benchmarking?
// if (benchmarking)
// System.out.printf("Slice %d: %d + %d\n", slice, dynamicMultiPathFitResult.extra,
// candidates.getSize());
// Create the slice results
final CandidateList fitted = gridManager.getFittedCandidates();
sliceResults.ensureCapacity(fitted.getSize());
for (int i = 0; i < fitted.getSize(); i++) {
if (fitted.get(i).fit) {
sliceResults.push(createResult(offsetx, offsety, fitted.get(i)));
}
}
if (logger != null) {
LoggerUtils.log(logger, Level.INFO, "Slice %d: %d / %d = %s", slice, success,
candidates.getSize(), TextUtils.pleural(fitted.getSize(), "result"));
}
}
this.results.addAll(sliceResults);
finishJob(job, start);
}
private CandidateList indentifySpots(FitJob job, int width, int height, FitParameters params) {
Spot[] spots = null;
int maxCandidate = 0;
int[] maxIndices = null;
// Only sub-classes may require the indices
final boolean requireIndices = (job.getClass() != FitJob.class);
if (params != null) {
maxCandidate = params.maxCandidate;
if (params.spots != null) {
spots = params.spots;
if (maxCandidate <= 0 || maxCandidate > spots.length) {
maxCandidate = spots.length;
}
// Get the indices for all candidates, even above the max candidate
// maxIndices = new int[maxCandidate];
// for (int n = 0; n < maxCandidate; n++)
// {
// maxIndices[n] = spots[n].y * width + spots[n].x;
// }
maxIndices = new int[spots.length];
for (int n = 0; n < maxIndices.length; n++) {
maxIndices[n] = spots[n].y * width + spots[n].x;
}
} else if (params.maxIndices != null) {
// Extract the desired spots
maxIndices = params.maxIndices;
if (maxCandidate <= 0 || maxCandidate > maxIndices.length) {
maxCandidate = maxIndices.length;
} else {
maxIndices = Arrays.copyOf(maxIndices, maxCandidate);
}
final float[] data2 = initialiseSpotFilter().preprocessData(data, width, height);
spots = new Spot[maxIndices.length];
for (int n = 0; n < maxIndices.length; n++) {
final int y = maxIndices[n] / width;
final int x = maxIndices[n] % width;
final float intensity = data2[maxIndices[n]];
spots[n] = new Spot(x, y, intensity);
}
// Sort the maxima
Arrays.sort(spots, SpotScoreComparator.getInstance());
}
}
if (spots == null) {
// Run the filter to get the spot
spots = initialiseSpotFilter().rank(data, width, height);
maxCandidate = spots.length;
// filteredData = spotFilter.getPreprocessedData();
// Extract the indices
if (requireIndices) {
maxIndices = new int[spots.length];
for (int n = 0; n < maxIndices.length; n++) {
maxIndices[n] = spots[n].y * width + spots[n].x;
}
}
}
if (logger != null) {
LoggerUtils.log(logger, Level.INFO, "%d: Slice %d: %d candidates", workerId, slice,
maxCandidate);
}
sliceResults = new LocalList<>(maxCandidate);
if (requireIndices) {
job.setResults(sliceResults);
job.setIndices(maxIndices);
}
final Candidate[] list = new Candidate[spots.length];
for (int i = 0; i < spots.length; i++) {
list[i] = new Candidate(spots[i], i);
}
return new CandidateList(maxCandidate, list);
}
private MaximaSpotFilter initialiseSpotFilter() {
// Use a per-pixel variance for weighting.
// Only get this if the bounds have changed to enable efficient caching.
if (cameraModel.isPerPixelModel() && spotFilter.isWeighted() && newBounds) {
// The weights should be for the raw data or the normalised data (gain subtracted)
float[] weights;
if (isFitCameraCounts) {
weights = cameraModel.getWeights(cc.dataBounds);
} else {
weights = cameraModel.getNormalisedWeights(cc.dataBounds);
}
spotFilter.setWeights(weights, cc.dataBounds.width, cc.dataBounds.height);
}
return spotFilter;
}
private void finishJob(FitJob job, final long start) {
time += System.nanoTime() - start;
job.finished();
}
private void initialiseFitting() {
// Note that a ParameterisedFitJob can pass in a maxCandidate and the list is longer
// than candidates.getSize(). We must allocate for the maximum candidate Id (even if not
// processed).
final int length = candidates.getLength();
candidateNeighbours = allocateArray(candidateNeighbours, length);
// Allocate enough room for all fits to be doublets
fittedNeighbours = allocateArray(fittedNeighbours, length * 2);
success = 0;
clearEstimates(length);
final int width = cc.dataBounds.width;
final int height = cc.dataBounds.height;
gridManager = new CandidateGridManager(width, height, 2 * fitting + 1);
for (int i = 0; i < length; i++) {
gridManager.putCandidateOnGrid(candidates.get(i));
}
// No longer used
// if newBounds:
// Allow weighted smoothing for the background estimation
// float[] w = cameraModel.getWeights(cc.dataBounds);
// backgroundSmoothing.setWeights(w, cc.dataBounds.width, cc.dataBounds.height);
}
/**
* Queue to grid.
*
*
Used to add results to the grid for the current fit position. This prevents filtering
* duplicates within the current fit results, only with all that has been fit before.
*
* @param result the result
*/
private void queueToGrid(Candidate result) {
if (queueSize == queue.length) {
queue = Arrays.copyOf(queue, queueSize * 2);
}
queue[queueSize++] = result;
}
/**
* Flush the results for the current position to the grid.
*
* @return true, if successful
*/
private boolean flushToGrid() {
if (queueSize == 0) {
return false;
}
for (int i = 0; i < queueSize; i++) {
gridManager.putFittedOnGrid(queue[i]);
}
queueSize = 0;
return true;
}
/**
* Clear the grid cache of the local neighbourhood.
*/
private void clearGridCache() {
gridManager.clearCache();
}
private static Candidate[] allocateArray(Candidate[] array, int length) {
if (array == null || array.length < length) {
array = new Candidate[length];
}
return array;
}
private void clearEstimates(int length) {
if (estimates.length < length) {
estimates = new Estimate[length];
estimates2 = new Estimate[length];
isValid = new boolean[length];
} else {
for (int i = 0; i < length; i++) {
estimates[i] = null;
estimates2[i] = null;
isValid[i] = false;
}
}
}
/**
* Add the result to the list. Only check for duplicates in the current results grid.
*
* @param candidateId the candidate id
* @param peakParams the peak params
* @param peakParamDevs the peak params dev
* @param error the error
* @param noise the noise
* @param locationVariance the location variance (in nm)
* @return true, if successful
*/
private boolean addSingleResult(int candidateId, float[] peakParams, float[] peakParamDevs,
double error, float noise, double locationVariance) {
final Candidate c = candidates.get(candidateId);
// Check if inside the allowed border
final boolean inside = insideBorder(peakParams[Gaussian2DFunction.X_POSITION],
peakParams[Gaussian2DFunction.Y_POSITION]);
// Add it to the grid of results (so we do not fit it again)
final int x = (int) peakParams[Gaussian2DFunction.X_POSITION];
final int y = (int) peakParams[Gaussian2DFunction.Y_POSITION];
final float u = (float) Gaussian2DPeakResultHelper.getMeanSignalUsingP05(
peakParams[Gaussian2DFunction.SIGNAL], peakParams[Gaussian2DFunction.X_SD],
peakParams[Gaussian2DFunction.Y_SD]);
final Candidate fitted =
c.createFitted(x, y, candidateId, peakParams, peakParamDevs, error, noise, u, inside);
if (locationVariance > 0) {
fitted.precision = Math.sqrt(locationVariance);
}
queueToGrid(fitted);
c.fit = true;
// Check if the position is inside the border tolerance
if (inside) {
fittedBackground.add(peakParams[Gaussian2DFunction.BACKGROUND]);
} else if (logger != null) {
LoggerUtils.log(logger, Level.INFO, "[%d] Ignoring peak within image border @ %.2f,%.2f",
slice, peakParams[Gaussian2DFunction.X_POSITION],
peakParams[Gaussian2DFunction.Y_POSITION]);
}
return true;
}
private PeakResult createResult(float offsetx, float offsety, Candidate fitted) {
final int candidateId = fitted.index;
final Candidate c = candidates.get(candidateId);
int x = c.x;
int y = c.y;
final float value = data[y * cc.dataBounds.width + x];
// Update to the global bounds.
x += offsetx;
y += offsety;
final float[] params = fitted.params;
params[Gaussian2DFunction.X_POSITION] += offsetx;
params[Gaussian2DFunction.Y_POSITION] += offsety;
return createResult(x, y, value, fitted.error, fitted.noise, fitted.meanIntensity, params,
fitted.paramDevs, candidateId, fitted.precision);
}
private PeakResult createResult(int origX, int origY, float origValue, double error, float noise,
float meanIntensity, float[] params, float[] paramDevs, int id, double precision) {
// Convert to a variable PSF parameter PeakResult
params = Gaussian2DPeakResultHelper.createParams(psfType, params);
if (paramDevs != null) {
paramDevs = Gaussian2DPeakResultHelper.createParams(psfType, paramDevs);
// Convert variances to standard deviations
for (int i = 0; i < paramDevs.length; i++) {
paramDevs[i] = (float) Math.sqrt(paramDevs[i]);
}
}
if (precision > 0) {
final AttributePeakResult r = new AttributePeakResult(slice, origX, origY, origValue, error,
noise, meanIntensity, params, paramDevs);
r.setId(id);
r.setPrecision(precision);
if (endT >= 0 && slice != endT) {
r.setEndFrame(endT);
}
return r;
}
if (endT >= 0 && slice != endT) {
return new ExtendedPeakResult(slice, origX, origY, origValue, error, noise, meanIntensity,
params, paramDevs, endT, id);
}
return new IdPeakResult(slice, origX, origY, origValue, error, noise, meanIntensity, params,
paramDevs, id);
}
/**
* Inside border.
*
* @param x the x
* @param y the y
* @return True if the fitted position is inside the border
*/
private boolean insideBorder(float x, float y) {
return (x > border && x < borderLimitX && y > border && y < borderLimitY);
}
/**
* Get the squared distance.
*
* @param params1 the params 1
* @param params2 the params 2
* @return The squared distance between the two points
*/
@SuppressWarnings("unused")
private static float distance2(float[] params1, float[] params2) {
final float dx =
params1[Gaussian2DFunction.X_POSITION] - params2[Gaussian2DFunction.X_POSITION];
final float dy =
params1[Gaussian2DFunction.Y_POSITION] - params2[Gaussian2DFunction.Y_POSITION];
return dx * dx + dy * dy;
}
/**
* Provide the ability to convert raw fitted results into PreprocessedPeakResult for validation.
*/
private abstract class ResultFactory {
final float offsetx;
final float offsety;
ResultFactory(float offsetx, float offsety) {
this.offsetx = offsetx;
this.offsety = offsety;
}
PreprocessedPeakResult createPreprocessedPeakResult(int candidateId, int n,
double[] initialParams, double[] params, double[] paramVariances,
PeakResultValidationData peakResultValidationData, ResultType resultType) {
// if (dynamicMultiPathFitResult.candidateId < candidateId && resultType == ResultType.NEW)
// System.out.println("WTF");
// Update the initial params since we may have used an estimate
// This will ensure that the width factor is computed correctly.
// Q. Should this be ignored for existing results? They have already passed validation.
// So we do not have to be as strict on their width and could just use the drift from
// the initial estimate.
// For now do a full validation since multi-fit results are only accepted if existing
// results are still valid.
final int offset = n * Gaussian2DFunction.PARAMETERS_PER_PEAK;
initialParams[Gaussian2DFunction.X_SD + offset] = xsd;
initialParams[Gaussian2DFunction.Y_SD + offset] = ysd;
return createResult(candidateId, n, initialParams, params, paramVariances,
peakResultValidationData, resultType);
}
abstract PreprocessedPeakResult createResult(int candidateId, int n, double[] initialParams,
double[] params, double[] paramVariances, PeakResultValidationData peakResultValidationData,
ResultType resultType);
}
/**
* Provide dynamic PreprocessedPeakResult. This is basically a wrapper around the result arrays
* that provides properties on-the-fly.
*/
private class DynamicResultFactory extends ResultFactory {
DynamicResultFactory(float offsetx, float offsety) {
super(offsetx, offsety);
}
@Override
PreprocessedPeakResult createResult(int candidateId, int n, double[] initialParams,
double[] params, double[] paramVariances, PeakResultValidationData peakResultValidationData,
ResultType resultType) {
return fitConfig.createDynamicPreprocessedPeakResult(candidateId, n, initialParams, params,
paramVariances, peakResultValidationData, resultType, offsetx, offsety);
}
}
/**
* Provide a materialised PreprocessedPeakResult as a new object with all properties computed.
*/
private class FixedResultFactory extends ResultFactory {
FixedResultFactory(float offsetx, float offsety) {
super(offsetx, offsety);
}
@Override
PreprocessedPeakResult createResult(int candidateId, int n, double[] initialParams,
double[] params, double[] paramVariances, PeakResultValidationData peakResultValidationData,
ResultType resultType) {
return fitConfig.createPreprocessedPeakResult(slice, candidateId, n, initialParams, params,
paramVariances, peakResultValidationData, resultType, offsetx, offsety);
}
}
/**
* Provide dynamic PeakResultValidationData.
*/
private abstract class DynamicPeakResultValidationData
implements FitConfiguration.PeakResultValidationData {
int peak;
double[] params;
double[][] localStats;
DynamicPeakResultValidationData(int peak) {
localStats = new double[peak][];
}
@Override
public void setResult(int peak, double[] initialParams, double[] params, double[] paramDevs) {
this.peak = peak;
this.params = params;
}
/**
* Compute validation data for the peak.
*
* @param peak the peak
*/
protected abstract void compute(int peak);
@Override
public double getLocalBackground() {
// Do not compute a local background unless it is required by the configuration
if (!localBackground) {
return 0;
}
if (localStats[peak] == null) {
compute(peak);
}
return localStats[peak][Gaussian2DFunction.BACKGROUND];
}
@Override
public double getNoise() {
if (localStats[peak] == null) {
compute(peak);
}
return localStats[peak][1];
}
}
/**
* Provide functionality to fit spots in a region using different methods. Decisions about what to
* accept are not performed. The fit results are just converted to PreprocessedPeakResult objects
* for validation.
*/
private class CandidateSpotFitter {
// TODO: When using an astigmatism z-model it is possible to fit two spots that are colocated.
// So the colocation checks to existing peaks should be refined to allow both spots if they
// have suitably different z-depths (i.e. X/Y widths).
final Gaussian2DFitter gf;
final ResultFactory resultFactory;
final double[] region;
final double[] region2; // ; final double[] varG2;
final Rectangle regionBounds;
final int candidateId;
final int width;
final int height;
static final int PARAMETERS_PER_PEAK = Gaussian2DFunction.PARAMETERS_PER_PEAK;
final FloatAreaSum area;
int neighbours;
double singleBackground = Double.NaN;
double multiBackground = Double.NaN;
/**
* Flag each neighbour peak that is pre-computed for the multi-fit. These are fitted peaks
* outside the fit region.
*/
private boolean[] precomputed;
/** The pre-computed fitted neighbour count. */
int precomputedFittedNeighbourCount = -1;
double[] precomputedFunctionParamsMulti;
double[] precomputedFittedNeighboursMulti;
MultiPathFitResult.FitResult resultMulti;
boolean computedMulti;
double[] residualsMulti;
double valueMulti;
MultiPathFitResult.FitResult resultDoubletMulti;
boolean computedDoubletMulti;
QuadrantAnalysis qaMulti;
double[] precomputedFunctionParamsSingle;
double[] precomputedFittedNeighboursSingle;
MultiPathFitResult.FitResult resultSingle;
double[] residualsSingle;
double valueSingle;
MultiPathFitResult.FitResult resultDoubletSingle;
boolean computedDoubletSingle;
QuadrantAnalysis qaSingle;
CandidateSpotFitter(Gaussian2DFitter gf, ResultFactory resultFactory, double[] region,
double[] region2, double[] varG2, Rectangle regionBounds, int candidateId,
FloatAreaSum area) {
this.gf = gf;
this.resultFactory = resultFactory;
this.region = region;
this.region2 = region2;
this.regionBounds = regionBounds;
this.candidateId = candidateId;
this.area = area;
// Initialise
width = regionBounds.width;
height = regionBounds.height;
fitConfig.setFitRegion(width, height, 0.5);
// The variance is always needed in each fit of the same data
fitConfig.setObservationWeights(varG2);
// Analyse neighbours and include them in the fit if they are within a set height of this
// peak.
resetNeighbours();
neighbours =
findNeighboursInRegion(regionBounds, candidateId, (float) getFittingBackgroundSingle());
}
@SuppressWarnings("unused")
private double getFittingBackgroundMulti() {
if (Double.isNaN(multiBackground)) {
multiBackground = 0;
if (fittedNeighbourCount > 0) {
// Use the average previously fitted background
// Add the details of the already fitted peaks
for (int i = 0; i < fittedNeighbourCount; i++) {
multiBackground += fittedNeighbours[i].params[Gaussian2DFunction.BACKGROUND];
}
multiBackground /= fittedNeighbourCount;
multiBackground = limitBackground(multiBackground);
} else {
multiBackground = this.getFittingBackgroundSingle();
}
}
return multiBackground;
}
private double getFittingBackgroundSingle() {
if (Double.isNaN(singleBackground)) {
// Use the min in smoothed data. This avoids noise
singleBackground = getDefaultBackground(region2, width, height);
}
return singleBackground;
}
private double getDefaultBackground(double[] region, int width, int height) {
// Use the minimum in the data.
// This is what is done in the fitter if the background is zero.
return limitBackground(Gaussian2DFitter.getBackground(region, width, height, 2));
}
private double limitBackground(double background) {
// Ensure we do not get a negative background
return (background < 0) ? 0 : background;
}
private double getMax(double[] region, int width, int height) {
double max = region[0];
for (int i = width * height; --i > 0;) {
if (max < region[i]) {
max = region[i];
}
}
return max;
}
private int getPrecomputedNeighbourCount() {
if (precomputedFittedNeighbourCount == -1) {
precomputedFittedNeighbourCount = 0;
if (fittedNeighbourCount > 0) {
precomputed = new boolean[fittedNeighbourCount];
// The fitted result will be relative to (0,0) in the fit data and already
// have an offset applied so that 0.5 is the centre of a pixel. (Note: the
// parameters were created from a PreprocessedPeakResult generated by the
// PreprocessedPeakResults factory.)
// We can test the coordinates exactly against the fit frame.
final float xmin = regionBounds.x;
final float xmax = xmin + regionBounds.width;
final float ymin = regionBounds.y;
final float ymax = ymin + regionBounds.height;
for (int i = 0; i < fittedNeighbourCount; i++) {
final float[] params = fittedNeighbours[i].params;
final float x = params[Gaussian2DFunction.X_POSITION];
final float y = params[Gaussian2DFunction.Y_POSITION];
// Pre-compute peaks if they are outside the fit region
if (x < xmin || x > xmax || y < ymin || y > ymax) {
precomputed[i] = true;
precomputedFittedNeighbourCount++;
}
}
}
}
return precomputedFittedNeighbourCount;
}
private double[] getPrecomputedFittedNeighbours() {
if (precomputedFittedNeighboursMulti == null && precomputedFittedNeighbourCount != 0) {
// The fitted result will be relative to (0,0) and have a 0.5 pixel offset applied
final double xOffset = regionBounds.x + 0.5;
final double yOffset = regionBounds.y + 0.5;
// Pre-compute the already fitted peaks.
precomputedFunctionParamsMulti =
new double[1 + PARAMETERS_PER_PEAK * precomputedFittedNeighbourCount];
for (int i = 0, j = 0; i < fittedNeighbourCount; i++) {
if (!precomputed[i]) {
continue;
}
copyFittedParams(i, precomputedFunctionParamsMulti, j);
// Adjust position relative to extracted region
precomputedFunctionParamsMulti[j + Gaussian2DFunction.X_POSITION] -= xOffset;
precomputedFunctionParamsMulti[j + Gaussian2DFunction.Y_POSITION] -= yOffset;
j += PARAMETERS_PER_PEAK;
}
final Gaussian2DFunction func =
fitConfig.createGaussianFunction(precomputedFittedNeighbourCount, width, height);
precomputedFittedNeighboursMulti =
new StandardValueProcedure().getValues(func, precomputedFunctionParamsMulti);
// uk.ac.sussex.gdsc.core.ij.Utils.display("precomputedFunctionValues",
// precomputedFittedNeighboursMulti, width, height);
}
return precomputedFittedNeighboursMulti;
}
MultiPathFitResult.FitResult getResultMulti() {
if (computedMulti) {
return resultMulti;
}
computedMulti = true;
// Do not do a multi-fit if the configuration is not set to include neighbours
if (neighbours == 0 || !config.isIncludeNeighbours()) {
return null;
}
// -=-=-=-
// TODO
//
// If we have fitted neighbours:
// Precompute them and then try a single fit. It should work if
// something is there. This can be used as an initial estimate for one of the
// peaks in the multiple fit (i.e. the closest one)
//
// If the fits fails then we can guess that the region has no good peaks and
// it is not worth doing a multiple fit.
// -=-=-=-
// Flag each peak that is precomputed
getPrecomputedNeighbourCount();
neighbours = candidateNeighbourCount + fittedNeighbourCount - precomputedFittedNeighbourCount;
if (neighbours == 0) {
// There are no neighbours after precomputation.
// This will be the same result as the single result so return.
return null;
}
// Background of fitted peaks within the region
double background = 0;
int backgroundCount = 0;
for (int i = 0; i < fittedNeighbourCount; i++) {
if (!precomputed[i]) {
// Used to estimate the background.
// Q. Should this only use those within the region?
background += fittedNeighbours[i].params[Gaussian2DFunction.BACKGROUND];
backgroundCount++;
}
}
// Create the parameters for the fit
final int npeaks = 1 + neighbours;
// Multiple-fit ...
if (logger != null) {
LoggerUtils.log(logger, Level.INFO,
"Slice %d: Multiple-fit (%d peaks : neighbours [%d + %d - %d])", slice, npeaks,
candidateNeighbourCount, fittedNeighbourCount, precomputedFittedNeighbourCount);
}
final double[] params = new double[1 + npeaks * PARAMETERS_PER_PEAK];
// Estimate background.
// Use the fitted peaks within the region or fall-back to the estimate for
// a single peak (i.e. low point of the region).
params[Gaussian2DFunction.BACKGROUND] =
(backgroundCount == 0) ? getFittingBackgroundSingle() : background / backgroundCount;
// Support bounds on the known fitted peaks
final double[] lower = new double[params.length];
final double[] upper = new double[params.length];
for (int i = 0; i < lower.length; i++) {
lower[i] = Double.NEGATIVE_INFINITY;
upper[i] = Double.POSITIVE_INFINITY;
}
// Note: If difference-of-smoothing is performed the heights have background subtracted so
// it must be added back
final boolean[] amplitudeEstimate = new boolean[npeaks];
// The main peak. We use a close estimate if we have one.
amplitudeEstimate[0] = getEstimate(candidates.get(candidateId), params, 0, true);
// The neighbours
for (int i = 0, j = PARAMETERS_PER_PEAK; i < candidateNeighbourCount;
i++, j += PARAMETERS_PER_PEAK) {
final Candidate candidateNeighbour = candidateNeighbours[i];
amplitudeEstimate[i + 1] = getEstimate(candidateNeighbour, params, j, true);
// Constrain the location using the candidate position.
// Do not use the current estimate as this will create drift over time if the estimate is
// updated.
final double candidateX = candidateNeighbour.x - regionBounds.x;
final double candidateY = candidateNeighbour.y - regionBounds.y;
lower[j + Gaussian2DFunction.X_POSITION] = candidateX - 1;
upper[j + Gaussian2DFunction.X_POSITION] = candidateX + 1;
lower[j + Gaussian2DFunction.Y_POSITION] = candidateY - 1;
upper[j + Gaussian2DFunction.Y_POSITION] = candidateY + 1;
}
// The fitted neighbours
// TODO - Test which is better: (1) precomputing fitted peaks; or (2) including them.
// Initial tests show that Chi-squared is much lower when including them in the fit.
if (fittedNeighbourCount > 0) {
// The fitted result will be relative to (0,0) and have a 0.5 pixel offset applied
final double xOffset = regionBounds.x + 0.5;
final double yOffset = regionBounds.y + 0.5;
getPrecomputedFittedNeighbours();
// Add the details of the already fitted peaks
for (int i = 0, j = (1 + candidateNeighbourCount) * PARAMETERS_PER_PEAK;
i < fittedNeighbourCount; i++) {
if (precomputed[i]) {
continue;
}
copyFittedParams(i, params, j);
// Adjust position relative to extracted region
params[j + Gaussian2DFunction.X_POSITION] -= xOffset;
params[j + Gaussian2DFunction.Y_POSITION] -= yOffset;
// Add support for constraining the known fit results using bounded coordinates.
// Currently we just constrain the location.
lower[j + Gaussian2DFunction.X_POSITION] =
params[j + Gaussian2DFunction.X_POSITION] - 0.5;
upper[j + Gaussian2DFunction.X_POSITION] =
params[j + Gaussian2DFunction.X_POSITION] + 0.5;
lower[j + Gaussian2DFunction.Y_POSITION] =
params[j + Gaussian2DFunction.Y_POSITION] - 0.5;
upper[j + Gaussian2DFunction.Y_POSITION] =
params[j + Gaussian2DFunction.Y_POSITION] + 0.5;
j += PARAMETERS_PER_PEAK;
}
}
// XXX Debugging the bad parameters
// double bbefore = params[Gaussian2DFunction.BACKGROUND];
// double[] before = params.clone();
// In the case of a bad background estimate (e.g. uneven illumination) the peaks may
// be below the background.
// Check the heights are positive.
if (containsAmplitudeEstimates(amplitudeEstimate)) {
// Find the min amplitude
double minSignal = Double.POSITIVE_INFINITY;
for (int j = Gaussian2DFunction.SIGNAL, i = 0; j < params.length;
j += PARAMETERS_PER_PEAK, i++) {
if (amplitudeEstimate[i] && minSignal > params[j]) {
minSignal = params[j];
}
}
// Note: Amplitude estimates are amplitude above the background so we compare to zero
if (minSignal <= 0) {
// Reset background to the minimum value in the data.
final double oldBackground = params[Gaussian2DFunction.BACKGROUND];
params[Gaussian2DFunction.BACKGROUND] = getDefaultBackground(region, width, height);
final double backgroundChange = oldBackground - params[Gaussian2DFunction.BACKGROUND];
// Make the amplitude estimates higher by the change in background
for (int j = Gaussian2DFunction.SIGNAL, i = 0; j < params.length;
j += PARAMETERS_PER_PEAK, i++) {
if (amplitudeEstimate[i]) {
params[j] += backgroundChange;
}
}
if ((minSignal + backgroundChange) <= 0) {
// This is probably extremely rare and the result of a poor candidate estimate.
// Set a small height based on the data range
final double defaultHeight = Math.max(1,
0.1 * (getMax(region, width, height) - params[Gaussian2DFunction.BACKGROUND]));
for (int j = Gaussian2DFunction.SIGNAL, i = 0; j < params.length;
j += PARAMETERS_PER_PEAK, i++) {
if (amplitudeEstimate[i] && params[j] <= 0) {
params[j] = defaultHeight;
}
}
}
}
}
// Note that if the input XY positions are on the integer grid then the fitter will estimate
// the position using a CoM estimate. To avoid this adjust the centres to be off-grid.
for (int i = Gaussian2DFunction.X_POSITION; i < params.length; i += PARAMETERS_PER_PEAK) {
for (int j = 0; j < 2; j++) {
if ((int) params[i + j] == params[i + j]) {
// If at the limit then decrement instead
params[i + j] += (params[i + j] == upper[i + j]) ? -0.001 : 0.001;
}
}
}
// -=-=-=-
// Note: Some of the unfitted neighbours may be bad candidates.
// Only validate the fitted neighbours and the current candidate.
// The unfitted neighbours are allowed to fail.
final boolean computeDeviationsFlag = fitConfig.getComputeDeviationsFlag();
// Check if the filter requires deviations as we temporarily disable the
// smart filter (so fitConfig.isComputeDeviations() could be false) but the
// deviations will be required later
if (fitConfig.isComputeDeviations()) {
fitConfig.setComputeDeviations(true);
}
fitConfig.setFitRegion(0, 0, 0);
// Increase the iterations for a multiple fit.
final int maxIterations = fitConfig.getMaxIterations();
final int maxEvaluations = fitConfig.getMaxFunctionEvaluations();
fitConfig.setMaxIterations(
maxIterations + maxIterations * (npeaks - 1) * ITERATION_INCREASE_FOR_MULTIPLE_PEAKS);
fitConfig.setMaxFunctionEvaluations(
maxEvaluations + maxEvaluations * (npeaks - 1) * EVALUATION_INCREASE_FOR_MULTIPLE_PEAKS);
gf.setBounds(lower, upper);
// Allow dynamic look-up of local background and noise
final DynamicPeakResultValidationData validationData =
new DynamicPeakResultValidationData(npeaks) {
@Override
protected void compute(int n) {
localStats[n] = getLocalStatistics(n, params);
}
};
fitConfig.setPeakResultValidationData(validationData);
fitConfig.setPrecomputedFunctionValues(precomputedFittedNeighboursMulti);
final FitResult fitResult = gf.fit(region, width, height, npeaks, params, amplitudeEstimate,
params[Gaussian2DFunction.BACKGROUND] == 0);
fitConfig.setPrecomputedFunctionValues(null);
valueMulti = getFitValue();
gf.setBounds(null, null);
// if (fitResult.getStatus() == FitStatus.BAD_PARAMETERS)
// {
// int x = candidates.get(candidateId).x;
// int y = candidates.get(candidateId).y;
// int index = (y-regionBounds.y) * width + (x-regionBounds.x);
// System.out.printf("Bad : [%d,%d] %d,%d %.1f (%.1f) B=%.1f (%.1f) : %s\n", slice,
// candidateId,
// x, y, candidates.get(candidateId).intensity, region[index],
// bbefore, background, Arrays.toString(before));
// if (filteredData != null)
// uk.ac.sussex.gdsc.core.ij.Utils.display("Filtered", new FloatProcessor(dataBounds.width,
// dataBounds.height, filteredData));
// }
// Restore
fitConfig.setComputeDeviations(computeDeviationsFlag);
fitConfig.setFitRegion(width, height, 0.5);
fitConfig.setMaxIterations(maxIterations);
fitConfig.setMaxFunctionEvaluations(maxEvaluations);
updateResult(fitResult);
// Ensure the initial parameters are at the candidate position since we may have used an
// estimate.
// This will ensure that drift is computed correctly.
final double[] fitParams = fitResult.getParameters();
final double[] initialParams = fitResult.getInitialParameters();
// Note: We ignore those parameters from peaks that were pre-computed in
// precomputedFittedNeighboursMulti.
// These are outside the fit region and so should not usually overlap enough to effect the
// computation
// of the fit deviations.
final double[] fitParamStdDevs = fitResult.getParameterDeviations();
initialParams[Gaussian2DFunction.X_POSITION] = candidates.get(candidateId).x - regionBounds.x;
initialParams[Gaussian2DFunction.Y_POSITION] = candidates.get(candidateId).y - regionBounds.y;
initialParams[Gaussian2DFunction.X_SD] = xsd;
initialParams[Gaussian2DFunction.Y_SD] = ysd;
// Perform validation of the candidate and existing peaks (other candidates are allowed to
// fail)
if (fitResult.getStatus() == FitStatus.OK) {
// The candidate peak
if (fitConfig.validatePeak(0, initialParams, fitParams, fitParamStdDevs) != FitStatus.OK) {
return resultMulti = createResult(fitResult, null, fitConfig.getValidationResult());
}
// Existing peaks
for (int n = candidateNeighbourCount + 1; n < npeaks; n++) {
if (fitConfig.validatePeak(n, initialParams, fitParams,
fitParamStdDevs) != FitStatus.OK) {
return resultMulti = createResult(fitResult, null, fitConfig.getValidationResult());
}
}
}
// Create the results
PreprocessedPeakResult[] results = null;
if (fitResult.getStatus() == FitStatus.OK) {
residualsMulti = gf.getResiduals();
// // Debug background estimates
// double base = 1; //params[0] - fitConfig.getBias();
// System.out.printf("[%d] %d %.1f : %.1f %.2f %.1f %.2f %.1f %.2f %.1f %.2f %.1f %.2f\n",
// slice,
// candidateId, params[0], background, (background - params[0]) / base,
// getMultiFittingBackground(), (getMultiFittingBackground() - params[0]) / base,
// getSingleFittingBackground(), (getSingleFittingBackground() - params[0]) / base,
// getDefaultBackground(this.region, width, height),
// (getDefaultBackground(this.region, width, height) - params[0]) / base,
// getDefaultBackground(region, width, height),
// (getDefaultBackground(region, width, height) - params[0]) / base);
// Debug estimates verses fit.
// Distinguish those we have estimated using the amplitudeEstimate array.
// Trivial analysis shows that estimates have a lower relative error than a default initial
// guess.
// // Primary candidate
// int offset = 0;
// System.out.printf("[%d] %d MC %b %.2f %.2f %.2f %.2f %.2f %.2f\n", slice, candidateId,
// !amplitudeEstimate[0],
// DoubleEquality.relativeError(initialParams[offset + 1], params[offset + 1]),
// DoubleEquality.relativeError(initialParams[offset + 2], params[offset + 2]),
// DoubleEquality.relativeError(initialParams[offset + 3], params[offset + 3]),
// DoubleEquality.relativeError(initialParams[offset + 4], params[offset + 4]),
// DoubleEquality.relativeError(initialParams[offset + 5], params[offset + 5]),
// DoubleEquality.relativeError(initialParams[offset + 6], params[offset + 6]));
//
// // Neighbours
// int nn = 1;
// for (int i = 0; i < candidateNeighbourCount; i++)
// {
// offset += Gaussian2DFunction.NUMBER_PER_PEAK;
// System.out.printf("[%d] %d N %b %.2f %.2f %.2f %.2f %.2f %.2f\n", slice, candidateId,
// !amplitudeEstimate[nn],
// DoubleEquality.relativeError(initialParams[offset + 1], params[offset + 1]),
// DoubleEquality.relativeError(initialParams[offset + 2], params[offset + 2]),
// DoubleEquality.relativeError(initialParams[offset + 3], params[offset + 3]),
// DoubleEquality.relativeError(initialParams[offset + 4], params[offset + 4]),
// DoubleEquality.relativeError(initialParams[offset + 5], params[offset + 5]),
// DoubleEquality.relativeError(initialParams[offset + 6], params[offset + 6]));
// nn++;
// }
//
// // Already fitted peaks
// for (int i = 0; i < fittedNeighbourCount; i++)
// {
// if (subtract[i])
// continue;
// offset += Gaussian2DFunction.NUMBER_PER_PEAK;
// System.out.printf("[%d] %d F %b %.2f %.2f %.2f %.2f %.2f %.2f\n", slice, candidateId,
// !amplitudeEstimate[nn],
// DoubleEquality.relativeError(initialParams[offset + 1], params[offset + 1]),
// DoubleEquality.relativeError(initialParams[offset + 2], params[offset + 2]),
// DoubleEquality.relativeError(initialParams[offset + 3], params[offset + 3]),
// DoubleEquality.relativeError(initialParams[offset + 4], params[offset + 4]),
// DoubleEquality.relativeError(initialParams[offset + 5], params[offset + 5]),
// DoubleEquality.relativeError(initialParams[offset + 6], params[offset + 6]));
// nn++;
// }
// The primary candidate is not bounded. Check it has not drifted close to
// a neighbour.
// 3. Check we are not closer to a fitted spot. This has already had a chance at
// fitting a doublet so is ignored.
// 4. Check if there is an unfit candidate spot closer than the current candidate.
// This represents drift out to fit another spot that will be fit later.
int otherId = candidateId;
ResultType resultType = ResultType.NEW;
final double xShift =
fitParams[Gaussian2DFunction.X_POSITION] - initialParams[Gaussian2DFunction.X_POSITION];
final double yShift =
fitParams[Gaussian2DFunction.Y_POSITION] - initialParams[Gaussian2DFunction.Y_POSITION];
// We must be closer to the current candidate than any other spots.
// This is true for candidates we have yet to fit or already fitted candidates.
// Distance to current candidate fitted as a single
final double distanceToSingleFit2 = xShift * xShift + yShift * yShift;
// 3. Check we are not closer to a fitted spot. This has already had a chance at
// fitting a doublet so is ignored..
final CandidateList peakNeighbours = findPeakNeighbours(candidates.get(candidateId));
if (peakNeighbours.getSize() != 0) {
// Coords for comparison to the real positions
final float fcx2 =
(float) (regionBounds.x + fitParams[Gaussian2DFunction.X_POSITION] + 0.5);
final float fcy2 =
(float) (regionBounds.y + fitParams[Gaussian2DFunction.Y_POSITION] + 0.5);
float mind2 = (float) distanceToSingleFit2;
int ii = -1;
for (int i = 0; i < peakNeighbours.getSize(); i++) {
final float d2 =
distance2(fcx2, fcy2, peakNeighbours.get(i).params[Gaussian2DFunction.X_POSITION],
peakNeighbours.get(i).params[Gaussian2DFunction.Y_POSITION]);
if (mind2 > d2) {
// There is another fitted result that is closer.
// Note: The fit region is not centred on the other spot so this fit will probably
// be worse and is discarded (not compared to the existing fit to get the best one).
mind2 = d2;
ii = i;
otherId = peakNeighbours.get(i).index;
}
}
if (otherId != candidateId) {
if (logger != null) {
LoggerUtils.log(logger, Level.INFO,
"Bad peak: Fitted coordinates moved closer to another result (%d,%d : "
+ "x=%.1f,y=%.1f : %.1f,%.1f)",
candidates.get(candidateId).x, candidates.get(candidateId).y, fcx2, fcy2,
peakNeighbours.get(ii).params[Gaussian2DFunction.X_POSITION],
peakNeighbours.get(ii).params[Gaussian2DFunction.Y_POSITION]);
}
// System.out.printf("Multi drift to another result: [%d,%d] %d\n", slice, candidateId,
// otherId);
resultType = ResultType.EXISTING;
// Update the initial parameters to the position of the existing result so
// that drift is correct for filtering
initialParams[Gaussian2DFunction.X_POSITION] =
peakNeighbours.get(ii).params[Gaussian2DFunction.X_POSITION]
- cc.fromFitRegionToGlobalX();
initialParams[Gaussian2DFunction.Y_POSITION] =
peakNeighbours.get(ii).params[Gaussian2DFunction.Y_POSITION]
- cc.fromFitRegionToGlobalY();
}
}
// 4. Check if there is an unfit candidate spot closer than the current candidate.
// This represents drift out to fit another unfitted spot.
if (otherId != candidateId) {
final CandidateList neighbours = findNeighbours(candidates.get(candidateId));
if (neighbours.getSize() != 0) {
// Position - do not add 0.5 pixel offset to allow distance comparison to integer
// candidate positions.
final float fcx2 = (float) (regionBounds.x + fitParams[Gaussian2DFunction.X_POSITION]);
final float fcy2 = (float) (regionBounds.y + fitParams[Gaussian2DFunction.Y_POSITION]);
double mind2 = distanceToSingleFit2;
for (int j = 0; j < neighbours.getSize(); j++) {
final int id = neighbours.get(j).index;
if (isFit(id)) {
// This will be in the already fitted results instead so ignore...
continue;
}
final double d2 = distance2(fcx2, fcy2, candidates.get(id));
if (mind2 > d2) {
mind2 = d2;
otherId = id;
}
}
if (otherId != candidateId) {
if (logger != null) {
LoggerUtils.log(logger, Level.INFO,
"Bad peak: Fitted coordinates moved closer to another candidate (%d,%d : "
+ "x=%.1f,y=%.1f : %d,%d)",
candidates.get(candidateId).x, candidates.get(candidateId).y, fcx2 + 0.5f,
fcy2 + 0.5f, candidates.get(otherId).x, candidates.get(otherId).y);
}
// There is another candidate to be fit later that is closer.
// This may be used as an estimate so we return it as such (i.e we do not ignore it)
// otherId = candidateId;
if (otherId > candidateId) {
resultType = ResultType.CANDIDATE;
}
// Update the initial parameters to the position of the candidate so
// that drift is correct for filtering
initialParams[Gaussian2DFunction.X_POSITION] =
candidates.get(otherId).x - regionBounds.x;
initialParams[Gaussian2DFunction.Y_POSITION] =
candidates.get(otherId).y - regionBounds.y;
}
}
}
results = new PreprocessedPeakResult[npeaks];
// We must compute a local background for all the spots
// final double[] frozenParams = fitParams.clone();
// final int flags = GaussianFunctionFactory.freeze(fitConfig.getFunctionFlags(),
// fitConfig.getAstigmatismZModel(), frozenParams);
validationData.setResult(0, initialParams, fitParams, fitParamStdDevs);
// Note: This could be the current candidate or drift to another candidate
validationData.getLocalBackground();
results[0] = resultFactory.createPreprocessedPeakResult(otherId, 0, initialParams,
fitParams, fitParamStdDevs, validationData, resultType);
// Neighbours
int resultCount = 1;
for (int i = 0; i < candidateNeighbourCount; i++) {
final Candidate candidateNeighbour = candidateNeighbours[i];
results[resultCount] =
resultFactory.createPreprocessedPeakResult(candidateNeighbour.index, resultCount,
initialParams, fitParams, fitParamStdDevs, validationData,
// getLocalBackground(n, npeaks, frozenParams, flags,
// precomputedFittedNeighboursMulti),
ResultType.CANDIDATE);
resultCount++;
}
// Already fitted peaks
for (int i = 0; i < fittedNeighbourCount; i++) {
if (precomputed[i]) {
continue;
}
final int candidateId = fittedNeighbours[i].index;
results[resultCount] = resultFactory.createPreprocessedPeakResult(candidateId,
resultCount, initialParams, fitParams, fitParamStdDevs, validationData,
// getLocalBackground(n, npeaks, frozenParams, flags,
// precomputedFittedNeighboursMulti),
ResultType.EXISTING);
resultCount++;
}
}
resultMulti = createResult(fitResult, results);
return resultMulti;
}
private boolean containsAmplitudeEstimates(boolean[] amplitudeEstimate) {
for (final boolean b : amplitudeEstimate) {
if (b) {
return true;
}
}
return false;
}
/**
* Gets the local background for the given peak.
*
*
This is computed using the sum of all the other peaks plus the fitted background plus the
* contribution from the pre-computed peaks.
*
* @param n the peak number
* @param npeaks the total number of peaks (must be above 1)
* @param params the params
* @param flags the function flags
* @param precomputedFunction the precomputed function
* @return the local background
*/
@SuppressWarnings("unused")
private double getLocalBackground(int n, int npeaks, double[] params, final int flags,
double[] precomputedFunction) {
// Note: This does not include any precomputed peaks.
// These will be outside the fit region but may have an effect on peaks near
// the edge of the fit region. They are added separately.
// Note: This computes each function around the target point. This will scale poorly
// when the density is high (n(n-1)), e.g. 4 peaks in the fit region = 12 evaluations.
// An alternative is to evaluate each peak in the region.
// Build the local background using all but the peak of interest summed
// from a small region around the peak.
// This will scale linearly with the number of peaks.
final double[] spotParams = extractSpotParams(params, n);
// Do not evaluate over a large region for speed.
// Use only +/- 1 SD as this is 68% of the Gaussian volume or 5 pixels.
final int maxx =
FastGaussianOverlapAnalysis.getRange(spotParams[Gaussian2DFunction.X_SD], 1, 5);
final int maxy =
FastGaussianOverlapAnalysis.getRange(spotParams[Gaussian2DFunction.Y_SD], 1, 5);
final FastGaussianOverlapAnalysis overlap =
new FastGaussianOverlapAnalysis(flags, null, spotParams, maxx, maxy);
overlap.add(extractOtherParams(params, n, npeaks));
final double o = overlap.getOverlap() / overlap.getSize();
// XXX Test verses the standard overlap analysis
// GaussianOverlapAnalysis overlap2 = new GaussianOverlapAnalysis(flags, null, spotParams,
// maxx, maxy);
// overlap2.setFraction(1);
// overlap2.add(extractOtherParams(params, n, npeaks), true);
// double[] overlapData = overlap2.getOverlapData();
// double o2 = overlapData[1] / overlap2.getOverlap();
// System.out.printf("Overlap %f vs %f\n", o, o2);
return o + params[Gaussian2DFunction.BACKGROUND]
+ getBackgroundContribution(precomputedFunction, spotParams);
}
/**
* Gets the mean background contribution from the precomputed function in a 3x3 area around the
* centre.
*
* @param precomputedFunction the precomputed function
* @param params the params
* @return the mean background contribution
*/
private double getBackgroundContribution(double[] precomputedFunction, double[] params) {
if (precomputedFunction == null) {
return 0;
}
// Find the centre pixel.
final int cx = (int) (params[Gaussian2DFunction.X_POSITION] + 0.5);
final int cy = (int) (params[Gaussian2DFunction.Y_POSITION] + 0.5);
// Use a 3x3 region around it.
final Rectangle r =
new Rectangle(width, height).intersection(new Rectangle(cx - 1, cy - 1, 3, 3));
if (r.width == 0 || r.height == 0) {
return 0;
}
double sum = 0;
for (int y = 0; y < r.height; y++) {
for (int x = 0, i = (r.y + y) * width + r.x; x < r.width; x++, i++) {
sum += precomputedFunction[i];
}
}
return sum / (r.width * r.height);
}
// Local Background
// ----------------
// Previously created from the fitted background and all the other fitted spots.
// This does not scale well with multiple spots.
//
// This should be changed to:
// localbackground = fitted background (single peak)
// = ([local sum] - [peak sum]) / area (multiple peaks)
// Noise
// -----
// Using the standard deviation around each spot generates a noise
// that is far too high. The standard deviation can only be used from
// background regions. This is the global noise estimate and is used
// when there is no calibration to convert to photons.
//
// When a calibration is available then the local background can be used
// assuming it is Poisson shot noise. The local background must be converted
// to photons and combined with the read noise of the camera in photons.
// If an EM-CCD camera the photon shot variance must be doubled
// (EM-CCD noise factor of 2).
//
// Note that the read noise must be in the same units as those used
// for fitting so it is then converted back if the fitting is done in counts.
// The FitConfiguration provides a flag for fitting in camera counts and
// can produce the total gain. If the total gain is 0 then no conversion
// to photons is possible and the noise defaults to the global estimate.
// If the fitted background is <= 0 and the camera model has no noise
// then the noise uses the global estimate.
/**
* Gets the local background and noise for the given peak assuming that multiple peaks were fit.
*
*
The local region is defined using the region within 50% of the volume of the Gaussian,
* clipped to {@code +/-[1, 3]}.
*
*
The local background is computed using the sum of the region minus the sum of the function
* to get the region average.
*
*
The noise is computed using the local background as Poisson noise added to the camera read
* noise from the local region.
*
* @param peakNumber the peak number
* @param params the params
* @return [local background, noise]
*/
double[] getLocalStatistics(int peakNumber, double[] params) {
// This obtains the parameters without the background
final double[] spotParams = extractSpotParams(params, peakNumber);
final double[] result = new double[2];
// Note: area statistics is for the data frame so
// adjust to the data bounds
spotParams[Gaussian2DFunction.X_POSITION] += cc.regionBounds.x;
spotParams[Gaussian2DFunction.Y_POSITION] += cc.regionBounds.y;
// Add the 0.5 pixel offset to get the centre pixel
final int x = (int) (spotParams[Gaussian2DFunction.X_POSITION] + 0.5);
final int y = (int) (spotParams[Gaussian2DFunction.Y_POSITION] + 0.5);
// Do not evaluate over a large region for speed.
// Use only 50% of the Gaussian volume or 3 pixels.
int nx =
getRange(spotParams[Gaussian2DFunction.X_SD] * Gaussian2DPeakResultHelper.R_2D_50, 3);
int ny =
getRange(spotParams[Gaussian2DFunction.Y_SD] * Gaussian2DPeakResultHelper.R_2D_50, 3);
final Rectangle r1 = new Rectangle(x - nx, y - ny, 2 * nx + 1, 2 * ny + 1);
final Rectangle r2 = r1.intersection(new Rectangle(0, 0, area.maxx, area.maxy));
final double[] stats = area.getStatistics(r2);
// If there are multiple peaks then the local background must subtract the
// value of the target peak over the same region.
// Adjust the coordinates for clipping (r2 will be >= r1).
// This effectively makes nx/ny represent the number of pixels before the centre pixel
nx -= r2.x - r1.x;
ny -= r2.y - r1.y;
// Put the spot in the centre of the region
spotParams[Gaussian2DFunction.X_POSITION] += nx - x;
spotParams[Gaussian2DFunction.Y_POSITION] += ny - y;
final Gaussian2DFunction f = fitConfig.createGaussianFunction(1, r2.width, r2.height);
result[0] = (stats[AreaStatistics.INDEX_SUM] - f.integral(spotParams))
/ stats[AreaStatistics.INDEX_COUNT];
if (result[0] < 0) {
result[0] = params[Gaussian2DFunction.BACKGROUND];
}
result[1] = noiseEstimateFromBackground(result[0], r2);
return result;
}
/**
* Gets the range over which to evaluate a Gaussian using a factor of the standard deviation.
*
*
The range is clipped to 1 to max.
*
* @param range the range factor
* @param max the max value to return
* @return the range
*/
int getRange(double range, int max) {
final double l = Math.ceil(range);
if (l < 1) {
return 1;
}
if (l >= max) {
return max;
}
return (int) l;
}
private double noiseEstimateFromBackground(double background, Rectangle bounds) {
if (isFitCameraCounts) {
if (totalGain == 0) {
// Unknown calibration so use the global noise estimate
return fitConfig.getNoise();
}
// Convert the local background to photons
background /= totalGain;
}
// Noise estimate based on Poisson model requires positive noise
background = Math.max(0, background);
// Apply the EM-CCD noise factor of 2 to the photon shot noise
if (isEmCcd) {
background *= 2;
}
// Using the mean variance allows an estimate for a per-pixel camera model.
// Use the normalised variance (i.e. the variance in photo-electrons).
// Note: The argument input bounds are relative to the data bounds so
// convert them to fit inside the camera model.
// assume: dataBounds == cameraModel.getBounds()
// bounds.x += cameraModel.getBounds().x;
// bounds.y += cameraModel.getBounds().y;
bounds.x += cc.dataBounds.x;
bounds.y += cc.dataBounds.y;
double noiseEstimate = Math.sqrt(background + cameraModel.getMeanNormalisedVariance(bounds));
if (noiseEstimate == 0) {
// Happens when the background is zero and there is no camera model noise.
// Fall back to the global noise estimate.
noiseEstimate = fitConfig.getNoise();
}
if (isFitCameraCounts) {
noiseEstimate *= totalGain;
}
return noiseEstimate;
}
/**
* Gets the local background and noise for a single fitted peak.
*
*
The local region is defined using the region within 50% of the volume of the Gaussian,
* clipped to {@code +/-[1, 3]}.
*
*
The local background is computed using the fitted background plus the contribution from
* the pre-computed peaks.
*
*
The noise is computed using the local background as Poisson noise added to the camera read
* noise from the local region.
*
* @param params the params
* @param precomputedFunction the precomputed function
* @return [local background, noise]
*/
double[] getLocalStatisticsSinglePeak(double[] params, double[] precomputedFunction) {
// This obtains the parameters without the background
final double[] spotParams = extractSpotParams(params, 0);
final double[] result = new double[2];
result[0] = params[Gaussian2DFunction.BACKGROUND]
+ getBackgroundContribution(precomputedFunction, spotParams);
// Note: area statistics is for the data frame so
// adjust to the data bounds
spotParams[Gaussian2DFunction.X_POSITION] += cc.regionBounds.x;
spotParams[Gaussian2DFunction.Y_POSITION] += cc.regionBounds.y;
// Add the 0.5 pixel offset to get the centre pixel
final int x = (int) (spotParams[Gaussian2DFunction.X_POSITION] + 0.5);
final int y = (int) (spotParams[Gaussian2DFunction.Y_POSITION] + 0.5);
// Do not evaluate over a large region for speed.
// Use only 50% of the Gaussian volume or 3 pixels.
final int nx =
getRange(spotParams[Gaussian2DFunction.X_SD] * Gaussian2DPeakResultHelper.R_2D_50, 3);
final int ny =
getRange(spotParams[Gaussian2DFunction.Y_SD] * Gaussian2DPeakResultHelper.R_2D_50, 3);
final Rectangle r1 = new Rectangle(x - nx, y - ny, 2 * nx + 1, 2 * ny + 1);
final Rectangle r2 = r1.intersection(new Rectangle(0, 0, area.maxx, area.maxy));
result[1] = noiseEstimateFromBackground(result[0], r2);
return result;
}
private boolean getEstimate(Candidate candidate, double[] params, int peakOffset,
boolean close) {
final double[] estimatedParams = getEstimate(candidate.index, close);
if (estimatedParams != null) {
// Re-use previous good multi-fit results to estimate the peak params...
params[peakOffset + Gaussian2DFunction.SIGNAL] = estimatedParams[Gaussian2DFunction.SIGNAL];
params[peakOffset + Gaussian2DFunction.X_POSITION] =
estimatedParams[Gaussian2DFunction.X_POSITION] - regionBounds.x;
params[peakOffset + Gaussian2DFunction.Y_POSITION] =
estimatedParams[Gaussian2DFunction.Y_POSITION] - regionBounds.y;
params[peakOffset + Gaussian2DFunction.Z_POSITION] =
estimatedParams[Gaussian2DFunction.Z_POSITION];
// Reset the width params if using an astigmatism z-model
if (fitConfig.getAstigmatismZModel() != null) {
params[peakOffset + Gaussian2DFunction.X_SD] = xsd;
params[peakOffset + Gaussian2DFunction.Y_SD] = ysd;
} else {
params[peakOffset + Gaussian2DFunction.X_SD] = estimatedParams[Gaussian2DFunction.X_SD];
params[peakOffset + Gaussian2DFunction.Y_SD] = estimatedParams[Gaussian2DFunction.Y_SD];
}
params[peakOffset + Gaussian2DFunction.ANGLE] = estimatedParams[Gaussian2DFunction.ANGLE];
return false;
}
// Amplitude estimate
params[peakOffset + Gaussian2DFunction.SIGNAL] =
candidate.intensity - ((relativeIntensity) ? 0 : params[Gaussian2DFunction.BACKGROUND]);
params[peakOffset + Gaussian2DFunction.X_POSITION] = candidate.x - regionBounds.x;
params[peakOffset + Gaussian2DFunction.Y_POSITION] = candidate.y - regionBounds.y;
return true;
}
/**
* Gets the estimate. Note estimates are classed as close (within 1 pixel) of the candidate
* position, or not. A candidate may have either or both types of estimate. The close estimate
* is used in preference to the other.
*
* @param index the candidate index
* @param close True if only considering the close estimate
* @return the estimate
*/
private double[] getEstimate(int index, boolean close) {
// Check the close estimate
if (estimates[index] != null) {
return estimates[index].params;
}
// Only return the second estimate if we do not require the close estimate
return (close || estimates2[index] == null) ? null : estimates2[index].params;
}
/**
* Gets the parameters from the fitted neighbours at the specified index and copies them into
* the provide parameters array at the specified offset.
*
* @param index the fitted neighbour index
* @param params the output params
* @param peakOffset the peak offset for the output parameters
*/
private void copyFittedParams(int index, double[] params, int peakOffset) {
final float[] fittedParams = fittedNeighbours[index].params;
params[peakOffset + Gaussian2DFunction.SIGNAL] = fittedParams[Gaussian2DFunction.SIGNAL];
params[peakOffset + Gaussian2DFunction.X_POSITION] =
fittedParams[Gaussian2DFunction.X_POSITION];
params[peakOffset + Gaussian2DFunction.Y_POSITION] =
fittedParams[Gaussian2DFunction.Y_POSITION];
params[peakOffset + Gaussian2DFunction.Z_POSITION] =
fittedParams[Gaussian2DFunction.Z_POSITION];
// Reset the width params if using an astigmatism z-model
if (fitConfig.getAstigmatismZModel() != null) {
params[peakOffset + Gaussian2DFunction.X_SD] = xsd;
params[peakOffset + Gaussian2DFunction.Y_SD] = ysd;
} else {
params[peakOffset + Gaussian2DFunction.X_SD] = fittedParams[Gaussian2DFunction.X_SD];
params[peakOffset + Gaussian2DFunction.Y_SD] = fittedParams[Gaussian2DFunction.Y_SD];
}
params[peakOffset + Gaussian2DFunction.ANGLE] = fittedParams[Gaussian2DFunction.ANGLE];
}
/**
* Update the result. This is run after a fit is performed.
*
* @param result the result
*/
private void updateResult(FitResult result) {
fitConfig.setPeakResultValidationData(null);
fitConfig.updateVariance(result.getParameterDeviations());
// Q. Should the parameters be mapped using the z-model. Currently the validatePeak(...)
// method of the FitConfiguration handles this dynamically and then the results are
// converted to PreProcessedPeakResults which also handles the mapping.
// The error is now set by the function solver. Not all function solvers can compute
// the sum-of-squares so we can no longer update the error to be the independent
// of the solver.
// final double r2 = 1 - (gf.getFinalResidualSumOfSquares() / gf.getTotalSumOfSquares());
// result.setError(r2);
}
double getQaScoreMulti() {
if (qaMulti != null) {
return qaMulti.score;
}
// Ensure we have a multi result
getResultMulti();
// Note this assumes that this method will be called after a multi fit and that the
// residuals were computed.
final double[] residuals = residualsMulti;
qaMulti = computeQa((residuals == null) ? null : (FitResult) resultMulti.data, regionBounds,
residuals);
return qaMulti.score;
}
MultiPathFitResult.FitResult getResultDoubletMulti(double residualsThreshold) {
// Fit a multi-fit. If all peaks are valid then precompute them, apart from the central
// candidate,
// then fit as a doublet. Validate against an updated neighbourhood using the multi-fit
// results
if (computedDoubletMulti) {
return resultDoubletMulti;
}
if (residualsThreshold >= 1 || residualsThreshold < 0) {
return null;
}
if (getQaScoreMulti() < residualsThreshold) {
return null;
}
computedDoubletMulti = true;
if (resultMulti.status != 0) {
return null;
}
// Note this assumes that this method will be called after a multi fit and that the
// residuals were computed.
if (residualsMulti == null) {
return null;
}
// Ideally all multi-results must be valid to proceed with doublet fitting.
// We do not perform validation of the results. So we assume that the results have
// been checked and are valid and continue.
final PreprocessedPeakResult[] fitResults = resultMulti.getResults();
// Get the background for the multi-fit result
final FitResult multiFitResult = (FitResult) resultMulti.data;
final double[] fittedParams = multiFitResult.getParameters();
final float background = (float) fittedParams[Gaussian2DFunction.BACKGROUND];
// Get the neighbours
final CandidateList neighbours = findNeighbours(candidates.get(candidateId)).copy();
// Exclude the fitted candidate neighbours from the candidate neighbours
neighbours.removeIf(candidate -> {
final int otherId = candidate.index;
for (final PreprocessedPeakResult fitResult : fitResults) {
if (fitResult.getCandidateId() == otherId) {
return true;
}
}
return false;
});
// Get the fitted neighbours
final CandidateList peakNeighbours = findPeakNeighbours(candidates.get(candidateId));
final CandidateList peakNeighbours2 = peakNeighbours.copy();
// Update with the fitted results from the multi fit
NEXT_RESULT: for (final PreprocessedPeakResult fitResult : fitResults) {
final int otherId = fitResult.getCandidateId();
if (otherId == candidateId) {
// Ignore this as it is the current candidate
continue;
}
// Check if this is already a fitted neighbour
for (int i = 0; i < peakNeighbours.getSize(); i++) {
if (otherId == peakNeighbours.get(i).index) {
// Choose option 3 for simplicity. Note that the original fitted coordinates
// should be more accurate as the fit region was centred around the spot. Also
// note that due to bounding the fit coordinates (from the multi-fit) of any
// existing spot will be limited to a single pixel shift in XY.
continue NEXT_RESULT;
}
}
// Create a new dummy fitted neighbour
// (Use similar logic to when we create the actual results in #add(SelectedResult))
// Note that we do not unfreeze the parameters (i.e. the widths of the astigmatism z-model)
// since we are only interested in the coordinates.
final double[] p = fitResult.toGaussian2DParameters();
final float[] params = new float[p.length];
params[Gaussian2DFunction.BACKGROUND] = background;
for (int i = 1; i < params.length; i++) {
params[i] = (float) p[i];
}
// Store slice results relative to the data frame (not the global bounds)
// Convert back so that 0,0 is the top left of the data bounds
params[Gaussian2DFunction.X_POSITION] -= cc.dataBounds.x;
params[Gaussian2DFunction.Y_POSITION] -= cc.dataBounds.y;
final int x = (int) params[Gaussian2DFunction.X_POSITION];
final int y = (int) params[Gaussian2DFunction.Y_POSITION];
peakNeighbours2.add(new Candidate(x, y, otherId, params, null, 0, 0, 0, true));
}
// Create the precomputed function values. This is the function defined by the
// fit result apart from the central candidate.
final int peakCount = fittedParams.length / Gaussian2DFunction.PARAMETERS_PER_PEAK;
double[] precomputedFunctionParams;
double[] precomputedFunctionValues = null;
if (peakCount > 1) {
// Remove the actual fitted peak.
precomputedFunctionParams = extractOtherParams(fittedParams, 0, peakCount);
final Gaussian2DFunction func =
fitConfig.createGaussianFunction(peakCount - 1, width, height);
precomputedFunctionValues =
new StandardValueProcedure().getValues(func, precomputedFunctionParams);
}
// Add any precomputed values already computed. These are from peaks outside the fit region.
if (precomputedFittedNeighboursMulti != null) {
if (precomputedFunctionValues == null) {
precomputedFunctionValues = precomputedFittedNeighboursMulti;
} else {
for (int i = 0; i < precomputedFunctionValues.length; i++) {
precomputedFunctionValues[i] += precomputedFittedNeighboursMulti[i];
}
}
}
// Build a dummy fit result object for fitting the single candidate to the data.
// This will be a single evaluation of the function against the data.
int parameterCount =
fitConfig.createGaussianFunction(1, width, height).gradientIndices().length;
final int degreesOfFreedom = Math.max(region.length - parameterCount, 0);
final double[] parameters =
Arrays.copyOf(fittedParams, 1 + Gaussian2DFunction.PARAMETERS_PER_PEAK);
final FitResult fitResult = new FitResult(FitStatus.OK, degreesOfFreedom, 0, null, parameters,
null, 1, parameterCount, null, 0, 0);
// Evaluate the multi fit as if fitted as a single peak.
// The resulting function value are used in the doublet fit
final double singleValue = valueMulti;
//// XXX: These should be the same
// {
// try
// {
// gf.setComputeResiduals(false);
// fitConfig.setPrecomputedFunctionValues(precomputedFunctionValues);
// if (!gf.evaluate(region, regionBounds.width, regionBounds.height, 1, parameters))
// return null;
// }
// finally
// {
// gf.setComputeResiduals(true);
// fitConfig.setPrecomputedFunctionValues(null);
// }
//
// singleValue = getFitValue();
// if (singleValue != valueMulti)
// System.err.printf("Not same value: %f != %f\n", singleValue, valueMulti);
// }
// // Debugging:
// // The evaluate computes the residuals. These should be similar to the original residuals
// double[] residuals2 = gf.getResiduals();
// for (int i = 0; i < region.length; i++)
// if
// (!uk.ac.sussex.gdsc.core.utils.DoubleEquality.almostEqualRelativeOrAbsolute(residuals[i],
// residuals2[i], 1e-5,
// 1e-6))
// {
// System.out.printf("Residuals error [%d] %f != %f\n", i, residuals[i], residuals2[i]);
// uk.ac.sussex.gdsc.core.ij.Utils.display("Residuals1", residuals, width, height);
// uk.ac.sussex.gdsc.core.ij.Utils.display("Residuals2", residuals2, width, height);
// uk.ac.sussex.gdsc.core.ij.Utils.display("Region", region, width, height);
// break;
// }
resultDoubletMulti = fitAsDoublet(fitResult, region, precomputedFunctionValues, neighbours,
peakNeighbours2, qaMulti, singleValue);
// if (resultMultiDoublet != null && resultMultiDoublet.status ==
// FitStatus.BAD_PARAMETERS.ordinal())
// {
// System.out.println("Bad params: " + Arrays.toString(parameters));
// //uk.ac.sussex.gdsc.core.ij.Utils.display("Region", region, width, height);
// //uk.ac.sussex.gdsc.core.ij.Utils.display("Residuals1", residuals, width, height);
// }
if (resultDoubletMulti != null && resultDoubletMulti.status == 0
&& resultDoubletMulti.getResults() != null) {
// The code below builds a combined result for the multi-fit and the primary candidate
// fitted as a doublet. However this means that validation will be repeated on the spots
// that have
// been validated before. This is a small overhead but allows using the multi-doublet result
// to
// return all the fit results combined.
// Fitting 2 spots is better than 1 and does not clash with any of the other multi-fit
// results.
// Build a combined result with the doublet and the other multi-fit results
final FitResult doubletFitResult =
(uk.ac.sussex.gdsc.smlm.fitting.FitResult) resultDoubletMulti.getData();
final double[] initialParams1 = doubletFitResult.getInitialParameters();
final double[] params1 = doubletFitResult.getParameters();
final double[] initialParams2 = multiFitResult.getInitialParameters();
final double[] params2 = multiFitResult.getParameters();
// Create the initial parameters by adding an extra spot
final double[] initialParams =
new double[initialParams2.length + Gaussian2DFunction.PARAMETERS_PER_PEAK];
final double[] params = new double[initialParams.length];
final int srcPos = Gaussian2DFunction.getIndex(1, Gaussian2DFunction.SIGNAL);
final int destPos = initialParams1.length;
final int length = initialParams2.length - srcPos;
System.arraycopy(initialParams1, 0, initialParams, 0, destPos);
System.arraycopy(initialParams2, srcPos, initialParams, destPos, length);
System.arraycopy(params1, 0, params, 0, destPos);
System.arraycopy(params2, srcPos, params, destPos, length);
final int npeaks = multiFitResult.getNumberOfPeaks() + 1;
double[] paramDevs = null;
if (multiFitResult.getParameterDeviations() != null) {
// Recompute the deviations with all the parameters
paramDevs = new double[params.length];
// Add the pre-computed function from outside the region. The parameters for this
// are ignored from the deviations computation.
fitConfig.setPrecomputedFunctionValues(precomputedFittedNeighboursMulti);
gf.computeDeviations(region, width, height, npeaks, params, paramDevs);
fitConfig.setPrecomputedFunctionValues(null);
}
// Create all the output results
final PreprocessedPeakResult[] results =
new PreprocessedPeakResult[resultDoubletMulti.getResults().length
+ resultMulti.getResults().length - 1];
// We must compute a local background for all the spots
final DynamicPeakResultValidationData validationData =
new DynamicPeakResultValidationData(npeaks) {
@Override
protected void compute(int n) {
localStats[n] = getLocalStatistics(n, params);
}
};
// final double[] frozenParams = params.clone();
// final int flags = GaussianFunctionFactory.freeze(fitConfig.getFunctionFlags(),
// fitConfig.getAstigmatismZModel(), frozenParams);
int resultIndex = 0;
for (int i = 0; i < resultDoubletMulti.getResults().length; i++) {
final PreprocessedPeakResult r = resultDoubletMulti.getResults()[i];
results[resultIndex] = resultFactory.createPreprocessedPeakResult(r.getCandidateId(),
r.getId(), initialParams, params, paramDevs,
// getLocalBackground(n, npeaks, frozenParams, flags,
// precomputedFittedNeighboursMulti),
validationData, (r.isExistingResult()) ? ResultType.EXISTING
: (r.isNewResult()) ? ResultType.NEW : ResultType.CANDIDATE);
resultIndex++;
}
// Ignore the first result (this was removed and fit as the doublet)
for (int i = 1; i < resultMulti.getResults().length; i++) {
final PreprocessedPeakResult r = resultMulti.getResults()[i];
// Increment the ID by one since the position in the parameters array is moved to
// accommodate 2 preceding peaks and not 1
results[resultIndex] = resultFactory.createPreprocessedPeakResult(r.getCandidateId(),
r.getId() + 1, initialParams, params, paramDevs,
// getLocalBackground(n, npeaks, frozenParams, flags,
// precomputedFittedNeighboursMulti),
validationData, (r.isExistingResult()) ? ResultType.EXISTING
: (r.isNewResult()) ? ResultType.NEW : ResultType.CANDIDATE);
resultIndex++;
}
final int adjust = (fitConfig.isBackgroundFitting()) ? 1 : 0;
parameterCount = npeaks * ((multiFitResult.getNumberOfFittedParameters() - adjust)
/ multiFitResult.getNumberOfPeaks()) + adjust;
//@formatter:off
final FitResult mdoubletFitResult =
doubletFitResult.toBuilder()
.setDegreesOfFreedom(Math.max(region.length - parameterCount, 0))
.setError(doubletFitResult.getError())
.setInitialParameters(initialParams)
.setParameters(params)
.setParameterDeviations(paramDevs)
.setNumberOfPeaks(npeaks)
.setNumberOfFittedParameters(parameterCount)
.setIterations(doubletFitResult.getIterations())
.setEvaluations(doubletFitResult.getEvaluations())
.build();
//@formatter:on
resultDoubletMulti = createResult(mdoubletFitResult, results);
}
// else:
// Nothing to return. Do not set to null to allow reporting of the errors
// resultMultiDoublet = null;
return resultDoubletMulti;
}
MultiPathFitResult.FitResult getResultSingle() {
if (resultSingle != null) {
return resultSingle;
}
// Get each peak that is pre-computed.
// This is done to compute equivalent deviations to the getResultMulti()
// by using only those fitted peaks within the region.
getPrecomputedNeighbourCount();
// Background of fitted peaks within the region
double background = 0;
int backgroundCount = 0;
// Subtract all fitted neighbours from the region
if (fittedNeighbourCount != 0) {
// The fitted result will be relative to (0,0) and have a 0.5 pixel offset applied
final double xOffset = regionBounds.x + 0.5;
final double yOffset = regionBounds.y + 0.5;
// Utils.display("Region", region, width, height);
precomputedFunctionParamsSingle =
new double[1 + PARAMETERS_PER_PEAK * fittedNeighbourCount];
for (int i = 0, j = 0; i < fittedNeighbourCount; i++, j += PARAMETERS_PER_PEAK) {
// Check if within the region
if (!precomputed[i]) {
background += fittedNeighbours[i].params[Gaussian2DFunction.BACKGROUND];
backgroundCount++;
}
copyFittedParams(i, precomputedFunctionParamsSingle, j);
// Adjust position relative to extracted region
precomputedFunctionParamsSingle[j + Gaussian2DFunction.X_POSITION] -= xOffset;
precomputedFunctionParamsSingle[j + Gaussian2DFunction.Y_POSITION] -= yOffset;
}
final Gaussian2DFunction func =
fitConfig.createGaussianFunction(fittedNeighbourCount, width, height);
precomputedFittedNeighboursSingle =
new StandardValueProcedure().getValues(func, precomputedFunctionParamsSingle);
}
final double[] params = new double[1 + Gaussian2DFunction.PARAMETERS_PER_PEAK];
// Estimate background.
// Use the multi-fitting background as this uses the background of fitted neighbours if
// available.
// Use the fitted peaks within the region or fall-back to the estimate for
// a single peak (i.e. low point of the region).
params[Gaussian2DFunction.BACKGROUND] =
(backgroundCount == 0) ? getFittingBackgroundSingle() : background / backgroundCount;
final boolean[] amplitudeEstimate = new boolean[1];
// Re-use an estimate if we have it. Note that this may be quite far from the candidate.
amplitudeEstimate[0] = getEstimate(candidates.get(candidateId), params, 0, false);
// If we have no estimate the default will be an amplitude estimate.
final boolean usingEstimate = !amplitudeEstimate[0];
if (amplitudeEstimate[0]) {
// We can estimate the signal here instead of using the amplitude.
// Do this when the fitting window covers enough of the Gaussian (e.g. 2.5xSD).
float signal = 0;
if (estimateSignal) {
final double oldBackground = params[Gaussian2DFunction.BACKGROUND];
double sum = 0;
final int size = width * height;
for (int i = size; i-- > 0;) {
sum += region[i];
}
// Subtract any fitted peaks
if (precomputedFittedNeighboursSingle != null) {
sum -= MathUtils.sum(precomputedFittedNeighboursSingle);
}
signal = (float) (sum - oldBackground * size);
}
if (signal > 0) {
amplitudeEstimate[0] = false;
params[Gaussian2DFunction.SIGNAL] = signal;
// Resort to default amplitude estimate. Ensure this is above zero.
} else if (params[Gaussian2DFunction.SIGNAL] <= 0) {
// Reset to the single fitting background
double oldBackground = params[Gaussian2DFunction.BACKGROUND];
params[Gaussian2DFunction.BACKGROUND] = getFittingBackgroundSingle();
double backgroundChange = oldBackground - params[Gaussian2DFunction.BACKGROUND];
params[Gaussian2DFunction.SIGNAL] += backgroundChange;
if (params[Gaussian2DFunction.SIGNAL] <= 0) {
// Reset to the minimum value in the data.
oldBackground = params[Gaussian2DFunction.BACKGROUND];
params[Gaussian2DFunction.BACKGROUND] = getDefaultBackground(region, width, height);
backgroundChange = oldBackground - params[Gaussian2DFunction.BACKGROUND];
// Make the amplitude estimate higher by the change in background
params[Gaussian2DFunction.SIGNAL] += backgroundChange;
if (params[Gaussian2DFunction.SIGNAL] <= 0) {
// This is probably extremely rare and the result of a poor candidate estimate.
// Set a small height based on the data range
final double defaultHeight = Math.max(1,
0.1 * (getMax(region, width, height) - params[Gaussian2DFunction.BACKGROUND]));
params[Gaussian2DFunction.SIGNAL] = defaultHeight;
}
}
}
}
// If there were neighbours or we have an estimate
// then use off grid pixels to prevent re-estimate of CoM
// (since the CoM estimate will be skewed by the neighbours, or is not needed)
if (fittedNeighbourCount != 0 || candidateNeighbourCount != 0 || usingEstimate) {
if ((int) params[Gaussian2DFunction.X_POSITION] == params[Gaussian2DFunction.X_POSITION]) {
params[Gaussian2DFunction.X_POSITION] += 0.001;
}
if ((int) params[Gaussian2DFunction.Y_POSITION] == params[Gaussian2DFunction.Y_POSITION]) {
params[Gaussian2DFunction.Y_POSITION] += 0.001;
}
}
// Allow dynamic look-up of local background and noise.
final DynamicPeakResultValidationData validationData =
new DynamicPeakResultValidationData(1) {
@Override
protected void compute(int n) {
localStats[n] = (fittedNeighbourCount == 0)
// If there are no other fitted peaks in the region then compute
// using the fitted background
? getLocalStatisticsSinglePeak(params, null)
// If there are other fitted peaks in the region then compute the local
// background using the mean without the function value
: getLocalStatistics(0, params);
}
};
fitConfig.setPeakResultValidationData(validationData);
fitConfig.setPrecomputedFunctionValues(precomputedFittedNeighboursSingle);
final FitResult fitResult = gf.fit(region, width, height, 1, params, amplitudeEstimate,
params[Gaussian2DFunction.BACKGROUND] == 0);
fitConfig.setPrecomputedFunctionValues(null);
valueSingle = getFitValue();
updateResult(fitResult);
// Ensure the initial parameters are at the candidate position since we may have used an
// estimate.
// This will ensure that drift is computed correctly.
final double[] initialParams = fitResult.getInitialParameters();
initialParams[Gaussian2DFunction.X_POSITION] = candidates.get(candidateId).x - regionBounds.x;
initialParams[Gaussian2DFunction.Y_POSITION] = candidates.get(candidateId).y - regionBounds.y;
// Create the results
PreprocessedPeakResult[] results = null;
if (fitResult.getStatus() == FitStatus.OK) {
// XXX When using a smart filter the validation is not yet complete. It is performed
// dynamically when selecting the result. So the following computation may proceed
// with status OK even if the peak will eventually be rejected.
residualsSingle = gf.getResiduals();
// Debug background estimates
// if (slice == 691 && candidateId == 0) {
// double base = params[0];
// System.out.printf("[%d] %d %.1f : %.1f %.2f %.1f %.2f %.1f %.2f %.1f %.2f\n", slice,
// candidateId, params[0], background, (background - params[0]) / base,
// getSingleFittingBackground(), (getSingleFittingBackground() - params[0]) / base,
// getDefaultBackground(this.region, width, height),
// (getDefaultBackground(this.region, width, height) - params[0]) / base,
// getDefaultBackground(region, width, height),
// (getDefaultBackground(region, width, height) - params[0]) / base);
//
// // Debug estimates verses fit.
// // Distinguish those we have estimated using the amplitudeEstimate array.
// // Trivial analysis shows that estimates have a lower relative error than a default
// // initial
// // guess.
// int offset = 0;
// System.out.printf("[%d] %d C %b %.2f %.2f %.2f %.2f %.2f %.2f\n", slice, candidateId,
// !amplitudeEstimate[0],
// DoubleEquality.relativeError(initialParams[offset + 1], params[offset + 1]),
// DoubleEquality.relativeError(initialParams[offset + 2], params[offset + 2]),
// DoubleEquality.relativeError(initialParams[offset + 3], params[offset + 3]),
// DoubleEquality.relativeError(initialParams[offset + 4], params[offset + 4]),
// DoubleEquality.relativeError(initialParams[offset + 5], params[offset + 5]),
// DoubleEquality.relativeError(initialParams[offset + 6], params[offset + 6]));
// }
// The primary candidate is not bounded. Check it has not drifted close to
// a neighbour.
// 3. Check we are not closer to a fitted spot. This has already had a chance at
// fitting a doublet so is ignored.
// 4. Check if there is an unfit candidate spot closer than the current candidate.
// This represents drift out to fit another spot that will be fit later.
final double[] fitParams = fitResult.getParameters();
int otherId = candidateId;
ResultType resultType = ResultType.NEW;
final double xShift =
fitParams[Gaussian2DFunction.X_POSITION] - initialParams[Gaussian2DFunction.X_POSITION];
final double yShift =
fitParams[Gaussian2DFunction.Y_POSITION] - initialParams[Gaussian2DFunction.Y_POSITION];
// We must be closer to the current candidate than any other spots.
// This is true for candidates we have yet to fit or already fitted candidates.
// Distance to current candidate fitted as a single
final double distanceToSingleFit2 = xShift * xShift + yShift * yShift;
// 3. Check we are not closer to a fitted spot. This has already had a chance at
// fitting a doublet so is ignored.
final CandidateList peakNeighbours = findPeakNeighbours(candidates.get(candidateId));
if (peakNeighbours.getSize() != 0) {
// Coords for comparison to the real positions
final float fcx2 =
(float) (regionBounds.x + fitParams[Gaussian2DFunction.X_POSITION] + 0.5);
final float fcy2 =
(float) (regionBounds.y + fitParams[Gaussian2DFunction.Y_POSITION] + 0.5);
float mind2 = (float) distanceToSingleFit2;
int ii = -1;
for (int i = 0; i < peakNeighbours.getSize(); i++) {
final float d2 =
distance2(fcx2, fcy2, peakNeighbours.get(i).params[Gaussian2DFunction.X_POSITION],
peakNeighbours.get(i).params[Gaussian2DFunction.Y_POSITION]);
if (mind2 > d2) {
// There is another fitted result that is closer.
// Note: The fit region is not centred on the other spot so this fit will probably
// be worse and is discarded (not compared to the existing fit to get the best one).
mind2 = d2;
ii = i;
otherId = peakNeighbours.get(i).index;
}
}
if (otherId != candidateId) {
if (logger != null) {
LoggerUtils.log(logger, Level.INFO,
"Bad peak: Fitted coordinates moved closer to another result (%d,%d : "
+ "x=%.1f,y=%.1f : %.1f,%.1f)",
candidates.get(candidateId).x, candidates.get(candidateId).y, fcx2, fcy2,
peakNeighbours.get(ii).params[Gaussian2DFunction.X_POSITION],
peakNeighbours.get(ii).params[Gaussian2DFunction.Y_POSITION]);
}
// System.out.printf("Single drift to another result: [%d,%d] %d\n", slice, candidateId,
// otherId);
resultType = ResultType.EXISTING;
// Update the initial parameters to the position of the existing result so
// that drift is correct for filtering
initialParams[Gaussian2DFunction.X_POSITION] =
peakNeighbours.get(ii).params[Gaussian2DFunction.X_POSITION]
- cc.fromFitRegionToGlobalX();
initialParams[Gaussian2DFunction.Y_POSITION] =
peakNeighbours.get(ii).params[Gaussian2DFunction.Y_POSITION]
- cc.fromFitRegionToGlobalY();
}
}
// 4. Check if there is an unfit candidate spot closer than the current candidate.
// This represents drift out to fit another unfitted spot.
if (otherId != candidateId) {
final CandidateList neighbours = findNeighbours(candidates.get(candidateId));
if (neighbours.getSize() != 0) {
// Position - do not add 0.5 pixel offset to allow distance comparison to integer
// candidate positions.
final float fcx2 = (float) (regionBounds.x + fitParams[Gaussian2DFunction.X_POSITION]);
final float fcy2 = (float) (regionBounds.y + fitParams[Gaussian2DFunction.Y_POSITION]);
double mind2 = distanceToSingleFit2;
for (int j = 0; j < neighbours.getSize(); j++) {
final int id = neighbours.get(j).index;
if (isFit(id)) {
// This will be in the already fitted results instead so ignore...
continue;
}
final double d2 = distance2(fcx2, fcy2, candidates.get(id));
if (mind2 > d2) {
mind2 = d2;
otherId = id;
}
}
if (otherId != candidateId) {
if (logger != null) {
LoggerUtils.log(logger, Level.INFO,
"Bad peak: Fitted coordinates moved closer to another candidate (%d,%d : "
+ "x=%.1f,y=%.1f : %d,%d)",
candidates.get(candidateId).x, candidates.get(candidateId).y, fcx2 + 0.5f,
fcy2 + 0.5f, candidates.get(otherId).x, candidates.get(otherId).y);
}
// There is another candidate to be fit later that is closer.
// This may be used as an estimate so we return it as such (i.e we do not ignore it)
// otherId = candidateId;
if (otherId > candidateId) {
resultType = ResultType.CANDIDATE;
}
// Update the initial parameters to the position of the candidate so
// that drift is correct for filtering
initialParams[Gaussian2DFunction.X_POSITION] =
candidates.get(otherId).x - regionBounds.x;
initialParams[Gaussian2DFunction.Y_POSITION] =
candidates.get(otherId).y - regionBounds.y;
}
}
}
results = new PreprocessedPeakResult[1];
final double[] fitParamStdDevs = fitResult.getParameterDeviations();
// int npeaks = 1 + fittedNeighbourCount - precomputedFittedNeighbourCount;
// if (npeaks > 1)
// {
// // We must compute a local background using the influence from neighbours.
// // For equivalence with the multi-fit we only include the fits within the region.
//
// // The fitted result will be relative to (0,0) and have a 0.5 pixel offset applied
// final double xOffset = regionBounds.x + 0.5;
// final double yOffset = regionBounds.y + 0.5;
//
// functionParamsSingle = new double[1 + parametersPerPeak * npeaks];
// System.arraycopy(fitParams, 0, functionParamsSingle, 0, fitParams.length);
// for (int i = 0, j = parametersPerPeak; i < fittedNeighbourCount; i++)
// {
// // Check if within the region
// if (!precomputed[i])
// {
// getFittedParams(i, functionParamsSingle, j);
//
// // Adjust position relative to extracted region
// functionParamsSingle[j + Gaussian2DFunction.X_POSITION] -= xOffset;
// functionParamsSingle[j + Gaussian2DFunction.Y_POSITION] -= yOffset;
// j += parametersPerPeak;
// }
// }
//
// final double[] frozenParams = functionParamsSingle.clone();
// final int flags = GaussianFunctionFactory.freeze(fitConfig.getFunctionFlags(),
// fitConfig.getAstigmatismZModel(), frozenParams);
// localBackgroundSingle = getLocalBackground(0, npeaks, frozenParams, flags,
// getPrecomputedFittedNeighbours());
//
// if (fitParamDevs != null)
// {
// // Recompute the deviations with all the parameters.
// double[] paramDevs1 = new double[functionParamsSingle.length];
// // These pre-computed values will be those peaks outside the region
// fitConfig.setPrecomputedFunctionValues(getPrecomputedFittedNeighbours());
// if (gf.computeDeviations(region, width, height, npeaks, functionParamsSingle,
// paramDevs1))
// System.arraycopy(paramDevs1, 0, fitParamDevs, 0, fitParamDevs.length);
// fitConfig.setPrecomputedFunctionValues(null);
// }
// }
// else if (precomputedFittedNeighbourCount != 0)
// {
// // Add the contribution from the precomputed neighbours
// localBackgroundSingle = fitParams[Gaussian2DFunction.BACKGROUND] +
// getBackgroundContribution(getPrecomputedFittedNeighbours(), fitParams);
// //// Debug if this ever adds a significant amount
// //System.out.printf("Background=%f, Neighbours=%f (%f)\n",
// fitParams[Gaussian2DFunction.BACKGROUND],
// // localBackgroundSingle - fitParams[Gaussian2DFunction.BACKGROUND],
// // localBackgroundSingle / fitParams[Gaussian2DFunction.BACKGROUND]);
// }
validationData.setResult(0, initialParams, fitParams, fitParamStdDevs);
validationData.getLocalBackground();
results[0] = resultFactory.createPreprocessedPeakResult(otherId, 0, initialParams,
fitParams, fitParamStdDevs, validationData, resultType);
}
resultSingle = createResult(fitResult, results);
return resultSingle;
}
private double getFitValue() {
final FunctionSolver solver = gf.getFunctionSolver();
final FunctionSolverType solverType = solver.getType();
if (solverType == FunctionSolverType.MLE) {
return ((MleFunctionSolver) solver).getLogLikelihood();
} else if (solverType == FunctionSolverType.WLSE) {
return ((WLseFunctionSolver) solver).getChiSquared();
} else {
return ((LseFunctionSolver) solver).getAdjustedCoefficientOfDetermination();
}
}
private String getFitValueName() {
final FunctionSolverType solverType = gf.getFunctionSolver().getType();
if (solverType == FunctionSolverType.MLE) {
return "Log-likelihood";
} else if (solverType == FunctionSolverType.WLSE) {
return "Chi-Squared";
} else {
return "Adjusted R^2";
}
}
private MultiPathFitResult.FitResult createResult(FitResult fitResult,
PreprocessedPeakResult[] results) {
return createResult(fitResult, results, fitResult.getStatus());
}
private MultiPathFitResult.FitResult createResult(FitResult fitResult,
PreprocessedPeakResult[] results, FitStatus fitStatus) {
final MultiPathFitResult.FitResult mfitResult =
new MultiPathFitResult.FitResult(fitStatus.ordinal(), fitResult);
mfitResult.setResults(results);
return mfitResult;
}
double getQaScoreSingle() {
if (qaSingle != null) {
return qaSingle.score;
}
// Ensure we have a single result
getResultSingle();
// Note this assumes that this method will be called after a single fit and that the
// residuals were computed.
final double[] residuals = residualsSingle;
qaSingle = computeQa((residuals == null) ? null : (FitResult) resultSingle.data, regionBounds,
residuals);
return qaSingle.score;
}
MultiPathFitResult.FitResult getResultDoubletSingle(double residualsThreshold) {
if (computedDoubletSingle) {
return resultDoubletSingle;
}
if (residualsThreshold >= 1 || residualsThreshold < 0) {
return null;
}
if (getQaScoreSingle() < residualsThreshold) {
return null;
}
computedDoubletSingle = true;
final CandidateList neighbours = findNeighbours(candidates.get(candidateId));
final CandidateList peakNeighbours = findPeakNeighbours(candidates.get(candidateId));
resultDoubletSingle = fitAsDoublet((FitResult) resultSingle.data, region,
precomputedFittedNeighboursSingle, neighbours, peakNeighbours, qaSingle, valueSingle);
// Check if the deviations require updating.
// This will be the case if we have stored function parameters for the single fit.
if (resultDoubletSingle != null && resultDoubletSingle.status == 0
&& resultDoubletSingle.getResults() != null && precomputedFunctionParamsSingle != null) {
// Recompute the deviations with all the parameters.
// For equivalence with the multi-fit we only include the fits within the region.
FitResult doubletFitResult =
(uk.ac.sussex.gdsc.smlm.fitting.FitResult) resultDoubletSingle.getData();
final double[] fitParams = doubletFitResult.getParameters();
final double[] fitParamDevs = new double[fitParams.length];
final int npeaks = 2 + fittedNeighbourCount - precomputedFittedNeighbourCount;
final double[] params = new double[1 + PARAMETERS_PER_PEAK * npeaks];
System.arraycopy(fitParams, 0, params, 0, fitParams.length);
System.arraycopy(precomputedFunctionParamsSingle, 1, params, fitParams.length,
params.length - fitParams.length);
final double[] paramDevs = new double[params.length];
// These pre-computed values will be those peaks outside the region
fitConfig.setPrecomputedFunctionValues(getPrecomputedFittedNeighbours());
if (gf.computeDeviations(region, width, height, npeaks, params, paramDevs)) {
System.arraycopy(paramDevs, 0, fitParamDevs, 0, fitParamDevs.length);
}
fitConfig.setPrecomputedFunctionValues(null);
// Use the updated deviations
doubletFitResult =
doubletFitResult.toBuilder().setParameterDeviations(fitParamDevs).build();
resultDoubletSingle = createResult(doubletFitResult, resultDoubletSingle.getResults());
final double[] initialParams = doubletFitResult.getInitialParameters();
final PreprocessedPeakResult[] results = resultDoubletSingle.getResults();
// We must compute a local background for all the spots
final DynamicPeakResultValidationData validationData =
new DynamicPeakResultValidationData(npeaks) {
@Override
protected void compute(int n) {
localStats[n] = getLocalStatistics(n, params);
}
};
for (int i = 0; i < results.length; i++) {
final PreprocessedPeakResult r = results[i];
results[i] = resultFactory.createPreprocessedPeakResult(r.getCandidateId(), r.getId(),
initialParams, params, paramDevs, validationData,
(r.isExistingResult()) ? ResultType.EXISTING
: (r.isNewResult()) ? ResultType.NEW : ResultType.CANDIDATE);
}
}
return resultDoubletSingle;
}
/**
* Perform quadrant analysis on the residuals.
*
*
Perform quadrant analysis as per rapidSTORM to analyse if the residuals of the fit are
* skewed around the single fit centre. This may indicate the result is actually two spots (a
* doublet).
*
* @param fitResult the fit result
* @param regionBounds the region bounds
* @param residuals the residuals
* @return the quadrant analysis
*/
private QuadrantAnalysis computeQa(FitResult fitResult, Rectangle regionBounds,
final double[] residuals) {
if (residuals == null) {
final QuadrantAnalysis qa = new QuadrantAnalysis();
qa.score = -1; // Set so that any residuals threshold will be ignored.
return qa;
}
final double[] params = fitResult.getParameters();
final int width = regionBounds.width;
final int height = regionBounds.height;
// Use rounding since the fit coords are not yet offset by 0.5 pixel to centre them
final int cx = (int) Math.round(params[Gaussian2DFunction.X_POSITION]);
final int cy = (int) Math.round(params[Gaussian2DFunction.Y_POSITION]);
// Q. The primary candidate may have drifted. Should we check it is reasonably centred in the
// region?.
final QuadrantAnalysis qa = new QuadrantAnalysis();
qa.quadrantAnalysis(residuals, width, height, cx, cy);
if (logger != null) {
LoggerUtils.log(logger, Level.INFO, "Residue analysis = %f (%d,%d)", qa.score, qa.vector[0],
qa.vector[1]);
}
return qa;
}
/**
* Fit the single spot location as two spots (a doublet).
*
* @param fitResult the fit result
* @param region the region
* @param precomputedFunctionValues the precomputed function values
* @param neighbours the neighbours
* @param peakNeighbours the peak neighbours
* @param qa the qa object that performed quadrant analysis
* @param singleValue the objective function value from fitting a single peak
* @return the fit result
*/
private MultiPathFitResult.FitResult fitAsDoublet(FitResult fitResult, double[] region,
double[] precomputedFunctionValues, CandidateList neighbours, CandidateList peakNeighbours,
QuadrantAnalysis qa, double singleValue) {
final double[] params = fitResult.getParameters();
// Use rounding since the fit coords are not yet offset by 0.5 pixel to centre them
final int cx = (int) Math.round(params[Gaussian2DFunction.X_POSITION]);
final int cy = (int) Math.round(params[Gaussian2DFunction.Y_POSITION]);
if (logger != null) {
LoggerUtils.log(logger, Level.INFO, "Computing 2-kernel model");
}
if (!qa.computeDoubletCentres(width, height, cx, cy, params[Gaussian2DFunction.X_SD],
params[Gaussian2DFunction.Y_SD])) {
return null;
}
// TODO - Locate the 2 new centres by moving out into the quadrant defined by the vector
// and finding the maxima on the original image data.
// -+-+-
// Estimate params using the single fitted peak
// -+-+-
final double[] doubletParams = new double[1 + 2 * Gaussian2DFunction.PARAMETERS_PER_PEAK];
// Note: Quadrant analysis sets the positions using 0.5,0.5 as the centre of the pixel.
// The fitting does not, so subtract 0.5.
// Note that we set position and signal but leave the z-position and widths to their
// standard values since this is 'new' fit.
doubletParams[Gaussian2DFunction.BACKGROUND] = params[Gaussian2DFunction.BACKGROUND];
doubletParams[Gaussian2DFunction.SIGNAL] = params[Gaussian2DFunction.SIGNAL] * 0.5;
doubletParams[Gaussian2DFunction.X_POSITION] = (qa.x1 - 0.5);
doubletParams[Gaussian2DFunction.Y_POSITION] = (qa.y1 - 0.5);
doubletParams[Gaussian2DFunction.PARAMETERS_PER_PEAK + Gaussian2DFunction.SIGNAL] =
params[Gaussian2DFunction.SIGNAL] * 0.5;
doubletParams[Gaussian2DFunction.PARAMETERS_PER_PEAK + Gaussian2DFunction.X_POSITION] =
(qa.x2 - 0.5);
doubletParams[Gaussian2DFunction.PARAMETERS_PER_PEAK + Gaussian2DFunction.Y_POSITION] =
(qa.y2 - 0.5);
// -+-+-
// Note: Filter validation is disabled in the fit configuration (fits are validated
// in the multi-path filter).
// - Disable checking within the fit region as we do that per peak
// (which is better than failing if either peak is outside the region)
// - Increase the iterations level then reset afterwards.
final int maxIterations = fitConfig.getMaxIterations();
final int maxEvaluations = fitConfig.getMaxFunctionEvaluations();
fitConfig.setFitRegion(0, 0, 0);
fitConfig.setMaxIterations(maxIterations * ITERATION_INCREASE_FOR_DOUBLETS);
fitConfig
.setMaxFunctionEvaluations(maxEvaluations * FitWorker.EVALUATION_INCREASE_FOR_DOUBLETS);
// We assume that residuals calculation is on but just in case something else turned it off we
// get the state.
final boolean isComputeResiduals = gf.isComputeResiduals();
gf.setComputeResiduals(false);
final boolean[] amplitudeEstimate = new boolean[2];
final DynamicPeakResultValidationData validationData =
new DynamicPeakResultValidationData(2) {
@Override
protected void compute(int n) {
localStats[n] = getLocalStatistics(n, params);
}
};
fitConfig.setPeakResultValidationData(validationData);
fitConfig.setPrecomputedFunctionValues(precomputedFunctionValues);
final FitResult newFitResult = gf.fit(region, width, height, 2, doubletParams,
amplitudeEstimate, doubletParams[Gaussian2DFunction.BACKGROUND] == 0);
fitConfig.setPrecomputedFunctionValues(null);
gf.setComputeResiduals(isComputeResiduals);
fitConfig.setFitRegion(width, height, 0.5);
fitConfig.setMaxIterations(maxIterations);
fitConfig.setMaxFunctionEvaluations(maxEvaluations);
updateResult(newFitResult);
if (newFitResult.getStatus() == FitStatus.OK) {
// Adjusted Coefficient of determination is not good for non-linear models. Use the
// Bayesian Information Criterion (BIC):
// TODO - Make the selection criteria for Doublets configurable:
// MLE - AIC, BIC, LLR
// WLSE - AIC, BIC, q-values of each chi-square
// LSE - Adjusted coefficient of determination
// Note: Numerical recipes pp 669 uses 0.1 for q-value for weighted least squares fitting
// This is 0.9 for p!
final double doubleValue = getFitValue();
final int length = width * height;
double ic1 = Double.NaN;
double ic2 = Double.NaN;
final boolean improvement;
final FunctionSolverType solverType = gf.getFunctionSolver().getType();
if (solverType == FunctionSolverType.MLE
// && fitConfig.isModelCamera()
) {
// ------------
// TODO: Check this is still true as we may need to change the improvement criterion.
// Note: the residuals are no longer computed for all solvers.
// The MLE is only good if we are modelling the camera noise.
// The MLE put out by the Poisson model is not better than using the IC from the fit
// residuals.
// ------------
ic1 = MathUtils.getBayesianInformationCriterion(singleValue, length,
fitResult.getNumberOfFittedParameters());
ic2 = MathUtils.getBayesianInformationCriterion(doubleValue, length,
newFitResult.getNumberOfFittedParameters());
// IC should be lower
improvement = ic2 < ic1;
if (logger != null) {
LoggerUtils.log(logger, Level.INFO,
"Model improvement - Log likelihood (IC) : %f (%f) => %f (%f) : %f", singleValue,
ic1, doubleValue, ic2, ic1 - ic2);
}
} else if (solverType == FunctionSolverType.WLSE) {
// If using the weighted least squares estimator then we can get the log likelihood from
// an approximation
ic1 = getBayesianInformationCriterionFromResiduals(singleValue, length,
fitResult.getNumberOfFittedParameters());
ic2 = getBayesianInformationCriterionFromResiduals(doubleValue, length,
newFitResult.getNumberOfFittedParameters());
// IC should be lower
improvement = ic2 < ic1;
if (logger != null) {
LoggerUtils.log(logger, Level.INFO,
"Model improvement - Chi-squared (IC) : %f (%f) => %f (%f) : %f", singleValue, ic1,
doubleValue, ic2, ic1 - ic2);
}
} else if (solverType == FunctionSolverType.LSE) {
// Adjusted r^2 should be higher
improvement = doubleValue > singleValue;
if (logger != null) {
LoggerUtils.log(logger, Level.INFO, "Model improvement - Adjusted R^2 : %f => %f",
singleValue, doubleValue);
}
} else {
throw new IllegalStateException("Unable to calculate solution improvement");
}
if (debugLogger != null) {
double[] peakParams = newFitResult.getParameters();
if (peakParams != null) {
peakParams = Arrays.copyOf(peakParams, peakParams.length);
final int npeaks = peakParams.length / Gaussian2DFunction.PARAMETERS_PER_PEAK;
for (int i = 0; i < npeaks; i++) {
peakParams[i * Gaussian2DFunction.PARAMETERS_PER_PEAK
+ Gaussian2DFunction.X_POSITION] += 0.5 + regionBounds.x;
peakParams[i * Gaussian2DFunction.PARAMETERS_PER_PEAK
+ Gaussian2DFunction.Y_POSITION] += 0.5 + regionBounds.y;
}
}
LoggerUtils.log(debugLogger, Level.INFO,
"Doublet %d [%d,%d] %s (%s) %s [%f -> %f] IC [%f -> %f] = %s\n", slice,
cc.fromRegionToGlobalX(cx), cc.fromRegionToGlobalY(cy), newFitResult.getStatus(),
newFitResult.getStatusData(), getFitValueName(), singleValue, doubleValue, ic1, ic2,
Arrays.toString(peakParams));
}
// Check if the predictive power of the model is better with two peaks:
if (!improvement) {
return createResult(newFitResult, null, FitStatus.NO_MODEL_IMPROVEMENT);
}
// Validation of fit. For each spot:
// 1. Check the spot is inside the region
// 2. Check the distance of each new centre from the original centre.
// If the shift is too far (e.g. half the distance to the edge), the centre
// must be in the correct quadrant.
// 3. Check we are not closer to a fitted spot. This has already had a chance at
// fitting a doublet so is ignored.
// 4. Check if there is an unfit candidate spot closer than the current candidate.
// This represents drift out to fit another spot that will be fit later.
final double[] fitParams = newFitResult.getParameters();
final double[] initialParams = newFitResult.getInitialParameters();
// Allow the shift to span half of the fitted window.
final double halfWindow = 0.5 * Math.min(regionBounds.width, regionBounds.height);
final int[] position = new int[2];
final int[] candidateIndex = new int[2];
int peakCount = 0;
NEXT_PEAK: for (int n = 0; n < 2; n++) {
final int offset = n * Gaussian2DFunction.PARAMETERS_PER_PEAK;
// Ensure the initial parameters are at the candidate position since we may have used an
// estimate.
// This will ensure that drift is computed correctly.
initialParams[Gaussian2DFunction.X_POSITION + offset] =
candidates.get(candidateId).x - regionBounds.x;
initialParams[Gaussian2DFunction.Y_POSITION + offset] =
candidates.get(candidateId).y - regionBounds.y;
// 1. Check the spot is inside the region
// Note that during processing the data is assumed to refer to the top-left
// corner of the pixel. The coordinates should be represented in the middle of the pixel
// so add a 0.5 shift to the coordinates.
final double xpos = fitParams[Gaussian2DFunction.X_POSITION + offset] + 0.5;
final double ypos = fitParams[Gaussian2DFunction.Y_POSITION + offset] + 0.5;
if (xpos < 0 || xpos > width || ypos < 0 || ypos > height) {
if (logger != null) {
LoggerUtils.log(logger, Level.INFO,
"Fitted coordinates too far outside the fitted region (x %g || y %g) in %dx%d",
xpos, ypos, width, height);
}
continue;
}
// 2. Check the distance of each new centre from the original centre.
// If the shift is too far (e.g. half the distance to the edge), the centre
// must be in the correct quadrant.
double xShift = fitParams[Gaussian2DFunction.X_POSITION + offset]
- params[Gaussian2DFunction.X_POSITION];
double yShift = fitParams[Gaussian2DFunction.Y_POSITION + offset]
- params[Gaussian2DFunction.Y_POSITION];
if (Math.abs(xShift) > halfWindow || Math.abs(yShift) > halfWindow) {
// Allow large shifts if they are along the vector
final double a = QuadrantAnalysis.getAngle(qa.vector, new double[] {xShift, yShift});
// 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 (a > 0.25 * Math.PI && a < 0.75 * Math.PI) {
if (logger != null) {
LoggerUtils.log(logger, Level.INFO,
"Bad peak %d: Fitted coordinates moved into wrong quadrant (x=%g,y=%g,a=%f)", n,
xShift, yShift, a * 57.29578);
}
continue;
}
}
// Spots from the doublet must be closer to the single fit than any other spots.
// This is true for candidates we have yet to fit or already fitted candidates.
// Distance to current candidate
xShift = fitParams[Gaussian2DFunction.X_POSITION + offset]
- initialParams[Gaussian2DFunction.X_POSITION];
yShift = fitParams[Gaussian2DFunction.Y_POSITION + offset]
- initialParams[Gaussian2DFunction.Y_POSITION];
final double distanceToSingleFit2 = xShift * xShift + yShift * yShift;
// 3. Check we are not closer to a fitted spot. This has already had a chance at
// fitting a doublet so is ignored.
if (peakNeighbours.getSize() != 0) {
// Coords for comparison to the real positions
final float fcx2 =
(float) (regionBounds.x + fitParams[Gaussian2DFunction.X_POSITION + offset] + 0.5);
final float fcy2 =
(float) (regionBounds.y + fitParams[Gaussian2DFunction.Y_POSITION + offset] + 0.5);
final float d2 = (float) distanceToSingleFit2;
for (int i = 0; i < peakNeighbours.getSize(); i++) {
if (d2 > distance2(fcx2, fcy2,
peakNeighbours.get(i).params[Gaussian2DFunction.X_POSITION],
peakNeighbours.get(i).params[Gaussian2DFunction.Y_POSITION])) {
if (logger != null) {
LoggerUtils.log(logger, Level.INFO,
"Bad peak %d: Fitted coordinates moved closer to another result (%d,%d : "
+ "x=%.1f,y=%.1f : %.1f,%.1f)",
n, candidates.get(candidateId).x, candidates.get(candidateId).y, fcx2, fcy2,
peakNeighbours.get(i).params[Gaussian2DFunction.X_POSITION],
peakNeighbours.get(i).params[Gaussian2DFunction.Y_POSITION]);
}
// There is another fitted result that is closer.
// Note: The fit region is not centred on the other spot so this fit will probably
// be worse and is discarded (not compared to the existing fit to get the best one).
// Q. Should this be returned for validation?
// A. Currently the MultiPathFilter accepts any new result and all existing results
// must pass.
// So we could return this as an existing result which would make validation
// tougher.
// However the existing result should have been subtracted from the input data so it
// will not
// be a full peak making validation incorrect. So at the moment we ignore this
// result.
// Note that any new result will still have to be valid.
continue NEXT_PEAK;
}
}
}
// 4. Check if there is an unfit candidate spot closer than the current candidate.
// This represents drift out to fit another unfitted spot.
int otherId = candidateId;
if (neighbours.getSize() != 0) {
// Position - do not add 0.5 pixel offset to allow distance comparison to integer
// candidate positions.
final float fcx2 =
(float) (regionBounds.x + fitParams[Gaussian2DFunction.X_POSITION + offset]);
final float fcy2 =
(float) (regionBounds.y + fitParams[Gaussian2DFunction.Y_POSITION + offset]);
double mind2 = distanceToSingleFit2;
for (int j = 0; j < neighbours.getSize(); j++) {
final int id = neighbours.get(j).index;
if (isFit(id)) {
// This will be in the already fitted results instead so ignore...
continue;
}
final double d2 = distance2(fcx2, fcy2, candidates.get(id));
if (mind2 > d2) {
mind2 = d2;
otherId = id;
}
}
if (otherId != candidateId) {
if (logger != null) {
LoggerUtils.log(logger, Level.INFO,
"Bad peak %d: Fitted coordinates moved closer to another candidate (%d,%d : "
+ "x=%.1f,y=%.1f : %d,%d)",
n, candidates.get(candidateId).x, candidates.get(candidateId).y, fcx2 + 0.5f,
fcy2 + 0.5f, candidates.get(otherId).x, candidates.get(otherId).y);
}
// There is another candidate to be fit later that is closer.
// This may be used as an estimate so we return it as such (i.e we do not ignore it)
// Update the initial parameters to the position of the candidate so
// that drift is correct for filtering
initialParams[Gaussian2DFunction.X_POSITION + offset] =
candidates.get(otherId).x - regionBounds.x;
initialParams[Gaussian2DFunction.Y_POSITION + offset] =
candidates.get(otherId).y - regionBounds.y;
}
}
candidateIndex[peakCount] = otherId;
position[peakCount++] = n;
}
if (peakCount == 0) {
return createResult(newFitResult, null, FitStatus.FAILED_VALIDATION);
}
// Return results for validation
final PreprocessedPeakResult[] results = new PreprocessedPeakResult[peakCount];
for (int i = 0; i < peakCount; i++) {
// If it is this candidate, or an earlier one that was not fit then this is a new result.
// Otherwise it is a candidate we will process later
final ResultType resultType =
(candidateIndex[i] <= candidateId) ? ResultType.NEW : ResultType.CANDIDATE;
final double[] fitParamDevs = (precomputedFunctionValues == null)
// For a single fit as a doublet we can use the deviations directly
? newFitResult.getParameterDeviations()
// If there was a pre-computed function then the deviations must be recomputed using
// the neighbours.
: null;
results[i] = resultFactory.createPreprocessedPeakResult(candidateIndex[i], position[i],
initialParams, fitParams, fitParamDevs, validationData, resultType);
}
return createResult(newFitResult, results);
}
if (logger != null) {
LoggerUtils.log(logger, Level.INFO, "Unable to fit 2-kernel model : %s",
newFitResult.getStatus());
}
if (debugLogger != null) {
double[] peakParams = newFitResult.getParameters();
if (peakParams != null) {
peakParams = Arrays.copyOf(peakParams, peakParams.length);
final int npeaks = peakParams.length / Gaussian2DFunction.PARAMETERS_PER_PEAK;
for (int i = 0; i < npeaks; i++) {
peakParams[i * Gaussian2DFunction.PARAMETERS_PER_PEAK
+ Gaussian2DFunction.X_POSITION] += 0.5 + regionBounds.x;
peakParams[i * Gaussian2DFunction.PARAMETERS_PER_PEAK
+ Gaussian2DFunction.Y_POSITION] += 0.5 + regionBounds.y;
}
}
LoggerUtils.log(debugLogger, Level.INFO, "Doublet %d [%d,%d] %s (%s) = %s\n", slice,
cc.fromRegionToGlobalX(cx), cc.fromRegionToGlobalY(cy), newFitResult.getStatus(),
newFitResult.getStatusData(), Arrays.toString(peakParams));
}
return createResult(newFitResult, null);
// return null;
}
/**
* Get the Bayesian Information Criterion (BIC) for a least squares estimate. This assumes that
* the residuals are distributed according to independent identical normal distributions (with
* zero mean).
*
* @param sumOfSquaredResiduals the sum of squared residuals from the nonlinear least-squares
* fit
* @param numberOfPoints The number of data points
* @param numberOfParameters The number of fitted parameters
* @return The Bayesian Information Criterion
* @see http://en.wikipedia.org/wiki/
* Bayesian_information_criterion
*/
private double getBayesianInformationCriterionFromResiduals(double sumOfSquaredResiduals,
int numberOfPoints, int numberOfParameters) {
return MathUtils.getBayesianInformationCriterion(
MathUtils.getLogLikelihood(sumOfSquaredResiduals, numberOfPoints), numberOfPoints,
numberOfParameters);
}
private float distance2(float cx, float cy, Spot spot) {
final float dx = cx - spot.x;
final float dy = cy - spot.y;
return dx * dx + dy * dy;
}
private float distance2(float cx, float cy, float x, float y) {
final float dx = cx - x;
final float dy = cy - y;
return dx * dx + dy * dy;
}
}
private void storeEstimate(int index, PreprocessedPeakResult peak, byte filterRank) {
final double[] params = peak.toGaussian2DParameters();
double precision;
switch (fitConfig.getPrecisionMethodValue()) {
case PrecisionMethod.MORTENSEN_VALUE:
precision = peak.getLocationVariance();
break;
case PrecisionMethod.MORTENSEN_LOCAL_BACKGROUND_VALUE:
precision = peak.getLocationVariance2();
break;
case PrecisionMethod.POISSON_CRLB_VALUE:
precision = peak.getLocationVarianceCrlb();
break;
default:
// Get a standard precision, even if inaccurate it is good enough for
// differentiating estimates
precision = peak.getLocationVariance();
// precision = 0;
break;
}
storeEstimate(index, params, precision, filterRank);
}
private void storeEstimate(int index, double[] params, double precision, byte filterRank) {
// Add region offset
params[Gaussian2DFunction.X_POSITION] += estimateOffsetx;
params[Gaussian2DFunction.Y_POSITION] += estimateOffsety;
// Compute distance to spot
final double dx = candidates.get(index).x - params[Gaussian2DFunction.X_POSITION];
final double dy = candidates.get(index).y - params[Gaussian2DFunction.Y_POSITION];
final double d2 = dx * dx + dy * dy;
final Estimate[] estimates;
// dx and dy should be <=1 pixel when a candidate is being fit since we use bounds.
// They can be larger if we drifted close to another candidate (e.g. during doublet fitting)
// or if this is the result of fitting the current candidate (which is not bounded).
if (dx < -1 || dx > 1 || dy < -1 || dy > 1) {
// if (dynamicMultiPathFitResult.candidateId != i)
// System.out.printf("Drift error: [%d,%d] %d %.1f %.1f\n", slice,
// dynamicMultiPathFitResult.candidateId,
// i, dx, dy);
// Ignore this as it is not a good estimate
if (d2 > 2) {
return;
}
// Store as a non-local estimate
estimates = this.estimates2;
} else {
// Store as a close estimate
estimates = this.estimates;
}
if (estimates[index] == null || estimates[index].isWeaker(filterRank, d2, precision)) {
estimates[index] = new Estimate(params, filterRank, d2, precision);
}
}
/**
* Extract parameters for the specified peak. The background is ignored.
*
* @param params the params
* @param peakNumber the peak
* @return the extracted params
*/
static double[] extractSpotParams(double[] params, int peakNumber) {
final double[] newParams = new double[1 + Gaussian2DFunction.PARAMETERS_PER_PEAK];
System.arraycopy(params, peakNumber * Gaussian2DFunction.PARAMETERS_PER_PEAK + 1, newParams, 1,
Gaussian2DFunction.PARAMETERS_PER_PEAK);
return newParams;
}
/**
* Extract parameters other than the specified peak. The background is ignored.
*
* @param params the params
* @param peakNumber the peak
* @param peakCount the number of peaks
* @return the extracted params
*/
static double[] extractOtherParams(double[] params, int peakNumber, int peakCount) {
final double[] newParams = new double[params.length - Gaussian2DFunction.PARAMETERS_PER_PEAK];
if (peakNumber > 0) {
System.arraycopy(params, 1, newParams, 1,
peakNumber * Gaussian2DFunction.PARAMETERS_PER_PEAK);
}
final int left = peakCount - (peakNumber + 1);
if (left > 0) {
System.arraycopy(params, (peakNumber + 1) * Gaussian2DFunction.PARAMETERS_PER_PEAK + 1,
newParams, peakNumber * Gaussian2DFunction.PARAMETERS_PER_PEAK + 1,
left * Gaussian2DFunction.PARAMETERS_PER_PEAK);
}
return newParams;
}
/**
* Join the parameters from the two parameter arrays. The background is taken from the first
* array. This function can be used to append the parameters from a pre-computed function onto the
* parameters from a fit result to build the entire function parameters.
*
* @param params1 the params 1
* @param params2 the params 2
* @return the double[]
*/
@SuppressWarnings("unused")
private static double[] joinParams(double[] params1, double[] params2) {
final double[] params = new double[params1.length + params2.length - 1];
System.arraycopy(params1, 0, params, 0, params1.length);
System.arraycopy(params2, 1, params, params1.length, params2.length - 1);
return params;
}
/**
* Gets the single fitting background.
*
* @return The background estimate when fitting a single peak.
*/
@SuppressWarnings("unused")
private float getSingleFittingBackground() {
final float background;
if (useFittedBackground && fittedBackground.getN() != 0) {
// Use the average background from all results
background = (float) (fittedBackground.getMean());
} else if (this.noise != 0) {
// Initial guess using the noise (assuming all noise is from Poisson background).
// EMCCD will have increase noise by a factor of sqrt(2)
final CalibrationReader r = new CalibrationReader(fitConfig.getCalibration());
final double gain = (isFitCameraCounts) ? r.getCountPerPhoton() : 1;
background = (float) (PeakResultHelper.noiseToLocalBackground(noise, gain, r.isEmCcd()));
} else {
// Initial guess using the data estimator
background = estimateBackground();
}
return background;
}
void resetNeighbours() {
clearGridCache();
candidateNeighbourCount = 0;
fittedNeighbourCount = 0;
allNeighbours = null;
allFittedNeighbours = null;
}
/**
* Find neighbours.
*
* @param candidate the candidate
* @return the candidate list
*/
CandidateList findNeighbours(Candidate candidate) {
if (allNeighbours == null) {
// Using the neighbour grid
allNeighbours = gridManager.getCandidateNeighbours(candidate);
allNeighbours.sort();
}
return allNeighbours;
}
/**
* Find peak neighbours.
*
* @param candidate the candidate
* @return the candidate list
*/
CandidateList findPeakNeighbours(Candidate candidate) {
if (allFittedNeighbours == null) {
// Using the neighbour grid
allFittedNeighbours = gridManager.getFittedNeighbours(candidate.x, candidate.y);
}
return allFittedNeighbours;
}
/**
* Search for any neighbours within a set height of the specified peak that is within the search
* region bounds.
*
* @param regionBounds the region bounds
* @param candidateId the candidate index
* @param background The background in the region
* @return The number of neighbours
*/
int findNeighboursInRegion(Rectangle regionBounds, int candidateId, float background) {
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 Candidate spot = candidates.get(candidateId);
final float heightThreshold;
if (relativeIntensity) {
// No background when spot filter has relative intensity
heightThreshold = (float) (spot.intensity * config.getNeighbourHeightThreshold());
} else if (spot.intensity < background) {
heightThreshold = spot.intensity;
} else {
heightThreshold =
(float) ((spot.intensity - background) * config.getNeighbourHeightThreshold()
+ background);
}
// Check all maxima that are lower than this
candidateNeighbourCount = 0;
// Using the neighbour grid.
// Note this will also include all higher intensity spots that failed to be fit.
// These may still have estimates.
CandidateList neighbours = findNeighbours(spot);
for (int i = 0; i < neighbours.getSize(); i++) {
final Candidate neighbour = neighbours.get(i);
if (isFit(neighbour.index) || canIgnore(neighbour.x, neighbour.y, xmin, xmax, ymin, ymax,
neighbour.intensity, heightThreshold)) {
continue;
}
candidateNeighbours[candidateNeighbourCount++] = neighbour;
}
// XXX Debugging
// int c = 0;
// // Processing all lower spots.
// //for (int i = n + 1; i < candidates.getSize(); i++)
// // Processing all spots.
// for (int i = 0; i < this.candidates.getSize(); i++)
// {
// if (i == n || isFit(i))
// continue;
// if (canIgnore(this.candidates.get(i).x, this.candidates.get(i).y, xmin, xmax, ymin, ymax,
// this.candidates.get(i).intensity, heightThreshold))
// continue;
// //neighbourIndices[c++] = i;
// if (neighbourIndices[c++] != i)
// throw new RuntimeException("invalid grid neighbours");
// }
// Check all existing maxima.
fittedNeighbourCount = 0;
if (fittedBackground.getN() != 0) {
// Since these will be higher than the current peak it is prudent to extend the range that
// should be considered.
// Use 2x the configured peak standard deviation.
// final float range = 2f *
// (float) Math.max(fitConfig.getInitialPeakStdDev0(), fitConfig.getInitialPeakStdDev1());
// final float xmin2 = regionBounds.x - range;
// final float xmax2 = regionBounds.x + regionBounds.width + range;
// final float ymin2 = regionBounds.y - range;
// final float ymax2 = regionBounds.y + regionBounds.height + range;
// for (int i = 0; i < sliceResults.size(); i++)
// {
// final PeakResult result = sliceResults.get(i);
// // No height threshold check as this is a validated peak
// if (canIgnore(result.getXPosition(), result.getYPosition(), xmin2, xmax2, ymin2, ymax2))
// continue;
// fittedNeighbourIndices[fittedNeighbourCount++] = i;
// }
// Note: A smarter filter would be to compute the bounding rectangle of each fitted result and
// see if it overlaps the target region. This would involve overlap analysis
final double x0min = regionBounds.x;
final double y0min = regionBounds.y;
final double x0max = regionBounds.x + regionBounds.width;
final double y0max = regionBounds.y + regionBounds.height;
neighbours = findPeakNeighbours(spot);
for (int i = 0; i < neighbours.getSize(); i++) {
final Candidate neighbour = neighbours.get(i);
// No height threshold check as this is a validated peak
final double xw = 2 * neighbour.params[Gaussian2DFunction.X_SD];
final double yw = 2 * neighbour.params[Gaussian2DFunction.Y_SD];
final double x = neighbour.params[Gaussian2DFunction.X_POSITION];
final double y = neighbour.params[Gaussian2DFunction.Y_POSITION];
if (intersects(x0min, y0min, x0max, y0max, x - xw, y - yw, x + xw, y + yw)) {
fittedNeighbours[fittedNeighbourCount++] = neighbour;
}
}
}
return candidateNeighbourCount + fittedNeighbourCount;
}
/**
* Copied from java.awt.geom.Rectangle2D and modified assuming width and height is non-zero.
*
* @param x0min the x 0 min
* @param y0min the y 0 min
* @param x0max the x 0 max
* @param y0max the y 0 max
* @param x1min the x 1 min
* @param y1min the y 1 min
* @param x1max the x 1 max
* @param y1max the y 1 max
* @return true if they intersect
*/
public boolean intersects(double x0min, double y0min, double x0max, double y0max, double x1min,
double y1min, double x1max, double y1max) {
return (x1max > x0min && y1max > y0min && x1min < x0max && y1min < y0max);
}
private static boolean canIgnore(int x, int y, int xmin, int xmax, int ymin, int ymax,
float height, float heightThreshold) {
return (x < xmin || x > xmax || y < ymin || y > ymax || height < heightThreshold);
}
/**
* Get an estimate of the background level using the median of the image.
*
* @return The background level
*/
private float estimateBackground() {
createDataEstimator();
// Use the median
return dataEstimator.getPercentile(50);
}
private float estimateNoise() {
createDataEstimator();
return estimateNoise(dataEstimator,
FitProtosHelper.convertNoiseEstimatorMethod(config.getNoiseMethod()));
}
/**
* Estimate the noise in the data.
*
* @param data the data
* @param width the width
* @param height the height
* @param method the method
* @return The noise
*/
public static float estimateNoise(float[] data, int width, int height,
NoiseEstimatorMethod method) {
// Do the same logic as the non-static method
final DataEstimator dataEstimator = newDataEstimator(data, width, height);
return estimateNoise(dataEstimator, FitProtosHelper.convertNoiseEstimatorMethod(method));
}
private static float estimateNoise(DataEstimator dataEstimator, NoiseEstimator.Method method) {
// No methods using a background region are good so we just use the global noise estimate
// if (dataEstimator.isBackgroundRegion())
// return dataEstimator.getNoise();
return dataEstimator.getNoise(method);
}
private void createDataEstimator() {
if (dataEstimator == null) {
final int width = job.getBounds().width;
final int height = job.getBounds().height;
dataEstimator = newDataEstimator(data, width, height);
}
}
private static DataEstimator newDataEstimator(float[] data, int width, int height) {
// TODO - add options to control the thresholding method and the background fraction
return new DataEstimator(data, width, height);
}
/**
* Identify failed peaks that seem quite high, e.g. if above the background + 3X the noise.
*
*
Updates the input failed array to contain the candidates.
*
* @param failed the failed
* @param failedCount the failed count
* @param background the background
* @param noise the noise
* @param maxIndices the max indices
* @param smoothData the smooth data
* @return The number of re-fit candidates
*/
@SuppressWarnings("unused")
private static int identifyRefitCandidates(int[] failed, int failedCount, float background,
float noise, int[] maxIndices, float[] smoothData) {
int candidates = 0;
final float threshold = background + 3 * noise;
for (int i = 0; i < failedCount; i++) {
if (smoothData[maxIndices[failed[i]]] > threshold) {
failed[candidates++] = i;
}
}
return candidates;
}
/**
* Gets the total time used for fitting.
*
* @return the total time used for fitting.
*/
public long getTime() {
return time;
}
/**
* Signal that the worker should end.
*/
public void finish() {
finished = true;
}
/**
* Checks if is finished.
*
* @return True if the worker has finished.
*/
public boolean isFinished() {
return finished;
}
/**
* Checks if using the average fitted background as the background estimate for new fits.
*
* @return true if using the average fitted background as the background estimate for new fits.
*/
public boolean isUseFittedBackground() {
return useFittedBackground;
}
/**
* Set to true to use the average fitted background as the background estimate for new fits. The
* default is to use the image average as the background for all fits.
*
* @param useFittedBackground the new use fitted background
*/
public void setUseFittedBackground(boolean useFittedBackground) {
this.useFittedBackground = useFittedBackground;
}
/**
* Sets the debug logger instance. This can be used for capturing debugging information.
*
* @param logger the new logger
*/
public void setDebugLogger(Logger logger) {
this.debugLogger = logger;
}
/**
* Set the counter. This can be used to count the type of fitting process that was performed.
*
* @param counter The counter
*/
public void setCounter(FitTypeCounter counter) {
this.counter = counter;
}
private void addFitType(FitType fitType) {
if (counter != null) {
counter.add(fitType);
}
}
@Override
public int getFrame() {
return slice;
}
@Override
public int getNumberOfResults() {
// This is the total number of results we produce.
// Note that although we have may a maximum candidate less than the length
// of the candidate list, we continue processing candidates if we have an
// estimate. This is possibly a candidate that was a good fit but was not labelled
// as a new result because of drift to another yet-to-be-processed candidate.
return candidates.getLength();
}
/**
* Provide the multi-path fit results dynamically.
*/
private class DynamicMultiPathFitResult extends MultiPathFitResult {
/** The constant for no Quadrant Analysis score. */
private static final double NO_QA_SCORE = -1;
final ImageExtractor ie;
final ImageExtractor ie2;
boolean dynamic;
Rectangle regionBounds;
double[] region;
double[] region2;
double[] varG2;
CandidateSpotFitter spotFitter;
final FitType fitType = new FitType();
boolean isValid;
@SuppressWarnings("unused")
int extra;
final FloatAreaSum area;
DynamicMultiPathFitResult(ImageExtractor ie, ImageExtractor ie2, boolean dynamic) {
this.setFrame(FitWorker.this.slice);
this.setWidth(cc.dataBounds.width);
this.setHeight(cc.dataBounds.height);
area = FloatAreaSum.wrap(data, getWidth(), getHeight());
this.ie = ie;
this.ie2 = ie2;
this.dynamic = dynamic;
}
void reset(int candidateId) {
this.setCandidateId(candidateId);
fitType.clear();
// Reset results
this.setMultiQaScore(NO_QA_SCORE);
this.setSingleQaScore(NO_QA_SCORE);
this.setMultiFitResult(null);
this.setMultiDoubletFitResult(null);
this.setSingleFitResult(null);
this.setDoubletFitResult(null);
// Only provide results if below the max candidate ID or we have a valid estimate
if (candidateId < candidates.getSize()) {
isValid = true;
} else if (isValid(candidateId)) {
extra++; // Count these for debugging
isValid = true;
} else {
isValid = false;
}
if (isValid) {
// Set fitting region
regionBounds = ie.getBoxRegionBounds(candidates.get(candidateId).x,
candidates.get(candidateId).y, fitting);
region = ie.crop(regionBounds, region);
region2 = ie2.crop(regionBounds, region2);
cc.setRegionBounds(regionBounds);
// Set up per-pixel noise
if (cameraModel.isPerPixelModel()) {
// Note: The region bounds are relative to the data bounds origin so
// convert them to absolute
final Rectangle bounds = new Rectangle(regionBounds);
bounds.x += cc.dataBounds.x;
bounds.y += cc.dataBounds.y;
final float[] v = (isFitCameraCounts) ? cameraModel.getVariance(bounds)
: cameraModel.getNormalisedVariance(bounds);
// Convert to double
if (ArrayUtils.getLength(varG2) == v.length) {
// Re-use space
for (int i = 0; i < v.length; i++) {
varG2[i] = v[i];
}
} else {
varG2 = SimpleArrayUtils.toDouble(v);
}
} else {
// Create a single valued weight array.
final float v = (isFitCameraCounts) ? cameraModel.getVariance(0, 0)
: cameraModel.getNormalisedVariance(0, 0);
// Only create if there is variance
if (v != 0) {
// Only create if the array size changes
final int length = regionBounds.width * regionBounds.height;
if (ArrayUtils.getLength(varG2) != length) {
varG2 = SimpleArrayUtils.newDoubleArray(length, v);
}
}
}
// Offsets to convert fit coordinates to the global reference frame
final float offsetx = cc.dataBounds.x + regionBounds.x + 0.5f;
final float offsety = cc.dataBounds.y + regionBounds.y + 0.5f;
// Note that the PreprocessedPeakResult will have coordinates
// in the global reference frame. We store estimates relative to
// the data bounds without the pixel offset making them suitable
// for initialising fitting.
estimateOffsetx = -cc.dataBounds.x - 0.5f;
estimateOffsety = -cc.dataBounds.y - 0.5f;
final ResultFactory factory = (dynamic) ? new DynamicResultFactory(offsetx, offsety)
: new FixedResultFactory(offsetx, offsety);
spotFitter = new CandidateSpotFitter(gf, factory, region, region2, varG2, regionBounds,
candidateId, area);
}
}
@Override
public FitResult getMultiFitResult() {
FitResult result = super.getMultiFitResult();
if (result == null && isValid) {
result = spotFitter.getResultMulti();
setMultiFitResult(result);
if (result != null) {
fitType.setMulti(true);
}
}
return result;
}
FitResult getSuperMultiFitResult() {
// Pass through the reference to the result
return super.getMultiFitResult();
}
@Override
public double getMultiQaScore() {
double score = super.getMultiQaScore();
if (score == NO_QA_SCORE && isValid) {
score = spotFitter.getQaScoreMulti();
this.setMultiQaScore(score);
}
return score;
}
@Override
public FitResult getMultiDoubletFitResult() {
FitResult result = super.getMultiDoubletFitResult();
if (result == null && isValid) {
result = spotFitter.getResultDoubletMulti(config.getResidualsThreshold());
setMultiDoubletFitResult(result);
fitType.setMultiDoublet(spotFitter.computedDoubletMulti);
}
return result;
}
FitResult getSuperMultiDoubletFitResult() {
// Pass through the reference to the result
return super.getMultiDoubletFitResult();
}
@Override
public FitResult getSingleFitResult() {
FitResult result = super.getSingleFitResult();
if (result == null && isValid) {
result = spotFitter.getResultSingle();
setSingleFitResult(result);
}
return result;
}
@Override
public double getSingleQaScore() {
double score = super.getSingleQaScore();
if (score == NO_QA_SCORE && isValid) {
score = spotFitter.getQaScoreSingle();
this.setSingleQaScore(score);
}
return score;
}
@Override
public FitResult getDoubletFitResult() {
FitResult result = super.getDoubletFitResult();
if (result == null && isValid) {
result = spotFitter.getResultDoubletSingle(config.getResidualsThreshold());
setDoubletFitResult(result);
fitType.setDoublet(spotFitter.computedDoubletSingle);
}
return result;
}
FitResult getSuperDoubletFitResult() {
// Pass through the reference to the result
return super.getDoubletFitResult();
}
}
@Override
public MultiPathFitResult getResult(int index) {
dynamicMultiPathFitResult.reset(index);
return dynamicMultiPathFitResult;
}
@Override
public void complete(int index) {
if (benchmarking) {
// When benchmarking we must generate all the results possible
// and store them in the job.
// We do not assess the results and we do not store estimates.
// This means that fitting results for the candidates that are not
// processed by the main routine may not be representative
// of fitting using a higher fail count or different residuals/neighbour
// thresholds.
// This means fitting and then selection of the best filter settings
// must be iterated until convergence to ensure the fitting+filter is
// optimum.
if (dynamicMultiPathFitResult.isValid) {
// Calling the spot fitter with zero residuals will force the result to be computed if
// possible
dynamicMultiPathFitResult.spotFitter.getResultDoubletMulti(0);
dynamicMultiPathFitResult.spotFitter.getResultDoubletSingle(0);
// Now update the result if they were previously null
dynamicMultiPathFitResult.getMultiFitResult();
dynamicMultiPathFitResult.getMultiQaScore();
dynamicMultiPathFitResult.getMultiDoubletFitResult();
dynamicMultiPathFitResult.getSingleFitResult();
dynamicMultiPathFitResult.getSingleQaScore();
dynamicMultiPathFitResult.getDoubletFitResult();
}
job.setMultiPathFitResult(index, dynamicMultiPathFitResult.copy(false));
}
// Send the actual results to the neighbour grid
if (flushToGrid()) {
// Count if there were any new results
success++;
}
}
@Override
public int getTotalCandidates() {
// This is the total number of candidates Ids we may produce
return candidates.getLength();
}
@Override
public void add(SelectedResult selectedResult) {
// TODO - Print the current state of the dynamicMultiPathFitResult to file.
// This will allow debugging what is different between the benchmark fit and the PeakFit.
// Output:
// slice
// candidate Id
// Initial and final params for each fit result.
// Details of the selected result.
// Add to the slice results.
final PreprocessedPeakResult[] results = selectedResult.results;
if (results == null) {
if (logger != null) {
final int candidateId = dynamicMultiPathFitResult.getCandidateId();
// The selected result does not include the filter failure status.
// There may be many filtering failures per candidate due to multiple fitting paths.
// Results that were fit and then filtered have an OK status. Only results
// that failed to produce a fit have a fit status that can be reported;
// this may occur for one or more fitting paths and we can only report on the
// selected result, otherwise leave blank.
String msg = "";
if (selectedResult.fitResult != null && selectedResult.fitResult.data != null) {
FitStatus status = ((FitResult) selectedResult.fitResult.data).getStatus();
if (status != FitStatus.OK) {
msg = status.toString();
}
}
//@formatter:off
LoggerUtils.log(logger, Level.INFO, "Not fit %d (%d,%d) %s", candidateId,
cc.fromDataToGlobalX(candidates.get(candidateId).x),
cc.fromDataToGlobalY(candidates.get(candidateId).y), msg);
//@formatter:on
}
// Reporting
if (this.counter != null) {
final FitType fitType = dynamicMultiPathFitResult.fitType;
addFitType(fitType);
}
return;
}
if (queueSize != 0) {
throw new IllegalStateException("There are results queued already!");
}
final int candidateId = dynamicMultiPathFitResult.getCandidateId();
final FitResult fitResult = (FitResult) selectedResult.fitResult.data;
// The background for each result was the local background. We want the fitted global background
final float background = (float) fitResult.getParameters()[0];
final double[] dev = fitResult.getParameterDeviations();
for (final PreprocessedPeakResult peak : results) {
if (peak.isExistingResult()) {
continue;
}
if (peak.isNewResult()) {
final double[] p = peak.toGaussian2DParameters();
// Store slice results relative to the data frame (not the global bounds)
// Convert back so that 0,0 is the top left of the data bounds
p[Gaussian2DFunction.X_POSITION] -= cc.dataBounds.x;
p[Gaussian2DFunction.Y_POSITION] -= cc.dataBounds.y;
final float[] params = new float[p.length];
params[Gaussian2DFunction.BACKGROUND] = background;
for (int j = 1; j < p.length; j++) {
params[j] = (float) p[j];
}
final float[] paramDevs;
if (dev == null) {
paramDevs = null;
} else {
paramDevs = new float[p.length];
paramDevs[Gaussian2DFunction.BACKGROUND] = (float) dev[Gaussian2DFunction.BACKGROUND];
final int offset = peak.getId() * Gaussian2DFunction.PARAMETERS_PER_PEAK;
for (int j = 1; j < p.length; j++) {
paramDevs[j] = (float) dev[offset + j];
}
}
addSingleResult(peak.getCandidateId(), params, paramDevs, fitResult.getError(),
peak.getNoise(), fitConfig.getLocationVariance(peak));
if (logger != null) {
// Show the shift, signal and width spread
LoggerUtils.log(logger, Level.INFO,
"Fit OK %d (%.1f,%.1f) [%d]: Shift = %.3f,%.3f : SNR = %.2f : Width = %.2f,%.2f",
peak.getCandidateId(), peak.getX(), peak.getY(), peak.getId(),
Math.sqrt(peak.getXRelativeShift2()), Math.sqrt(peak.getYRelativeShift2()),
peak.getSnr(), peak.getXSdFactor(), peak.getYSdFactor());
}
}
// else:
// This is a candidate that passed validation. Store the estimate as passing the primary
// filter.
// We now do this is the pass() method.
// storeEstimate(results[i].getCandidateId(), results[i], FILTER_RANK_PRIMARY);
}
job.setFitResult(candidateId, fitResult);
// Reporting
if (this.counter != null) {
final FitType fitType = dynamicMultiPathFitResult.fitType;
if (selectedResult.fitResult.getStatus() == 0) {
fitType.setOk(true);
if (dynamicMultiPathFitResult.getSuperMultiFitResult() == selectedResult.fitResult) {
fitType.setMultiOk(true);
} else if (dynamicMultiPathFitResult
.getSuperMultiDoubletFitResult() == selectedResult.fitResult) {
fitType.setMultiDoubletOk(true);
} else if (dynamicMultiPathFitResult
.getSuperDoubletFitResult() == selectedResult.fitResult) {
fitType.setDoubletOk(true);
}
}
addFitType(fitType);
}
if (logger != null) {
switch (fitResult.getStatus()) {
case OK:
// We log good results in the loop above.
break;
case BAD_PARAMETERS:
case FAILED_TO_ESTIMATE_WIDTH:
logger.log(Level.INFO,
() -> "Bad parameters: " + Arrays.toString(fitResult.getInitialParameters()));
break;
default:
logger.log(Level.INFO, "{0}", fitResult.getStatus());
break;
}
}
// Debugging
if (debugLogger != null) {
double[] peakParams = fitResult.getParameters();
if (peakParams != null) {
// Parameters are the raw values from fitting the region. Convert for logging.
peakParams = Arrays.copyOf(peakParams, peakParams.length);
final int npeaks = peakParams.length / Gaussian2DFunction.PARAMETERS_PER_PEAK;
for (int i = 0; i < npeaks; i++) {
peakParams[i * Gaussian2DFunction.PARAMETERS_PER_PEAK + Gaussian2DFunction.X_POSITION] +=
cc.fromFitRegionToGlobalX();
peakParams[i * Gaussian2DFunction.PARAMETERS_PER_PEAK + Gaussian2DFunction.Y_POSITION] +=
cc.fromFitRegionToGlobalY();
if (fitConfig.isAngleFitting()) {
peakParams[i * Gaussian2DFunction.PARAMETERS_PER_PEAK + Gaussian2DFunction.ANGLE] *=
180.0 / Math.PI;
}
}
}
final int x = candidates.get(candidateId).x;
final int y = candidates.get(candidateId).y;
LoggerUtils.log(debugLogger, Level.INFO, "%d:%d [%d,%d] %s (%s) = %s", slice, candidateId,
cc.fromDataToGlobalX(x), cc.fromDataToGlobalY(y), fitResult.getStatus(),
fitResult.getStatusData(), Arrays.toString(peakParams));
}
}
@Override
public boolean isFit(int candidateId) {
// Return if we already have a fit result for this candidate
return candidates.get(candidateId).fit;
}
@Override
public boolean isValid(int candidateId) {
// If we have an estimate then this is a valid candidate for fitting.
// Q. Should we attempt fitting is we have only passed the min filter?
// return (estimates[candidateId] != null && estimates[candidateId].filterRank ==
// FILTER_RANK_PRIMARY) ||
// (estimates2[candidateId] != null && estimates2[candidateId].filterRank ==
// FILTER_RANK_PRIMARY);
return isValid[candidateId];
}
@Override
public void pass(PreprocessedPeakResult result) {
// Do not ignore these. They may be from a fit result that is eventually not selected so we
// cannot
// wait until the add(...) method is called with the selected result.
storeEstimate(result.getCandidateId(), result, FILTER_RANK_PRIMARY);
// We must implement the same logic as the default SimpleSelectedResultStore which visits every
// candidate that has passed the main filter
isValid[result.getCandidateId()] = true;
}
@Override
public void passMin(PreprocessedPeakResult result) {
// This is a candidate that passed validation. Store the estimate as passing the minimal filter.
storeEstimate(result.getCandidateId(), result, FILTER_RANK_MINIMAL);
}
/**
* Create a minimum filter to use for storing estimates.
*
* @param precisionMethod the precision method
* @return The minimal filter
*/
public static IDirectFilter createMinimalFilter(PrecisionMethod precisionMethod) {
final double signal = 30;
// Note: SNR is the mean signal to noise. Rose criterion sets a minimum level at 5.
// https://en.wikipedia.org/wiki/Signal-to-noise_ratio#Alternative_definition
// However a lower level is often observed for SMLM spot data.
final float snr = 2;
final double minWidth = 0.5;
final double maxWidth = 4;
final double shift = 2;
final double eshift = 0;
final double precision = 60;
// No Z-filtering
final float minZ = 0;
final float maxZ = 0;
switch (precisionMethod) {
case MORTENSEN:
return new MultiFilter(signal, snr, minWidth, maxWidth, shift, eshift, precision, minZ,
maxZ);
case MORTENSEN_LOCAL_BACKGROUND:
return new MultiFilter2(signal, snr, minWidth, maxWidth, shift, eshift, precision, minZ,
maxZ);
case POISSON_CRLB:
return new MultiFilterCrlb(signal, snr, minWidth, maxWidth, shift, eshift, precision, minZ,
maxZ);
case PRECISION_METHOD_NA:
// Turn off precision
return new MultiFilter(signal, snr, minWidth, maxWidth, shift, eshift, 0, minZ, maxZ);
default:
throw new IllegalArgumentException("Unknown precision method: " + precisionMethod);
}
}
/**
* Gets the noise estimate for the last processed job.
*
* @return the noise estimate
*/
public float getNoise() {
return noise;
}
}