package gdsc.smlm.ij.plugins;
import java.awt.BorderLayout;
import java.awt.Button;
import java.awt.Choice;
import java.awt.Color;
import java.awt.Component;
import java.awt.FlowLayout;
import java.awt.Frame;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.awt.Label;
import java.awt.Panel;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.TextField;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.ItemEvent;
import java.awt.event.ItemListener;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.event.WindowEvent;
import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.InputMismatchException;
import java.util.LinkedList;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.Scanner;
import java.util.TreeSet;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import javax.swing.DefaultListModel;
import javax.swing.JList;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import org.apache.commons.math3.analysis.interpolation.LoessInterpolator;
import org.apache.commons.math3.analysis.polynomials.PolynomialSplineFunction;
import gdsc.core.ij.Utils;
import gdsc.core.utils.Maths;
import gdsc.core.utils.Statistics;
/*-----------------------------------------------------------------------------
* GDSC Plugins for ImageJ
*
* Copyright (C) 2011 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 gdsc.smlm.fitting.FitConfiguration;
import gdsc.smlm.fitting.FitFunction;
import gdsc.smlm.fitting.FitResult;
import gdsc.smlm.fitting.FitStatus;
import gdsc.smlm.fitting.Gaussian2DFitter;
import gdsc.smlm.function.gaussian.Gaussian2DFunction;
import gdsc.smlm.ij.IJImageSource;
import gdsc.smlm.ij.utils.ImageROIPainter;
import gdsc.smlm.results.MemoryPeakResults;
import gdsc.smlm.results.PeakResult;
import gdsc.smlm.results.Trace;
import gnu.trove.list.array.TIntArrayList;
import gnu.trove.map.hash.TIntObjectHashMap;
import ij.IJ;
import ij.ImageListener;
import ij.ImagePlus;
import ij.ImageStack;
import ij.Menus;
import ij.Prefs;
import ij.WindowManager;
import ij.gui.GUI;
import ij.gui.GenericDialog;
import ij.gui.Plot2;
import ij.gui.PointRoi;
import ij.gui.Roi;
import ij.plugin.ZProjector;
import ij.plugin.filter.GaussianBlur;
import ij.plugin.frame.PlugInFrame;
import ij.process.ImageProcessor;
import ij.text.TextWindow;
/**
* Allows analysis of the signal and on/off times for fixed fluorophore spots in an image stack.
*/
public class SpotAnalysis extends PlugInFrame
implements ActionListener, ItemListener, Runnable, ImageListener, ListSelectionListener, KeyListener
{
private static final long serialVersionUID = 1L;
private static final String TITLE = "Spot Analysis";
private class Spot implements Comparable<Spot>
{
int frame;
double signal;
public Spot(int frame, double signal)
{
this.frame = frame;
this.signal = signal;
}
@Override
public String toString()
{
//return String.format("%d : %.2f", frame, signal);
return String.format("%d : %.2f", frame, getSignal(frame));
}
/*
* (non-Javadoc)
*
* @see java.lang.Comparable#compareTo(java.lang.Object)
*/
public int compareTo(Spot o)
{
if (o == null)
throw new NullPointerException();
return frame - o.frame;
}
/*
* (non-Javadoc)
*
* @see java.lang.Object#equals(java.lang.Object)
*/
@Override
public boolean equals(Object obj)
{
if (obj == null || !(obj instanceof Spot))
return false;
Spot o = (Spot) obj;
return frame == o.frame;
}
/*
* (non-Javadoc)
*
* @see java.lang.Object#hashCode()
*/
@Override
public int hashCode()
{
return new Integer(frame).hashCode();
}
}
private class TraceResult
{
Spot spot;
Trace trace;
public TraceResult(Spot spot, Trace trace)
{
this.spot = spot;
this.trace = trace;
}
}
/**
* Use a runnable for the image generation to allow multi-threaded operation. Input parameters
* that are manipulated should have synchronized methods.
*/
private class BlurWorker implements Runnable
{
ImageStack inputStack, outputStack;
int slice, slices;
Rectangle bounds;
double blur;
public BlurWorker(ImageStack inputStack, int slice, int slices, Rectangle bounds, double blur,
ImageStack outputStack)
{
this.inputStack = inputStack;
this.slice = slice;
this.slices = slices;
this.bounds = bounds;
this.blur = blur;
this.outputStack = outputStack;
}
/*
* (non-Javadoc)
*
* @see java.lang.Runnable#run()
*/
public void run()
{
GaussianBlur gb = new GaussianBlur();
for (int i = 0; i < slices && slice <= inputStack.getSize(); i++, slice++)
{
IJ.showStatus(
String.format("Calculating blur ... %.1f%%", (100.0 * ++blurCount) / inputStack.getSize()));
ImageProcessor ip = inputStack.getProcessor(slice).duplicate();
ip.setRoi(bounds);
ip.snapshot();
gb.blurGaussian(ip, blur, blur, 0.002);
outputStack.setPixels(ip.getPixels(), slice);
}
}
}
// Image titles
private static final String rawMeanTitle = TITLE + " Raw mean";
private static final String rawSDTitle = TITLE + " Raw SD";
private static final String rawSpotTitle = TITLE + " Raw spot";
private static final String blurSpotTitle = TITLE + " Blur spot";
private static final String avgSpotTitle = TITLE + " Average spot";
private static final String[] resultsTitles = new String[] { rawMeanTitle, rawSDTitle, rawSpotTitle, blurSpotTitle,
avgSpotTitle };
private static Frame instance;
private static TextWindow resultsWindow = null;
private Choice inputChoice;
private TextField widthTextField;
private TextField blurTextField;
private TextField gainTextField;
private TextField exposureTextField;
private TextField smoothingTextField;
private Button profileButton;
private Button addButton;
private Button deleteButton;
private Button saveButton;
private Button saveTracesButton;
private Label currentLabel;
private Label rawFittedLabel;
private Label blurFittedLabel;
@SuppressWarnings("rawtypes")
private DefaultListModel listModel;
@SuppressWarnings("rawtypes")
private JList onFramesList;
//private final int fontWidth = 12;
//private final Font monoFont = new Font("Monospaced", 0, fontWidth);
final String OPT_LOCATION = "CT.location";
//private ImageJ ij;
private int runMode = 0;
private ImagePlus imp, rawImp, blurImp;
private double gain, msPerFrame;
private double[] xValues;
private double[] rawMean;
private double[] smoothMean;
private double[] rawSd;
private double[] smoothSd;
private int area;
private int currentSlice;
private Rectangle areaBounds;
private TreeSet<Spot> onFrames = new TreeSet<Spot>();
private TIntArrayList candidateFrames = new TIntArrayList();
private TIntObjectHashMap<Trace> traces = new TIntObjectHashMap<Trace>();
private int id = 0;
private boolean updated = false;
private int blurCount = 0;
private Object runLock = new Object();
// Stores the list of images last used in the selection options
private ArrayList<String> imageList = new ArrayList<String>();
public SpotAnalysis()
{
super(TITLE);
}
public void run(String arg)
{
SMLMUsageTracker.recordPlugin(this.getClass(), arg);
if (WindowManager.getImageCount() == 0)
{
IJ.showMessage(TITLE, "No images opened.");
return;
}
if ("add".equals(arg))
{
if (instance != null)
((SpotAnalysis) instance).addFrame();
return;
}
if (instance != null)
{
instance.toFront();
return;
}
instance = this;
IJ.register(SpotAnalysis.class);
WindowManager.addWindow(this);
ImagePlus.addImageListener(this);
//ij = IJ.getInstance();
createFrame();
setup();
//addKeyListener(ij);
addKeyListener(this);
pack();
Point loc = Prefs.getLocation(OPT_LOCATION);
if (loc != null)
setLocation(loc);
else
{
GUI.center(this);
}
if (IJ.isMacOSX())
setResizable(false);
setVisible(true);
// Install shortcut for adding a slice
String command = "Spot Analysis (Add)";
String shortcut = "6";
String plugin = "ij.plugin.Hotkeys(" + "\"" + command + "\")";
Menus.installPlugin(plugin, Menus.SHORTCUTS_MENU, "*" + command, shortcut, IJ.getInstance());
}
private void setup()
{
ImagePlus imp = WindowManager.getCurrentImage();
if (imp == null)
return;
fillImagesList();
}
private void fillImagesList()
{
// Find the currently open images
ArrayList<String> newImageList = new ArrayList<String>();
int[] idList = WindowManager.getIDList();
if (idList != null)
for (int id : idList)
{
ImagePlus imp = WindowManager.getImage(id);
// Image must be greyscale stacks
if (imp != null && (imp.getType() == ImagePlus.GRAY8 || imp.getType() == ImagePlus.GRAY16 ||
imp.getType() == ImagePlus.GRAY32) && imp.getStackSize() > 2)
{
// Exclude previous results
if (previousResult(imp.getTitle()))
continue;
newImageList.add(imp.getTitle());
}
}
// Check if the image list has changed
if (imageList.equals(newImageList))
return;
imageList = newImageList;
// Re-populate the image lists
String oldChoice = inputChoice.getSelectedItem();
inputChoice.removeAll();
for (String imageTitle : newImageList)
{
inputChoice.add(imageTitle);
}
// Ensure the drop-downs are resized
pack();
// Restore previous selection
inputChoice.select(oldChoice);
}
private boolean previousResult(String title)
{
for (String resultTitle : resultsTitles)
{
if (title.startsWith(resultTitle))
{
return true;
}
}
return false;
}
/*
* (non-Javadoc)
*
* @see java.awt.event.ActionListener#actionPerformed(java.awt.event.ActionEvent)
*/
public synchronized void actionPerformed(ActionEvent e)
{
Object actioner = e.getSource();
if (actioner == null || runMode > 0)
return;
synchronized (runLock)
{
if (runMode > 0)
return;
if (((Button) actioner == profileButton) && (parametersReady()))
{
runMode = 1;
}
else if ((Button) actioner == addButton)
{
runMode = 2;
}
else if ((Button) actioner == deleteButton)
{
runMode = 3;
}
else if ((Button) actioner == saveButton)
{
runMode = 4;
}
else if ((Button) actioner == saveTracesButton)
{
runMode = 5;
}
}
if (runMode > 0)
{
Thread thread = new Thread(this, TITLE);
thread.start();
}
super.notify();
}
/*
* (non-Javadoc)
*
* @see java.awt.event.ItemListener#itemStateChanged(java.awt.event.ItemEvent)
*/
public void itemStateChanged(ItemEvent e)
{
}
/*
* (non-Javadoc)
*
* @see ij.plugin.frame.PlugInFrame#windowClosing(java.awt.event.WindowEvent)
*/
public void windowClosing(WindowEvent e)
{
Prefs.saveLocation(OPT_LOCATION, getLocation());
close();
}
/*
* (non-Javadoc)
*
* @see ij.plugin.frame.PlugInFrame#close()
*/
public void close()
{
instance = null;
super.close();
}
/*
* (non-Javadoc)
*
* @see ij.plugin.frame.PlugInFrame#windowActivated(java.awt.event.WindowEvent)
*/
public void windowActivated(WindowEvent e)
{
fillImagesList();
super.windowActivated(e);
WindowManager.setWindow(this);
}
/*
* (non-Javadoc)
*
* @see java.lang.Runnable#run()
*/
public void run()
{
synchronized (runLock)
{
try
{
switch (runMode)
{
case 1:
createProfile();
break;
case 2:
addFrame();
break;
case 3:
deleteFrames();
break;
case 4:
saveSpot();
break;
case 5:
saveTraces();
break;
}
}
finally
{
runMode = 0;
}
}
synchronized (this)
{
super.notify();
}
}
private void createProfile()
{
if (!parametersReady())
{
return;
}
double psfWidth, blur;
// Read settings
try
{
psfWidth = Double.parseDouble(widthTextField.getText());
blur = Double.parseDouble(blurTextField.getText());
gain = Double.parseDouble(gainTextField.getText());
msPerFrame = Double.parseDouble(exposureTextField.getText());
}
catch (NumberFormatException e)
{
IJ.error(TITLE, "Invalid numbers in the input parameters");
return;
}
ImagePlus imp = WindowManager.getImage(inputChoice.getSelectedItem());
// This should not be a problem but leave it in for now
if (imp == null || (imp.getType() != ImagePlus.GRAY8 && imp.getType() != ImagePlus.GRAY16 &&
imp.getType() != ImagePlus.GRAY32))
{
IJ.showMessage(TITLE, "Images must be grayscale.");
return;
}
Roi roi = imp.getRoi();
if (roi == null || !roi.isArea())
{
IJ.showMessage(TITLE, "Image must have an area ROI");
return;
}
int recommendedSize = (int) Math.ceil(8 * psfWidth);
Rectangle bounds = roi.getBounds();
if (bounds.width < recommendedSize || bounds.height < recommendedSize)
{
IJ.showMessage(TITLE, String.format("Recommend using an ROI of at least %d x %d for the PSF width",
recommendedSize, recommendedSize));
return;
}
// Check no existing spots are within the ROI
if (resultsWithinBounds(bounds))
{
GenericDialog gd = new GenericDialog(TITLE);
gd.enableYesNoCancel();
gd.hideCancelButton();
gd.addMessage("The results list contains a spot within the selected bounds\n \nDo you want to continue?");
gd.showDialog();
if (!gd.wasOKed())
return;
}
createProfile(imp, bounds, psfWidth, blur);
}
private boolean resultsWithinBounds(Rectangle bounds)
{
if (resultsWindowShowing())
{
float minx = bounds.x;
float maxx = minx + bounds.width;
float miny = bounds.y;
float maxy = miny + bounds.height;
for (int i = 0; i < resultsWindow.getTextPanel().getLineCount(); i++)
{
String line = resultsWindow.getTextPanel().getLine(i);
Scanner s = new Scanner(line);
s.useDelimiter("\t");
s.nextInt();
float cx = s.nextFloat(); // cx
float cy = s.nextFloat(); // cy
s.close();
if (cx >= minx && cx <= maxx && cy >= miny && cy <= maxy)
{
return true;
}
}
}
return false;
}
private boolean resultsWindowShowing()
{
if (resultsWindow == null || !resultsWindow.isShowing())
return false;
return true;
}
private boolean parametersReady()
{
if (inputChoice.getItemCount() == 0)
{
IJ.showMessage(TITLE, "No available images. Images must be 8-bit or 16-bit grayscale.");
return false;
}
if (!onFrames.isEmpty() && updated)
{
GenericDialog gd = new GenericDialog(TITLE);
gd.enableYesNoCancel();
gd.hideCancelButton();
gd.addMessage(
"The list contains unsaved selected frames. Creating a new profile will erase them.\n \nDo you want to continue?");
gd.showDialog();
if (!gd.wasOKed())
return false;
clearSelectedFrames();
}
return (inputChoice.getSelectedIndex() != -1);
}
private void createProfile(ImagePlus imp, Rectangle bounds, double psfWidth, double blur)
{
areaBounds = bounds;
this.imp = imp;
area = bounds.width * bounds.height;
clearSelectedFrames();
// Get a profile through the images
IJ.showStatus("Calculating raw profile");
final int nSlices = imp.getStackSize();
ImageStack rawSpot = new ImageStack(bounds.width, bounds.height, nSlices);
double[][] profile = extractSpotProfile(imp, bounds, rawSpot);
// Retain the existing display range
double min = 0, max = Double.POSITIVE_INFINITY;
if (rawImp != null)
{
min = rawImp.getDisplayRangeMin();
max = rawImp.getDisplayRangeMax();
}
rawImp = showSpot(rawSpotTitle, rawSpot);
if (max != Double.POSITIVE_INFINITY)
{
rawImp.setDisplayRange(min, max);
}
rawMean = profile[0];
rawSd = profile[1];
// Check if there are fitted results in memory
addCandidateFrames(imp.getTitle());
updateProfilePlots();
if (blur > 0)
{
IJ.showStatus("Calculating blur ...");
ImageStack stack = imp.getImageStack();
ImageStack newStack = new ImageStack(stack.getWidth(), stack.getHeight(), stack.getSize());
// Multi-thread the blur stage
ExecutorService threadPool = Executors.newFixedThreadPool(Prefs.getThreads());
List<Future<?>> futures = new LinkedList<Future<?>>();
Utils.setShowProgress(false);
blurCount = 0;
// TODO - See if this is faster if processing multiple slices in each worker
int slices = 5;
for (int n = 1; n <= nSlices; n += slices)
{
futures.add(threadPool.submit(new BlurWorker(stack, n, slices, bounds, blur * psfWidth, newStack)));
}
IJ.showStatus("Calculating blur ... Finishing");
Utils.waitForCompletion(futures);
threadPool.shutdown();
Utils.setShowProgress(false);
IJ.showStatus("Calculating blur ... Drawing");
ImageStack blurSpot = new ImageStack(bounds.width, bounds.height, nSlices);
extractSpotProfile(new ImagePlus("Blur", newStack), bounds, blurSpot);
// Retain the existing display range
max = Double.POSITIVE_INFINITY;
if (blurImp != null)
{
min = blurImp.getDisplayRangeMin();
max = blurImp.getDisplayRangeMax();
}
blurImp = showSpot(blurSpotTitle, blurSpot);
if (max != Double.POSITIVE_INFINITY)
{
blurImp.setDisplayRange(min, max);
}
IJ.showStatus("");
}
else
{
blurImp = null;
}
// Add a z-projection of the blur/original image
ZProjector project = new ZProjector((blurImp == null) ? rawImp : blurImp);
project.setMethod(ZProjector.AVG_METHOD);
project.doProjection();
showSpot(avgSpotTitle, project.getProjection().getImageStack());
if (!candidateFrames.isEmpty())
// Set the first candidate frame
rawImp.setSlice(candidateFrames.get(0));
else
updateCurrentSlice(rawImp.getCurrentSlice());
IJ.showStatus("");
}
private void clearSelectedFrames()
{
currentSlice = -1;
onFrames.clear();
listModel.clear();
candidateFrames.resetQuick();
updated = false;
}
private double[][] extractSpotProfile(ImagePlus imp, Rectangle bounds, ImageStack rawSpot)
{
final int nSlices = imp.getStackSize();
IJImageSource rawSource = new IJImageSource(imp);
double[][] profile = new double[2][nSlices];
for (int n = 0; n < nSlices; n++)
{
IJ.showProgress(n, nSlices);
float[] data = rawSource.next(bounds);
rawSpot.setPixels(data, n + 1);
Statistics stats = new Statistics(data);
profile[0][n] = stats.getMean() / gain;
profile[1][n] = stats.getStandardDeviation() / gain;
}
return profile;
}
private ImagePlus showSpot(String title, ImageStack spot)
{
ImagePlus imp = Utils.display(title, spot);
if (Utils.isNewWindow() || imp.getWindow().getCanvas().getMagnification() == 1)
{
for (int i = 9; i-- > 0;)
imp.getWindow().getCanvas().zoomIn(imp.getWidth() / 2, imp.getHeight() / 2);
}
return imp;
}
private void addCandidateFrames(String title)
{
for (MemoryPeakResults r : MemoryPeakResults.getAllResults())
{
if (r.getSource() instanceof IJImageSource && r.getSource().getName().equals(title))
{
float minx = areaBounds.x;
float maxx = minx + areaBounds.width;
float miny = areaBounds.y;
float maxy = miny + areaBounds.height;
for (PeakResult p : r.getResults())
{
if (p.getXPosition() >= minx && p.getXPosition() <= maxx && p.getYPosition() >= miny &&
p.getYPosition() <= maxy)
{
candidateFrames.add(p.getFrame());
}
}
}
}
}
private void updateProfilePlots()
{
final int nSlices = rawMean.length;
xValues = new double[nSlices];
for (int i = 0; i < nSlices; i++)
xValues[i] = i + 1;
smoothMean = interpolate(xValues, rawMean);
smoothSd = interpolate(xValues, rawSd);
drawProfiles();
}
private double[] interpolate(double[] xValues2, double[] yValues)
{
// Smooth the values not in the current on-frames
double[] newX = Arrays.copyOf(xValues, xValues.length);
double[] newY = Arrays.copyOf(yValues, yValues.length);
for (Spot s : onFrames)
{
newX[s.frame - 1] = -1;
}
int c = 0;
for (int i = 0; i < newX.length; i++)
{
if (newX[i] == -1)
continue;
newX[c] = newX[i];
newY[c] = newY[i];
c++;
}
newX = Arrays.copyOf(newX, c);
newY = Arrays.copyOf(newY, c);
double smoothing = 0.25;
try
{
smoothing = Double.parseDouble(smoothingTextField.getText());
if (smoothing < 0.01 || smoothing > 0.9)
smoothing = 0.25;
}
catch (NumberFormatException e)
{
}
LoessInterpolator loess = new LoessInterpolator(smoothing, 1);
PolynomialSplineFunction f = loess.interpolate(newX, newY);
// Interpolate
double[] plotSmooth = new double[xValues.length];
for (int i = 0; i < xValues.length; i++)
{
// Cannot interpolate outside the bounds of the input data
if (xValues[i] < newX[0])
plotSmooth[i] = newY[0];
else if (xValues[i] > newX[newX.length - 1])
plotSmooth[i] = newY[newX.length - 1];
else
plotSmooth[i] = f.value(xValues[i]);
}
return plotSmooth;
}
private void drawProfiles()
{
showProfile(rawMeanTitle, "Mean", xValues, rawMean, smoothMean);
showProfile(rawSDTitle, "SD", xValues, rawSd, smoothSd);
}
private void showProfile(String title, String yTitle, double[] xValues, double[] yValues, double[] yValues2)
{
Plot2 plot = new Plot2(title, "Frame", yTitle, xValues, yValues);
double[] limits = Maths.limits(yValues);
plot.setLimits(xValues[0], xValues[xValues.length - 1], limits[0], limits[1]);
plot.draw();
plot.setColor(Color.red);
plot.addPoints(xValues, yValues2, Plot2.LINE);
plot.setColor(Color.magenta);
// Add the on-frames
if (!onFrames.isEmpty())
{
double[] onx = new double[onFrames.size()];
double[] ony = new double[onx.length];
int c = 0;
for (Spot s : onFrames)
{
onx[c] = s.frame;
ony[c] = yValues[s.frame - 1];
c++;
}
plot.addPoints(onx, ony, Plot2.CIRCLE);
}
// Add the candidate frames
if (!candidateFrames.isEmpty())
{
plot.setColor(Color.cyan);
double[] onx = new double[candidateFrames.size()];
double[] ony = new double[onx.length];
int c = 0;
for (int i = 0; i < candidateFrames.size(); i++)
{
int frame = candidateFrames.getQuick(i);
onx[c] = frame;
ony[c] = yValues[frame - 1];
c++;
}
plot.addPoints(onx, ony, Plot2.BOX);
plot.setColor(Color.magenta);
}
// Overlay current position
plot.addPoints(new double[] { rawImp.getCurrentSlice(), rawImp.getCurrentSlice() }, limits, Plot2.LINE);
plot.setColor(Color.blue);
Utils.display(title, plot);
}
@SuppressWarnings("unchecked")
private void addFrame()
{
if (rawImp != null)
{
int slice = rawImp.getCurrentSlice();
double signal = getSignal(slice);
Spot s = new Spot(slice, signal);
if (onFrames.add(s))
{
onFramesList.clearSelection();
// Find the location to insert in order
int i = 0;
while (i < listModel.size())
{
Spot s2 = (Spot) listModel.get(i);
if (s.compareTo(s2) < 0)
break;
i++;
}
listModel.add(i, s);
updateProfilePlots();
updated = true;
//pack();
}
}
}
private double getSignal(int slice)
{
double signal = (rawMean[slice - 1] - smoothMean[slice - 1]) * area;
return signal;
}
private void deleteFrames()
{
int[] indices = onFramesList.getSelectedIndices();
if (indices.length > 0)
updated = true;
onFramesList.clearSelection();
for (int i = indices.length; i-- > 0;)
{
Spot removed = (Spot) listModel.get(indices[i]);
onFrames.remove(removed);
}
if (onFrames.isEmpty())
{
listModel.clear();
}
else
{
for (int i = indices.length; i-- > 0;)
{
listModel.remove(indices[i]);
}
}
//pack();
}
private void saveSpot()
{
if (onFrames.isEmpty() || !updated)
return;
createResultsWindow();
id++;
double signal = 0;
Trace trace = null;
float psfWidth = Float.parseFloat(widthTextField.getText());
float cx = areaBounds.x + areaBounds.width / 2.0f;
float cy = areaBounds.y + areaBounds.height / 2.0f;
for (Spot s : onFrames)
{
// Get the signal again since the background may have changed.
final double spotSignal = getSignal(s.frame);
signal += spotSignal;
//signal += s.signal;
float[] params = new float[7];
params[Gaussian2DFunction.X_POSITION] = cx;
params[Gaussian2DFunction.Y_POSITION] = cy;
params[Gaussian2DFunction.SIGNAL] = (float) (spotSignal);
params[Gaussian2DFunction.X_SD] = params[Gaussian2DFunction.Y_SD] = psfWidth;
PeakResult result = new PeakResult(s.frame, (int) cx, (int) cy, 0, 0, 0, params, null);
if (trace == null)
trace = new Trace(result);
else
trace.add(result);
}
Statistics tOn = new Statistics(trace.getOnTimes());
Statistics tOff = new Statistics(trace.getOffTimes());
resultsWindow.append(
String.format("%d\t%.1f\t%.1f\t%s\t%s\t%s\t%d\t%s\t%s\t%s", id, cx, cy, Utils.rounded(signal, 4),
Utils.rounded(tOn.getSum() * msPerFrame, 3), Utils.rounded(tOff.getSum() * msPerFrame, 3),
trace.getNBlinks() - 1, Utils.rounded(tOn.getMean() * msPerFrame, 3),
Utils.rounded(tOff.getMean() * msPerFrame, 3), imp.getTitle()));
// Save the individual on/off times for use in creating a histogram
traces.put(id, trace);
updated = false;
//clearSelectedFrames();
}
private void createResultsWindow()
{
if (!resultsWindowShowing())
{
resultsWindow = new TextWindow(TITLE + " Results",
"Id\tcx\tcy\tSignal\tt-On (ms)\tt-Off (ms)\tBlinks\tAv.t-On\tAv.t-Off\tSource", "", 600, 200);
resultsWindow.setVisible(true);
}
}
private void saveTraces()
{
if (!onFrames.isEmpty() && updated)
{
GenericDialog gd = new GenericDialog(TITLE);
gd.enableYesNoCancel();
gd.hideCancelButton();
gd.addMessage("The list contains unsaved selected frames.\n \nDo you want to continue?");
gd.showDialog();
if (!gd.wasOKed())
return;
}
// For all spots in the results window, get the ID and then save the traces to memory
if (!resultsWindowShowing())
return;
// Create a results set in memory
MemoryPeakResults results = new MemoryPeakResults();
results.setName(TITLE);
results.begin();
MemoryPeakResults.addResults(results);
ArrayList<TraceResult> traceResults = new ArrayList<TraceResult>(resultsWindow.getTextPanel().getLineCount());
for (int i = 0; i < resultsWindow.getTextPanel().getLineCount(); i++)
{
String line = resultsWindow.getTextPanel().getLine(i);
Scanner s = new Scanner(line);
s.useDelimiter("\t");
int id = -1;
double signal = -1;
// Be careful as the text panel may not contain what we expect, i.e. empty lines, etc
if (s.hasNextInt())
{
id = s.nextInt();
try
{
s.nextDouble(); // cx
s.nextDouble(); // cy
signal = s.nextDouble();
}
catch (InputMismatchException e)
{
// Ignore
}
catch (NoSuchElementException e)
{
// Ignore
}
}
s.close();
if (id != -1 && signal != -1)
{
Trace trace = traces.get(id);
if (trace != null)
{
results.addAll(trace.getPoints());
traceResults.add(new TraceResult(new Spot(id, signal), trace));
}
}
}
results.end();
saveTracesToFile(traceResults);
IJ.showStatus("Saved traces");
}
private void saveTracesToFile(ArrayList<TraceResult> traceResults)
{
String resultsDirectory = IJ.getDirectory(TITLE);
if (resultsDirectory == null)
return;
// Save the traces to a single file.
// Also save the blinks and on/off times into data files for histogram analysis
BufferedWriter[] files = new BufferedWriter[5];
try
{
files[0] = openBufferedWriter(resultsDirectory + "traces.txt", String.format(
"#ms/frame = %s\n#Id\tcx\tcy\tsignal\tn-Blinks\tStart\tStop\t...", Utils.rounded(msPerFrame, 3)));
files[1] = openBufferedWriter(resultsDirectory + "tOn.txt", "");
files[2] = openBufferedWriter(resultsDirectory + "tOff.txt", "");
files[3] = openBufferedWriter(resultsDirectory + "blinks.txt", "");
files[4] = openBufferedWriter(resultsDirectory + "signal.txt", "");
for (TraceResult traceResult : traceResults)
{
StringBuffer sb = new StringBuffer();
sb.append(traceResult.spot.frame).append("\t");
sb.append(traceResult.trace.getHead().getXPosition()).append("\t");
sb.append(traceResult.trace.getHead().getYPosition()).append("\t");
sb.append(traceResult.spot.signal).append("\t");
int nBlinks = traceResult.trace.getNBlinks() - 1;
sb.append(nBlinks);
int[] on = traceResult.trace.getOnTimes();
int[] off = traceResult.trace.getOffTimes();
int t = traceResult.trace.getHead().getFrame();
for (int i = 0; i < on.length; i++)
{
writeLine(files[1], Double.toString(msPerFrame * on[i]));
sb.append("\t").append(t).append("\t").append(t + on[i] - 1);
if (off != null && i < off.length)
{
writeLine(files[2], Double.toString(msPerFrame * off[i]));
t += on[i] + off[i];
}
}
writeLine(files[0], sb.toString());
writeLine(files[3], Integer.toString(nBlinks));
writeLine(files[4], String.format("# Id=%d, Blinks=%d, Signal=%f", traceResult.spot.frame, nBlinks,
traceResult.spot.signal));
for (PeakResult r : traceResult.trace.getPoints())
writeLine(files[4], String.format("%d %f", r.getFrame(), r.getSignal()));
}
}
catch (Exception e)
{
// Q. Add better handling of errors?
e.printStackTrace();
IJ.log("Failed to save traces to results directory: " + resultsDirectory);
}
finally
{
for (BufferedWriter tracesFile : files)
{
if (tracesFile != null)
{
try
{
tracesFile.close();
}
catch (IOException e)
{
e.printStackTrace();
}
}
}
}
}
private BufferedWriter openBufferedWriter(String filename, String header) throws IOException
{
BufferedWriter tracesFile = new BufferedWriter(new FileWriter(filename));
if (header != null && header.length() > 0)
{
writeLine(tracesFile, header);
}
return tracesFile;
}
private void writeLine(BufferedWriter tracesFile, String line) throws IOException
{
tracesFile.write(line);
tracesFile.newLine();
}
@SuppressWarnings({ "rawtypes", "unchecked" })
private void createFrame()
{
Panel mainPanel = new Panel();
add(mainPanel);
inputChoice = new Choice();
mainPanel.add(createChoicePanel(inputChoice, ""));
widthTextField = new TextField();
mainPanel.add(createTextPanel(widthTextField, "PSF width", "1.2"));
blurTextField = new TextField();
mainPanel.add(createTextPanel(blurTextField, "Blur (relative to width)", "1"));
gainTextField = new TextField();
mainPanel.add(createTextPanel(gainTextField, "Gain", "37.7"));
exposureTextField = new TextField();
mainPanel.add(createTextPanel(exposureTextField, "ms/Frame", "20"));
smoothingTextField = new TextField();
mainPanel.add(createTextPanel(smoothingTextField, "Smoothing", "0.25"));
profileButton = new Button("Profile");
profileButton.addActionListener(this);
addButton = new Button("Add");
addButton.addActionListener(this);
deleteButton = new Button("Remove");
deleteButton.addActionListener(this);
saveButton = new Button("Save");
saveButton.addActionListener(this);
saveTracesButton = new Button("Save Traces");
saveTracesButton.addActionListener(this);
currentLabel = new Label();
mainPanel.add(createLabelPanel(currentLabel, "", ""));
rawFittedLabel = new Label();
mainPanel.add(createLabelPanel(rawFittedLabel, "", ""));
blurFittedLabel = new Label();
mainPanel.add(createLabelPanel(blurFittedLabel, "", ""));
JPanel buttonPanel = new JPanel();
FlowLayout l = new FlowLayout();
l.setVgap(0);
buttonPanel.setLayout(l);
buttonPanel.add(profileButton, BorderLayout.CENTER);
buttonPanel.add(addButton, BorderLayout.CENTER);
buttonPanel.add(deleteButton, BorderLayout.CENTER);
buttonPanel.add(saveButton, BorderLayout.CENTER);
buttonPanel.add(saveTracesButton, BorderLayout.CENTER);
mainPanel.add(buttonPanel);
listModel = new DefaultListModel();
onFramesList = new JList(listModel);
onFramesList.setVisibleRowCount(15);
onFramesList.addListSelectionListener(this);
//mainPanel.add(onFramesList);
JScrollPane scrollPane = new JScrollPane(onFramesList);
scrollPane.getVerticalScrollBarPolicy();
mainPanel.add(scrollPane);
GridBagLayout mainGrid = new GridBagLayout();
int y = 0;
GridBagConstraints c = new GridBagConstraints();
c.gridx = 0;
c.fill = GridBagConstraints.BOTH;
c.anchor = GridBagConstraints.WEST;
c.gridwidth = 1;
c.insets = new Insets(2, 2, 2, 2);
for (Component comp : mainPanel.getComponents())
{
c.gridy = y++;
mainGrid.setConstraints(comp, c);
}
mainPanel.setLayout(mainGrid);
}
private Panel createChoicePanel(Choice list, String label)
{
Panel panel = new Panel();
panel.setLayout(new BorderLayout());
Label listLabel = new Label(label, 0);
//listLabel.setFont(monoFont);
//list.setSize(fontWidth * 3, fontWidth);
panel.add(listLabel, BorderLayout.WEST);
panel.add(list, BorderLayout.CENTER);
return panel;
}
private Panel createTextPanel(TextField textField, String label, String value)
{
Panel panel = new Panel();
panel.setLayout(new BorderLayout());
Label listLabel = new Label(label, 0);
//listLabel.setFont(monoFont);
//textField.setSize(fontWidth * 3, fontWidth);
textField.setText(value);
panel.add(listLabel, BorderLayout.WEST);
panel.add(textField, BorderLayout.CENTER);
return panel;
}
private Panel createLabelPanel(Label field, String label, String value)
{
Panel panel = new Panel();
panel.setLayout(new BorderLayout());
Label listLabel = new Label(label, 0);
//listLabel.setFont(monoFont);
//textField.setSize(fontWidth * 3, fontWidth);
field.setText(value);
panel.add(listLabel, BorderLayout.WEST);
panel.add(field, BorderLayout.CENTER);
return panel;
}
/*
* (non-Javadoc)
*
* @see ij.ImageListener#imageOpened(ij.ImagePlus)
*/
public void imageOpened(ImagePlus imp)
{
}
/*
* (non-Javadoc)
*
* @see ij.ImageListener#imageClosed(ij.ImagePlus)
*/
public void imageClosed(ImagePlus imp)
{
}
/*
* (non-Javadoc)
*
* @see ij.ImageListener#imageUpdated(ij.ImagePlus)
*/
public void imageUpdated(ImagePlus imp)
{
ImagePlus from = null, to = null;
if (imp == rawImp)
{
from = rawImp;
to = blurImp;
}
else if (imp == blurImp)
{
from = blurImp;
to = rawImp;
}
if (from != null)
{
int slice = from.getCurrentSlice();
updateCurrentSlice(slice);
if (to != null)
{
if (to.getCurrentSlice() != slice)
{
//System.out.println("updating image");
to.setSlice(slice);
//to.resetDisplayRange();
}
}
}
}
private void updateCurrentSlice(int slice)
{
if (slice != currentSlice)
{
currentSlice = slice;
double signal = getSignal(slice);
double noise = smoothSd[slice - 1];
currentLabel.setText(String.format("Frame %d: Signal = %s, SNR = %s", slice, Utils.rounded(signal, 4),
Utils.rounded(signal / noise, 3)));
drawProfiles();
// Fit the PSF using a Gaussian
float[] data2 = (float[]) rawImp.getImageStack().getProcessor(slice).getPixels();
double[] data = Utils.toDouble(data2);
FitConfiguration fitConfiguration = new FitConfiguration();
fitConfiguration.setFitFunction(FitFunction.FIXED);
fitConfiguration.setBackgroundFitting(true);
fitConfiguration.setSignalStrength(0);
fitConfiguration.setCoordinateShift(rawImp.getWidth() / 4.0f);
fitConfiguration.setComputeResiduals(false);
fitConfiguration.setComputeDeviations(false);
Gaussian2DFitter gf = new Gaussian2DFitter(fitConfiguration);
double[] params = new double[7];
double psfWidth = Double.parseDouble(widthTextField.getText());
params[Gaussian2DFunction.BACKGROUND] = smoothMean[slice - 1];
params[Gaussian2DFunction.SIGNAL] = (gain * signal);
params[Gaussian2DFunction.X_POSITION] = rawImp.getWidth() / 2.0f;
params[Gaussian2DFunction.Y_POSITION] = rawImp.getHeight() / 2.0f;
params[Gaussian2DFunction.X_SD] = params[Gaussian2DFunction.Y_SD] = psfWidth;
FitResult fitResult = gf.fit(data, rawImp.getWidth(), rawImp.getHeight(), 1, params, new boolean[1]);
if (fitResult.getStatus() == FitStatus.OK)
{
params = fitResult.getParameters();
final double spotSignal = params[Gaussian2DFunction.SIGNAL] / gain;
rawFittedLabel.setText(String.format("Raw fit: Signal = %s, SNR = %s", Utils.rounded(spotSignal, 4),
Utils.rounded(spotSignal / noise, 3)));
ImageROIPainter.addRoi(rawImp, slice,
new PointRoi(params[Gaussian2DFunction.X_POSITION], params[Gaussian2DFunction.Y_POSITION]));
}
else
{
rawFittedLabel.setText("");
rawImp.setOverlay(null);
}
// Fit the PSF using a Gaussian
if (blurImp == null)
return;
data2 = (float[]) blurImp.getImageStack().getProcessor(slice).getPixels();
data = Utils.toDouble(data2);
params = new double[7];
//float psfWidth = Float.parseFloat(widthTextField.getText());
params[Gaussian2DFunction.BACKGROUND] = (float) smoothMean[slice - 1];
params[Gaussian2DFunction.SIGNAL] = (float) (gain * signal);
params[Gaussian2DFunction.X_POSITION] = rawImp.getWidth() / 2.0f;
params[Gaussian2DFunction.Y_POSITION] = rawImp.getHeight() / 2.0f;
params[Gaussian2DFunction.X_SD] = params[Gaussian2DFunction.Y_SD] = psfWidth;
fitResult = gf.fit(data, rawImp.getWidth(), rawImp.getHeight(), 1, params, new boolean[1]);
if (fitResult.getStatus() == FitStatus.OK)
{
params = fitResult.getParameters();
final double spotSignal = params[Gaussian2DFunction.SIGNAL] / gain;
blurFittedLabel.setText(String.format("Blur fit: Signal = %s, SNR = %s", Utils.rounded(spotSignal, 4),
Utils.rounded(spotSignal / noise, 3)));
ImageROIPainter.addRoi(blurImp, slice,
new PointRoi(params[Gaussian2DFunction.X_POSITION], params[Gaussian2DFunction.Y_POSITION]));
}
else
{
blurFittedLabel.setText("");
blurImp.setOverlay(null);
}
}
}
public void valueChanged(ListSelectionEvent e)
{
if (!e.getValueIsAdjusting())
{
int index = onFramesList.getSelectedIndex();
//int index = e.getFirstIndex();
if (index >= 0 && index < listModel.size())
{
//Utils.log("index = %d, %b", index, e.getValueIsAdjusting());
Spot spot = (Spot) listModel.get(index);
rawImp.setSlice(spot.frame);
}
}
}
public void keyTyped(KeyEvent e)
{
System.out.println("keyTyped");
switch (e.getKeyChar())
{
case 'a':
addFrame();
break;
case ',':
rawImp.setSlice(rawImp.getSlice() + 1);
break;
case '.':
rawImp.setSlice(rawImp.getSlice() - 1);
break;
}
}
public void keyPressed(KeyEvent e)
{
System.out.println("keyPressed");
}
public void keyReleased(KeyEvent e)
{
System.out.println("keyReleased");
}
}