package gdsc.smlm.ij.plugins;
import java.awt.AWTEvent;
import java.awt.Choice;
import java.awt.Point;
/*-----------------------------------------------------------------------------
* 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 java.io.BufferedReader;
import java.io.File;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.regex.Pattern;
import gdsc.core.ij.Utils;
import gdsc.core.utils.TurboList;
import gdsc.core.utils.TurboList.SimplePredicate;
import gdsc.smlm.engine.DataFilter;
import gdsc.smlm.engine.FitEngineConfiguration;
import gdsc.smlm.fitting.FitConfiguration;
import gdsc.smlm.fitting.FitSolver;
import gdsc.smlm.ij.settings.GlobalSettings;
import gdsc.smlm.ij.settings.SettingsManager;
import gnu.trove.map.hash.TIntObjectHashMap;
import ij.IJ;
import ij.ImageListener;
import ij.ImagePlus;
import ij.WindowManager;
import ij.gui.DialogListener;
import ij.gui.GenericDialog;
import ij.gui.ImageWindow;
import ij.gui.NonBlockingGenericDialog;
import ij.io.Opener;
import ij.plugin.PlugIn;
import ij.text.TextWindow;
import ij.util.StringSorter;
/**
* This plugin loads configuration templates for the localisation fitting settings
*/
public class ConfigurationTemplate implements PlugIn, DialogListener, ImageListener
{
/**
* Describes the details of a template that can be loaded from the JAR resources folder
*/
static class TemplateResource
{
final String path;
final String tifPath;
final String name;
final boolean optional;
TemplateResource(String path, String name, boolean optional, String tifPath)
{
this.path = path;
this.name = name;
this.optional = optional;
this.tifPath = tifPath;
}
@Override
public String toString()
{
String text = String.format("path=%s, name=%s, optional=%b", path, name, optional);
if (tifPath != null)
text += ", tifPath=" + tifPath;
return text;
}
}
private static class Template
{
GlobalSettings settings;
final boolean custom;
final File file;
long timestamp;
// An example image from the data used to build the template
String tifPath;
public Template(GlobalSettings settings, boolean custom, File file, String tifPath)
{
this.settings = settings;
this.custom = custom;
this.file = file;
timestamp = (file != null) ? file.lastModified() : 0;
// Resource templates may have a tif image as a resource
if (tifPath != null)
{
this.tifPath = tifPath;
}
// Templates with a file may have a corresponding tif image
else if (file != null)
{
tifPath = Utils.replaceExtension(file.getPath(), ".tif");
if (new File(tifPath).exists())
this.tifPath = tifPath;
}
}
public void update()
{
// Check if we can update from the file
if (file != null)
{
if (file.lastModified() != timestamp)
{
GlobalSettings settings = SettingsManager.unsafeLoadSettings(file.getPath(), false);
if (settings != null)
{
this.settings = settings;
timestamp = file.lastModified();
}
}
}
}
/**
* Save the settings to file
*
* @param file
*
* @return true, if successful, False if failed (or no file to save to)
*/
public boolean save(File file)
{
boolean result = false;
if (file != null)
{
result = SettingsManager.saveSettings(settings, file.getPath());
}
return result;
}
public boolean hasImage()
{
return tifPath != null;
}
public ImagePlus loadImage()
{
if (!hasImage())
return null;
Opener opener = new Opener();
opener.setSilentMode(true);
// The tifPath may be a system resource or it may be a file
File file = new File(tifPath);
if (file.exists())
{
// Load directly from a file path
return opener.openImage(tifPath);
}
// IJ has support for loading TIFs from an InputStream
Class<ConfigurationTemplate> resourceClass = ConfigurationTemplate.class;
InputStream inputStream = resourceClass.getResourceAsStream(tifPath);
if (inputStream != null)
{
return opener.openTiff(inputStream, Utils.removeExtension(file.getName()));
}
return null;
}
}
private static LinkedHashMap<String, Template> map;
private static boolean selectStandardTemplates = true;
private static boolean selectCustomDirectory = false;
private static String configurationDirectory;
// Used for the multiMode option
private static ArrayList<String> selected;
private String TITLE;
private static String template = "";
private static boolean close = true;
private ImagePlus imp;
private int currentSlice = 0;
private TextWindow resultsWindow, infoWindow;
private int templateId;
private String headings;
private TIntObjectHashMap<String> text;
static
{
// Maintain the names in the order they are added
map = new LinkedHashMap<String, ConfigurationTemplate.Template>();
String currentUsersHomeDir = System.getProperty("user.home");
configurationDirectory = currentUsersHomeDir + File.separator + "gdsc.smlm";
// Q. What settings should be in the template?
FitConfiguration fitConfig = new FitConfiguration();
FitEngineConfiguration config = new FitEngineConfiguration(fitConfig);
fitConfig.setPrecisionUsingBackground(true);
config.setFailuresLimit(1);
// LSE
fitConfig.setFitSolver(FitSolver.LVM);
config.setDataFilter(DataFilter.MEAN, 1.2, 0);
fitConfig.setCoordinateShiftFactor(1.2);
fitConfig.setSignalStrength(35);
fitConfig.setMinPhotons(30);
fitConfig.setMinWidthFactor(1 / 1.8); // Original code used the reciprocal
fitConfig.setWidthFactor(1.8);
fitConfig.setPrecisionThreshold(45);
addTemplate("PALM LSE", config);
// Add settings for STORM ...
config.setResidualsThreshold(0.4);
config.setFailuresLimit(3);
addTemplate("STORM LSE", config);
config.setResidualsThreshold(1);
config.setFailuresLimit(1);
// Change settings for different fit engines
fitConfig.setFitSolver(FitSolver.MLE);
config.setDataFilter(DataFilter.GAUSSIAN, 1.2, 0);
fitConfig.setCoordinateShiftFactor(1.2);
fitConfig.setSignalStrength(32);
fitConfig.setMinPhotons(30);
fitConfig.setMinWidthFactor(1 / 1.8); // Original code used the reciprocal
fitConfig.setWidthFactor(1.8);
fitConfig.setPrecisionThreshold(47);
addTemplate("PALM MLE", config);
// Add settings for STORM ...
config.setResidualsThreshold(0.4);
config.setFailuresLimit(3);
addTemplate("STORM MLE", config);
config.setResidualsThreshold(1);
config.setFailuresLimit(1);
fitConfig.setModelCamera(true);
fitConfig.setCoordinateShiftFactor(1.5);
fitConfig.setSignalStrength(30);
fitConfig.setMinPhotons(30);
fitConfig.setMinWidthFactor(1 / 1.8); // Original code used the reciprocal
fitConfig.setWidthFactor(1.8);
fitConfig.setPrecisionThreshold(50);
addTemplate("PALM MLE Camera", config);
// Add settings for STORM ...
config.setResidualsThreshold(0.4);
config.setFailuresLimit(3);
addTemplate("STORM MLE Camera", config);
loadStandardTemplates();
}
/**
* Adds the template using the configuration. This should be used to add templates that have not been produced using
* benchmarking on a specific image. Those can be added to the /gdsc/smlm/templates/ resources directory.
*
* @param name
* the name
* @param config
* the config
*/
private static void addTemplate(String name, FitEngineConfiguration config)
{
GlobalSettings settings = new GlobalSettings();
settings.setFitEngineConfiguration(config.clone());
addTemplate(name, settings, false, null, null);
}
/**
* Load standard templates (those not marked as optional).
*/
private static void loadStandardTemplates()
{
TemplateResource[] templates = listTemplates(true, false);
loadTemplates(templates);
}
/**
* List the templates from package resources.
*
* @param loadMandatory
* Set to true to list the mandatory templates
* @param loadOptional
* Set to true to list the optional templates
* @return the templates
*/
static TemplateResource[] listTemplates(boolean loadMandatory, boolean loadOptional)
{
// Load templates from package resources
String templateDir = "/gdsc/smlm/templates/";
Class<ConfigurationTemplate> resourceClass = ConfigurationTemplate.class;
InputStream templateListStream = resourceClass.getResourceAsStream(templateDir + "list.txt");
if (templateListStream == null)
return new TemplateResource[0];
BufferedReader input = new BufferedReader(new InputStreamReader(templateListStream));
String line;
ArrayList<TemplateResource> list = new ArrayList<TemplateResource>();
Pattern p = Pattern.compile("\\*");
try
{
while ((line = input.readLine()) != null)
{
// Skip comment character
if (line.length() == 0 || line.charAt(0) == '#')
continue;
String template = line;
boolean optional = true;
// Mandatory templates have a '*' suffix
int index = template.indexOf('*');
if (index >= 0)
{
template = p.matcher(template).replaceAll("");
optional = false;
}
if (optional)
{
if (!loadOptional)
continue;
}
else
{
if (!loadMandatory)
continue;
}
// Check the resource exists
String path = templateDir + template;
InputStream templateStream = resourceClass.getResourceAsStream(path);
if (templateStream == null)
continue;
// Create a simple name
String name = Utils.removeExtension(template);
// Check if an example TIF file exists for the template
String tifPath = templateDir + name + ".tif";
InputStream tifStream = resourceClass.getResourceAsStream(tifPath);
if (tifStream == null)
tifPath = null;
list.add(new TemplateResource(path, name, optional, tifPath));
}
}
catch (IOException e)
{
}
return list.toArray(new TemplateResource[list.size()]);
}
/**
* Load templates from package resources.
*
* @param templates
* the templates
*/
static int loadTemplates(TemplateResource[] templates)
{
if (templates == null || templates.length == 0)
return 0;
int count = 0;
Class<ConfigurationTemplate> resourceClass = ConfigurationTemplate.class;
for (TemplateResource template : templates)
{
// Skip those already done
if (map.containsKey(template.name))
continue;
InputStream templateStream = resourceClass.getResourceAsStream(template.path);
if (templateStream == null)
continue;
GlobalSettings settings = SettingsManager.unsafeLoadSettings(templateStream, true);
if (settings != null)
{
count++;
addTemplate(template.name, settings, false, null, template.tifPath);
}
}
return count;
}
private static void addTemplate(String name, GlobalSettings settings, boolean custom, File file, String tifPath)
{
map.put(name, new Template(settings, custom, file, tifPath));
}
/**
* Get the template configuration
*
* @param name
* The name of the template
* @return The template
*/
public static GlobalSettings getTemplate(String name)
{
Template template = map.get(name);
if (template == null)
return null;
template.update();
return template.settings;
}
public static ImagePlus getTemplateImage(String name)
{
Template template = map.get(name);
if (template == null)
return null;
return template.loadImage();
}
static void clearTemplates()
{
map.clear();
}
/**
* Save template configuration. If an existing template exists with the same name it will be over-written. If an
* existing template was loaded from file it will be saved back to the same file, or optionally a different file.
*
* @param name
* The name of the template
* @param settings
* The template settings
* @param file
* The file to save the template (over-riding the file the template was loaded from)
* @return true, if successful
*/
public static boolean saveTemplate(String name, GlobalSettings settings, File file)
{
Template template = map.get(name);
if (template == null)
{
addTemplate(name, settings, true, file, null);
return true;
}
template.settings = settings;
if (file != null)
return template.save(file);
if (template.file != null)
return template.save(template.file);
return true;
}
/**
* Check if this is a custom template, i.e. not a standard GDSC SMLM template
*
* @param name
* The name of the template
* @return True if a custom template
*/
public static boolean isCustomTemplate(String name)
{
Template template = map.get(name);
return (template == null) ? null : template.custom;
}
/**
* Get the names of the available templates.
*
* @return The template names
*/
public static String[] getTemplateNames()
{
return getTemplateNames(false);
}
/**
* Get the names of the available templates
*
* @param includeNone
* Set to true to include [None] in the list of names
* @return The template names
*/
public static String[] getTemplateNames(boolean includeNone)
{
int length = (includeNone) ? map.size() + 1 : map.size();
String[] templateNames = new String[length];
int i = 0;
if (includeNone)
templateNames[i++] = "[None]";
for (String name : map.keySet())
templateNames[i++] = name;
return templateNames;
}
/**
* Get the names of the available templates that have an example image.
*
* @return The template names
*/
public static String[] getTemplateNamesWithImage()
{
TurboList<String> templateNames = new TurboList<String>(map.size());
for (Map.Entry<String, Template> entry : map.entrySet())
if (entry.getValue().hasImage())
templateNames.add(entry.getKey());
return templateNames.toArray(new String[templateNames.size()]);
}
/*
* (non-Javadoc)
*
* @see ij.plugin.PlugIn#run(java.lang.String)
*/
public void run(String arg)
{
SMLMUsageTracker.recordPlugin(this.getClass(), arg);
if ("images".equals(arg))
{
showTemplateImages();
return;
}
TITLE = "Template Configuration";
GenericDialog gd = new GenericDialog(TITLE);
gd.addCheckbox("Select_standard_templates", selectStandardTemplates);
gd.addCheckbox("Select_custom_directory", selectCustomDirectory);
gd.showDialog();
if (gd.wasCanceled())
return;
selectStandardTemplates = gd.getNextBoolean();
selectCustomDirectory = gd.getNextBoolean();
if (selectStandardTemplates)
loadSelectedStandardTemplates();
if (selectCustomDirectory)
loadTemplatesFromDirectory();
}
private void loadSelectedStandardTemplates()
{
final TemplateResource[] templates = listTemplates(false, true);
if (templates.length == 0)
return;
MultiDialog md = new MultiDialog("Select Templates", new MultiDialog.BaseItems()
{
public int size()
{
return templates.length;
}
public String getFormattedName(int i)
{
return templates[i].name;
}
});
md.addSelected(selected);
md.showDialog();
if (md.wasCanceled())
return;
selected = md.getSelectedResults();
if (selected.isEmpty())
return;
// Use list filtering to get the selected templates
TurboList<TemplateResource> list = new TurboList<TemplateResource>(Arrays.asList(templates));
list.removeIf(new SimplePredicate<TemplateResource>()
{
public boolean test(TemplateResource t)
{
return !(selected.contains(t.name));
}
});
int count = loadTemplates(list.toArray(new TemplateResource[list.size()]));
IJ.showMessage("Loaded " + Utils.pleural(count, "standard template"));
}
private void loadTemplatesFromDirectory()
{
// Allow the user to specify a configuration directory
String newDirectory = Utils.getDirectory("Template_directory", configurationDirectory);
if (newDirectory == null)
return;
configurationDirectory = newDirectory;
// Search the configuration directory and add any custom templates that can be deserialised from XML files
File[] fileList = (new File(configurationDirectory)).listFiles(new FilenameFilter()
{
public boolean accept(File arg0, String arg1)
{
return arg1.toLowerCase().endsWith("xml");
}
});
if (fileList == null)
return;
// Sort partially numerically
String[] list = new String[fileList.length];
int n = 0;
for (File file : fileList)
{
if (file.isFile())
{
list[n++] = file.getPath();
}
}
list = StringSorter.sortNumerically(list);
int count = 0;
for (String path : list)
{
GlobalSettings settings = SettingsManager.unsafeLoadSettings(path, false);
if (settings != null)
{
count++;
File file = new File(path);
String name = Utils.removeExtension(file.getName());
addTemplate(name, settings, true, file, null);
}
}
IJ.showMessage("Loaded " + Utils.pleural(count, "custom template"));
}
private void showTemplateImages()
{
TITLE = "Template Example Images";
String[] names = getTemplateNamesWithImage();
if (names.length == 0)
{
IJ.error(TITLE, "No templates with example images");
return;
}
// Follow when the image slice is changed
ImagePlus.addImageListener(this);
NonBlockingGenericDialog gd = new NonBlockingGenericDialog(TITLE);
gd.addMessage("View the example source image");
gd.addChoice("Template", names, template);
gd.addCheckbox("Close_on_exit", close);
gd.hideCancelButton();
gd.addDialogListener(this);
// Show the first template
template = ((Choice) (gd.getChoices().get(0))).getSelectedItem();
showTemplateImage(template);
gd.showDialog();
template = gd.getNextChoice();
close = gd.getNextBoolean();
ImagePlus.removeImageListener(this);
if (close)
{
if (imp != null)
imp.close();
closeResults();
closeInfo();
}
}
private void showTemplateImage(String name)
{
ImagePlus imp = getTemplateImage(name);
if (imp == null)
{
IJ.error(TITLE, "Failed to load example image for template: " + name);
}
else
{
this.imp = displayTemplate(TITLE, imp);
if (Utils.isNewWindow())
{
// Zoom a bit
ImageWindow iw = this.imp.getWindow();
for (int i = 7; i-- > 0 && Math.max(iw.getWidth(), iw.getHeight()) < 512;)
{
iw.getCanvas().zoomIn(0, 0);
}
}
createResults(this.imp);
showTemplateInfo(name);
}
}
/**
* Display the template image in an image window with the specified title. If the window exists it will be reused
* and the appropriate properties updated.
*
* @param title
* the title
* @param templateImp
* the template image
* @return the image plus
*/
public static ImagePlus displayTemplate(String title, ImagePlus templateImp)
{
ImagePlus imp = Utils.display(title, templateImp.getStack());
imp.setOverlay(templateImp.getOverlay());
imp.setProperty("Info", templateImp.getProperty("Info"));
imp.setCalibration(templateImp.getCalibration());
return imp;
}
public boolean dialogItemChanged(GenericDialog gd, AWTEvent e)
{
if (e != null && e.getSource() instanceof Choice)
{
template = ((Choice) (e.getSource())).getSelectedItem();
showTemplateImage(template);
}
return true;
}
public void imageOpened(ImagePlus imp)
{
}
public void imageClosed(ImagePlus imp)
{
}
public void imageUpdated(ImagePlus imp)
{
if (imp != null && imp == this.imp)
{
updateResults(imp.getCurrentSlice());
}
}
/**
* Creates a results window showing the localisation results from a template image. This will be positioned next to
* the input template image plus if it is currently displayed.
*
* @param templateImp
* the template image
* @return the text window
*/
public TextWindow createResults(ImagePlus templateImp)
{
if (TITLE == null)
TITLE = templateImp.getTitle();
templateId = templateImp.getID();
currentSlice = 0;
headings = "";
text = new TIntObjectHashMap<String>();
Object info = templateImp.getProperty("Info");
if (info != null)
{
// First line is the headings
String[] lines = info.toString().split("\n");
headings = lines[0].replace(' ', '\t');
// The remaining lines are the data for each stack position
StringBuilder sb = new StringBuilder();
int last = 0;
for (int i = 1; i < lines.length; i++)
{
// Get the position
String[] data = lines[i].split(" ");
int slice = Integer.parseInt(data[0]);
if (last != slice)
{
text.put(last, sb.toString());
last = slice;
sb.setLength(0);
}
sb.append(slice);
for (int j = 1; j < data.length; j++)
{
sb.append('\t').append(data[j]);
}
sb.append('\n');
}
text.put(last, sb.toString());
}
return updateResults(templateImp.getCurrentSlice());
}
/**
* Update the results window using the current selected slice from the template image.
*
* @param slice
* the slice
* @return the text window
*/
public TextWindow updateResults(int slice)
{
if (slice == currentSlice || text == null)
return resultsWindow;
currentSlice = slice;
if (resultsWindow == null || !resultsWindow.isVisible())
{
resultsWindow = new TextWindow(TITLE + " Results", headings, "", 450, 250);
// Put next to the image
ImagePlus imp = WindowManager.getImage(templateId);
if (imp != null && imp.getWindow() != null)
{
ImageWindow iw = imp.getWindow();
Point p = iw.getLocation();
p.x += iw.getWidth();
resultsWindow.setLocation(p);
}
}
resultsWindow.getTextPanel().clear();
String data = text.get(slice);
if (!Utils.isNullOrEmpty(data))
resultsWindow.append(data);
return resultsWindow;
}
public void closeResults()
{
if (resultsWindow != null)
resultsWindow.close();
}
/**
* Show the info from the template.
*
* @param name
* the name
*/
private void showTemplateInfo(String name)
{
GlobalSettings settings = getTemplate(name);
if (settings == null || Utils.isNullOrEmpty(settings.getNotes()))
return;
if (infoWindow == null || !infoWindow.isVisible())
{
infoWindow = new TextWindow(TITLE + " Info", "", "", 450, 250);
// Put underneath the results window
if (resultsWindow != null)
{
Point p = resultsWindow.getLocation();
p.y += resultsWindow.getHeight();
infoWindow.setLocation(p);
}
}
infoWindow.getTextPanel().clear();
// Text window cannot show tabs
infoWindow.append(settings.getNotes().replace('\t', ','));
}
private void closeInfo()
{
if (infoWindow != null)
infoWindow.close();
}
}