package gdsc.foci.controller;
/*-----------------------------------------------------------------------------
* GDSC Plugins for ImageJ
*
* Copyright (C) 2016 Alex Herbert
* Genome Damage and Stability Centre
* University of Sussex, UK
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*---------------------------------------------------------------------------*/
import java.util.ArrayList;
import gdsc.foci.FindFoci;
import gdsc.foci.FindFociBaseProcessor;
import gdsc.foci.FindFociInitResults;
import gdsc.foci.FindFociMergeResults;
import gdsc.foci.FindFociMergeTempResults;
import gdsc.foci.FindFociPrelimResults;
import gdsc.foci.FindFociResult;
import gdsc.foci.FindFociResults;
import gdsc.foci.FindFociSearchResults;
import gdsc.foci.controller.MessageListener.MessageType;
import gdsc.foci.model.FindFociModel;
import gdsc.foci.model.FindFociState;
import ij.IJ;
import ij.ImagePlus;
import ij.WindowManager;
/**
* Runs the {@link gdsc.foci.FindFoci } algorithm using input from a
* synchronised queueing method.
*/
public class FindFociRunner extends Thread
{
private FindFociModel model = null;
private FindFociModel previousModel = null;
private final Object lock = new Object();
private final MessageListener listener;
private boolean running = true;
// Used for the staged FindFoci results
FindFoci ff = new FindFoci();
ImagePlus imp2;
FindFociInitResults initResults;
FindFociInitResults searchInitResults;
FindFociSearchResults searchArray;
FindFociInitResults mergeInitResults;
FindFociInitResults resultsInitResults;
FindFociInitResults maskInitResults;
FindFociMergeTempResults mergePeakResults;
FindFociMergeTempResults mergeSizeResults;
FindFociMergeResults mergeResults;
FindFociPrelimResults prelimResults;
FindFociResults results;
public FindFociRunner(MessageListener listener)
{
this.listener = listener;
notify(MessageType.READY);
}
/*
* (non-Javadoc)
*
* @see java.lang.Thread#run()
*/
public void run()
{
try
{
while (running)
{
// Check if there is a model to run. Synchronized to avoid conflict with the queue() method.
FindFociModel modelToRun = null;
synchronized (lock)
{
if (model != null)
{
modelToRun = model.deepCopy();
model = null;
}
}
if (modelToRun != null)
{
// TODO - This system currently has to wait for the last calculation to finish before
// looking for the next model to run. This means for long running calculations the
// user may have to wait a long time for the last one to finish, then wait again for
// the next.
// Ideally we would run the calculation on a different thread. We can then do the model
// comparison here. If the next model to run resets to an earlier state than the
// current model then we should cancel the current calculation and start again, part way
// through...
// Basically this needs better interrupt handling for long running jobs.
runFindFoci(modelToRun);
}
else
{
// Wait for a new model to be queued
pause();
}
}
}
catch (InterruptedException e)
{
if (running)
{
IJ.log("FindPeakRunner interupted: " + e.getLocalizedMessage());
}
}
catch (Throwable t)
{
// Log this to ImageJ. Do not bubble up exceptions
if (t.getMessage() != null)
IJ.log("A error occurred during processing: " + t.getMessage());
else
IJ.log("A error occurred during processing");
t.printStackTrace();
notify(MessageType.ERROR, t);
}
finally
{
ff = null;
imp2 = null;
initResults = null;
searchInitResults = null;
searchArray = null;
mergeInitResults = null;
resultsInitResults = null;
maskInitResults = null;
mergeResults = null;
prelimResults = null;
results = null;
finish();
}
}
private void notify(MessageType messageType, Object... params)
{
if (listener != null)
listener.notify(messageType, params);
}
/**
* Invoke the Thread.wait() method
*
* @throws InterruptedException
*/
private synchronized void pause() throws InterruptedException
{
wait();
}
/**
* Add a FindFociModel for processing
*
* @param newModel
*/
public synchronized void queue(FindFociModel newModel)
{
if (newModel != null)
{
synchronized (lock)
{
model = newModel;
}
notifyAll();
}
}
private void runFindFoci(FindFociModel model)
{
// Get the selected image
String title = model.getSelectedImage();
ImagePlus imp = WindowManager.getImage(title);
if (null == imp)
{
notify(MessageType.ERROR);
return;
}
if (!FindFoci.isSupported(imp.getBitDepth()))
{
notify(MessageType.ERROR);
return;
}
// Set-up the FindFoci variables
String maskImage = model.getMaskImage();
int backgroundMethod = model.getBackgroundMethod();
double backgroundParameter = model.getBackgroundParameter();
String thresholdMethod = model.getThresholdMethod();
String statisticsMode = model.getStatisticsMode();
int searchMethod = model.getSearchMethod();
double searchParameter = model.getSearchParameter();
int minSize = model.getMinSize();
boolean minimumAboveSaddle = model.isMinimumAboveSaddle();
boolean connectedAboveSaddle = model.isConnectedAboveSaddle();
int peakMethod = model.getPeakMethod();
double peakParameter = model.getPeakParameter();
int sortMethod = model.getSortMethod();
int maxPeaks = model.getMaxPeaks();
int showMask = model.getShowMask();
boolean overlayMask = model.isOverlayMask();
boolean showTable = model.isShowTable();
boolean clearTable = model.isClearTable();
boolean markMaxima = model.isMarkMaxima();
boolean markROIMaxima = model.isMarkROIMaxima();
boolean markUsingOverlay = model.isMarkUsingOverlay();
boolean hideLabels = model.isHideLabels();
boolean showMaskMaximaAsDots = model.isShowMaskMaximaAsDots();
// Ignore: model.isShowLogMessages()
boolean removeEdgeMaxima = model.isRemoveEdgeMaxima();
// Ignore: model.isSaveResults();
// Ignore: model.getResultsDirectory();
boolean objectAnalysis = model.isObjectAnalysis();
boolean showObjectMask = model.isShowObjectMask();
boolean saveToMemory = model.isSaveToMemory();
double gaussianBlur = model.getGaussianBlur();
int centreMethod = model.getCentreMethod();
double centreParameter = model.getCentreParameter();
double fractionParameter = model.getFractionParameter();
int outputType = FindFoci.getOutputMaskFlags(showMask);
if (overlayMask)
outputType += FindFoci.OUTPUT_OVERLAY_MASK;
if (showTable)
outputType += FindFoci.OUTPUT_RESULTS_TABLE;
if (clearTable)
outputType += FindFoci.OUTPUT_CLEAR_RESULTS_TABLE;
if (markMaxima)
outputType += FindFoci.OUTPUT_ROI_SELECTION;
if (markROIMaxima)
outputType += FindFoci.OUTPUT_MASK_ROI_SELECTION;
if (markUsingOverlay)
outputType += FindFoci.OUTPUT_ROI_USING_OVERLAY;
if (hideLabels)
outputType += FindFoci.OUTPUT_HIDE_LABELS;
if (!showMaskMaximaAsDots)
outputType += FindFoci.OUTPUT_MASK_NO_PEAK_DOTS;
int options = 0;
if (minimumAboveSaddle)
options |= FindFoci.OPTION_MINIMUM_ABOVE_SADDLE;
if (connectedAboveSaddle)
options |= FindFoci.OPTION_CONTIGUOUS_ABOVE_SADDLE;
if (statisticsMode.equalsIgnoreCase("inside"))
options |= FindFoci.OPTION_STATS_INSIDE;
else if (statisticsMode.equalsIgnoreCase("outside"))
options |= FindFoci.OPTION_STATS_OUTSIDE;
if (removeEdgeMaxima)
options |= FindFoci.OPTION_REMOVE_EDGE_MAXIMA;
if (objectAnalysis)
{
options |= FindFoci.OPTION_OBJECT_ANALYSIS;
if (showObjectMask)
options |= FindFoci.OPTION_SHOW_OBJECT_MASK;
}
if (saveToMemory)
options |= FindFoci.OPTION_SAVE_TO_MEMORY;
if (outputType == 0)
{
notify(MessageType.ERROR);
return;
}
ImagePlus mask = WindowManager.getImage(maskImage);
IJ.showStatus(FindFoci.TITLE + " calculating ...");
notify(MessageType.RUNNING);
// Compare this model with the previously computed results and
// only update the parts that are necessary.
FindFociState state = compareModels(model, previousModel);
previousModel = null;
//System.out.println("Updating from " + state);
if (state.ordinal() <= FindFociState.INITIAL.ordinal())
{
imp2 = ff.blur(imp, gaussianBlur);
if (imp2 == null)
{
IJ.showStatus(FindFoci.TITLE + " failed");
notify(MessageType.FAILED);
return;
}
}
if (state.ordinal() <= FindFociState.FIND_MAXIMA.ordinal())
{
initResults = ff.findMaximaInit(imp, imp2, mask, backgroundMethod, thresholdMethod, options);
if (initResults == null)
{
IJ.showStatus(FindFoci.TITLE + " failed");
notify(MessageType.FAILED);
return;
}
notify(MessageType.BACKGROUND_LEVEL, initResults.stats.background);
}
if (state.ordinal() <= FindFociState.SEARCH.ordinal())
{
searchInitResults = ff.clone(initResults, searchInitResults);
searchArray = ff.findMaximaSearch(searchInitResults, backgroundMethod, backgroundParameter, searchMethod,
searchParameter);
if (searchArray == null)
{
IJ.showStatus(FindFoci.TITLE + " failed");
notify(MessageType.FAILED);
return;
}
notify(MessageType.BACKGROUND_LEVEL, searchInitResults.stats.background);
}
if (state.ordinal() <= FindFociState.MERGE_HEIGHT.ordinal())
{
// No clone as the maxima and types are not changed
mergePeakResults = ff.findMaximaMergePeak(searchInitResults, searchArray, peakMethod, peakParameter);
if (mergePeakResults == null)
{
IJ.showStatus(FindFoci.TITLE + " failed");
notify(MessageType.FAILED);
return;
}
}
if (state.ordinal() <= FindFociState.MERGE_SIZE.ordinal())
{
// No clone as the maxima and types are not changed
mergeSizeResults = ff.findMaximaMergeSize(searchInitResults, mergePeakResults, minSize);
if (mergeSizeResults == null)
{
IJ.showStatus(FindFoci.TITLE + " failed");
notify(MessageType.FAILED);
return;
}
}
if (state.ordinal() <= FindFociState.MERGE_SADDLE.ordinal())
{
mergeInitResults = ff.clone(searchInitResults, mergeInitResults);
mergeResults = ff.findMaximaMergeFinal(mergeInitResults, mergeSizeResults, minSize, options, gaussianBlur);
if (mergeResults == null)
{
IJ.showStatus(FindFoci.TITLE + " failed");
notify(MessageType.FAILED);
return;
}
}
if (state.ordinal() <= FindFociState.CALCULATE_RESULTS.ordinal())
{
if (initResults.stats.imageMinimum < 0 &&
FindFociBaseProcessor.isSortIndexSenstiveToNegativeValues(sortMethod))
notify(MessageType.SORT_INDEX_SENSITIVE_TO_NEGATIVE_VALUES, initResults.stats.imageMinimum);
else
notify(MessageType.SORT_INDEX_OK, initResults.stats.imageMinimum);
resultsInitResults = ff.clone(mergeInitResults, resultsInitResults);
prelimResults = ff.findMaximaPrelimResults(resultsInitResults, mergeResults, maxPeaks, sortMethod,
centreMethod, centreParameter);
if (prelimResults == null)
{
IJ.showStatus(FindFoci.TITLE + " failed");
notify(MessageType.FAILED);
return;
}
}
if (state.ordinal() <= FindFociState.CALCULATE_OUTPUT_MASK.ordinal())
{
maskInitResults = ff.clone(resultsInitResults, maskInitResults);
results = ff.findMaximaMaskResults(maskInitResults, mergeResults, prelimResults, outputType,
thresholdMethod, imp.getTitle(), fractionParameter);
if (results == null)
{
IJ.showStatus(FindFoci.TITLE + " failed");
notify(MessageType.FAILED);
return;
}
}
if (state.ordinal() <= FindFociState.SHOW_RESULTS.ordinal())
{
ff.showResults(imp, mask, backgroundMethod, backgroundParameter, thresholdMethod, searchParameter, maxPeaks,
minSize, peakMethod, peakParameter, outputType, sortMethod, options, results);
}
IJ.showStatus(FindFoci.TITLE + " finished");
notify(MessageType.DONE);
previousModel = model;
}
/**
* Compare two models and identify the state of the calculation
*
* @param model
* @param previousModel
* @return The state
*/
private FindFociState compareModels(FindFociModel model, FindFociModel previousModel)
{
boolean ignoreChange = false;
if (previousModel == null || notEqual(model.getSelectedImage(), previousModel.getSelectedImage()) ||
notEqual(model.getGaussianBlur(), previousModel.getGaussianBlur()))
{
return FindFociState.INITIAL;
}
if (notEqual(model.getBackgroundMethod(), previousModel.getBackgroundMethod()) ||
notEqual(model.getMaskImage(), previousModel.getMaskImage()) ||
notEqual(model.getBackgroundParameter(), previousModel.getBackgroundParameter()) ||
notEqual(model.getThresholdMethod(), previousModel.getThresholdMethod())
//|| notEqual(model.isShowLogMessages(), previousModel.isShowLogMessages())
)
{
return FindFociState.FIND_MAXIMA;
}
if (notEqual(model.getSearchMethod(), previousModel.getSearchMethod()) ||
notEqual(model.getSearchParameter(), previousModel.getSearchParameter()))
{
return FindFociState.SEARCH;
}
if (notEqual(model.getPeakMethod(), previousModel.getPeakMethod()) ||
notEqual(model.getPeakParameter(), previousModel.getPeakParameter()))
{
return FindFociState.MERGE_HEIGHT;
}
if (notEqual(model.getMinSize(), previousModel.getMinSize()))
{
return FindFociState.MERGE_SIZE;
}
if (notEqual(model.isMinimumAboveSaddle(), previousModel.isMinimumAboveSaddle()) ||
notEqual(model.isRemoveEdgeMaxima(), previousModel.isRemoveEdgeMaxima()))
{
return FindFociState.MERGE_SADDLE;
}
if (notEqual(model.isConnectedAboveSaddle(), previousModel.isConnectedAboveSaddle()))
{
if (model.isMinimumAboveSaddle()) // Only do this if computing above saddle
return FindFociState.MERGE_SADDLE;
ignoreChange = true;
}
if (notEqual(model.getSortMethod(), previousModel.getSortMethod()) ||
notEqual(model.getCentreMethod(), previousModel.getCentreMethod()) ||
notEqual(model.getCentreParameter(), previousModel.getCentreParameter()))
{
return FindFociState.CALCULATE_RESULTS;
}
// Special case where the change is only relevant if previous model was at the limit
if (notEqual(model.getMaxPeaks(), previousModel.getMaxPeaks()))
{
ArrayList<FindFociResult> resultsArrayList = results.results;
int change = model.getMaxPeaks() - previousModel.getMaxPeaks();
if ((change > 0 && resultsArrayList.size() >= previousModel.getMaxPeaks()) ||
(change < 0 && resultsArrayList.size() > model.getMaxPeaks()))
{
//System.out.println("Updating change to maxpeaks: " + previousModel.getMaxPeaks() + " => " + model.getMaxPeaks());
return FindFociState.CALCULATE_RESULTS;
}
ignoreChange = true;
//System.out.println("Ignoring change to maxpeaks: " + previousModel.getMaxPeaks() + " => " + model.getMaxPeaks());
}
if (notEqual(model.getShowMask(), previousModel.getShowMask()) ||
notEqual(model.isOverlayMask(), previousModel.isOverlayMask()))
{
return FindFociState.CALCULATE_OUTPUT_MASK;
}
if (notEqual(model.getFractionParameter(), previousModel.getFractionParameter()))
{
if (model.getShowMask() >= 5) // Only do this if using the fraction parameter
return FindFociState.CALCULATE_OUTPUT_MASK;
ignoreChange = true;
}
if (notEqual(model.isShowTable(), previousModel.isShowTable()) ||
notEqual(model.isClearTable(), previousModel.isClearTable()) ||
notEqual(model.isMarkMaxima(), previousModel.isMarkMaxima()) ||
notEqual(model.isMarkUsingOverlay(), previousModel.isMarkUsingOverlay()) ||
notEqual(model.isHideLabels(), previousModel.isHideLabels()) ||
notEqual(model.isMarkROIMaxima(), previousModel.isMarkROIMaxima()))
{
return FindFociState.SHOW_RESULTS;
}
if (notEqual(model.isObjectAnalysis(), previousModel.isObjectAnalysis()))
{
// Only repeat if switched on. We can ignore it if the flag is switched off since the results
// will be the same.
if (model.isObjectAnalysis())
return FindFociState.SHOW_RESULTS;
ignoreChange = true;
}
if (notEqual(model.isShowObjectMask(), previousModel.isShowObjectMask()))
{
// Only repeat if the mask is to be shown
if (model.isObjectAnalysis() && model.isShowObjectMask())
return FindFociState.SHOW_RESULTS;
ignoreChange = true;
}
if (notEqual(model.isSaveToMemory(), previousModel.isSaveToMemory()))
{
// Only repeat if switched on. We can ignore it if the flag is switched off since the results
// will be the same.
if (model.isSaveToMemory())
return FindFociState.SHOW_RESULTS;
ignoreChange = true;
}
// Options to ignore
if (notEqual(model.isShowLogMessages(), previousModel.isShowLogMessages()) ||
notEqual(model.getResultsDirectory(), previousModel.getResultsDirectory()) ||
notEqual(model.isSaveResults(), previousModel.isSaveResults()))
{
ignoreChange = true;
}
if (ignoreChange)
{
return FindFociState.COMPLETE;
}
// Default to do the entire calculation
return FindFociState.INITIAL;
}
private boolean notEqual(double newValue, double oldValue)
{
return newValue != oldValue;
}
private boolean notEqual(int newValue, int oldValue)
{
return newValue != oldValue;
}
private boolean notEqual(boolean newValue, boolean oldValue)
{
return newValue != oldValue;
}
private boolean notEqual(String newValue, String oldValue)
{
return !(oldValue != null && newValue != null && oldValue.equals(newValue));
}
public void finish()
{
notify(MessageType.BACKGROUND_LEVEL, 0.0f);
notify(MessageType.SORT_INDEX_OK, 0.0f);
notify(MessageType.FINISHED);
running = false;
}
}