package gdsc.smlm.ij.plugins;
import java.awt.Color;
import java.awt.Component;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Label;
import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.regex.Pattern;
import org.apache.commons.math3.util.FastMath;
/*-----------------------------------------------------------------------------
* GDSC SMLM Software
*
* Copyright (C) 2013 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.core.ij.Utils;
import gdsc.core.utils.UnicodeReader;
import gdsc.smlm.function.gaussian.Gaussian2DFunction;
import gdsc.smlm.ij.settings.CreateDataSettings;
import gdsc.smlm.ij.settings.GlobalSettings;
import gdsc.smlm.ij.settings.SettingsManager;
import gdsc.smlm.results.AttributePeakResult;
import gdsc.smlm.results.Calibration;
import gdsc.smlm.results.MemoryPeakResults;
import gdsc.smlm.results.PeakResult;
import ij.IJ;
import ij.gui.GenericDialog;
import ij.io.OpenDialog;
import ij.plugin.PlugIn;
/**
* Loads generic localisation files into memory
*/
public class LoadLocalisations implements PlugIn
{
public static class Localisation
{
int t, id;
float x, y, z, intensity, sx = -1, sy = -1, precision = -1;
}
public enum DistanceUnit
{
PIXEL, NM
}
public enum IntensityUnit
{
PHOTON, COUNT
}
public static class LocalisationList extends ArrayList<Localisation>
{
private static final long serialVersionUID = 6616011992365324247L;
public final DistanceUnit distanceUnit;
public final IntensityUnit intensityUnit;
public final double gain;
public final double pixelPitch;
public final double exposureTime;
public LocalisationList(DistanceUnit distanceUnit, IntensityUnit intensityUnit, double gain, double pixelPitch,
double exposureTime)
{
this.distanceUnit = distanceUnit;
this.intensityUnit = intensityUnit;
this.gain = gain;
this.pixelPitch = pixelPitch;
this.exposureTime = exposureTime;
}
private LocalisationList(int distanceUnit, int intensityUnit, double gain, double pixelPitch,
double exposureTime)
{
this(DistanceUnit.values()[distanceUnit], IntensityUnit.values()[intensityUnit], gain, pixelPitch,
exposureTime);
}
public MemoryPeakResults toPeakResults()
{
MemoryPeakResults results = new MemoryPeakResults();
results.setName(name);
results.setCalibration(new Calibration(pixelPitch, gain, exposureTime));
// Convert to ADU count and pixels
final double convertI = (intensityUnit == IntensityUnit.PHOTON) ? gain : 1;
final double convertDtoPx = (distanceUnit == DistanceUnit.NM) ? 1 / pixelPitch : 1;
final double convertDtoNm = (distanceUnit == DistanceUnit.NM) ? 1 : pixelPitch;
for (int i = 0; i < size(); i++)
{
final Localisation l = get(i);
final float[] params = new float[7];
if (l.intensity <= 0)
params[Gaussian2DFunction.SIGNAL] = 1;
else
params[Gaussian2DFunction.SIGNAL] = (float) (l.intensity * convertI);
params[Gaussian2DFunction.X_POSITION] = (float) (l.x * convertDtoPx);
params[Gaussian2DFunction.Y_POSITION] = (float) (l.y * convertDtoPx);
// We may not have read in the widths
if (l.sx == -1)
params[Gaussian2DFunction.X_SD] = 1;
else
params[Gaussian2DFunction.X_SD] = (float) (l.sx * convertDtoPx);
if (l.sy == -1)
params[Gaussian2DFunction.Y_SD] = 1;
else
params[Gaussian2DFunction.Y_SD] = (float) (l.sy * convertDtoPx);
AttributePeakResult peakResult = new AttributePeakResult(l.t,
(int) params[Gaussian2DFunction.X_POSITION], (int) params[Gaussian2DFunction.Y_POSITION], 0,
l.z * convertDtoPx, 0, params, null);
peakResult.setId(l.id);
peakResult.setPrecision(l.precision * convertDtoNm);
results.add(peakResult);
}
return results;
}
}
private static final String TITLE = "Load Localisations";
private static boolean limitZ = false;
private boolean myLimitZ = false;
private static double minz = -5;
private static double maxz = 5;
private static int it = 0;
private static int iid = -1;
private static int ix = 1;
private static int iy = 2;
private static int iz = -1;
private static int ii = 3;
private static int isx = -1;
private static int isy = -1;
private static int ip = -1;
private static int header = 1;
private static String comment = "#";
private static String delimiter = "\\s+";
private static String name = "Localisations";
private static int distanceUnit = 0;
private static int intensityUnit = 0;
private static double gain;
private static double pixelPitch;
private static double exposureTime;
/*
* (non-Javadoc)
*
* @see ij.plugin.PlugIn#run(java.lang.String)
*/
public void run(String arg)
{
SMLMUsageTracker.recordPlugin(this.getClass(), arg);
GlobalSettings globalSettings = SettingsManager.loadSettings();
CreateDataSettings settings = globalSettings.getCreateDataSettings();
String[] path = Utils.decodePath(settings.localisationsFilename);
OpenDialog chooser = new OpenDialog("Localisations_File", path[0], path[1]);
if (chooser.getFileName() == null)
return;
settings.localisationsFilename = chooser.getDirectory() + chooser.getFileName();
SettingsManager.saveSettings(globalSettings);
LocalisationList localisations = loadLocalisations(settings.localisationsFilename);
if (localisations == null)
// Cancelled
return;
if (localisations.isEmpty())
{
IJ.error(TITLE, "No localisations could be loaded");
return;
}
MemoryPeakResults results = localisations.toPeakResults();
// Ask the user what depth to use to create the in-memory results
if (!getZDepth(results))
return;
if (myLimitZ)
{
MemoryPeakResults results2 = new MemoryPeakResults(results.size());
results.setName(name);
results.copySettings(results);
for (PeakResult peak : results.getResults())
{
if (peak.error < minz || peak.error > maxz)
continue;
results2.add(peak);
}
results = results2;
}
// Create the in-memory results
if (results.size() > 0)
{
MemoryPeakResults.addResults(results);
}
IJ.showStatus(String.format("Loaded %d localisations", results.size()));
if (myLimitZ)
Utils.log("Loaded %d localisations, z between %.2f - %.2f", results.size(), minz, maxz);
else
Utils.log("Loaded %d localisations", results.size());
}
private boolean getZDepth(MemoryPeakResults results)
{
// The z-depth is stored in pixels in the error field
double min = results.getHead().error;
double max = min;
for (PeakResult peak : results.getResults())
{
if (min > peak.error)
min = peak.error;
else if (max < peak.error)
max = peak.error;
}
// No z-depth
if (min == max && min == 0)
return true;
maxz = FastMath.min(maxz, max);
minz = FastMath.max(minz, min);
// Display in nm
final double pp = results.getNmPerPixel();
min *= pp;
max *= pp;
String msg = String.format("%d localisations with %.2f <= z <= %.2f", results.size(), min, max);
min = Math.floor(min);
max = Math.ceil(max);
GenericDialog gd = new GenericDialog(TITLE);
gd.addMessage(msg);
gd.addCheckbox("Limit Z-depth", limitZ);
gd.addSlider("minZ", min, max, minz * pp);
gd.addSlider("maxZ", min, max, maxz * pp);
gd.showDialog();
if (gd.wasCanceled() || gd.invalidNumber())
{
return false;
}
myLimitZ = limitZ = gd.getNextBoolean();
minz = gd.getNextNumber() / pp;
maxz = gd.getNextNumber() / pp;
return true;
}
static LocalisationList loadLocalisations(String filename)
{
if (!getFields())
return null;
LocalisationList localisations = new LocalisationList(distanceUnit, intensityUnit, gain, pixelPitch,
exposureTime);
final boolean hasComment = !Utils.isNullOrEmpty(comment);
int errors = 0;
int count = 0;
int h = Math.max(0, header);
BufferedReader input = null;
try
{
FileInputStream fis = new FileInputStream(filename);
input = new BufferedReader(new UnicodeReader(fis, null));
Pattern p = Pattern.compile(delimiter);
String line;
while ((line = input.readLine()) != null)
{
// Skip header
if (h-- > 0)
continue;
// Skip empty lines
if (line.length() == 0)
continue;
// Skip comments
if (hasComment && line.startsWith(comment))
continue;
count++;
final String[] fields = p.split(line);
Localisation l = new Localisation();
try
{
if (it >= 0)
l.t = Integer.parseInt(fields[it]);
if (iid >= 0)
l.id = Integer.parseInt(fields[iid]);
l.x = Float.parseFloat(fields[ix]);
l.y = Float.parseFloat(fields[iy]);
if (iz >= 0)
l.z = Float.parseFloat(fields[iz]);
if (ii >= 0)
l.intensity = Float.parseFloat(fields[ii]);
if (isx >= 0)
l.sy = l.sx = Integer.parseInt(fields[isx]);
if (isy >= 0)
l.sy = Integer.parseInt(fields[isy]);
if (ip >= 0)
l.precision = Float.parseFloat(fields[ip]);
localisations.add(l);
}
catch (NumberFormatException e)
{
if (errors++ == 0)
Utils.log("%s error on record %d: %s", TITLE, count, e.getMessage());
}
catch (IndexOutOfBoundsException e)
{
if (errors++ == 0)
Utils.log("%s error on record %d: %s", TITLE, count, e.getMessage());
}
}
}
catch (IOException e)
{
Utils.log("%s IO error: %s", TITLE, e.getMessage());
}
finally
{
try
{
if (input != null)
input.close();
}
catch (IOException e)
{
// Ignore
}
if (errors != 0)
Utils.log("%s has %d / %d error lines", TITLE, errors, count);
}
return localisations;
}
private static boolean getFields()
{
GenericDialog gd = new GenericDialog(TITLE);
gd.addMessage("Load delimited localisations");
gd.addStringField("Dataset_name", name, 30);
gd.addMessage("Calibration:");
gd.addNumericField("Pixel_size", pixelPitch, 3, 8, "nm");
gd.addNumericField("Gain", gain, 3, 8, "Count/photon");
gd.addNumericField("Exposure_time", exposureTime, 3, 8, "ms");
gd.addMessage("Records:");
gd.addNumericField("Header_lines", header, 0);
gd.addStringField("Comment", comment);
gd.addStringField("Delimiter", delimiter);
String[] dUnits = SettingsManager.getNames((Object[]) DistanceUnit.values());
gd.addChoice("Distance_unit", dUnits, dUnits[distanceUnit]);
String[] iUnits = SettingsManager.getNames((Object[]) IntensityUnit.values());
gd.addChoice("Intensity_unit", iUnits, iUnits[intensityUnit]);
gd.addMessage("Define the fields:");
Label l = (Label) gd.getMessage();
gd.addNumericField("T", it, 0);
gd.addNumericField("ID", iid, 0);
gd.addNumericField("X", ix, 0);
gd.addNumericField("Y", iy, 0);
gd.addNumericField("Z", iz, 0);
gd.addNumericField("Intensity", ii, 0);
gd.addNumericField("Sx", isx, 0);
gd.addNumericField("Sy", isy, 0);
gd.addNumericField("Precision", ip, 0);
// Rearrange
if (gd.getLayout() != null)
{
GridBagLayout grid = (GridBagLayout) gd.getLayout();
int xOffset = 0, yOffset = 0;
int lastY = -1, rowCount = 0;
for (Component comp : gd.getComponents())
{
// Check if this should be the second major column
if (comp == l)
{
xOffset += 2;
yOffset = yOffset - rowCount + 1; // Skip title row
}
// Reposition the field
GridBagConstraints c = grid.getConstraints(comp);
if (lastY != c.gridy)
rowCount++;
lastY = c.gridy;
c.gridx = c.gridx + xOffset;
c.gridy = c.gridy + yOffset;
c.insets.left = c.insets.left + 10 * xOffset;
c.insets.top = 0;
c.insets.bottom = 0;
grid.setConstraints(comp, c);
}
if (IJ.isLinux())
gd.setBackground(new Color(238, 238, 238));
}
gd.showDialog();
if (gd.wasCanceled())
{
return false;
}
name = getNextString(gd, name);
pixelPitch = gd.getNextNumber();
gain = gd.getNextNumber();
exposureTime = gd.getNextNumber();
header = (int) gd.getNextNumber();
comment = gd.getNextString();
delimiter = getNextString(gd, delimiter);
distanceUnit = gd.getNextChoiceIndex();
intensityUnit = gd.getNextChoiceIndex();
int[] columns = new int[9];
for (int i = 0; i < columns.length; i++)
columns[i] = (int) gd.getNextNumber();
{
int i = 0;
it = columns[i++];
iid = columns[i++];
ix = columns[i++];
iy = columns[i++];
iz = columns[i++];
ii = columns[i++];
isx = columns[i++];
isy = columns[i++];
ip = columns[i++];
}
// Validate after reading the dialog (so the static fields store the last entered values)
if (gd.invalidNumber())
{
IJ.error(TITLE, "Invalid number in input fields");
return false;
}
for (int i = 0; i < columns.length; i++)
{
if (columns[i] < 0)
continue;
for (int j = i + 1; j < columns.length; j++)
{
if (columns[j] < 0)
continue;
if (columns[i] == columns[j])
{
IJ.error(TITLE, "Duplicate indicies: " + columns[i]);
return false;
}
}
}
if (gain <= 0 || pixelPitch <= 0)
{
IJ.error(TITLE, "Require positive gain and pixel pitch");
return false;
}
if (ix < 0 || iy < 0)
{
IJ.error(TITLE, "Require valid X and Y indices");
return false;
}
return true;
}
private static String getNextString(GenericDialog gd, String defaultValue)
{
String value = gd.getNextString();
if (Utils.isNullOrEmpty(value))
return defaultValue;
return value;
}
}