package gdsc.smlm.ij.plugins;
import java.awt.AWTEvent;
import java.awt.Color;
import java.awt.Label;
import java.awt.Rectangle;
import java.util.ArrayList;
import java.util.List;
import gdsc.core.ij.Utils;
import gdsc.core.match.BasePoint;
import gdsc.core.match.Coordinate;
import gdsc.core.match.MatchCalculator;
import gdsc.core.match.MatchResult;
import gdsc.core.match.PointPair;
/*-----------------------------------------------------------------------------
* GDSC SMLM Software
*
* Copyright (C) 2016 Alex Herbert
* Genome Damage and Stability Centre
* University of Sussex, UK
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*---------------------------------------------------------------------------*/
import gdsc.smlm.engine.DataFilter;
import gdsc.smlm.engine.DataFilterType;
import gdsc.smlm.engine.FitEngineConfiguration;
import gdsc.smlm.filters.MaximaSpotFilter;
import gdsc.smlm.filters.Spot;
import gdsc.smlm.fitting.FitConfiguration;
import gdsc.smlm.ij.settings.GlobalSettings;
import gdsc.smlm.ij.settings.SettingsManager;
import gdsc.smlm.results.MemoryPeakResults;
import gnu.trove.map.hash.TIntObjectHashMap;
import ij.IJ;
import ij.ImageListener;
import ij.ImagePlus;
import ij.gui.DialogListener;
import ij.gui.GenericDialog;
import ij.gui.ImageRoi;
import ij.gui.NonBlockingGenericDialog;
import ij.gui.Overlay;
import ij.gui.PointRoi;
import ij.gui.Roi;
import ij.plugin.filter.ExtendedPlugInFilter;
import ij.plugin.filter.PlugInFilterRunner;
import ij.process.FloatProcessor;
import ij.process.ImageProcessor;
/**
* Runs the candidate maxima identification of the image and provides a preview using an overlay
*/
public class SpotFinderPreview implements ExtendedPlugInFilter, DialogListener, ImageListener
{
private final static String TITLE = "Spot Finder Preview";
private int flags = DOES_16 | DOES_8G | DOES_32;
private FitEngineConfiguration config = null;
private FitConfiguration fitConfig = null;
private Overlay o = null;
private ImagePlus imp = null;
private boolean preview = false;
private Label label = null;
private TIntObjectHashMap<ArrayList<Coordinate>> actualCoordinates = null;
private static double distance = 1.5;
private static boolean showTP = true;
private static boolean showFP = true;
/*
* (non-Javadoc)
*
* @see ij.plugin.filter.PlugInFilter#setup(java.lang.String, ij.ImagePlus)
*/
public int setup(String arg, ImagePlus imp)
{
SMLMUsageTracker.recordPlugin(this.getClass(), arg);
if (imp == null)
{
IJ.noImage();
return DONE;
}
Roi roi = imp.getRoi();
if (roi != null && roi.getType() != Roi.RECTANGLE)
{
IJ.error("Rectangular ROI required");
return DONE;
}
return flags;
}
/*
* (non-Javadoc)
*
* @see ij.plugin.filter.ExtendedPlugInFilter#showDialog(ij.ImagePlus, java.lang.String,
* ij.plugin.filter.PlugInFilterRunner)
*/
public int showDialog(ImagePlus imp, String command, PlugInFilterRunner pfr)
{
this.o = imp.getOverlay();
this.imp = imp;
String filename = SettingsManager.getSettingsFilename();
GlobalSettings settings = SettingsManager.loadSettings(filename);
config = settings.getFitEngineConfiguration();
fitConfig = config.getFitConfiguration();
NonBlockingGenericDialog gd = new NonBlockingGenericDialog(TITLE);
gd.addHelp(About.HELP_URL);
gd.addMessage("Preview candidate maxima");
String[] templates = ConfigurationTemplate.getTemplateNames(true);
gd.addChoice("Template", templates, templates[0]);
gd.addStringField("Config_file", filename, 40);
gd.addNumericField("Initial_StdDev0", fitConfig.getInitialPeakStdDev0(), 3);
String[] filterTypes = SettingsManager.getNames((Object[]) DataFilterType.values());
gd.addChoice("Spot_filter_type", filterTypes, filterTypes[config.getDataFilterType().ordinal()]);
String[] filterNames = SettingsManager.getNames((Object[]) DataFilter.values());
gd.addChoice("Spot_filter", filterNames, filterNames[config.getDataFilter(0).ordinal()]);
gd.addSlider("Smoothing", 0, 2.5, config.getSmooth(0));
gd.addSlider("Search_width", 0.5, 2.5, config.getSearch());
gd.addSlider("Border", 0.5, 2.5, config.getBorder());
// Find if this image was created with ground truth data
if (imp.getID() == CreateData.getImageId())
{
MemoryPeakResults results = CreateData.getResults();
if (results != null)
{
gd.addSlider("Match_distance", 0, 2.5, distance);
gd.addCheckbox("Show TP", showTP);
gd.addCheckbox("Show FP", showFP);
gd.addMessage("");
label = (Label) gd.getMessage();
// Integer coords
actualCoordinates = ResultsMatchCalculator.getCoordinates(results.getResults(), true);
}
}
if (!(IJ.isMacro() || java.awt.GraphicsEnvironment.isHeadless()))
{
// Listen for changes to an image
ImagePlus.addImageListener(this);
}
gd.addPreviewCheckbox(pfr);
gd.addDialogListener(this);
gd.hideCancelButton();
gd.setOKLabel("Close");
gd.showDialog();
if (!(IJ.isMacro() || java.awt.GraphicsEnvironment.isHeadless()))
ImagePlus.removeImageListener(this);
if (!gd.wasCanceled())
{
filename = gd.getNextString();
if (SettingsManager.saveSettings(settings, filename, true))
SettingsManager.saveSettingsFilename(filename);
else
IJ.error(TITLE, "Failed to save settings to file " + filename);
}
// Reset
imp.setOverlay(o);
return DONE;
}
/*
* (non-Javadoc)
*
* @see ij.gui.DialogListener#dialogItemChanged(ij.gui.GenericDialog, java.awt.AWTEvent)
*/
public boolean dialogItemChanged(GenericDialog gd, AWTEvent e)
{
gd.getNextChoice();
gd.getNextString();
fitConfig.setInitialPeakStdDev0(gd.getNextNumber());
config.setDataFilterType(gd.getNextChoiceIndex());
config.setDataFilter(gd.getNextChoiceIndex(), Math.abs(gd.getNextNumber()), 0);
config.setSearch(gd.getNextNumber());
config.setBorder(gd.getNextNumber());
if (label != null)
{
distance = gd.getNextNumber();
showTP = gd.getNextBoolean();
showFP = gd.getNextBoolean();
}
preview = gd.getNextBoolean();
if (!preview)
{
setLabel("");
this.imp.setOverlay(o);
}
return !gd.invalidNumber();
}
private void setLabel(String message)
{
if (label == null)
return;
label.setText(message);
}
/*
* (non-Javadoc)
*
* @see ij.plugin.filter.PlugInFilter#run(ij.process.ImageProcessor)
*/
public void run(ImageProcessor ip)
{
Rectangle bounds = ip.getRoi();
MaximaSpotFilter filter = config.createSpotFilter(true);
// Crop to the ROI
FloatProcessor fp = ip.crop().toFloat(0, null);
float[] data = (float[]) fp.getPixels();
int width = fp.getWidth();
int height = fp.getHeight();
Spot[] spots = filter.rank(data, width, height);
data = filter.getPreprocessedData();
fp = new FloatProcessor(width, height, data);
ip = ip.duplicate();
ip.insert(fp, bounds.x, bounds.y);
//ip.resetMinAndMax();
ip.setMinAndMax(fp.getMin(), fp.getMax());
Overlay o = new Overlay();
o.add(new ImageRoi(0, 0, ip));
if (label != null)
{
// Get results for frame
Coordinate[] actual = ResultsMatchCalculator.getCoordinates(actualCoordinates, imp.getCurrentSlice());
Coordinate[] predicted = new Coordinate[spots.length];
for (int i = 0; i < spots.length; i++)
{
predicted[i] = new BasePoint(spots[i].x + bounds.x, spots[i].y + bounds.y);
}
// Q. Should this use partial scoring with multi-matches allowed.
// If so then this needs to be refactored out of the BenchmarkSpotFilter class.
// TODO - compute AUC and max jaccard and plot
// Compute matches
List<PointPair> matches = new ArrayList<PointPair>(Math.min(actual.length, predicted.length));
List<Coordinate> FP = new ArrayList<Coordinate>(predicted.length);
MatchResult result = MatchCalculator.analyseResults2D(actual, predicted,
distance * fitConfig.getInitialPeakStdDev0(), null, FP, null, matches);
// Show scores
setLabel(String.format("P=%s, R=%s, J=%s", Utils.rounded(result.getPrecision()),
Utils.rounded(result.getRecall()), Utils.rounded(result.getJaccard())));
// Create Rois for TP and FP
if (showTP)
{
float[] x = new float[matches.size()];
float[] y = new float[x.length];
int n = 0;
for (PointPair pair : matches)
{
BasePoint p = (BasePoint) pair.getPoint2();
x[n] = p.getX() + 0.5f;
y[n] = p.getY() + 0.5f;
n++;
}
addRoi(0, o, x, y, n, Color.green);
}
if (showFP)
{
float[] x = new float[predicted.length - matches.size()];
float[] y = new float[x.length];
int n = 0;
for (Coordinate c : FP)
{
BasePoint p = (BasePoint) c;
x[n] = p.getX() + 0.5f;
y[n] = p.getY() + 0.5f;
n++;
}
addRoi(0, o, x, y, n, Color.red);
}
}
else
{
float[] x = new float[spots.length];
float[] y = new float[x.length];
for (int i = 0; i < spots.length; i++)
{
x[i] = spots[i].x + bounds.x + 0.5f;
y[i] = spots[i].y + bounds.y + 0.5f;
}
PointRoi roi = new PointRoi(x, y);
// Add options to configure colour and labels
o.add(roi);
}
imp.setOverlay(o);
}
public static void addRoi(int frame, Overlay o, float[] x, float[] y, int n, Color colour)
{
// Add as a circle
addRoi(frame, o, x, y, n, colour, 3);
}
public static void addRoi(int frame, Overlay o, float[] x, float[] y, int n, Color colour, int pointType)
{
if (n == 0)
return;
PointRoi roi = new PointRoi(x, y, n);
roi.setPointType(pointType);
roi.setFillColor(colour);
roi.setStrokeColor(colour);
if (frame != 0)
roi.setPosition(frame);
o.add(roi);
}
/*
* (non-Javadoc)
*
* @see ij.plugin.filter.ExtendedPlugInFilter#setNPasses(int)
*/
public void setNPasses(int nPasses)
{
// Nothing to do
}
public void imageOpened(ImagePlus imp)
{
}
public void imageClosed(ImagePlus imp)
{
}
public void imageUpdated(ImagePlus imp)
{
if (this.imp.getID() == imp.getID() && preview)
{
run(imp.getProcessor());
}
}
}